手写SkyWalking-Agent模块(初始化、MySQL、MVC插件编写)
  hzIe5RkCbU7L 2023年12月15日 75 0

手写SkyWalking-Agent模块(初始化、MySQL、MVC插件编写)

使用 ByteBuddy类库 和 JavaAgent机制 实现 类似 SkyWalking-Agent 模式下的无侵入式的动态可插拔功能


准备工作

新建一个springboot程序,带有一个查询数据库的接口

由于程序简单,这里只贴出一些需要的代码

@RestController
@RequestMapping("/userInfo")
public class UserInfoController {
    @Resource
    private UserInfoService userInfoService;

    @GetMapping("/list")
    public List<UserInfoDO> selectUserList(String userName) {
        return userInfoService.selectUserList(userName);
    }

}

SQL

create database app CHARACTER SET utf8mb4;
use app;

drop table if exists user_info;
CREATE TABLE `user_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(50) NOT NULL COMMENT '用户名',
  `pwd` varchar(50) NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 comment '管理员表';
insert into user_info(user_name,pwd) values ('user1','123456');
insert into user_info(user_name,pwd) values ('user2','123456');
insert into user_info(user_name,pwd) values ('user3','123456');
insert into user_info(user_name,pwd) values ('user4','123456');

运行程序访问接口成功,并打印日志:

image-20230908194550553

image-20230908194539244


Agent 模块初始状态

一、SpringMVC-agent编写(实现接口访问耗时统计)

编写流程梳理:

这里用bytebuddy作了如下处理

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

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

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

4.将转换器装载到instrumentation上

1. pom依赖 及 打包插件


    <dependencies>
        <!--byte-buddy-->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.12.10</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.29</version>
        </dependency>
    </dependencies>

    <!--Agent 打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.5</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <!--Premain 入口类地址-->
                            <Premain-Class>com.roadjava.skywalking.agent.demo.standalone.plugins.springmvc.AgentDemo</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <!--jar包的名称-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>one_jar</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

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

mvn clean package

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


2. permain 入口类

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

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

代码:

@Slf4j
public class AgentDemo {
    // @Controller注解的路径
    private static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";

    // @RestController注解的路径
    private static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";

    public static void premain(String args, Instrumentation instrumentation){
        log.info("进入到premain,args:{}",args);
        // 构建 AgentBuilder
        AgentBuilder builder = new AgentBuilder.Default()
                // type() 需要拦截的类 的规则
           .type(isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME))))
                // 拦截完后 执行的转换器
                .transform(new AgentTransformer());
        builder.installOn(instrumentation);
    }

}

3. MVCAgentTransformer 转化器编写

  1. 实现 AgentBuilder.Transformer 接口,重写transform()方法

  2. 编写 builder.method() 拦截方法的规则逻辑

  3. 拦截成功后,传递给mvc的拦截器进行增强

代码:

@Slf4j
public class AgentTransformer  implements AgentBuilder.Transformer{

    private static final String MAPPING_PKG_PREFIX = "org.springframework.web.bind.annotation";
    private static final String MAPPING_SUFFIX = "Mapping";
    
    /**
     *  当被type(ELementMatcher<.?super TypeDescription>typeMatcher)匹配后会进入到此方法
     * @param typeDescription 表示要被加载的类的信息
     * @return
     */
    @Override
    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                            TypeDescription typeDescription,
                                            ClassLoader classLoader, // 加载 typeDescription这个类的类加载器
                                            JavaModule javaModule) {
        log.info("actualName to transform:{}",typeDescription.getActualName());
        // 拦截方法
        // 规则: 不是静态方法 并且 被 @xxxMapping注解修饰的方法
        return  builder.method(
                        not(isStatic()).and(isAnnotatedWith(
                                nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
                        )))
                // 拦截到方法后,传递给 SpringMvcInterceptor拦截器 做 逻辑增强
                .intercept(MethodDelegation.to(new SpringMvcInterceptor()));
    }
}

4. McvInterceptor拦截器 字节码方法增强逻辑

​ 在 SpringControllerInterceptor中,定义如上的intercept方法,加入@RuntimeType注解,就可以进行切片操作了

注解解释:

@This Object targetObj,增强的目标类

@Origin Method method,目标方法对象

@AllArguments Object[] args,用于注入目标方法执行时的参数列表,

@SuperCall Callable<?> callable,个人理解为目标方法执行所在线程或者其控制器,使用callable.call()方式触发目标方法执行。这里把拦截的方法的方法名、参数列表、返回值、异常信息及执行耗时打印出来

代码:

@Slf4j
public class SpringMvcInterceptor {
	@RuntimeType
    public Object interceptor(
            @This Object targetObj, // 增强的目标类
            @Origin Method targetMethod, // 目标方法信息
            @AllArguments Object[] targetMethodArgs, // 方法信息
            @SuperCall Callable<?> zuper // 执行方法调用
    ) {
        log.info("before controller exec,methodName:{},args:{}", targetMethod.getName(), Arrays.toString(targetMethodArgs));
        long start = System.currentTimeMillis();

        Object call = null;
        try {
            call = zuper.call();
            log.info("after controller exec,result:{}", call);
        } catch (Exception e) {
            log.error("controller exec error", e);
        } finally {
            long end = System.currentTimeMillis();
            log.info("finally controller exec in {} ms", end - start);
        }

        return call;
    }
}


常用注解含义

  • @RuntimeType 注解

    告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。

  • @This 注解:

    注入被拦截的目标对象。

  • @AllArguments 注解

    注入目标方法的全部参数,是不是感觉与 Java 反射的那套 API 有点类似了?

  • @Origin 注解

    注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。

  • @Super 注解

    注入目标对象。通过该对象可以调用目标对象的所有方法。

  • @SuperCall

    这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,

    @SuperCall与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。

    另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。


5. package 打包

注意修改 pom 文件中 <Premain-Class> 路径

将插件打包

image-20230908204557274

选择这个带依赖的Jar包,进行Agent的加载

image-20230908204837431

拷贝Jar包的绝对路径:

D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar

6. 修改启动VM参数 -javaagent:Jar包路径,启动程序

-javaagent:D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar

image-20230908205610522


7. 启动成功,字节码植入,通过Arthas反编译

启动成功截图:

image-20230908214103387

Arthas 反编译查看代码

image-20230908214400339


二、MySQL -agent (统计DDL的耗时)

和上面步骤相似,不做过多书写,只放核心逻辑


1. premain 入口类

阅读mysql.jdbc的源码,找到我们需要拦截的 运行DDL语句的类

  1. com.mysql.cj.jdbc.ClientPreparedStatement
  2. com.mysql.cj.jdbc.ServerPreparedStatement
@Slf4j
public class AgentDemo {
    private static final String CLIENT_PS_NAME = "com.mysql.cj.jdbc.ClientPreparedStatement";
    private static final String SERVER_PS_NAME = "com.mysql.cj.jdbc.ServerPreparedStatement";
    public static void premain(String args, Instrumentation instrumentation) {
        log.info("进入到premain,args:{}",args);
        AgentBuilder builder = new AgentBuilder.Default()
                .type(named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)))
                .transform(new AgentTransfAormer());
        builder.installOn(instrumentation);
    }
}

2. MySQLAgentTransfAormer 编写

拦截 方法名是 execute,executeUpdate,executeQuery 的实现

@Slf4j
public class AgentTransformer implements AgentBuilder.Transformer {

    /**
     * @param typeDescription 表示要被加载的类的信息
     */
    @Override
    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                            TypeDescription typeDescription,
                                            ClassLoader classLoader, // 加载 typeDescription这个类的类加载器
                                            JavaModule module) {
        log.info("actualName to transform:{}", typeDescription.getActualName());
        DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
                .method(
                        named("execute")
                                .or(named("executeUpdate"))
                                .or(named("executeQuery"))
                ).intercept(MethodDelegation.to(new MysqlInterceptor()));
        return intercept;
    }
}

3. MysqlLnterceptor 拦截器 字节码方法增强

统计 运行耗时,打印在控制台

@Slf4j
public class MysqlInterceptor {

    @RuntimeType
    public Object intercept(
            @This Object targetObj,
            @Origin Method targetMethod,
            @AllArguments Object[] targetMethodArgs,
            @SuperCall Callable<?> zuper
            ) {
        log.info("before mysql exec,methodName:{},args:{}",targetMethod.getName(),Arrays.toString(targetMethodArgs));
        long start = System.currentTimeMillis();
        Object call = null;
        try {
            call = zuper.call();
            log.info("after mysql exec,result:{}",call);
        }catch (Exception e) {
            log.error("mysql exec error",e);
        }finally {
            long end = System.currentTimeMillis();
            log.info("finally mysql exec in {} ms",end - start);
        }
        return call;
    }
}

4. 启动成功,运行结果

打包,-javaagent:路径运行

目前指令需要挂载多个agent,接下来需要去优化,类似SkyWalking只需要一个javaagent:就可以挂载需要的plugin

-javaagent:D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar -javaagent:D:\Agent-SkyWalking\standlone-plugins\mysql-standalone-plugin\target\mysql-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar*

image-20230909151321410


总结

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


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月15日 0

暂无评论

hzIe5RkCbU7L