代理设计模式及Java常见代理实现(JDK Proxy、Cglib、ASM、javassist)
基本概念
字节码动态代理运用了代理模式、字节码操作、反射等技术,在介绍常见字节码操作类库前,先了解一下一些基本概念。
名词解释
- 字节码文件:Java源代码经过编译后生成的二进制流文件,通常一个接口或者一个类对应一个class文件,但是由于动态字节码技术的存在一个字节码并不一定对应一个真实的磁盘文件。字节码中的二进制可以被转换为16进制,Java虚拟机在加载读取class文件时以字节为单位,这也是被称为字节码文件的缘由。
- 编译型语言:源码文件通过编译器编译成机器码才能执行的语言。一般需经过编译、链接这两个步骤,链接是把各个模块的机器码和依赖库串连起来生成可执行文件。符合这个特性的语言如C、C++、Java等。
- 解释型语言: 解释性语言的程序不需要编译,在运行程序的时通过解释器逐行翻译,源码修改即时生效,但是效率不如编译型语言。代表语言如Python、JavaScript。Java语言也符合这个特性,需要JVM解释。
- 编译期:指把源码交给编译器编译成计算机可以执行的文件的过程。如:Java源代码转换为字节码的过程。Java类在编译期会进行类型的合规检查,是否符合Java虚拟机规范,通常一个类的空间占用在编译期就已经确定了。
- 运行时:指把编译后的文件交给计算机执行,直到程序运行结束。如:Java中虚拟机加载读取字节码开始执行字节码指令的过程,运行时才会进行对象在堆内存空间的创建、销毁、垃圾回收等操作。
- 动态语言:在运行时可以改变代码自身结构的语言,强调代码结构的变动,需要与动态类型语言区分,后者是指在运行期间才去做数据类型检查的语言,重点在数据类型。常见的动态语言有Python、JavaScript等。Java在JDK7引入invokedynamic指令,实现了动态特性,可以在运行时进行操作方法或者属性。
- 静态语言:与动态语言相对应,运行时结构不可变的语言就是静态语言,如Java、C、C++等。
- 反射:指的是在运行时可以动态改变类的方法和属性,这也是Java介于动态和静态语言之间的一个临界点,不完全是动态语言,但又具有动态特性。
代理设计模式
使用案例
代理模式数据结构型设计模式,使用该设计模式的优点是,各角色之间职责清晰,边界明确,便于拓展,通常包含以下角色:
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
package proxy;
/**
* 代理测试
*
* @author starsray
* @date 2022/06/22
*/
public class ProxyTest {
public static void main(String[] args) {
Subject proxy = new Proxy("proxy arg");
proxy.request();
}
}
/**
* 通过接口定义要实现的抽象业务方法
*
* @author starsray
* @date 2022/06/22
*/
interface Subject {
/**
* 请求
*/
void request();
}
/**
* 真实主题
*
* @author starsray
* @date 2022/06/22
*/
class RealSubject implements Subject {
private final String arg;
public RealSubject(String arg) {
this.arg = arg;
}
@Override
public void request() {
System.out.println("real object : " + arg);
}
}
/**
* 代理对象
*
* @author starsray
* @date 2022/06/22
*/
class Proxy implements Subject {
private RealSubject realSubject;
private final String arg;
public Proxy(String arg) {
this.arg = arg;
}
@Override
public void request() {
if (realSubject == null) {
// 通过代理对象创建真实对象
realSubject = new RealSubject(this.arg);
}
realSubject.request();
}
}
上面代码中展示的代理模式各角色之间的关系图如下图所示:
各角色之间分工如下所示:
- Subject:为定义了某种业务、功能的接口或者抽象类
- RealSubject:代理模式的功能主体,处理业务、功能的真实对象
- Proxy:代理模式的代理对象,用户的请求往往通过代理对象传递给真实对象处理
在这种模型中,代理模式起到了很好的扩展性和访问控制功能,代理对象和真实对象都可以灵活扩展,用户端请求不能直接到达真实对象,实现了访问控制的能力。如:服务器访问为了安全考虑通常会使用VPN或者堡垒机。
代理模式也存在一些缺点,客户请求和真实对象之间增加了代理对象,会使得请求链路复杂,增加额外开销,降低使用效率。如:实际生活中的中间商赚差价,这也是一种代理成本带来的额外开销。
代理模式还可以按照代理(委托)关系的已知性分为静态代理和动态代理,静态代理在编码时代理对象和真实对象的关系已经清晰。动态代理真实对象和代理对象在运行时才能确定,如Spring中AOP的使用。
动态代理与静态代理相比,优势在于接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理如:JDK动态代理InvocationHandler.invoke。在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行处理。
说明:在使用代理模式时需要注意和其他设计模式的区别
- 代理模式和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
- 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
应用场景
关于代理模式的使用案例和使用上面已经进行了简单的演示,代理模式在Spring中使用最多的就是在AOP中的体现,通过代理思想,在不改变原有功能的基础上,实现业务增强,关于AOP的具体应用如下:
- 记录日志:在方法调用前、后做一些日志增强,减少繁琐的代码量
- 监控性能:统计方法运行时间
- 权限控制:通过切面在,调用方法前校验是否有权限
- 事务管理:调用方法前开启事务,调用方法后提交关闭事务
- 缓存优化:利用AOP切面思想,配置缓存,减少重复代码量
字节码操作
Java源码文件.java经过javac编译后会转变为字节码文件,字节码文件内容包含的是二进制流,因此字节码也可以动态生成。从另一种角度来说javac本身也就是字节码生成技术的鼻祖。Java中用于字节码生成的类库有很多,比如:Javassist、CGLIB、ASM等等。除此之外,早期的JSP技术以及AOP相关框架以及常用动态代理技术、甚至反射操作等都有可能生成字节码文件。
Java中java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler都提供了动态代理的能力。在Spring内部都是通过动态代理对Bean进行增强的,所谓的动态是相对于硬编码实现的静态代理而言,动态代理的好处在于原始类和接口还未知的时候,就确定代理类的代理行为,代理类和原始类脱离直接联系后,可以灵活运用与不同的场景之中。
JDK动态代理
JDK动态代理是由JDK提供的一套基于接口和反射实现动态生成真实对象并实现代理的一种方式,其模型接口类似于静态代理,但是区别在于不需要真实对象,因为其真实对象是在运行时生成的字节码。JDK动态代理的实现主要依赖java.lang.reflect
包中的InvocationHandler
、Proxy
类实现,如下代码所示:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* JdkDynamicProxyTest
*
* @author starsray
* @date 2022/06/22
*/
public class JdkDynamicProxyTest {
/**
* 定义一个接口
*
* @author starsray
* @date 2022/06/22
*/
interface IHello {
/**
* sayHello
*/
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
/**
* 动态代理 创建一个代理类实现InvocationHandler接口的invoke方法
*
* @author starsray
* @date 2022/06/22
*/
static class DynamicProxy implements InvocationHandler {
Object originalObj;
Object bind(Object originalObj) {
this.originalObj = originalObj;
// JDK动态代理的实现原理的重点
// 这一步会进行验证、优化、缓存、同步、生成字节码、显式类加载等操作
// 调用sun.misc.ProxyGenerator::generateProxyClass()方法生成字节码,该方法会在运行时产生一个描述代理类的字节码byte[]数组。
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 参数解释
// proxy – 调用该方法的代理实例
// method – Method实例对应于代理实例上调用的接口方法。 Method对象的声明类将是声明该方法的接口,该接口可能是代理类继承该方法的代理接口的超接口。
// args – 一个对象数组,其中包含在代理实例上的方法调用中传递的参数值,如果接口方法不接受任何参数,则null 。原始类型的参数被包装在适当的原始包装类的实例中,例如java.lang.Integer或java.lang.Boolean
System.out.println("welcome");
// 调用对象方法
return method.invoke(originalObj, args);
}
}
public static void main(String[] args) {
// 加入这行代码,磁盘中将会产生一个名为 $Proxy0.class 的代理类Class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 测试JDK动态代理
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
JDK的动态代理,通过反射包的工具实现了Java一些动态特性,这里简单演示了基于InvocationHandler
、Proxy
实现动态代理的例子,除了基于反射的方式,还有一些高性能的框架如cglib、asm等直接操作字节码的方式实现的动态代理。
CGLIB
CGLIB是Code Generation Library的缩写,CGLIB是一个功能强大,高性能的字节码操作库,不同于JDK动态代理依赖于接口,为没有实现接口的类提供代理。
引入maven依赖:
<!--cglib-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.7</version>
</dependency>
实现步骤:
- 创建被代理的类及方法;
- 创建一个实现接口 MethodInterceptor 的代理类,重写 intercept 方法;
- 创建获取被代理类的方法 getInstance(Object target);
- 获取代理类,通过代理调用方法。
代码案例:
package cglib;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* cglib代理测试
*
* @author starsray
* @date 2022/06/23
*/
public class CglibProxy {
static class Service {
public final void run() {
System.out.println("service run");
}
public void printName() {
System.out.println("service name");
}
}
/**
* 自定义方法拦截器
*
* @author starsray
* @date 2022/06/27
*/
static class MyMethodInterceptor implements MethodInterceptor {
/**
* 所有生成的代理方法都调用此方法而不是原始方法。原始方法可以使用 Method 对象通过正常反射调用,也可以使用 MethodProxy(更快)调用。
*
* 参数:
* obj - “this”,增强对象
* method - 拦截方法
* args参数数组;原始类型被包装
* proxy – 用于调用 super(非拦截方法);可以根据需要多次调用
* return:与代理方法的签名兼容的任何值。返回 void 的方法将忽略此值。
* throws:Throwable任何异常都可能被抛出;如果是这样,超级方法将不会被调用
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("---> 拦截增强前 <---");
System.out.println("Object:" + obj.getClass());
System.out.println("Method:" + method);
System.out.println("Object[]:" + Arrays.toString(args));
System.out.println("MethodProxy:" + proxy);
Object o = proxy.invokeSuper(obj, args);
System.out.println("---> 拦截增强后 <---");
return o;
}
}
public static void main(String[] args) {
// 指定被代理生成的class文件存储路径
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "dynamicClass/cglib");
// 创建Enhancer对象,类似于JDK动态代理的Proxy类
Enhancer enhancer = new Enhancer();
// 设置目标类的字节码文件
enhancer.setSuperclass(Service.class);
// 设置回调函数
enhancer.setCallback(new MyMethodInterceptor());
// 调用creat方法创建代理类对象
Service service = (Service) enhancer.create();
// 通过代理对象调用final方法,对比是否会被代理
service.printName();
// 调用被代理方法
service.run();
}
}
CGLIB动态代理不需要要求被代理的对线具有接口,其原理是对指定目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。通过下面图片中可以看出cglib代理相对于JDK动态代理生成了大量的字节码文件,这是一种空间换时间的策略,在生成字节码的时候效率低于JDK,相比于反射机制,CGLIB用到了FastClass机制,通过索引取调用方法,调用效率要高于JDK代理。
FastClass
FastClass不使用反射类(Constructor或Method)调用委托类方法,动态生成一个新的类(继承FastClass),向类中写入委托类实例直接调用方法的语句,用模板方式解决Java语法不支持问题,同时改善Java反射性能。
如下代码所示:
public class CglibProxy$Service$$FastClassByCGLIB$$cd612fcb extends FastClass {
// ...
}
public class CglibProxy$Service$$EnhancerByCGLIB$$a89958fd$$FastClassByCGLIB$$b4650393 extends FastClass {
// ...
}
动态类为委托类方法调用语句建立索引,使用者根据方法签名(方法名+参数类型)得到索引值,再通过索引值进入相应的方法调用语句,得到调用结果。
根据生成的class文件反编译,如下生成代码片段所示:
// 根据方法签名获得对应的方法索引
public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch (var10000.hashCode()) {
case -919875318:
if (var10000.equals("run()V")) {
return 0;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return 2;
}
break;
case 1861880221:
if (var10000.equals("printName()V")) {
return 1;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return 3;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return 4;
}
}
return -1;
}
// invoke实际调用执行
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
// var2为代理创建对象
CglibProxy.Service var10000 = (CglibProxy.Service)var2;
int var10001 = var1;
try {
// 根据索引编号进行方法调用
switch (var10001) {
case 0:
var10000.run();
return null;
case 1:
var10000.printName();
return null;
case 2:
return new Boolean(var10000.equals(var3[0]));
case 3:
return var10000.toString();
case 4:
return new Integer(var10000.hashCode());
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
总结来说,FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。
ASM
ASM是一个通用的Java字节码操作和分析框架。它可以用来修改现有的类或直接以二进制形式动态生成类。ASM提供了一些通用的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但主要关注性能。因为它的设计和实现是尽可能的小和快,它非常适合在动态系统中使用(但当然也可以以静态的方式使用,例如在编译器中)。ASM在很多项目中都有使用,包括:
- 生成lambda调用站点的OpenJDK,以及在Nashorn编译器中,
- Groovy编译器和Kotlin编译器,
- Cobertura和Jacoco,来测量代码覆盖率,
- Byte Buddy,用于动态生成类,它本身用于其他项目,如Mockito(用于生成模拟类),
- 在运行时生成一些类。
CGLIB底层对字节码的操作使用的就是asm,可以在maven依赖中看到asm的包。
有兴趣的可以查看官方文档:https://asm.ow2.io
Javassist
Javassist是一个开源的Java字节码操作类库。由东京工业大学的数学和计算机科学系的Shigeru Chiba创建。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。
其功能与JDK自带的反射功能类似,但比反射功能更强大,可以用来检查、动态修改以及创建Java类,以下是其常用的功能:
- Generating a Java Class
- Loading Bytecode Instructions of Class
- Adding Fields to Existing Class Bytecode
- Adding Constructor to Class Bytecode
引入maven依赖
<!--javassist-->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.0-GA</version>
</dependency>
示例代码如下:
import javassist.*;
/**
* javassist测试
*
* @author starsray
* @date 2022/06/22
*/
public class JavassistTest {
/**
* 创建Computer对象
*
* @throws Exception 异常
*/
public static void createComputer() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 1. 创建一个Computer类
CtClass cc = pool.makeClass("com.starsray.test.Computer");
// 2. 新增一个字段
CtField param = new CtField(pool.get("java.lang.String"), "cpu", cc);
param.setModifiers(Modifier.PRIVATE);
cc.addField(param, CtField.Initializer.constant("intel Core i7"));
// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));
// 4. 添加无参的构造函数
CtConstructor noArgsConstructor = new CtConstructor(new CtClass[]{}, cc);
cc.addConstructor(noArgsConstructor);
// 5. 添加有参的构造函数
CtConstructor argsConstructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
argsConstructor.setBody("{$0.cpu = $1;}");
cc.addConstructor(argsConstructor);
// 6. 创建一个方法,无参数,无返回值,输出cpu值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printCpu", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(cpu);}");
cc.addMethod(ctMethod);
// 将生成的字节码文件写入磁盘指定位置
cc.writeFile("target/classes");
}
public static void main(String[] args) {
try {
createComputer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在当前target/classes
目录下生成了字节码文件
package com.starsray.test;
public class Computer {
private String cpu = "intel Core i7";
public void setName(String var1) {
this.cpu = var1;
}
public String getName() {
return this.cpu;
}
public Computer() {
}
public Computer(String var1) {
this.cpu = var1;
}
public void printCpu() {
System.out.println(this.cpu);
}
}
总结
- 代理模式是一种结构型模式,各角色之间分工明确,职责清晰,方便业务的扩展和访问控制,用户请求不能直接和真实对象交互,需要代理对象进行中转。
- JDK动态代理基于接口实现,用到了Java反射包的工具类,性能弱于基于字节码操作的其他工具框架,CGLIB无法代理final修饰的方法,其底层实现基于ASM,Spring中提供的动态代理,如果有接口使用JDK Proxy,其他常见使用CGLIB,也可以配置全部使用CGLIB。
- 动态代理使得作为静态类型语言的Java有了动态语言特性,便于框架实现功能的灵活性,减少了代码量,提高了开发效率。
代码源码地址:https://gitee.com/starsray/learning-notes/tree/master/dynamicProxy
参考资料:
- 《深入理解Java虚拟机 第三版》、JDK Proxy源码
- JDK动态代理:https://www.jianshu.com/p/1a76e516aa53
- javassist:https://www.baeldung.com/javassist
- javassist:https://github.com/jboss-javassist/javassist
- cglib github:https://github.com/cglib/cglib
- asm:https://asm.ow2.io
本文来自博客园,作者:星光Starsray,转载请注明原文链接:https://www.cnblogs.com/starsray/p/16456585.html