项目以前为servlet 项目。因产品架构升级为多产品SOA架构,想基于现有的项目架构作快速集成, 因此将其升级到spring。 开始方案为:因项目action处理模型与spring mvc不一样,所以只取spring ,而不使用spring mvc。即便用注解 @ServletComponentScan 启用servlet注解方式集成spring。java
但在后续集成spring-cloud-starter-consul-discovery和config时发现,consul-discovery 客户端注册consul不能选择TTL模式进行注入(见本文底),并配置config 后,刷新配置逻辑须要重写。git
所以打算将原action动态注册到dispatcherServlet。查看spring 源码和资料后,找到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping能够对spring的Handler进行管理。github
方案2:注册自定义Action到spring RequestMapping中。web
在原系统中,action的结构为:spring
其中action1 run 对应接口 /web/action1/runapi
会话执行时序图为:架构
1. mvc 中 RequestMappingHandler 在Bean RequestMappingHandlerMapping中注册注册信息为 1).org.springframework.web.servlet.mvc.method.RequestMappingInfo handlerMethod 请求条件注解信息 2).org.springframework.web.method.HandlerMethod 请求处理程序信息 接口为:mvc
/** * Register the given mapping. * <p>This method may be invoked at runtime after initialization has completed. * [@param](https://my.oschina.net/u/2303379) mapping the mapping for the handler method * [@param](https://my.oschina.net/u/2303379) handler the handler * [@param](https://my.oschina.net/u/2303379) method the method */ public void registerMapping(T mapping, Object handler, Method method)
2. 使用 javassist 生成某method 的代理method method 定义主要分为 注解 定义 方法主体。定义中包含有 修饰符 返回类型 方法名称 参数(参数类型及参数名称) 其中注解 修饰符 返回类型 方法名称 参数类型都可经过 java.lang.reflect.Method 得到。 参数名称能够经过jdk1.8 或者asm方式获取。app
ClassPool pool = ClassPool.getDefault(); //生成特殊惟一的HandlerClass CtClass handleCtClz = pool.makeClass(Handler.class.getName() + "$" + c.getSimpleName()+ "$" + m.getName() + "K" + MD5.md5(c.getName() + m.getName()) + "R"+ (new Random().nextInt(1024)), pool.get(Handler.class.getName())); //对HandlerClass操做,生成Method // 设置方法名 修饰符 返回类型 StringBuilder sb = new StringBuilder(); sb.append(Modifier.toString(m.getModifiers()))// 修饰符 .append(" ").append(m.getReturnType().getName())// 放回类型 .append(" ").append(m.getName())// 方法名 .append("("); // 设置参数 List<String> pars = new ArrayList<>(); for (int i = 0; i < m.getParameterCount(); i++) { StringBuilder sb2 = new StringBuilder(); sb2.append(m.getParameterTypes()[i].getName()).append(" ") .append(u.getParameterNames(m)[i]); pars.add(sb2.toString()); } pars.add("javax.servlet.http.HttpServletRequest req"); pars.add("javax.servlet.http.HttpServletResponse resp"); sb.append(StringUtils.join(pars, " , ")).append(")").append("\n throws Throwable ")//参数即异常声明 //.append(StringUtils.join(Stream.of(m.getExceptionTypes()).map(Class::getName),",")) .append("{\n"); // body 返回值 及 case if (!m.getReturnType().getName().equals("void")) { sb.append("return (").append(m.getReturnType().getName()).append(") "); } sb.append("invoke(req, resp, "); if (m.getParameterCount() == 0) sb.append("new Object[0]"); else sb.append("new Object[] {").append(StringUtils.join(u.getParameterNames(m), " , ")) .append("}"); sb.append(");\n }"); CtMethod newCtMethod = CtNewMethod.make(sb.toString(), handleCtClz); // TODO 注解 handleCtClz.addMethod(newCtMethod);
以上便完成了method 的生成,但在spring 中有一套详细的功能注解。若是须要使用,那么还得把注解进行拷贝; 注解不能强制经过方法定义字符串进行定义,但能经过AnnotationsAttribute进行定义。dom
ClassFile ccFile = handleCtClz.getClassFile(); ConstPool constpool = ccFile.getConstPool(); AnnotationsAttribute methodAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag); MethodInfo info = newCtMethod.getMethodInfo(); info.addAttribute(methodAttr); // method 注解拷贝 for (Annotation a : m.getAnnotations()) { methodAttr.addAnnotation(toJavassistAnnotation(a.annotationType().getName(), a, constpool)); } ParameterAnnotationsAttribute paa = new ParameterAnnotationsAttribute(constpool,ParameterAnnotationsAttribute.visibleTag); javassist.bytecode.annotation.Annotation[][] nAs = new javassist.bytecode.annotation.Annotation[m.getParameterCount() + 2][]; Annotation[][] as = m.getParameterAnnotations(); for (int i = 0; i < as.length; i++) { if (as[i].length == 0) { nAs[i] = new javassist.bytecode.annotation.Annotation[1]; nAs[i][0] = getDefaultRequestParam(u.getParameterNames(m)[i], constpool); } else { nAs[i] = new javassist.bytecode.annotation.Annotation[as[i].length]; for (int j = 0; j < as[i].length; j++) { nAs[i][j] = toJavassistAnnotation(as[i][j].annotationType().getName(),as[i][j], constpool); } } } nAs[nAs.length - 2] = new javassist.bytecode.annotation.Annotation[0]; nAs[nAs.length - 1] = new javassist.bytecode.annotation.Annotation[0]; paa.setAnnotations(nAs); info.addAttribute(paa);
最后,经过生成的handlerclass得到实例,注册到requestMappingHandlerMapping中,便可使用。
Class<?> clazz = appClassLoader.findClassByBytes(handleCtClz.getName(),handleCtClz.toBytecode()); obj = new Handler(handleAction); obj = (Handler) getObj(clazz, obj, new Class[] { HandlerAction.class },new Object[] { handleAction }); List<Class<?>> li = new ArrayList<>(); Stream.of(m.getParameterTypes()).forEach(li::add); li.add(HttpServletRequest.class); li.add(HttpServletResponse.class); newMethod = clazz.getMethod(m.getName(), li.toArray(new Class<?>[] {})); requestMappingHandlerMapping.registerMapping(minfoB.build(), obj, newMethod);
结果:
包结构及Action示意:
结果截图:
关于方案一中出现的问题:
1.@ServletComponentScan启用后,dispatcherServlet不被启用。consul-discovery原打算使用ttl模式注册,但发现逻辑存在问题,参见
https://github.com/spring-cloud/spring-cloud-consul/issues/470 具体为org.springframework.cloud.consul.discovery.TtlScheduler中,checkId被写死了"service:",致使不存在对应service。所以只得手写。 ConsulHeartbeatTask(String serviceId) { this.checkId = serviceId; if (!checkId.startsWith("service:")) { checkId = "service:" + checkId; } }
项目关键技术demo:
github: https://github.com/yuyizyk/note/tree/master/notes/spring-dynamic-handlerMapping