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

相关文章

如何突破底层思维方式的牢笼

我始终认为,牛人和普通人的根本区别在于思维方式的不同,而非知识多少、阅历多少。 在这个世界上总有一帮神一样的人物存在。就像读到的那句话:“人类就像是一条历史长河中的鱼,只有某几条鱼跳出河面,看到世界的法则,但是却无法改变,当那几条鱼中有跳上岸,进化了,改变河道流向,那样才能改变法则。”  最近一段时间一直在不断寻在内心的东西,同时也在不断的去反省和否定自己的一些思维模式,尝试重

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

idea lanyu方式激活

访问http://idea.lanyus.com/这个地址。根据提示将0.0.0.0 account.jetbrains.com添加到hosts文件中,hosts文件在C:\Windows\System32\drivers\etc目录下。点击获得注册码即可。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001