文章目录
前言
最近需要对存量的项目进行批量日志打印操作,要求对代码零侵入,显然AOP是行不通了,于是考虑使用agent来实现对方法的拦截,但是一顿操作下来,神奇的事情发生了,我自定义的agent里的premain方法被调用了,但是定义的拦截器没有生效,即使我拦截规则用的是any(),即拦截所有请求,也不会生效,更尴尬的是,我一时找不到原因,于是就新建了一个新的springboot项目,而这个新项目对agent有效,最后通过配置main方法,修改bytebuddy的版本解决了,今天用这篇文章主要记录下排查过程,以便给跟我出现同样问题的小伙伴一个参考;因为我这个问题网上都搜烂了,也没找到解决方案;
一、自定义agent
- 新建一个maven项目,pom如下
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>logging-agent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<log4j2.version>2.17.1</log4j2.version>
<bytebuddy.version>1.12.0</bytebuddy.version>
<dubbo.version>3.0.0</dubbo.version>
<spring.boot.version>3.0.2</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${bytebuddy.version}</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${bytebuddy.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!-- Spring Boot Starter (optional if you're interacting with Spring Boot) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
<scope>provided</scope>
</dependency>
<!-- Spring Boot Starter Web (optional for HTTP request handling) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.4</version>
<scope>provided</scope>
</dependency>
<!-- For Dubbo Integration (if needed) -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>指定你类的方法入口,main方法</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>指定你自定义的Agent的全限定名</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Spring Boot Maven Plugin for running the app (optional) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</build>
</project>
- 定义agent,代码如下:
package com.dll.logagent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
public class CommonLogAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("..............premain.start.............."+CommonLogAgent.class.getClassLoader());
/**
* 拦截项目里对外提供的http请求
*/
new AgentBuilder.Default()
.type(ElementMatchers.nameEndsWith("Controller"))
.transform((builder, typeDescription, classLoader, module,a) -> {
return builder
.method(ElementMatchers.any())
.intercept(Advice.to(HttpLogInterceptor.class));
})
// .with(new AgentBuilder.Listener.StreamWriting(System.out))
.installOn(inst);
/**
* 拦截项目里对外发送的http请求,此demo只支持RestTemplate.postForEntity
*/
new AgentBuilder.Default()
.type(ElementMatchers.named("org.springframework.web.client.RestTemplate")
)
.transform((builder, typeDescription, classLoader, module,a) -> builder
.method(ElementMatchers.named("postForEntity"))
.intercept(Advice.to(LoggingInterceptor.class)))
.installOn(inst);
}
// 如果你跟我一样,无论如何拦截器都不生效的话就把main方法加上吧,同时记得在pom文件里的mainClass配置下
public static void main(String[] args) {
System.out.println(" =======AGENT.MAIN====== ");
}
- 定义拦截器,这里只展示HttpLogInterceptor,代码如下:
package com.dll.logagent;
import net.bytebuddy.asm.Advice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpLogInterceptor {
public static final Logger logger = LoggerFactory.getLogger(HttpLogInterceptor.class);
@Advice.OnMethodEnter
public static long onEnter(@Advice.Origin String methodName, @Advice.AllArguments Object[] args) {
long startTime = System.currentTimeMillis();
// logger.info("methodName:{}onEnter...........",methodName);
if (!methodName.contains("com.dll")) {
return startTime;
}
for (Object arg : args) {
logger.info("HttpLogInterceptor.Start.METHOD_NAME:"+methodName + ",PARAMS:" + arg);
}
return startTime;
}
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(@Advice.Origin String methodName,@Advice.Enter long startTime, @Advice.Return Object obj,@Advice.Thrown Throwable throwable) {
if (!methodName.contains("com.dll")) {
return;
}
long time = System.currentTimeMillis() - startTime;
if (throwable != null) {
logger.error("方法:{}调用异常:{}",methodName,throwable.getLocalizedMessage());
}else {
logger.info("HttpLogInterceptor.End.METHOD_NAME:" + methodName + ",RESULT:" + obj+",COST:"+time);
}
}
}
- 代码至此算是结束了,接下来就是在你需要植入的项目启动时新增命令行参数(jar包路径记得修改):
-javaagent:D:\workspace\dll-logagent\target\jzx-logagent-1.0-SNAPSHOT.jar
至此,如果不出意外的话其实功能就是可用的了,只是我这次开发并不顺利,我的拦截器没生效
二、问题排查
- 先将jar包加到项目里,通过idea看下编译后的源码,特别需要关注META-INF下的MANIFEST.MF文件内容,观察里面的Premain-Class和Main-Class是否是你pom里面配置的值,我检查后发现一致,但是Main-Class标红,百度发现只要指定了Premain-Class那么Main-Class用不上,所以当时没有在意
- 观察类加载顺序,我们自定义的agent是否在Controller之前被加载了,如果agent后加载那么肯定不会生效,可以添加虚拟机参数-verbose:class打印,我这里观察后发现也没问题
- 观察类加载器是否一致,于是我在agent和Controller里都将类加载器打印出来了,都是AppClasseLoader,所以也没问题
- 观察agent里springboot、spring的版本和需要代理的项目是否一致,我这边检查了,两者不一致,于是将agent里的版本调整成和项目一致后测试,发现还是不行;
至此,已经没有思路了,网上也没有任何有价值的方案,关键我这里感觉agent是被加载了,因为premain方法被执行了,但是发现项目使用的OpenTelemetry的agent能正常运行,万般无奈之下智能再看看opentelemetry-javaagent.jar的源码,对比发现,人家MANIFEST.MF文件里也配了Main-Class,并且没报红,于是我改了下自己的agent代码,在agent类里下了个main方法,里面就打印了下日志,没做任何逻辑操作,然后重新测试,于是神奇的事情发生了,我的拦截器生效了,然后我就愉快的关机下班了
本以为事情就这么愉快的解决了,谁知第二天一来,啥都没干,又失效了,让我怀疑自己是不是出现了错觉,但是昨晚打印的拦截日志可是历历在目啊,于是又束手无策了。。。后来实在没办法就想着改改bytebuddy的源码试试,我当前是1.12.0,我调成1.10,1.11都不行,最后调整1.13.0,然后我的拦截器就又生效了,这次是真的了,因为我已经测试一天了。。。
总结
如果你跟我一样premain方法生效了但是拦截器不生效,那么可以尝试跟我一样,配个main方法,改个bytebuddy的版本试试;
这次的事情让我明白了一个道理,就是你尝试接触一个未知的技术栈时,运气好可能应用起来很顺利很简单,但是你不懂原理的话一遇到稍微难点的问题就束手无策了,所以尽量不要图快直接上代码,最好还是先看下它实现的原理或者是运转流程