Spring动态部署Bean/Controller/Groovy Controller

最近有好几个咨询如何动态部署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类上,所以会得到如下异常:

写道
java.lang.ClassCastException: com.sishuok.spring.controller.GroovyController cannot be cast to com.sishuok.spring.controller.GroovyController
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类即可,这也是没有办法的办法Spring动态部署Bean/Controller/Groovy Controller

4、如果你的Groovy Controller又有依赖注入,如@Autowired private UserController userController;又完蛋了,因为对于@Autowired注解是通过AutowiredAnnotationBeanPostProcessor实现,而其又缓存了注入信息;如果刷新了脚本就会得到如下异常:

写道
java.lang.IllegalArgumentException: Can not set com.sishuok.spring.controller.UserController field com.sishuok.spring.controller.GroovyController.userController to com.sishuok.spring.controller.GroovyController

原因和之前的类似,因为AutowiredAnnotationBeanPostProcessor缓存了InjectionMetadata,即注入的元数据;而这些元数据又存储了目标类、注入的字段/方法信息;所以会得到如上信息;只能通过Hack清除缓存信息了;通过重载RefreshableScriptTargetSource得到一个ReplaceAndRefreshableScriptTargetSource:然后在其刷新时调用的方法obtainFreshBean中调用removeInjectCache(beanFactory, beanName)清除注入元数据缓存即可完美工作了。

涉及的类:

DynamicDeployBeans2.java

ScriptFactoryPostProcessor.java 

ScriptReplaceClassInfoMethodInterceptor.java 

ReplaceAndRefreshableScriptTargetSource.java 

这种方式不推荐使用:

需要覆盖重写其ScriptFactoryPostProcessor,如果未来发生变化需要跟着维护;

如果在Groovy Controller里添加新的方法是无法注册到RequestMappingHandlerMapping中的;还需要自己手工注册一遍;

所以以上Hack意义不是特别大了,接下来再给大家另一种比较完美的方案。即完全自己定制注册逻辑,不依赖于Spring相关的基础组件:

DynamicDeployBeans.java

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 

如无必要请不要这样用,请尽量考虑动态脚本语言/框架。

相关推荐