从代理到修改字节码,来看看实现AOP的N种姿势

1,364 阅读17分钟

本文作为"姿势要多"系列的第二篇, 看来一下实现AOP的N种姿势.

在开始之前, 我们来设置一个最简单的AOP场景: 假设现在有以下一个"搜索"的接口, 接口需要根据传进来的参数,返回这个key检索到的内容;

接口的定义如下:

/**
 * @author lhh
 */
public interface Search {
    String search(String key);
}

该接口有个简单的实现SimpleSearch.为表示它足够简单, 我们这个直接返回hashCode~


import com.north.lat.proxylat.service.Search;
public class SimpleSearch implements Search {
    @Override
    public String search(String key) {
        return  key.hashCode() + "";
    }
}

好, 现在的要求是, 不改动Search和SimpleSearch的情况下, 请打印出search()方法的执行时间.

解决办法肯定就是AOP啦, 在方法执行的前后分别记下当前时间, 然后两个时间一相减即可.

静态代理模式

要说实现AOP, 第一个想到的办法就是代理模式了.

在代理模式里面, SimpleSearch接口就叫"委托类", 而我们马上要写的SearchProxy就叫"代理类" .

静态代理模式一般的套路是:

  1. 代理类和委托类实现同样的接口. 即Search接口
  2. 代理类要持有委托类的对象. 即SearchProxy类有个SimpleSearch类型的成员
  3. 代理类要执行父类的方法时, 实际上是调用委托类的方法.

知悉了以上套路后, 一个静态代理类就这样写出来了:

import com.north.lat.proxylat.service.Search;

public class SearchProxy implements Search {
    private Search search;
    public SearchProxy(Search search){
        this.search = search;
    }

    @Override
    public String search(String key) {
        long bt = System.currentTimeMillis();
        String ret = search.search(key);
        long et = System.currentTimeMillis();
        System.out.println("SearchProxy.search cost : " + (et - bt));
        return ret;
    }
}

在SearchProxy类中, 真正调用Search.search()的前后, 都记录了当前时间, 然后两个方法一减, 就是整个search方法的执行时间了.

测试代码如下:


import com.north.lat.proxylat.service.impl.SimpleSearch;
import com.north.lat.proxylat.service.impl.proxy.SearchProxy;

public class SearchSomething {
    public static void main(String[] args) {
        SimpleSearch search = new SimpleSearch();
        String key = "keyWord";
        // 代理模式
        SearchProxy searchProxy = new SearchProxy(search);
        System.out.println("searchProxy search : " + searchProxy.search(key));
    }
}

动态代理

既然有静态代理, 那肯定也存在对应的动态代理了. 所谓静态和动态指的是什么呢, 其实就是指"代理类"

在静态代理里面, 代理类是写死的, 在编译时期就已经知道了代理类是谁.

反过来看, 如果使用的是静态代理的话, 每一个"委托类"都要为它写一个代理类. 那如果有一万个委托类, 岂不是要写一万个代理类.那还不如直接在委托类修改算了.

这就是动态代理存在的意义了.

在动态代理里面,虽然实际上还是静态代理那一套路, 但是代理类已经不需要我们人工去写了, 我们只需要为代理类指定委托类是谁, 以及代理的具体动作即可.

在java里面, 动态代理也分JDK动态代理和CGLIG两种, 这两种代理有什么区别? 来看一下例子就知道.

JDK动态代理

JDK的动态代理的核心就两个类, 一个是Proxy类, 用来动态生成代理类, 另外一个是InvocationHandler类, 用于表示代理的具体动作.

首先来实现一个InvocationHandler类, 来实现代理的动作, 在这里就是打印响应时间:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ProxyInvocationHandler implements InvocationHandler{

    /**
     * target  一般指委托类实例
     */
    private Object target;
    public ProxyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long bt = System.currentTimeMillis();
        // 通过调用target的method
        String ret = (String) method.invoke(target, args);
        long et = System.currentTimeMillis();
        System.out.println("SearchJdkProxy.search  cost : " + (et - bt));
        return ret;
    }
}

整个ProxyInvocationHandler类非常简洁,只有几行代码. 而且值得注意的是, 由于target是Object类型的, 所以理论上委托类可以是任何类, 这个handler都可以打印它每个方法的执行时间.这就是动态代理的好处之一

再来看看Proxy类怎么生成的

        // proxy的handler
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(search);

        // 动态生成代理类, 需要三个参数, 1, classloader, 2, 委托类的class 3. InvocationHandler实例
        Search searchProxy = (Search) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{Search.class},
                proxyInvocationHandler);

直接调用Proxy的静态方法newProxyInstance就可以生成一个代理类, 其中需要三个参数:

1. classloader,  用于加载生成的代理类
2. class [], 指定委托类的class
3. invocationHandler, 即是上面的ProxyInvocationHandler实例

通过上面3个参数就可以大概猜到了Proxy类里面的逻辑了:

1. 使用class生成代理类, 使用classloader加载
2. 当调用代理类的任何方法时, 最终都是调用invocationHandler的invoke()方法

下面来证实一下我们的猜测.

先在main方法中调用一下代理类的search方法:



import com.north.lat.proxylat.service.Search;
import com.north.lat.proxylat.service.impl.SimpleSearch;
import com.north.lat.proxylat.service.impl.jdkproxy.ProxyInvocationHandler;

import java.lang.reflect.Proxy;

public class SearchSomething {
    public static void main(String[] args) {
        // 指定这个配置可以让系统输出动态生成的jdkproxy的class文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        // 委托类
        SimpleSearch search = new SimpleSearch();
        String key = "keyWord";

        // 生成proxy的handler
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(search);

        // 动态生成代理类, 需要三个参数, 1, classloader, 2, 委托类的class 3. InvocationHandler实例
        Search searchProxy = (Search) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{Search.class},
                proxyInvocationHandler);
        // 使用代理类执行search
        String result = searchProxy.search(key);
        System.out.println("jdkProxy search : " + result);
    }
}

其中这行:

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

会让JVM在当前项目输出生成的动态代理的class文件(一般类名都是$Proxyxxxx).如下面, 我们反编译后, 可以看到它长这样(去掉了一些继承Object的方法):

package com.sun.proxy;

import com.north.lat.proxylat.service.Search;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Search {
    private static Method m1;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final String search(String var1) throws  {
        try {
            return (String)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }


    static {
            m1 = Class.forName("com.north.lat.proxylat.service.Search").getMethod("search", Class.forName("java.lang.String"));
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到跟我们猜想的差不多:

1. 实现了class(委托类)指定的接口
2. 通过反射获取class的method
3. 重载委托类的方法, 通过调用invocationhandler来执行

其实乍一看, 是不是还是跟我们的静态代理差不多意思?

Cglib动态代理

上面的例子可以看到, JDK动态代理的实现其实是跟静态代理差不多的, 也是实现了委托类的接口. 这就涉及到了一个问题, 如果委托类不是一个接口(interface), 而是一个具体的实现类, 那怎么办?

例如说, 我在SimpleSearch新增了一个prettySearch()方法, 用于优化search()的返回值, 如下:

import com.north.lat.proxylat.service.Search;


public class SimpleSearch implements Search {

    @Override
    public String search(String key) {
        return  key.hashCode() + "";
    }

    public String prettySearch(String key){
       String result = search(key);
       // 优化一下返回结果
       result = result.replace("-","");
       return result;
    }
}

那么如果现在我想通过AOP打印prettySearch方法的执行时间,要怎么做呢?

可以肯定的是, 使用jdk动态代理是不行,在Proxy.newProxyInstance方法里面, 如果第二个参数传的是一个非interface的class参数的话, 会抛出以下异常:

Exception in thread "main" java.lang.IllegalArgumentException: com.north.lat.proxylat.service.impl.SimpleSearch is not an interface

那怎么办呢?

cglib动态代理刚好可以弥补这个不足.

跟jdk动态代理一样, cglib也涉及到两个核心类:

1. MethodInterceptor, 和InvocationHandler一样, 用于定义代理的具体动作
2. Enhancer, 这个就跟Proxy类一样作用了, 用于生成代理类

先了实现MethodInterceptor

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {
    private Object target;

    public CglibProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long bt = System.currentTimeMillis();
        String ret = (String) method.invoke(target, objects);
        long et = System.currentTimeMillis();
        System.out.println("CglibProxy." + method.getName() + "  cost : " + (et - bt));
        return ret;
    }
}

逻辑基本跟InvocationHandler一样, 没什么可说的.

再来看看Enhancer怎么使用:

        Enhancer enhancer = new Enhancer();
        // 指定委托类的class
        enhancer.setSuperclass(SimpleSearch.class);
        // 指定回调类, 即MethodInterceptor实例
        enhancer.setCallback(new CglibProxy(search));
        // 生成代理类
        SimpleSearch simpleSearchProxy =  (SimpleSearch) enhancer.create();

其实也是跟Proxy大同小异, 只是类名和API变了而已.

最后看一下Cglib生成的代理类.

同样, 在执行main方法的第一行加上:

  System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");

可以输出生成的代理的class文件

完整的main方法如下:

import com.north.lat.proxylat.service.impl.SimpleSearch;
import com.north.lat.proxylat.service.impl.cglib.CglibProxy;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;

public class SearchSomething {
    public static void main(String[] args) {
        // 指定代理类的calss的生成位置, . 代表当前目录
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");

        SimpleSearch search = new SimpleSearch();
        String key = "keyWord";

        Enhancer enhancer = new Enhancer();
        // 指定委托类的class
        enhancer.setSuperclass(SimpleSearch.class);
        // 指定回调类, 即MethodInterceptor实例
        enhancer.setCallback(new CglibProxy(search));
        // 生成代理类
        SimpleSearch simpleSearchProxy =  (SimpleSearch) enhancer.create();

        simpleSearchProxy.search(key);
    }
}

cglib生成的动态代理类内容比较多,看起来比较费劲 我们只关注的类声明就好, 例如下面是一个cglib生成的动态代理类:

public class SimpleSearch$$EnhancerByCGLIB$$abc27e61 extends SimpleSearch implements Factory {
    // 这里面一大堆逻辑
}

可以看到, 跟JDK动态代理不一样的是,cglib是通过继承(extends)了委托类来实现代理的, 而jdk动态代理是通过实现接口. 这就可以解释了, 为什么cglib不要求委托类是接口了.

写到这里, 我突然想起我们平时如果想增强(或修改)一个类的功能, 而又无法修改到那个类(没有源码)时, 唯二的手段也就是继承或复制粘贴~~

aspectj

虽然动态代理已经很强大了. 但是就实现AOP而言, 还是存在一些不足:

1. 动态代理都是基于反射, java的反射是存在一定的性能损耗的
2. 生成代理类时, 实际上都是要先new一个委托类的对象(new SimpleSearch()). 用起来不够方便, 还略显累赘

那还有没有更好的AOP的方案呢? 那肯定是有的. 例如aspectj.

第一,aspectj是编译期间来做AOP的(直接修改了委托类的代码), 没有反射的性能损耗.

第二, 由于第一点, aspectj也是不需要new一个委托类对象的.

所以它完美解决动态代理存在的问题.

使用aspectj也非常简单(其实也不简单). 首先, 你得引入aspectj的依赖:

    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
   <dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjweaver</artifactId>
       <version>1.9.4</version>
   </dependency>
   <!-- https://mvnrepository.com/artifact/aspectj/aspectjrt -->
   <dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjrt</artifactId>
       <version>1.8.9</version>
   </dependency>

接着, 正如上面提到的, aspectj是编译期间对我们的代码进行修改的. 所以我们还得依赖一下aspectj的编译器, 需要通过aspectj的编译器对我们的代码进行修改.

不过幸好这个已经有专门的工具了, 我们直接引入一个aspectj的maven插件即可:

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.10</version>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                    <source>1.8</source>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <!-- <goal>test-compile</goal>-->
                        </goals>
                    </execution>
                </executions>
            </plugin>

再接着, 我们要告诉aspectj哪些方法需要打印时间. 我们这里的方法是定义一个@NeedMonitor注解, 如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedMonitor {
}

然后通过@Aspect注解的配置类告诉aspectj编译器, 添加了@NeedMonitor注解的方法都要打印执行时间:

@Aspect
public class CoreAspetJ {

    /**
     *  只有标注了@NeedMonitor 注解的方法才需要打印执行时间
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* *(..)) &&  @annotation(com.north.lat.proxylat.service.impl.aspectj.NeedMonitor)")
    public Object printCost(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long bt = System.currentTimeMillis();
        Object object = proceedingJoinPoint.proceed();
        long et = System.currentTimeMillis();
        System.out.println(proceedingJoinPoint.getSignature() + " cost: " + (et - bt));
        return object;
    }
}

最后. 在SimpleSearch的两个方法上都加上@NeedMonitor注解:

import com.north.lat.proxylat.service.Search;
import com.north.lat.proxylat.service.impl.aspectj.NeedMonitor;

public class SimpleSearch implements Search {

    @NeedMonitor
    @Override
    public String search(String key) {
        return  key.hashCode() + "";
    }

    @NeedMonitor
    public String prettySearch(String key){
       String result = search(key);
       // 优化一下返回结果
       result = result.replace("-","");
       return result;
    }
}

在main方法中执行:

import com.north.lat.proxylat.service.impl.SimpleSearch;

public class SearchSomething {
    public static void main(String[] args) {
        SimpleSearch search = new SimpleSearch();
        String key = "keyWord";
        search.prettySearch(key);
    }
}

整个main方法没有出现任何proxy类, 但是最终console还是打印出了每个方法的执行时间. 简直完美.

jvm-sandbox

从上面的例子来看, aspectj也已经很完美了. 可以在完全不用改动原有类的情况下, 就实现了AOP的功能.

然而它还是存在一定的问题:

1. 它在编译期间修改了我们的代码.把上面的SimpleSearch.class 反编译出来之后. 你会发现多了一大坨不是我们写的代码, 还看不懂是啥意思, 你说烦人不烦人.

2. aspectj的方式还是需要我们去修改(整个项目的)代码. 假设你的老板要求你要在不能重新发版,
也不能重启JVM的情况下实现AOP的功能, 那你怎么办呢?

如何在不修改任何代码, 不重启应用的情况下实现AOP?

办法肯定是有的, 只要钱到位, 啊不, 只要各位大佬的点赞点到位, 就没有什么实现不了的.

为了实现这个功能, 这里要祭出一个神器: jvm-sandbox.

jvm-sandbox是阿里开源的"一种JVM的非侵入式运行期AOP解决方案". 说人话呢, 就是jvm-sandbox是一款非侵入式的JVM工具, 它可以在不重启JVM应用的情况下, 实现AOP(增强字节码), JVM信息监控等等.

jvm-sandbox可以做的事情很多, 用来实现我们这么简单的AOP功能, 有点用屠龙宝刀来杀鸡的意味. 不过没关系, 我们就是这么任性.

安装jvm-sandbox

要使用jvm-sandbox, 首先是要安装.安装也非常简单:

# 下载最新版本的JVM-SANDBOX
wget http://ompc.oss-cn-hangzhou.aliyuncs.com/jvm-sandbox/release/sandbox-stable-bin.zip

# 解压
unzip sandbox-stable-bin.zip

在linux或Mac下, 以上两步就可以成功安装好jvm-sandbox.

编写module

jvm-sandbox是基于模块(module)进行管理的. 在每个module里面, 我们可以定义针对哪些类(class)的哪些行为(方法)进行监控, 进而实现AOP.

说起来比较抽象, 我们来直接实现我们的需求.针对"com.north.lat.proxylat.service.impl.SimpleSearch"类的"search"和""prettySearch"两个方法进行AOP, 打印出执行时间.

编写的PrintCostModule如下:

import com.alibaba.jvm.sandbox.api.Information;
import com.alibaba.jvm.sandbox.api.Module;
import com.alibaba.jvm.sandbox.api.annotation.Command;
import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;
import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;
import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;
import org.kohsuke.MetaInfServices;

import javax.annotation.Resource;


@MetaInfServices(Module.class)
@Information(id = "print-cost-time")
public class PrintCostModule implements Module {
    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Command("printCostTime")
    public void printCostTime() {

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.north.lat.proxylat.service.impl.SimpleSearch")
                .onBehavior("prettySearch")
                .onBehavior("search")
                .onWatch(new AdviceListener() {

                    @Override
                    protected void before(Advice advice) throws Throwable {
                        super.before(advice);
                        // 先把当前的时间时间, 和方法名放到attach暂存着. 在after方法中再取出来
                        advice.attach(System.currentTimeMillis(), advice.getBehavior().getName());
                    }

                    @Override
                    protected void after(Advice advice) throws Throwable {
                        super.after(advice);
                        try{
                            String method = advice.getBehavior().getName();
                            // 如果这个method执行过before方法, 那么这里肯定为true
                            if(advice.hasMark(method)){
                                Object attachment = advice.attachment();
                                // attachment即为执行before方法的系统时间
                                Long bt = Long.parseLong(attachment.toString());
                                System.out.println(method +" cost: " + (System.currentTimeMillis()- bt));
                            }
                        }catch (Throwable e){
                             e.printStackTrace();
                             throw e;
                        }


                    }
                });

    }
}

其中:

     .onClass("com.north.lat.proxylat.service.impl.SimpleSearch")
                .onBehavior("prettySearch")
                .onBehavior("search")

几行代码就是指定了监控那些类和方法.

另外, 还要注意:

@Information(id = "print-cost-time")和  @Command("printCostTime")

这个注解, 指定了这个module的id和执行aop指令的command. 这两个等下都会用到

当然, 等下module是以jar包的形式添加到sandbox中去到. 所以只有一个PrintCostModule是不行的, 还需要把这个PrintCostModule类打包成一个jar包. 另外PrintCostModule也是需要依赖jvm-sandbox的一些类的, 因此我们新建一个项目, 结构如下图所示:

image

其实整个项目就两个文件: 一个PrintCostModule和一个pom文件; 不过pom文件也比较重用, 它里面直接引入了sandbox-module-starter做parent, 这样就可以使用jvm-sandbox的一些类了.

<?xml version="1.0" encoding="UTF-8"?>
<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">
    <parent>
        <groupId>com.alibaba.jvm.sandbox</groupId>
        <artifactId>sandbox-module-starter</artifactId>
        <version>1.2.2</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>proxy-lat-sandbox-module</artifactId>


</project>

安装module

  1. 把上面的module项目打包成proxy-lat-sandbox-module-1.2.2.jar
  2. 把proxy-lat-sandbox-module-1.2.2.jar放到了{sanbox_home}/sandbox-module目录下.其中{sanbox_home}是jvm-sandbox的安装目录

见证奇迹的时候

为了看出对比效果, 我们先将SearchSomething类改成功无限循环的模式, 这样可以看到, 没有任何AOP的情况下, 它不会打印执行时间, 代码如下:



import com.north.lat.proxylat.service.impl.SimpleSearch;

public class SearchSomething {
    public static void main(String[] args) throws InterruptedException {
        SimpleSearch search = new SimpleSearch();
        while (true){
            System.out.println("====begin===");
            String result = search.prettySearch("123");
            System.out.println("search result = [" + result + "]");
            System.out.println("====end===");
            Thread.sleep(3000);
        }

    }
}

我们没有使用jvm-sandbox做AOP之前, 它的控制台输出是这样的:

====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===

接着我们找到SearchSomething运行的进程ID, 这里是 52863

有了进程ID后, 我们使用jvm-sandbox对这个JVM进行增强, 命令如下:

sh bin/sandbox.sh -p 52863  -d 'print-cost-time/printCostTime'

其中print-cost-time和printCostTime分布是PrintCostModule中的@Information(id = "print-cost-time")和 @Command("printCostTime")指定的ID.

执行完这个命令之后, 我们再观察SearchSomething的控制台输出, 已经变成了下面这样:

====begin===
search cost: 1
prettySearch cost: 6
search result = [48690]
====end===
====begin===
search cost: 0
prettySearch cost: 1
search result = [48690]
====end===
====begin===
search cost: 0
prettySearch cost: 1
search result = [48690]
====end===
====begin===
search cost: 1
prettySearch cost: 1
search result = [48690]
====end===

可以看到每次执行prettySearch和search都会打印出执行时间.

最后, 我们执行

sh bin/sandbox.sh -p 52863  -S

即关掉jvm-sandbox, 发现SearchSomething的控制台输出又变成了原来的样子:

====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===

总结

可以看到, jvm-sandbox是可以在不改动原来项目任何一行代码的情况下实现AOP功能; 而且还是那种"挥之则来,挥之则去"的AOP, 可以在有需要的时候才打开, 而在不需要的时候就关闭. 给你这样的工具, 你喜不喜欢咯.

javassist 和 instrument

jvm-sandbox确实很强大.但是上面一顿花里胡哨的操作下来, 还是没看出来它是如何实现的.

知其然而不知其所以然不应该是我们学习的态度.所以这一节, 我们来模拟一个最简化的jvm-sandbox--只实现它的动态AOP的功能, 进而理解jvm-sandbox的内在原理.

首先, 要实现这个动态AOP的功能, 是有两个技术点是要突破的.

第一是字节码增强技术. 即通过修改一个class的字节码,例如在它的方法前后记录时间戳, 来达到打印方法执行时间的目的. 这一点, 我们可以使用javassist框架来实现

第二是如果把修改后的字节码, 重新"替换"到JVM中去. 这个毋庸置疑, 就需要用java的Instrumentation技术了

javassist

字节码增强技术其实挺多的. 例如上面的CGLIG(底层用到了ASM), ASM, btrace 等等. 但是我还是最喜欢使用javassist, 理由也挺简单, 因为实在记不住JVM那两百多个指令(捂脸). 而javassist不需要我们了解虚拟机的细节, 直接通过面向对象的方式就可以实现字节码操作.

下面来展示一下怎么使用javassist实现AOP.

首先引入javassist的依赖:

    <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
    </dependency>

字节码增强的代码如下:

            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get("com.north.lat.proxylat.service.impl.SimpleSearch");
            CtMethod[]  ctMethods = cc.getDeclaredMethods();
            // 在这个类的每个方法上都做AOP, 打印出执行时间
            for(CtMethod ctMethod : ctMethods){
                ctMethod.addLocalVariable("bt", CtClass.longType);
                ctMethod.addLocalVariable("et", CtClass.longType);
                ctMethod.insertBefore("bt = System.currentTimeMillis();");
                ctMethod.insertAfter(" et = System.currentTimeMillis();");
                ctMethod.insertAfter("System.out.println(\"javassist." + ctMethod.getName() + " cost : \" + (et - bt));");
            }
            return cc.toBytecode();

其实代码非常简单, 无非就是通过ClassPool拿到这个类的CtClass对象, 从而能获取到所有的Method, 一一对每个方法进行增强后, 调用toBytecode()方法返回修改后的字节码.

java instrument

JDK6后的java instrument可以让我们能动态地attach到一个正在运行JVM中去.

attach到JVM后, 我们就可以做两件事:

1. 获取到JVM的内部信息; 这个特性一般被应用来做JVM监控, 线上故障排查等等.

2. 获取或者替换JVM的类信息. 这个特性就可以用来做字节码增强了, 例如我们正在做的AOP.

实现instrument需要我们先定义一个jar包, jar包的要求如下:

1. 要有一个称为Agent-Class的类, 这个类必须要有一个方法签名为
"void agentmain(String agentArgs, Instrumentation inst)"
的agentmain方法
2. jar包中的MATA-INF/Manifest.MF文件中需要指定Agent-Class具体是哪个类.

为了达到上面两点, 我们再来新建一个项目, 先新建agent-class类:

package com.north.lat.proxylat.agent;

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;


/**
 * agent,  使用javassist框架修改字节码, 然后使用Instrumentation替换字节码
 * @author lhh
 */
public class PrintTimeCostAgent {

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

    public static void agentmain(String agentArgs, Instrumentation inst){
        System.out.println("agentArgs = [" + agentArgs + "], inst = [" + inst + "]");
        // 要修改的类, 例如com.north.lat.proxylat.service.impl.SimpleSearch
        String className = agentArgs;
        Class clazz;
        try {
            // 先要加载一下看这个类是否存在, 如果类本身不存在, 那自然也无法修改了
            clazz = Thread.currentThread().getContextClassLoader().loadClass(className);

            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get(className);
            // 使用javassist修改字节码
            byte[] classBytes = getRedefineClass(cc);
            if(classBytes == null){
                throw new IllegalStateException("getRedefineClass return null");
            }
            redefineClass(inst, clazz, classBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(" agentmain end");
    }
    private static void redefineClass(Instrumentation inst, Class clazz, byte [] classBytes) throws UnmodifiableClassException, ClassNotFoundException {
        // 使用Instrumentation替换JVM中的字节码
        ClassDefinition classDefinition = new ClassDefinition(clazz, classBytes);
        inst.redefineClasses(classDefinition);
    }

    private static byte[] getRedefineClass(CtClass cc){
        try {
            CtMethod[]  ctMethods = cc.getDeclaredMethods();
            // 在这个类的每个方法上都做AOP, 打印出执行时间
            for(CtMethod ctMethod : ctMethods){
                ctMethod.addLocalVariable("bt", CtClass.longType);
                ctMethod.addLocalVariable("et", CtClass.longType);
                ctMethod.insertBefore("bt = System.currentTimeMillis();");
                ctMethod.insertAfter(" et = System.currentTimeMillis();");
                ctMethod.insertAfter("System.out.println(\"javassist." + ctMethod.getName() + " cost : \" + (et - bt));");
            }
            return cc.toBytecode();
        } catch (CannotCompileException | IOException e) {
            e.printStackTrace();
        }
        // 异常情况返回null
        return null;
    }

}

该agent类的功能非常简单, 即直接使用javassist增强SimpleSearch类, 然后调用了Instrumentation的redefineClasses方法, 重新加载了该类, 从而实现了字节码增强.

接着还要在pom.xml定义好Manifest .MF文件的内容,我们直接使用了maven-shade-plugin:

 <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.north.lat.proxylat.agent.PrintTimeCostAgent</Premain-Class>
                                        <Agent-Class>com.north.lat.proxylat.agent.PrintTimeCostAgent</Agent-Class>
                                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

可以看到, 我们在manifest中指定了四个配置项:


    <Premain-Class>com.north.lat.proxylat.agent.PrintTimeCostAgent</Premain-Class>
    <Agent-Class>com.north.lat.proxylat.agent.PrintTimeCostAgent</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                 

一切准备好后, 我们把这个项目打包成一个proxy-lat-aop-agent-1.0-SNAPSHOT.jar, 等一下会用到

attach和loadAgent

上面的agent中的字节码增强的逻辑已经准备好了, 但是并没有被调用到, 所以这时的SimpleSearch类还是不会打印出执行时间的. 这也符合我们的设计需求. 我们的目的是, 在需要它打印的时候,才让它打印.

那现在我们来触发一下字节码增强的逻辑.

首先我们要测试的类还是SearchSomething:


public class SearchSomething {
    public static void main(String[] args) throws InterruptedException {
        SimpleSearch search = new SimpleSearch();
        while (true){
            System.out.println("====begin===");
            String result = search.prettySearch("123");
            System.out.println("search result = [" + result + "]");
            System.out.println("====end===");
            Thread.sleep(3000);
        }

    }
}

正常情况它会一直打印:

====begin===
search result = [48690]
====end===
====begin===
search result = [48690]
====end===

然后我们新建一个类PrintTimeCostController, 来模拟上面jvm-sandbox的"sh bin/sandbox.sh -p 52863 -d 'print-cost-time/printCostTime'" 动作 PrintTimeCostController的代码如下:


/**
 * @author lhh
 */
public class PrintTimeCostController {
    /**
     * 目标JVM的pid
     */
    private static final String PID = "53569";
    /**
     * agent 所在的路径
     */
    private static final String AGENT_PATH = "/data/code/proxylat/proxy-lat-aop-agent/target/proxy-lat-aop-agent-1.0-SNAPSHOT.jar";

    /**
     * 要增强的类的全限定名称
     */
    private static final String CLASS_NAME = "com.north.lat.proxylat.service.impl.SimpleSearch";

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        VirtualMachine virtualMachine = null;
        for(VirtualMachineDescriptor virtualMachineDescriptor : list){
            // 如果是我们的目标PID, 才执行attach
            if(PID.equals(virtualMachineDescriptor.id())){
                virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
                // attach成功后, 执行loadAgent方法
                virtualMachine.loadAgent(AGENT_PATH, CLASS_NAME);
            }
        }
    }
}

执行一下PrintTimeCostController之后, 会发现SearchSomething的输出变成了:

agentArgs = [com.north.lat.proxylat.service.impl.SimpleSearch], inst = [sun.instrument.InstrumentationImpl@30691022]
 agentmain end
====begin===
javassist.search cost : 0
javassist.prettySearch cost : 0
search result = [48690]
====end===
====begin===
javassist.search cost : 0
javassist.prettySearch cost : 0
search result = [48690]
====end===
====begin===
javassist.search cost : 0
javassist.prettySearch cost : 1
search result = [48690]
====end===

说明我们的简单版jvm-sandbox的AOP功能是实现成功了的.

源码

最后, 文本的完整代码, 见:

github.com/NorthWard/p…