一步一步写APM(一)--ByteBuddy初步应用及初步搭建

准备

前面我们已经通过相关知识知道一般APM项目都是通过javaagent来对原项目代码进行一些改造以实现调用链需要的相关操作-- 比如加入上下文,添加必要的调用监控数据。 我们的主要开发工具就是ByteBuddy

注意,本系列相关文章涉及的源码放在了github上,有些代码不会贴出,请查看源码。地址:https://github.com/YounianC/APMSample 整个文章由于不停修改代码,为避免不知道当前文章所处代码阶段,一般会注明阶段Tag:XXX

首先实现javaagent,在main方法之前执行。

  1. 创建一个maven项目
  2. 创建类Agent作为javaagent入口类
public class Agent {
    public static void premain(String agentArgs, Instrumentation instrumentation){
        System.out.println("Enter premain.....");
    }
}
  1. 并在POM 添加manifest信息
<build>
        <finalName>APMAgent</finalName>
        <plugins>
            <plugin>
                <!-- https://maven.apache.org/plugins/maven-shade-plugin/shade-mojo.html -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <shadedArtifactAttached>false</shadedArtifactAttached>
                            <createDependencyReducedPom>true</createDependencyReducedPom>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>${premain.class}</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  1. mvn构建一下,到target目录查看有APMAgent.jar即可
  2. 随便写一个程序,只有一个main方法都行,然后在启动参数VMoptions中加上

-javaagent:path\APMSample\target\APMAgent.jar image.png

  1. 运行发现控制台打印出了Enter premain.....即可。

到这里我们已经使用javaagent能够实现在main方法之前执行一些操作,但是到目前为止没有任何的功能。那么接下来使用ByteBuddy来进行一些更多的操作。

添加ByteBuddy进行高级操作,阶段Tag初步实现ByteBuddy拦截类

  1. 在pom.xml文件中添加dependency
<dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.5.7</version>
</dependency>
  1. premain中添加以下代码:
new AgentBuilder.Default().type(nameStartsWith("younian.apmsample.agent.test")).transform(new AgentBuilder.Transformer() {
            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader) {
                System.out.println("transform...");
                return builder;
            }
        }).with(new AgentBuilder.Listener() {
            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module,
                                         DynamicType dynamicType) {
            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
            }

            @Override
            public void onError(String typeName, ClassLoader classLoader, JavaModule module, Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void onComplete(String typeName, ClassLoader classLoader, JavaModule module) {
            }
        }).installOn(instrumentation);

这个操作的目的就是用nameStartsWith("younian.apmsample.agent.test")限定的类在加载时将经过ByteBuddy的一个transform操作,具体操作是public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader)的内容,现在重新package以下agent,然后执行新加的APMAgentTest中的Test类的main方法(注意依然需要加上javaagent参数)。 可以看到输出:

Enter premain.....
transform...
main.....

现在已经实现能够通过ByteBuddy拦截指定类,这里拦截类的过滤方法是使用的nameStartWith,其实在net.bytebuddy.matcher.ElementMatchers类下包括很多的其他限定,几种常用的限定包括:

  • named(ClassName) 精确限定某个类
  • nameStartsWith、nameEndsWith 限定一系列类
  • isStatic 等限定
  • annotationType 通过注解限定(比如被@controller 标注的类)
  • takesArgument(s) 通过参数个数和类型限定
  • is、or、not等逻辑判断限定
  • 等其他

现在只是在类加载时有了操作,但是transform除了一行打印没有别的操作,接下来我们进一步扩展,开始改造拦截的类的方法,阶段Tag改造拦截的类的方法

  1. 添加transform方法:
 ClassInstanceMethodInterceptor methodInterceptor = new ClassInstanceMethodInterceptor();
                ElementMatcher matcher = named("test1").or(named("test2")).or(named("testStatic"));
                builder = builder.method(not(isStatic()).and(matcher)).intercept(MethodDelegation.to(methodInterceptor));

这里的主要含义是:将不是静态方法的且(函数名为test1、test2)的函数拦截,并且委托给一个ClassInstanceMethodInterceptor 实例执行。 2. 添加ClassInstanceMethodInterceptor类:

public class ClassInstanceMethodInterceptor {
    @RuntimeType
    public Object intercept(@This Object obj, @AllArguments Object[] allArguments, @Origin Method method,
                            @SuperCall Callable<?> zuper) throws Throwable {

        System.out.println("intercept " + method.getName());
        Object result = null;
        try {
            result = zuper.call();
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return result;

    }
}

这里的intercept方法就是委托之后的具体调用方法。调用时将不会再直接调用方法,而是调用上面的委托方法。zuper.call()是指调用被拦截函数病返回。这个拦截方法只是在函数执行之前加了一行打印当前拦截的方法名 3. 修改Test类方法:

public class Test {

    public static void main(String[] args) {
        Test.testStatic();
        Test test = new Test();
        test.test1();
        test.test2();
        test.test3();
    }

    public void test1(){
        System.out.println("test1");
    }

    public void test2(){
        System.out.println("test2");
    }

    public void test3(){
        System.out.println("test3");
    }

    public static void testStatic(){
        System.out.println("testStatic");
    }
}
  1. 执行main方法(依然需要javaagent)
Enter premain.....
transform...
testStatic
intercept test1
test1
intercept test2
test2
test3

根据打印可以发现,只有test1test2被拦截了,原因是test3没加入match且testStatic方法是静态方法而我们限制了not(isStatic()

结语

其实到这里,基本的雏形已经出来了,具备了拦截具体方法并且加入自定义操作的能力; 试想一下:

  1. 如果能够拦截一个tomcat的类,这个类是分发所有用户请求的类
  2. 我们在intercept方法中加入一些信息,比如:请求时间、请求内容、请求IP、本服务器IP等。并且将这些信息发送给某个服务器进行统计汇总。 那么我们是不是就有了监控tomcat请求的能力?

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×