對Spring和SpringBoot動態新增Bean的一點思考
每天總結一個小知識點,工作小記第5回; 正在學習如何把一個東西給別人講的很簡單。
現在想要對已有的一批公司的java應用進行效能分析,裡面用的部分中介軟體是自行研發的,而且要求是無侵入的,不需要業務上做任何改造,也不需要對已有的程式包進行改造。
這種需求,使用JavaAgent就比較合適,因為通過位元組碼增強,不需要對原有的程式碼和程式包做任何修改,就能加入特定的邏輯。
雖然JavaAgent是萬能的,但是其操作風險和開發成本還是比較高的,即使用ByteBuddy,負擔還是不小。但是大部分的監控切面都圍繞著Spring的Bean。
所以,我在想,能不能涉及到Spring相關的,都用Bean解決,特別是利用Spring的AOP能力解決這類監控,或者利用中介軟體的擴充套件機制實現。剩下的搞不定的,再用位元組碼增強實現。好處舉幾個例子如下:
1、動態新增 Mybatis的Plugin,實現特定的SQL邏輯統計、攔截、監控。
2、動態新增Dubbo的Filter的邏輯,實現特定的RPC的統計、攔截、監控。
3、對OpenFeign,或者其他的Bean,直接宣告AOP,進行呼叫攔截。
目前Spring的應用主要有兩大類:
1.Spring MVC的,跑在Tomcat上。
2.SpringBoot的,可能是Fatjar內建了Tomcat容器;也可能是ThinJar,使用外接Tomcat。
如果我想動態的新增一些Bean,讓Spring容器能感知到這些額外的Bean;然後再讓這些Bean通過AOP、BeanFactory或者Aware介面來實現我們特定的監控邏輯,那麼監控的邏輯開發就比位元組碼增強簡單很多。
想讓Spring能動態感知到額外的Bean,我目前總結的有如下方式:
1、通過SpringBoot的 META-INF/spring.factories的機制,動態感知到加入的Bean。前提是:SpringBoot要能掃描到包,但是在FatJar模式下,依賴的所有jar都已經在壓縮包內了,勢必需要修改這個釋出包,這就違背了初衷,不需要動程式包。而且不通用,SpringMVC不識別。
2、如果是SpringBoot的Thinjar模式或者Spring MVC,可以把動態Bean的程式碼新增到Tomcat的webapps的lib裡,讓Spring的component scan去發現。但是又不能動態的修改程式包的componet scan配置。
針對上面的問題,我想到了一個辦法,並測試成功,就是針對Spring的bean載入流程進行位元組碼增強。先讓Spring的Classloader能載入外部的動態jar檔案,再把外部Bean註冊到Spring裡。具體實現使用ByteBuddy進行操作
如果是FatJar的SpringBoot,增強 org.springframework.boot.loader.Launcher.createClassLoader,因為這裡是從Fatjar載入所有依賴包的邏輯,可以增強它,讓他發現額外的外部依賴包。
# 其中的AppendSpringBootJarLoaderAdvisor 就是追加 addUrlList 到Fatjar中已有的jar包列表中 # 因為SpringBoot使用了自定義的ClassLoader,這種增強可以繞過自定義ClassLoader的限制,載入到外部檔案 private static AgentBuilder appendSpringBootJarList(AgentBuilder agentBuilder, List<URL> addUrlList){ AppendSpringBootJarLoaderAdvisor.addUrlList = addUrlList; return agentBuilder.type(named("org.springframework.boot.loader.Launcher")) .transform((builder, typeDescription, classLoader, module) -> builder.method(named("createClassLoader").and(takesArgument(0, URL[].class))).intercept( MethodDelegation.withDefaultConfiguration().withBinders( Morph.Binder.install(DelegateCall.class) ).to(AppendSpringBootJarLoaderAdvisor.class) )); }
如果是ThinJar或者SpringMVC的,可以通過Agent的 instrumentation.appendToSystemClassLoaderSearch ,直接新增。
for(PluginConfig pluginConfig : pluginConfigList){ try{ File f = new File(JarUtils.findJarUrl(pluginConfig.getConfigUrl()).getFile()); instrumentation.appendToSystemClassLoaderSearch(new JarFile(f)); }catch (Exception e){ logger.error("append to system classlaoder error.", e); } }
前面的兩種方案,都是讓Spring的ClassLoader能載入到Jar檔案,接下來就是讓Spring發現這些jar裡的Bean。
我們增強 org.springframework.context.support.AbstractApplicationContext.getBeanFactoryPostProcessors,這樣SpringMVC和SpringBoot通用。
# 要求我們外部的Bean都必須實現BeanFactoryPostProcessor, 在IOC容器啟動的第一輪,就被Sping所識別 private static AgentBuilder appendBeanPostProcessor(AgentBuilder agentBuilder){ List<BeanFactoryPostProcessor> appendList = new ArrayList<>(); appendList.add(new PrefAgentBeanPostProcessor(PluginRegistry.listSpringConfigurationList())); return agentBuilder.type(named("org.springframework.context.support.AbstractApplicationContext")) .transform((builder, typeDescription, classLoader, module) -> builder.method(named("getBeanFactoryPostProcessors").and(isPublic())).intercept( MethodDelegation.withDefaultConfiguration() .to(new AppendSpringBeanFactory(appendList)) )); }
如此,我們就能在Spring中不管是MVC,還是FatJar或者ThinJar模式的SpringBoot中動態插入任何Bean邏輯。