1. 程式人生 > 其它 >對Spring和SpringBoot動態新增Bean的一點思考

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