记一次基于bytebuddy创建agent拦截不生效的坑


前言

最近需要对存量的项目进行批量日志打印操作,要求对代码零侵入,显然AOP是行不通了,于是考虑使用agent来实现对方法的拦截,但是一顿操作下来,神奇的事情发生了,我自定义的agent里的premain方法被调用了,但是定义的拦截器没有生效,即使我拦截规则用的是any(),即拦截所有请求,也不会生效,更尴尬的是,我一时找不到原因,于是就新建了一个新的springboot项目,而这个新项目对agent有效,最后通过配置main方法,修改bytebuddy的版本解决了,今天用这篇文章主要记录下排查过程,以便给跟我出现同样问题的小伙伴一个参考;因为我这个问题网上都搜烂了,也没找到解决方案;

一、自定义agent

  1. 新建一个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>
  1. 定义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====== ");
   }
  1. 定义拦截器,这里只展示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);
       }
   }

}
  1. 代码至此算是结束了,接下来就是在你需要植入的项目启动时新增命令行参数(jar包路径记得修改):
-javaagent:D:\workspace\dll-logagent\target\jzx-logagent-1.0-SNAPSHOT.jar

至此,如果不出意外的话其实功能就是可用的了,只是我这次开发并不顺利,我的拦截器没生效

二、问题排查

  1. 先将jar包加到项目里,通过idea看下编译后的源码,特别需要关注META-INF下的MANIFEST.MF文件内容,观察里面的Premain-Class和Main-Class是否是你pom里面配置的值,我检查后发现一致,但是Main-Class标红,百度发现只要指定了Premain-Class那么Main-Class用不上,所以当时没有在意
  2. 观察类加载顺序,我们自定义的agent是否在Controller之前被加载了,如果agent后加载那么肯定不会生效,可以添加虚拟机参数-verbose:class打印,我这里观察后发现也没问题
  3. 观察类加载器是否一致,于是我在agent和Controller里都将类加载器打印出来了,都是AppClasseLoader,所以也没问题
  4. 观察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的版本试试;
这次的事情让我明白了一个道理,就是你尝试接触一个未知的技术栈时,运气好可能应用起来很顺利很简单,但是你不懂原理的话一遇到稍微难点的问题就束手无策了,所以尽量不要图快直接上代码,最好还是先看下它实现的原理或者是运转流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值