`
jinnianshilongnian
  • 浏览: 21434473 次
  • 性别: Icon_minigender_1
博客专栏
5c8dac6a-21dc-3466-8abb-057664ab39c7
跟我学spring3
浏览量:2405044
D659df3e-4ad7-3b12-8b9a-1e94abd75ac3
Spring杂谈
浏览量:2997694
43989fe4-8b6b-3109-aaec-379d27dd4090
跟开涛学SpringMVC...
浏览量:5631469
1df97887-a9e1-3328-b6da-091f51f886a1
Servlet3.1规范翻...
浏览量:257571
4f347843-a078-36c1-977f-797c7fc123fc
springmvc杂谈
浏览量:1593160
22722232-95c1-34f2-b8e1-d059493d3d98
hibernate杂谈
浏览量:248971
45b32b6f-7468-3077-be40-00a5853c9a48
跟我学Shiro
浏览量:5847542
Group-logo
跟我学Nginx+Lua开...
浏览量:698161
5041f67a-12b2-30ba-814d-b55f466529d5
亿级流量网站架构核心技术
浏览量:780453
社区版块
存档分类
最新评论

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类即可,这也是没有办法的办法皱眉

 

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 

 

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

 

 

14
12
分享到:
评论
7 楼 feiweiwei 2017-08-24  
涛神,最近有个地方要动态更新controller,正好参考了您的文章,您例子里使用的是spring4.0,但是我们用的springboot使用的spring是4.3.9,里面RequestMappingHandlerMapping类的handlerMethods和urlMap都没有了,后来看了4.3.9的源码发现,这两个Map变量都没有了,而是在AbstractHandlerMethodMapping类中实现了一个MappingRegistry内部类里找到了Map<T, HandlerMethod> mappingLookup和MultiValueMap<String, T> urlLookup,怀疑是原来的handlerMethod和urlMap,但是这两个因为是protected内部类的对象无法反射出来,请问如何处理,还有个问题就是例子里spring4.0的源码也看了下,handlerMethod的map源码里是定义成final的,remove不会报错吗?谢谢解答。
6 楼 biran1980 2016-01-27  
涛ge,拜读了代码,想请教下,如果我不使用注解RequestMapping,如何能自定义设置请求,比如假设用这种方式,dynamicDeployBeans2.registerController("/hello".clazz);
5 楼 jacky_zz 2014-01-07  
jinnianshilongnian 写道
jacky_zz 写道
通过与开涛几天的讨论以及自己的一些尝试,解决了项目的一些技术需求,感谢开涛!
PS:开涛对spring的理解太深入了。

:oops: ,可别这么说;有问题再讨论


经过这几天的尝试,觉得将groovy脚本放在service层更加合适一点,放在controller层还是不太合适,当然开涛你的方案也是可行的。
4 楼 jinnianshilongnian 2014-01-06  
jacky_zz 写道
通过与开涛几天的讨论以及自己的一些尝试,解决了项目的一些技术需求,感谢开涛!
PS:开涛对spring的理解太深入了。

:oops: ,可别这么说;有问题再讨论
3 楼 jinnianshilongnian 2014-01-06  
hngmduyi 写道
支持一个。

2 楼 jacky_zz 2014-01-06  
通过与开涛几天的讨论以及自己的一些尝试,解决了项目的一些技术需求,感谢开涛!
PS:开涛对spring的理解太深入了。
1 楼 hngmduyi 2014-01-06  
支持一个。

相关推荐

Global site tag (gtag.js) - Google Analytics