java agent+bytebuddy 作aop实现监控、性能检测、日志记录等

java agent

        jdk1.5以后引入的字节码插桩技术,可以在代码中加入切点,独立于目标程序,业务侵入性相比于普通的AOP编程要低,可以用作接口的性能检测,参数可性能监控等,常见的微服务链路跟踪的实现原理之一

        jdk1.5后新增了类java.lang.instrument.Instrumentation,它提供在运行时重新加载某个类的的class文件的api,部分源码如下:

public interface Instrumentation {
    void
    addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    
    void
    addTransformer(ClassFileTransformer transformer);

    boolean
    removeTransformer(ClassFileTransformer transformer);

    boolean
    isRetransformClassesSupported();

    void
    retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

        通过addTransformer可以加入一个转换器,转换器可以实现对类加载的事件进行拦截并返回转换后新的字节码,通过redefineClasses或retransformClasses都可以触发类的重新加载事件。通过这几个方法的组合,就可以实现不修改原来代码并使用切片的目的 

        使用方式主要分这么几步:1.新建切片程序实现切片逻辑;2.程序入口由main函数改为premain;3.加入自定义转换器;4.修改原先程序启动方式,加入-javaagent参数(更多参数可以参考java --help)


bytebuddy

        bytebuddy是一个可以在运行时动态生成java class的类库,无需编译器帮助,使用它的主要目的是它可以创建任意类,不限于实现用于创建运行时代理的接口,而且它提供了一种方便的API,可以使用java代理或在构建过程中手动更改类,算是功能强大而交互性好的类库,官网:https://bytebuddy.net,中文文档:https://notes.diguage.com/byte-buddy-tutorial

        下面通过例子实现用java agent监控springboot程序的controller并打印交互数据


示例

新建目标程序

        新建一个helloworld的springboot的web程序,由于程序简单,这里只贴出一些需要的代码

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @PostMapping("/say")
    public String say(@RequestParam("word") String word) {
        return "say: " + word;
    }
}

新建agent程序

        新建agent程序,定义程序入口的premain方法,在MF文件中定义premain方法

public static void premain(String agentArgs, Instrumentation inst) {}

        这个方法相当于普通Java程序的入口方法main,agentArgs相当于main方法里的 String[] args,但是不是数组状态,而是命令行怎么输入的,这里就怎么传递,需要手动处理,inst,就是刚刚说到的Instrumentation类,其由sun或openjdk来对其进行实现,所以具体实现可以不用管,只管应用接口即可,当然有兴趣的也可以打开源码了解一下

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.3.0</version>
        <configuration>
            <appendAssemblyId>false</appendAssemblyId>
            <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
            <archive>
                <manifestEntries>
                    <Premain-Class>org.example.Main</Premain-Class>
                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
        </configuration>
        <executions>
            <execution>
                <id>make-assembly</id>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

        这个pom.xml中的plugin配置,因为要打包,这里我用assembly直接maven打包,命令:

mvn clean package

        这样可以直接打包并加入引用的jar,自动生成配置好的MF文件,比较方便

public class Main {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.printf("premain, agentArgs: %s%n", agentArgs);
        System.out.printf("premain, inst: %s%n", inst);

        try {
            // 拦截spring controller
            AgentBuilder.Identified.Extendable builder1 = new AgentBuilder.Default()
                    // 拦截@Controller 和 @RestController的类
                    .type(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.stereotype.Controller")
                            .or(ElementMatchers.named("org.springframework.web.bind.annotation.RestController"))))
                    .transform((builder, typeDescription, classLoader, javaModule) ->
                            // 拦截 @RestMapping 或者 @Get/Post/Put/DeleteMapping
                            builder.method(ElementMatchers.isPublic().and(ElementMatchers.isAnnotatedWith(
                                    ElementMatchers.nameStartsWith("org.springframework.web.bind.annotation")
                                            .and(ElementMatchers.nameEndsWith("Mapping")))))
                                    // 拦截后交给 SpringControllerInterceptor 处理
                                    .intercept(MethodDelegation.to(SpringControllerInterceptor.class)));
            // 装载到 instrumentation 上
            builder1.installOn(inst);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

        这里用bytebuddy作了如下处理

        1.监听使用@Controller或@RestController的类

        2.注册自定义转换器对监听的类进行处理

        3.在自定义转换器中对使用了@org.springframework.web.bind.annotation.xxxMaping注解的共有方法进行拦截,交给类SpringControllerInterceptor进行处理

        4.将转换器装载到instrumentation上

@Slf4j
public class SpringControllerInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method,
                                 @AllArguments Object[] args,
                                 @SuperCall Callable<?> callable) {
        log.info("before controller: {}", method.getName());
        log.info("args: {}", Arrays.toString(args));
        long start = System.currentTimeMillis();
        try {
            Object res = callable.call();
            log.info("result: {}", res);
            return res;
        } catch(Exception e) {
            log.error("controller error: ", e);
        } finally {
            long end = System.currentTimeMillis();
            log.info("after controller execute in {} ms", end - start);
        }
        return null;
    }
}

        在 SpringControllerInterceptor中,定义如上的intercept方法,加入@net.bytebuddy.implementation.bind.annotation.RuntimeType注解,就可以进行切片操作了。“@Origin Method method”,用于注入目标方法的反射对象,“@AllArguments Object[] args”,用于注入目标方法执行时的参数列表,“@SuperCall Callable<?> callable”,暂时不了解其含义,个人理解为目标方法执行所在线程或者其控制器,使用callable.call()方式触发目标方法执行。这里把拦截的方法的方法名、参数列表、返回值、异常信息及执行耗时打印出来

执行

        先用 mvn assembly:single 将agent程序打包,然后执行目标程序,启动项如下:

nohup java -javaagent:/path/to/agent.jar spring-demo.jar > log.log &
nohup java -javaagent:./agent.jar -jar demo.jar > log.log &                        
[1] 69448

ll
total 56160
drwxr-xr-x   5 lixun04  wheel   160B  6 30 17:59 .
drwxrwxrwt  23 root     wheel   736B  6 30 17:59 ..
-rw-r--r--@  1 lixun04  wheel   9.3M  6 30 17:58 agent.jar
-rw-r--r--   1 lixun04  wheel    18M  6 30 17:53 demo.jar
-rw-r--r--   1 lixun04  wheel    84B  6 30 17:59 log.log

cat log.log 
premain, agentArgs: null
premain, inst: sun.instrument.InstrumentationImpl@2626b418

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.1)

2021-06-30 17:59:30.817 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - Starting SpringInstrumentDemoApplication v0.0.1-SNAPSHOT using Java 1.8.0_282 on lixundeMacBook-Pro.local with PID 69448 (/private/tmp/instrument/demo.jar started by lixun04 in /private/tmp/instrument)
2021-06-30 17:59:30.822 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - No active profile set, falling back to default profiles: default
2021-06-30 17:59:33.629 [main] [INFO] org.springframework.boot.web.embedded.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8080 (http)
2021-06-30 17:59:33.647 [main] [INFO] org.apache.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
2021-06-30 17:59:33.650 [main] [INFO] org.apache.catalina.core.StandardService - Starting service [Tomcat]
2021-06-30 17:59:33.650 [main] [INFO] org.apache.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.46]
2021-06-30 17:59:33.752 [main] [INFO] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2021-06-30 17:59:33.754 [main] [INFO] org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 2685 ms

 

        用postman访问localhost:8080/hello接口,观察日志输出:

tail -f log.log 
2021-06-30 17:59:35.093 [main] [INFO] org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
2021-06-30 17:59:35.131 [main] [INFO] org.springframework.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path ''
2021-06-30 17:59:35.157 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - Started SpringInstrumentDemoApplication in 5.386 seconds (JVM running for 6.636)
2021-06-30 18:01:49.866 [http-nio-8080-exec-2] [INFO] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-06-30 18:01:49.868 [http-nio-8080-exec-2] [INFO] org.springframework.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
2021-06-30 18:01:49.876 [http-nio-8080-exec-2] [INFO] org.springframework.web.servlet.DispatcherServlet - Completed initialization in 8 ms
2021-06-30 18:01:49.991 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - before controller: hello
2021-06-30 18:01:49.997 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - args: []
2021-06-30 18:01:49.997 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - result: hello
2021-06-30 18:01:49.998 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - after controller execute in 0 ms


总结

        java agent主要用于监控,所以一般来说,正常的写业务的人员很少用到,而jvm中自带的工具,如jmap,jstat,jstack等都是用javaagent方式监控,抓取jvm信息以供分析,但是我们也可以了解一下相关的内容,用于自己业务上定制的监控,如针对某些特定接口的性能监控,参数记录等,可以方便平时开发或者线上的问题排查,而使用bytebuddy则让我们更高效的处理字节码,相比于asm,javaassist,bytebuddy更强大高效


参考

https://zhuanlan.zhihu.com/p/151843984

https://www.cnblogs.com/xiaofuge/p/12868783.html

https://zhuanlan.zhihu.com/p/74255330

https://www.cnblogs.com/rickiyang/p/11368932.html

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
ByteBuddy一个可以在运行时动态生成Java类的类库,它可以创建任意类,不限于实现用于创建运行时代理的接口。使用ByteBuddy可以方便地使用Java代理或在构建过程中手动更改类。它提供了一个方便的API,可以用于实现AOP(面向切面编程)功能。在使用ByteBuddy实现AOP时,可以通过定义注解和拦截器来对目标方法进行增强。首先,需要定义一个注解,比如`@Log`,然后在目标类的方法上添加该注解。接下来,使用ByteBuddy对目标类进行字节码增强,通过拦截器对被注解的方法进行增强逻辑的定义。最后,通过类加载器加载增强后的类,并创建实例进行调用。这样就实现了基于ByteBuddyAOP功能。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [java agent+bytebuddy aop实现监控性能检测日志记录等](https://blog.csdn.net/qq_17589253/article/details/118364827)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [ByteBuddy快速实现AOP](https://blog.csdn.net/qq_44787816/article/details/127271772)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值