項目以前爲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