Java代理-动态字节码生成代理的5种方式

上篇讲到了代理模式出现的原因,实现方式以及跟其他相似设计模式的区别。传送门@_@ http://blog.csdn.net/wonking666/article/details/79497547

1.静态代理的不足

设计模式里面的代理模式,代理类是需要手动去写的。但是手写代理的问题颇多

1.如果不同类型的目标对象需要执行同样一套代理的逻辑,比如说在方法调用前后打印参数和结果,那么仍然需要为每一个类型写一个代理类,将会产生大量的样板代码,书写起来非常枯燥

2.一个类中会有多个方法,对于不需要拦截的方法,我们还是要手动调一下目标对象对应的方法,虽然只有一行代码,但写多了还是感觉蠢蠢哒

3.还有,手写的代理类是静态代理,编译之后就代码就定死了,没办法做到动态的变化,缺少一些灵动


2.对现存问题的思考

回想我们使用代理类最初的目的,就只是为了拦截某个方法的调用,在其前后执行一些额外逻辑,额外逻辑和目标方法并不具有强关联。方法调用会形成一个调用栈,可以把代理想象为是在这个栈上切了一刀,在切口上做的逻辑,我们叫切面逻辑

于是聪明的程序员想到,将纯净的切面逻辑抽出来,再定义一套切入规则,然后让工具自动生成代理类,岂不是美滋滋

所以说代理几乎天生是用来做切面的,二者有着剪不断理还乱的关系

PS: 从分析来看,AOP只需要关注3点即可:切谁?什么时候切?在切口上做什么?

最讨厌那些整一大套花里胡哨的理论东西了,什么Joinpoint,Pointcut,Advice,Before/After/Around Advice,取的名字这么不形象,故作高深只会误导吃瓜群众,哼,我掀了你的小板凳 (╯—﹏—)╯(┷━━━┷


3.如何自动生成代理

我们知道,一个类从编写,到运行时调用,中间大概会经过这几个步骤


所以生成代理可以有三个思路,一,在编译期修改源代码;二,在字节码加载前修改字节码;三,在字节码加载后动态创建代理类的字节码


类别 机制 原理 优点 缺点 技术
静态AOP 静态织入 在编译期,切面直接以字节码的形式编译到目标字节码文件中 对系统无性能影响 灵活性不够 AspectJ
动态AOP 动态代理 在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中 相对于静态AOP更加灵活 切入的关注点需要实现接口。对系统有一点性能影响 JDK dynamic proxy
动态字节码生成 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中   没有接口也可以织入 扩展类的实例方法为final时,则无法进行织入 cglib
自定义类加载器 在运行期,目标加载前,将切面逻辑加到目标字节码里   可以对绝大部分类进行织入 代码中如果使用了其他类加载器,则这些类将不会被织入  
字节码转换 在运行期,所有类加载器加载字节码前,前进行拦截   可以对所有类进行织入    


4.AspectJ生成静态代理

AspectJ 是 Java 语言的一个 AOP 实现,其主要包括两个部分:第一个部分定义了如何表达、定义 AOP 编程中的语法规范,通过这套语言规范,我们可以方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题;另一个部分是工具部分,包括编译器、调试工具等

下载、安装 AspectJ 比较简单,读者登录 AspectJ 官网(http://www.eclipse.org/aspectj),即可下载到一个可执行的 JAR 包,使用 java -jar aspectj-1.x.x.jar 命令、多次单击“Next”按钮即可成功安装 AspectJ

AspectJ 的用法非常简单,就像我们使用 JDK 编译、运行 Java 程序一样,下面是一个简单的示例


编写一个POJO业务类

public class HelloAspectJ {
    public void sayHello() {
        System.out.println("Hello AspectJ");
    }

    public static void main(String[] args) {
        HelloAspectJ h = new HelloAspectJ();
        h.sayHello();
    }
}

使用AspectJ编写一个Aspect

public aspect TestAspect{
   void around():call(void Hello.sayHello()){
        System.out.println("开始事务 ...");
        proceed();
        System.out.println("事务结束 ...");
   }
}

使用AspectJ的编译器编译

ajc -d . Hello.java TxAspect.aj

注意:在定义切面的时候我们使用了 aspect 关键字而非class,这个并不是Java里面的关键字,所以无法用javac来编译,只能用ajc来编译

运行Java程序

java test.HelloAspectJ

5.JDK动态代理生成

以一个简单的事务实现原理作为示例

首先定义接口

public interface IUserDao {
    boolean login(String name, String password) throws RuntimeException;
}

给个接口的实现

public class UserDaoImpl implements IUserDao {
    @Override
    public boolean login(String name, String password) throws RuntimeException{
        return "wonking".equals(name) && "666".equals(password);
    }
}

使用JDK的Proxy.newProxyInstance生成代理

public class ProxyFactory {

    private ProxyFactory(){
    }

    public static <T> Object getProxyInstance(final T t){
        return Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("-----begin transaction-----");
                        Object returnValue=null;
                        try{
                            returnValue=method.invoke(t, args);
                        }catch (Exception e){
                            System.out.println("-----do rollback-----");
                        }
                        System.out.println("-----finish transaction-----");
                        return returnValue==null ? false : returnValue;
                    }
                });
    }
}

使用代理

public class ApplicationProxy {
    public static void main(String[] args) {
        UserDaoImpl target=new UserDaoImpl();
        IUserDao proxy= (IUserDao) ProxyFactory.getProxyInstance(target);
        Object result=proxy.login(null,"666");
    }
}

注:后面会单独开一篇文章讲解Proxy.newProxyInstance(ClassLoader loader, Class<?> interface, InvocationHandler h)背后的原理


6.cglib动态字节码生成

public class CglibProxyTest {
    public static void main(String[] args) {
        //创建一个织入器
               Enhancer enhancer = new Enhancer();
        //设置父类
               enhancer.setSuperclass(IUserDao.class);
        //设置需要织入的逻辑
               enhancer.setCallback(new LogIntercept());
        //使用织入器创建子类
               IUserDao dao = (IUserDao) enhancer.create();
        dao.login(null,"666");
    }
    
    public static class LogIntercept implements MethodInterceptor {
        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            //执行原有逻辑,注意这里是invokeSuper
            Object rev = proxy.invokeSuper(target, args);
            //执行织入的日志
                     if (method.getName().equals("doSomeThing2")) {
                System.out.println("记录日志");
            }
            return rev;
        }
    }
}


7.自定义类加载器

如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接

Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制。

我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑

代码:类加载监听器

public class ClassLoaderListener implements Translator {
    public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
    }

    public void onLoad(ClassPool pool, String classname) {
        if (!"test$UserDaoImpl".equals(classname)) {
            return;
        }
        try {
            CtClass cc = pool.get(classname);
            CtMethod m = cc.getDeclaredMethod("doSomeThing");
            m.insertBefore("{ System.out.println(\"记录日志\"); }");
        } catch (NotFoundException e) {
        } catch (CannotCompileException e) {
        }
    }

    public static void main(String[] args) {
        UserDaoImpl dao = new UserDaoImpl();
        dao.login("wonking","666");
    }
}

代码:类加载器启动器

public class ClassLoaderBootstrap {
    public static void main(String[] args) {
        //获取存放CtClass的容器ClassPool
        ClassPool cp = ClassPool.getDefault();
        //创建一个类加载器
              Loader cl = new Loader();
        try {
            //增加一个转换器
                      cl.addTranslator(cp, new ClassLoaderListener());
            //启动MyTranslatormain函数
                     cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

    }
}

8.自定义字节码转换器

首先需要创建字节码转换器,该转换器负责拦截UserDaoImpl类,并在UserDaoImpl类的login方法前使用javassist加入记录日志的代码

代码:字节码转换器

public class ClassByteTranslator implements ClassFileTransformer {
    /**
     * 字节码加载到虚拟机前会进入这个方法  
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        System.out.println(className);
        //如果加载UserDaoImpl类才拦截   
              if (!"test.UserDaoImpl".equals(className)) {
            return null;
        }
        try {
           //通过包名获取类文件   
                   CtClass cc = ClassPool.getDefault().get(className);
           //获得指定方法名的方法   
                    CtMethod m = cc.getDeclaredMethod("doSomeThing");
           //在方法执行前插入代码   
                    m.insertBefore("{ System.out.println(\"记录日志\"); }");
           return cc.toBytecode();
        } catch (NotFoundException e) {
        } catch (CannotCompileException e) {
        } catch (IOException e) {
            //ignore  
        }
        return null;
    }
  //注册字节码转换器
  public static void premain(String options, Instrumentation ins) {
      ins.addTransformer(new ClassByteTranslator());
  }
}

配置&执行

需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件

代码:修改MANIFEST.MF

  1. Manifest-Version: 1.0
  2. Premain-Class: test.ClassByteTranslator

然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

代码:main函数

public class ClassLoaderTransBootstrap {
    public static void main(String[] args) {
        new UserDaoImpl().login("wonking","666");
    }
}

执行main函数,你会发现切入的代码无侵入性的织入进去了

猜你喜欢

转载自blog.csdn.net/u012129558/article/details/80901864