最近有好几个咨询如何动态部署Bean/动态部署Spring mvc 控制器;首先声明下:基于普通Java/JavaEE环境的不适合做动态部署;如果你有这种需求请考虑使用如Play Framework/Grails这种框架。但是还是有少量朋友会有这种需求:我的应用中只有少量几个需要动态部署的组件;好吧,那我来写一个能动态部署Bean/Controller的工具类吧。
注意,因为Spring整个框架非常好的遵循开闭原则,所以只能通过反射来操作,而且目前不考虑Spring 3.1版本以下的(或者使用DefaultAnnotationHandlerMapping,从Spring3.1开始使用RequestMappingHandlerMapping,之前实现了对DefaultAnnotationHandlerMapping的支持,但是想了想还是请考虑升级吧,因为spring向下兼容性非常好),如果想在Spring 3.1之前版本使用请考虑自己修改代码/升级框架。
对于动态注册Groovy脚本,Spring内部提供了支持,使用如<lang:groovy>标签;但是对于需要动态修改的Controller就不那么完美了;
1、如果开启其refresh-check-delay(即多久重载一下脚本),这个目前实现很土,即假设我设置为500毫秒,不管文件修改/没修改都会自动reload,所以请考虑不要使用它的这种刷新脚本机制;我们需要的是检查如文件修改否再刷新;
2、如果开启了refresh-check-delay,其内部是通过Aop完成的,如果没有设置其是proxy-target-class="true",那么它是走JDK动态代理,因为我们大部分控制器是没有实现接口的,所以即使你注册到Spring mvc,也会映射不到的,因此请使用CGLIB代理;创建代理是通过ScriptFactoryPostProcessor来完成的;
3、如果你注册到Spring MVC了,又刷新了脚本,那么它是通过ScriptFactoryPostProcessor注册到proxy一个RefreshableScriptTargetSource,通过这个TargetSource刷新的;问题来了:
对于Spring mvc进行映射是通过RequestMappingHandlerMapping实现,那么RequestMappingHandlerMapping通过如下字段来保持映射关系的;
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>(); //RequestMappingInfo--->HandlerMethod(保持了controllerBean method) private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>(); //url--->RequestMappingInfo
因此如果你刷新了脚本,相当于又创建了一个新的controllerBean,因此拿着的是老的controllerBean和Methond(来的controllerBean类的),而当我们调用时会把Method最终绑定到新的controllerBean类上,所以会得到如下异常:
at com.sishuok.spring.controller.GroovyController$$FastClassByCGLIB$$bb52fd90.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646)
at com.sishuok.spring.controller.GroovyController$$EnhancerByCGLIB$$5c30e5e0.hello(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:214)
"GroovyController cannot be cast to GroovyController",类名一样,那就是ClassLoader不一样了,即刷新脚本时又加载了一个GroovyController类。由于Spring mvc实现机制的问题,无法通过框架本身解决,也就是说动态刷新的Groovy脚本不能用作控制器;具体原因请参考: https://jira.springsource.org/browse/SPR-5749 ;怎么办呢?想到一个办法就是在反射调用Method之前把老的controllerBean类替换为新的controllerBean类即可:通过修改ScriptFactoryPostProcessor的postProcessBeforeInstantiation方法中调用的createRefreshableProxy方法:为proxyFactory添加一个增强:proxyFactory.addAdvice(new ScriptReplaceClassInfoMethodInterceptor()):
@Override public Object invoke(MethodInvocation mi) throws Throwable { boolean isCglibMi = mi.getClass().getName().equals("org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation"); if (isCglibMi && mi.getMethod().getDeclaringClass() != mi.getThis().getClass()) { MethodProxy methodProxy = (MethodProxy) ReflectionUtils.getField(methodProxyField, mi); Object fastClassInfo = ReflectionUtils.getField(fastClassInfoField, methodProxy); ReflectionUtils.setField(fastClassInfoF1Field, fastClassInfo, FastClass.create(mi.getThis().getClass())); } return mi.proceed(); }
该增强通过反射替换老的controllerBean类为新的controllerBean类即可,这也是没有办法的办法 。
4、如果你的Groovy Controller又有依赖注入,如@Autowired private UserController userController;又完蛋了,因为对于@Autowired注解是通过AutowiredAnnotationBeanPostProcessor实现,而其又缓存了注入信息;如果刷新了脚本就会得到如下异常:
原因和之前的类似,因为AutowiredAnnotationBeanPostProcessor缓存了InjectionMetadata,即注入的元数据;而这些元数据又存储了目标类、注入的字段/方法信息;所以会得到如上信息;只能通过Hack清除缓存信息了;通过重载RefreshableScriptTargetSource得到一个ReplaceAndRefreshableScriptTargetSource:然后在其刷新时调用的方法obtainFreshBean中调用removeInjectCache(beanFactory, beanName)清除注入元数据缓存即可完美工作了。
涉及的类:
ScriptFactoryPostProcessor.java
ScriptReplaceClassInfoMethodInterceptor.java
ReplaceAndRefreshableScriptTargetSource.java
这种方式不推荐使用:
需要覆盖重写其ScriptFactoryPostProcessor,如果未来发生变化需要跟着维护;
如果在Groovy Controller里添加新的方法是无法注册到RequestMappingHandlerMapping中的;还需要自己手工注册一遍;
所以以上Hack意义不是特别大了,接下来再给大家另一种比较完美的方案。即完全自己定制注册逻辑,不依赖于Spring相关的基础组件:
dynamicDeployBeans.registerBean(DynamicService1.class); //注册一般的Class类 dynamicDeployBeans.registerBean(DynamicService2.class); //注册一般的Class类 注意DynamicService2依赖于DynamicService1 dynamicDeployBeans.registerController(DynamicController.class); //注册一般的控制器(可以重复注册) dynamicDeployBeans2.registerGroovyController("classpath:com/sishuok/spring/dynamic/GroovyController.groovy"); //注册Groovy Controller 注册后根据scriptCheckInterval会定期检查脚本有没有更新
这种方式可以对控制器的动态修改提供更好的支持:
动态修改代码;
动态增/删/改方法,即可以删除一个已有的映射,或者添加一个新的映射,不会抛出映射二义性错误;
依赖注入的支持。
具体请参考我的github
https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-dynamic
如无必要请不要这样用,请尽量考虑动态脚本语言/框架。