最近有好幾個(gè)咨詢?nèi)绾蝿?dòng)態(tài)部署B(yǎng)ean/動(dòng)態(tài)部署Spring mvc 控制器,;首先聲明下:基于普通Java/JavaEE環(huán)境的不適合做動(dòng)態(tài)部署,;如果你有這種需求請(qǐng)考慮使用如Play Framework/Grails這種框架,。但是還是有少量朋友會(huì)有這種需求:我的應(yīng)用中只有少量幾個(gè)需要?jiǎng)討B(tài)部署的組件;好吧,,那我來(lái)寫一個(gè)能動(dòng)態(tài)部署B(yǎng)ean/Controller的工具類吧,。
注意,因?yàn)镾pring整個(gè)框架非常好的遵循開(kāi)閉原則,,所以只能通過(guò)反射來(lái)操作,,而且目前不考慮Spring 3.1版本以下的(或者使用DefaultAnnotationHandlerMapping,從Spring3.1開(kāi)始使用RequestMappingHandlerMapping,之前實(shí)現(xiàn)了對(duì)DefaultAnnotationHandlerMapping的支持,,但是想了想還是請(qǐng)考慮升級(jí)吧,因?yàn)閟pring向下兼容性非常好),,如果想在Spring 3.1之前版本使用請(qǐng)考慮自己修改代碼/升級(jí)框架,。
對(duì)于動(dòng)態(tài)注冊(cè)Groovy腳本,Spring內(nèi)部提供了支持,,使用如<lang:groovy>標(biāo)簽,;但是對(duì)于需要?jiǎng)討B(tài)修改的Controller就不那么完美了; 1,、如果開(kāi)啟其refresh-check-delay(即多久重載一下腳本),,這個(gè)目前實(shí)現(xiàn)很土,即假設(shè)我設(shè)置為500毫秒,,不管文件修改/沒(méi)修改都會(huì)自動(dòng)reload,,所以請(qǐng)考慮不要使用它的這種刷新腳本機(jī)制;我們需要的是檢查如文件修改否再刷新,; 2,、如果開(kāi)啟了refresh-check-delay,其內(nèi)部是通過(guò)Aop完成的,,如果沒(méi)有設(shè)置其是proxy-target-class="true",,那么它是走JDK動(dòng)態(tài)代理,因?yàn)槲覀兇蟛糠挚刂破魇菦](méi)有實(shí)現(xiàn)接口的,,所以即使你注冊(cè)到Spring mvc,,也會(huì)映射不到的,因此請(qǐng)使用CGLIB代理,;創(chuàng)建代理是通過(guò)ScriptFactoryPostProcessor來(lái)完成的,; 3、如果你注冊(cè)到Spring MVC了,,又刷新了腳本,,那么它是通過(guò)ScriptFactoryPostProcessor注冊(cè)到proxy一個(gè)RefreshableScriptTargetSource,通過(guò)這個(gè)TargetSource刷新的,;問(wèn)題來(lái)了: 對(duì)于Spring mvc進(jìn)行映射是通過(guò)RequestMappingHandlerMapping實(shí)現(xiàn),,那么RequestMappingHandlerMapping通過(guò)如下字段來(lái)保持映射關(guān)系的;
因此如果你刷新了腳本,,相當(dāng)于又創(chuàng)建了一個(gè)新的controllerBean,,因此拿著的是老的controllerBean和Methond(來(lái)的controllerBean類的),而當(dāng)我們調(diào)用時(shí)會(huì)把Method最終綁定到新的controllerBean類上,,所以會(huì)得到如下異常:
寫道 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不一樣了,即刷新腳本時(shí)又加載了一個(gè)GroovyController類,。由于Spring mvc實(shí)現(xiàn)機(jī)制的問(wèn)題,,無(wú)法通過(guò)框架本身解決,也就是說(shuō)動(dòng)態(tài)刷新的Groovy腳本不能用作控制器,;具體原因請(qǐng)參考:https://jira./browse/SPR-5749,;怎么辦呢?想到一個(gè)辦法就是在反射調(diào)用Method之前把老的controllerBean類替換為新的controllerBean類即可:通過(guò)修改ScriptFactoryPostProcessor的postProcessBeforeInstantiation方法中調(diào)用的createRefreshableProxy方法:為proxyFactory添加一個(gè)增強(qiáng):proxyFactory.addAdvice(new ScriptReplaceClassInfoMethodInterceptor()):
該增強(qiáng)通過(guò)反射替換老的controllerBean類為新的controllerBean類即可,,這也是沒(méi)有辦法的辦法,。
4、如果你的Groovy Controller又有依賴注入,,如@Autowired private UserController userController,;又完蛋了,因?yàn)閷?duì)于@Autowired注解是通過(guò)AutowiredAnnotationBeanPostProcessor實(shí)現(xiàn),,而其又緩存了注入信息,;如果刷新了腳本就會(huì)得到如下異常: 寫道 java.lang.IllegalArgumentException: Can not set com.sishuok.spring.controller.UserController field com.sishuok.spring.controller.GroovyController.userController to com.sishuok.spring.controller.GroovyController 原因和之前的類似,因?yàn)锳utowiredAnnotationBeanPostProcessor緩存了InjectionMetadata,,即注入的元數(shù)據(jù),;而這些元數(shù)據(jù)又存儲(chǔ)了目標(biāo)類、注入的字段/方法信息,;所以會(huì)得到如上信息,;只能通過(guò)Hack清除緩存信息了;通過(guò)重載RefreshableScriptTargetSource得到一個(gè)ReplaceAndRefreshableScriptTargetSource:然后在其刷新時(shí)調(diào)用的方法obtainFreshBean中調(diào)用removeInjectCache(beanFactory, beanName)清除注入元數(shù)據(jù)緩存即可完美工作了,。
涉及的類: ScriptFactoryPostProcessor.java ScriptReplaceClassInfoMethodInterceptor.java ReplaceAndRefreshableScriptTargetSource.java
這種方式不推薦使用: 需要覆蓋重寫其ScriptFactoryPostProcessor,,如果未來(lái)發(fā)生變化需要跟著維護(hù); 如果在Groovy Controller里添加新的方法是無(wú)法注冊(cè)到RequestMappingHandlerMapping中的,;還需要自己手工注冊(cè)一遍,;
所以以上Hack意義不是特別大了,接下來(lái)再給大家另一種比較完美的方案,。即完全自己定制注冊(cè)邏輯,,不依賴于Spring相關(guān)的基礎(chǔ)組件:
這種方式可以對(duì)控制器的動(dòng)態(tài)修改提供更好的支持: 動(dòng)態(tài)修改代碼; 動(dòng)態(tài)增/刪/改方法,,即可以刪除一個(gè)已有的映射,,或者添加一個(gè)新的映射,不會(huì)拋出映射二義性錯(cuò)誤,; 依賴注入的支持,。
具體請(qǐng)參考我的github https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-dynamic
如無(wú)必要請(qǐng)不要這樣用,請(qǐng)盡量考慮動(dòng)態(tài)腳本語(yǔ)言/框架,。
|
|