场景
通常来说,探针不会引入太重量级的框架,会更多地使用 JDK
原生的接口。然而最近发现,当探针依附在用户应用中时( Spring
应用),有时难免需要使用反射调用用户接口或 Spring
接口,而反射调用需要类实例,使用 Spring
进行依赖注入的框架中,这个实例必须从 Spring Context
中去取。这就造成了一个问题,如何取到 Spring Context
呢,难道一定要在探针中引入 Sping
框架吗?
理论上其实很容易想到——代理,那么具体怎么做呢,不多说,直接看代码。
这里使用的基础代码来自 java探针技术I——如何写一个 java agent
定义 ApplicationContextHolder
首先我们需要一个地方,存放获取到的 Spring Context
,以便在后续的代码中,随时可以取用。
新增 ApplicationContextHolder
类
public class ApplicationContextHolder {
public static Object applicationContextObj;
}
记住,我们已经没有 Spring
了,一切皆对象
添加代理类
使用提供代理功能的类库,这里使用到的是 bytebuddy ,还有一些其他的如 cglib 。挑自己顺手的就好,目的是在 Spring
启动时,拦截特定的类,获取 Spring Context
。
添加代理类 ContextAdvice
public class ContextAdvice {
@Advice.OnMethodEnter
static void enter(@Advice.AllArguments Object[] args) {
ApplicationContextHolder.applicationContextObj = args[0];
}
}
语法就不细说了,可以参考官网。这里是定义在进入特定的方法时,将参数赋值给我们的 ApplicationContextHolder
。
这个代理要绑定到哪里呢,根据经验(写过),可以从ApplicationContextAware
下手。
Spring
在启动时会调用所有 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.
* 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
的哪个版本开始引入的,但是根据一系列谷歌,就是蛮久的啦,应该可以兼容到我读初中时候的 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 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
的所有内容。
引入依赖
com.sparkjava
spark-core
2.8.0
在 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
,再使用反射调用这个 Bean
的方法或者属性。总之,对着 API
盲写代码,没有 Spring
,只有 Object
,Class
,Method
…..是个体力活!