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

2024-06-24 07:48

本文主要是介绍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函数,你会发现切入的代码无侵入性的织入进去了

这篇关于Java代理-动态字节码生成代理的5种方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089548

相关文章

Spring boot整合dubbo+zookeeper的详细过程

《Springboot整合dubbo+zookeeper的详细过程》本文讲解SpringBoot整合Dubbo与Zookeeper实现API、Provider、Consumer模式,包含依赖配置、... 目录Spring boot整合dubbo+zookeeper1.创建父工程2.父工程引入依赖3.创建ap

Linux线程之线程的创建、属性、回收、退出、取消方式

《Linux线程之线程的创建、属性、回收、退出、取消方式》文章总结了线程管理核心知识:线程号唯一、创建方式、属性设置(如分离状态与栈大小)、回收机制(join/detach)、退出方法(返回/pthr... 目录1. 线程号2. 线程的创建3. 线程属性4. 线程的回收5. 线程的退出6. 线程的取消7.

SpringBoot结合Docker进行容器化处理指南

《SpringBoot结合Docker进行容器化处理指南》在当今快速发展的软件工程领域,SpringBoot和Docker已经成为现代Java开发者的必备工具,本文将深入讲解如何将一个SpringBo... 目录前言一、为什么选择 Spring Bootjavascript + docker1. 快速部署与

golang程序打包成脚本部署到Linux系统方式

《golang程序打包成脚本部署到Linux系统方式》Golang程序通过本地编译(设置GOOS为linux生成无后缀二进制文件),上传至Linux服务器后赋权执行,使用nohup命令实现后台运行,完... 目录本地编译golang程序上传Golang二进制文件到linux服务器总结本地编译Golang程序

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

Spring Boot spring-boot-maven-plugin 参数配置详解(最新推荐)

《SpringBootspring-boot-maven-plugin参数配置详解(最新推荐)》文章介绍了SpringBootMaven插件的5个核心目标(repackage、run、start... 目录一 spring-boot-maven-plugin 插件的5个Goals二 应用场景1 重新打包应用

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并

Spring Boot集成Druid实现数据源管理与监控的详细步骤

《SpringBoot集成Druid实现数据源管理与监控的详细步骤》本文介绍如何在SpringBoot项目中集成Druid数据库连接池,包括环境搭建、Maven依赖配置、SpringBoot配置文件... 目录1. 引言1.1 环境准备1.2 Druid介绍2. 配置Druid连接池3. 查看Druid监控

Linux在线解压jar包的实现方式

《Linux在线解压jar包的实现方式》:本文主要介绍Linux在线解压jar包的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux在线解压jar包解压 jar包的步骤总结Linux在线解压jar包在 Centos 中解压 jar 包可以使用 u

Java中读取YAML文件配置信息常见问题及解决方法

《Java中读取YAML文件配置信息常见问题及解决方法》:本文主要介绍Java中读取YAML文件配置信息常见问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 目录1 使用Spring Boot的@ConfigurationProperties2. 使用@Valu