場景
通常來說烈涮,探針不會引入太重量級的框架,會更多地使用 JDK
原生的接口窖剑。然而最近發(fā)現(xiàn)坚洽,當探針依附在用戶應用中時( Spring
應用),有時難免需要使用反射調(diào)用用戶接口或 Spring
接口西土,而反射調(diào)用需要類實例讶舰,使用 Spring
進行依賴注入的框架中,這個實例必須從 Spring Context
中去取需了。這就造成了一個問題跳昼,如何取到 Spring Context
呢,難道一定要在探針中引入 Sping
框架嗎肋乍?
理論上其實很容易想到——代理鹅颊,那么具體怎么做呢,不多說住拭,直接看代碼挪略。
這里使用的基礎代碼來自 java探針技術I——如何寫一個 java agent
定義 ApplicationContextHolder
首先我們需要一個地方历帚,存放獲取到的 Spring Context
滔岳,以便在后續(xù)的代碼中,隨時可以取用挽牢。
新增 ApplicationContextHolder
類
public class ApplicationContextHolder {
public static Object applicationContextObj;
}
記住谱煤,我們已經(jīng)沒有 Spring
了,一切皆對象
添加代理類
使用提供代理功能的類庫禽拔,這里使用到的是 bytebuddy 刘离,還有一些其他的如 cglib 。挑自己順手的就好睹栖,目的是在 Spring
啟動時硫惕,攔截特定的類,獲取 Spring Context
野来。
添加代理類 ContextAdvice
public class ContextAdvice {
@Advice.OnMethodEnter
static void enter(@Advice.AllArguments Object[] args) {
ApplicationContextHolder.applicationContextObj = args[0];
}
}
語法就不細說了恼除,可以參考官網(wǎng)。這里是定義在進入特定的方法時,將參數(shù)賦值給我們的 ApplicationContextHolder
豁辉。
這個代理要綁定到哪里呢令野,根據(jù)經(jīng)驗(寫過),可以從ApplicationContextAware
下手徽级。
Spring
在啟動時會調(diào)用所有 ApplicationContextAware
接口气破,并將 Context
通知到 setApplicationContext
方法,其源碼如下
···
public interface ApplicationContextAware extends Aware {
/**
* Set the ApplicationContext that this object runs in.
* Normally this call will be used to initialize the object.
* <p>Invoked after population of normal bean properties but before an init callback such
* as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
* or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},
* {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and
* {@link MessageSourceAware}, if applicable.
* @param applicationContext the ApplicationContext object to be used by this object
* @throws ApplicationContextException in case of context initialization errors
* @throws BeansException if thrown by application context methods
* @see org.springframework.beans.factory.BeanInitializationException
*/
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
官方文檔中還有一句
ApplicationObjectSupport
is a convenience base class for application objects, implementing this interface.
雖然文檔中并沒有說明這個類具體是從Spring
的哪個版本開始引入的餐抢,但是根據(jù)一系列谷歌现使,就是蠻久的啦,應該可以兼容到我讀初中時候的 Spring
版本弹澎。朴下。
所以,在探針啟動時苦蒿,我們將該代理類綁定到 ApplicationObjectSupport
類的 setApplicationContext
方法上殴胧。
修改 StartUp
類
public class StartUp {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("hello, i'am agent!");
final ElementMatcher.Junction<NamedElement> springApplicationType = ElementMatchers.nameEndsWith("ApplicationObjectSupport");
final AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder.method(ElementMatchers.named("setApplicationContext"))
.intercept(Advice.to(ContextAdvice.class));
}
};
new AgentBuilder.Default()
.with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
.type(springApplicationType)
.transform(transformer)
.installOn(inst);
}
}
驗證
為了方便驗證結果,又暗搓搓地使用 spark 暴露了一個 web
接口佩迟,這個接口返回 Spring Context
的所有內(nèi)容团滥。
引入依賴
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.8.0</version>
</dependency>
在 StartUp
類的末尾添加一行
get("/spring", (req, res) -> ApplicationContextHolder.applicationContextObj);
好了,打包报强,編譯灸姊,運行!
Demo 項目啟動完畢之后秉溉,訪問 http://localhost:4567/spring (spark
默認 4567)力惯,可以看到如下返回信息,表示我們成功獲取到 Spring Context
啦
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3d34d211, started on Wed Jul 17 16:09:09 CST 2019
啰嗦幾句
接下來就是怎么使用的問題了召嘶,首先用反射從 Spring Context
中獲取 Bean
父晶,再使用反射調(diào)用這個 Bean
的方法或者屬性∨總之甲喝,對著 API
盲寫代碼,沒有 Spring
铛只,只有 Object
埠胖,Class
,Method
…..是個體力活淳玩!