對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邏輯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM