需求
为每个http请求响应时增加header信息是哪个controller中的哪个方法返回的. 如下图:
提供demo
根据start.spring.io 创建一个spring-boot项目. 编写两个controller,如下图:
- com.example.demo.web.controller.IndexController 内容如下
@GetMapping("")
public String hello(HttpServletRequest request, HttpServletResponse response, String name) {
return "hello " + name;
}
编写 agent
-
结构
-
HttpAgent.java 内容
public static void premain(String agentArgs, Instrumentation inst) {
try {
// debug 需要暂停, 方便启动时 debug
Thread.sleep(5000);
} catch (Exception ignore) {
}
LogUtils.info("基于javaagent链路追踪");
new AgentBuilder.Default()
.type(
nameStartsWith("com.example.demo.web.controller")
)
.transform((builder, typeDescription, classLoader, module) -> {
builder = builder.visit(Advice
.to(MyAdvice.class)
.on(isMethod()
.and(not(isConstructor()))
.and(not(isStatic()))
.and(not(isSetter()))
.and(not(isGetter()))
.and(not(isToString()))
.and(any())
.and(not(nameStartsWith("main")))));
return builder;
})
.installOn(inst);
}
- MyAdvice.java 内容
public class MyAdvice {
// 方法执行前
@Advice.OnMethodEnter, inline = true 必须使用默认, 否则会出错
public static void enter(@Advice.This Object obj,
@Advice.AllArguments Object[] allArguments,
@Advice.Origin("#t") String className,
@Advice.Origin("#m") String methodName) {
System.out.println(className + "." + methodName);
}
// 方法执行后, inline = true 必须使用默认, 否则会出错
@Advice.OnMethodExit
public static void exit(@Advice.This Object obj,
@Advice.AllArguments Object[] allArguments,
@Advice.Origin("#t") String className,
@Advice.Origin("#m") String methodName) {
for (Object argument : allArguments) {
if (argument instanceof HttpServletResponse) {
HttpServletResponse response = (HttpServletResponse) argument;
response.setHeader("t_id", UUID.randomUUID().toString());
response.setHeader("t_c_n", className);
response.setHeader("t_m_n", methodName);
}
}
}
}
说明:
- MyAdvice 方法必须为 static
- 方法中内容, 只能在方法中编写, 不可调用其他静态方法, 比如使用 RequestContextHolder 来获取 request/response. 因为 agent 和 demo 是不同的 ClassLoader 加载的. 具体可以问度娘/谷哥
- 被拦截的方法中需要包含 HttpServletResponse/HttpServletRequest
- @Advice.OnMethodExit/@Advice.OnMethodEnter byte-buddy 注解中的属性 inline 一定是 true, 即默认. 否则同样在拦截时,会报错. 原理同第2步说明.
- MyAdvice 内容会在方法增强时, 被织入拦截的方法中, 增强之后的代码如下:
@GetMapping("")
public String hello(HttpServletRequest request, HttpServletResponse response, String name) {
// MyAdvice.enter() 方法体的内容全部放在此处
String a = "hello " + name;
// MyAdvice.exit() 方法体的内容全部放在此处
return a;
}
运行并测试
- demo进行打包
- agent进行打包, 有关javaagent相关打包内容, 请网上搜索一下.
- 启动
java \
// 如果需要在 ide 中 debug, 需要加上此命令
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \
// agent包的绝对路径
-javaagent:/dist/agent-core.jar \
-Dserver.port=7001 \
-jar target/spring-boot-demo.jar
- 打开浏览器 输入 http://localhost:7001, 请求并查看响应header内容
header头已经返回自定义内容
本文仅是为项目统一增加 header头提供一种思路. 并非一定要使用byte-buddy来解决. 实际项目中. 完全可以使用 sprng aop 来解决. 当然用 agent 好处是可随时插拔,完全与项目解耦.
使用 arthas 查看增强类信息
具体arthas使用方法, arthas用户文档
- 查看 IndexController 类是被哪个ClassLoader 加载的. 如下图, 因此agent.jar里的类是不能被此类直接使用.
- jad 反编译看下织入结果, 红框就是 com.example.agent.interceptor.MyAdvice#enter和 com.example.agent.interceptor.MyAdvice#exit 两个方法的内容.