前言
在Java中,有两种主要的代理方式:JDK动态代理和CGLIB动态代理。本文将深入探讨这两种动态代理的工作原理、使用场景以及他们之间的差异。
代理是一种设计模式,通过使用代理对象来控制对目标对象的访问。代理可以在不改变目标对象的功能的情况下,添加额外的逻辑。它通常用于实现横切关注点(cross-cutting concerns),如日志记录、性能监控、事务管理等。
在Java中,JDK动态代理是基于接口进行代理的方式。它利用Java的反射机制,在运行时动态生成代理类,并实现目标接口。通过实现InvocationHandler接口,我们可以自定义代理类的行为。JDK动态代理只能对实现了接口的类进行代理,因此它更适用于基于接口的编程模型。
与之相反,CGLIB动态代理是基于继承的方式。它通过生成目标类的子类来实现代理。CGLIB动态代理不需要目标类实现接口,因此可以代理没有实现接口的类。它通过继承目标类并重写方法来添加代理逻辑。
在实际应用中,选择使用哪种动态代理取决于具体情况。如果目标对象实现了接口,并且希望代理的行为与目标对象一致,则JDK动态代理是一个不错的选择。它提供了更好的性能和更低的内存占用。另一方面,如果目标对象没有实现接口,或者需要代理目标类的非公有方法,则CGLIB动态代理是更适合的选择。
在接下来的文章中,我们将详细介绍JDK动态代理和CGLIB动态代理的实现原理以及使用示例。我们将探索如何使用这两种动态代理方式,以及它们在实际项目中的应用场景。无论您是Java初学者还是有经验的开发人员,这篇文章都将为您提供深入理解动态代理的知识。让我们开始吧!
一、开始学习
1、新建项目,结构如下
2、添加 spring 依赖
<!-- spring 的核心依赖 -->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cglib/cglib-nodep -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
3、 简单代理 -- (静态代理)
在开始学习 jdk 动态 和 cglib 动态代理之前,先通过一个简单的代理案例来了解一下什么是代理。
1)在 simple 包下新建一个接口 DemoProxy ,在 impl 新建一个 B 类、DemoProxy 代理类
DemoProxy 接口
/**
* @Date 2023-10-10
* @Author qiu
* 抽象接口,用于代理和目标对象的实现
*/
public interface DemoInf {
void doSomething();
}
B 类
/**
* @Date 2023-10-10
* @Author qiu
* 目标对象(被代理的对象)
*/
@Slf4j
public class B implements DemoInf {
@Override
public void doSomething() {
log.info("执行目标对象的方法");
}
}
DemoProxy 代理类
/**
* @Date 2023-10-10
* @Author qiu
* 代理对象,代理对象的核心作用就是
* 对目标对象的行为进行增强
*/
@Slf4j
public class DemoProxy implements DemoInf {
/**
* 传入一个被代理的对象
*/
private DemoInf demoInf;
public DemoProxy(DemoInf demoInf) {
this.demoInf = demoInf;
}
@Override
public void doSomething() {
// 调用前增强
before();
// 调用目标对象的方法
demoInf.doSomething();
// 调用后增强
after();
}
private void before() {
log.info("目标调用前");
}
private void after() {
log.info("目前调用后");
}
}
2、看图分析
所谓的代理就是可以理解为是一个中间类(中介),当我们需要调用 B 类的方法时,我们通过一个代理类来调用 B 类的方法,为什么需要代理类呢,是因为当我们调用 B 类的方法时需要增加其他的业务逻辑,但是又不能对 B 类去修改,所以使用代理类,在代理类中调用 B 类的方法,并且在代理类中调用 dosomething 方法时,在之前和之后都做了相应业务逻辑处理。而我们用户调用的还是 dosomething 方法,用户不用关心代理类中做了什么事情,它只关注它调用的是它想要的方法即可。
代理类:就是在调用目标对象的方法时,对目标对象的方法进行了增强或者修改。
注意:我们现在写的这个案例是静态代理,什么是静态代理呢?
静态代理就是:在编译时确定代理类,需要手动编写代理类。
4、测试
public static void main(String[] args) {
// 创建代理对象
DemoProxy demoProxy = new DemoProxy(new B());
demoProxy.doSomething();
}
运行结果
我们创建了一个
DemoProxy
对象,并将一个B
对象作为参数传递给它的构造函数。这意味着我们要对B
对象进行代理操作。接下来,我们调用
demoProxy
对象的doSomething
方法。这个方法将执行我们在代理对象中定义的逻辑。在上述代码中,
DemoProxy
是一个自定义的代理类。它接收一个目标对象作为参数,并在其内部定义了一些额外的逻辑。通过调用doSomething
方法,我们可以触发代理逻辑的执行。当执行
doSomething
方法时,代理对象可能会在执行目标对象的相应方法之前或之后执行一些额外的操作。总之,这段代码展示了如何创建一个代理对象并使用它来调用方法。通过使用代理模式,我们可以在不修改目标对象的情况下,增加额外的功能和行为。这种动态代理的机制为我们提供了一种灵活的方式来管理和控制对象的访问。
二、jdk 动态代理
1、什么是动态代理?
动态代理是一种在运行时动态生成代理类的技术,它可以在不修改目标类代码的情况下为目标类提供代理功能,从而在被代理对象的方法执行前后添加额外的逻辑。动态代理主要由两个部分组成:代理工厂和回调处理器。
代理工厂用于生成代理对象,通常使用java.lang.reflect.Proxy
类来实现。回调处理器则用于拦截对代理对象方法的调用,并实现增强逻辑。常见的回调处理器有InvocationHandler
接口、MethodInterceptor
接口等。在调用代理对象的方法时,代理对象会将方法调用请求转发给回调处理器,并在回调处理器中执行增强逻辑。
相比于静态代理,动态代理的优势在于可以动态地生成代理类,具有更好的灵活性和可扩展性。
动态代理:在运行时通过反射动态生成代理类,不需要手动编写代理类。
2、在 jdk 包下新建 UserService 接口和 UserServiceInvocationHandler 回调处理器,并在 impl 包下新建一个 UserServiceImpl 实现类
UserService 接口
public interface UserService {
void add();
}
UserServiceImpl 实现类
/**
* @Date 2023-10-10
* @Author qiu
* 目标对象(被代理的对象)
*/
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户信息....");
}
}
UserServiceInvocationHandler 回调处理器
/**
* @Date 2023-10-10
* @Author qiu
* 动态代理执行的回调处理器
* 回调处理器必须实现 InvocationHandler 接口
*/
@Slf4j
public class UserServiceInvocationHandler implements InvocationHandler {
/**
* 目标对象(被代理的对象)
*/
private Object target;
/**
* 通过构造方法传入目标对象
*
* @param target
*/
public UserServiceInvocationHandler(Object target) {
this.target = target;
}
/**
* 核心的回调方法,目的是负责调用目标对象的方法
* 这样可以在调用目标方法前后额外执行一些增强的逻辑
*
* @param proxy 由 jdk 动态创建出来的代理对象
* @param method 目标对象的具体方法
* @param args 目标对象方法所需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用前增强
before();
// 反射调用目标对象的方法,target 就是目标对象的实例
// args 是方法需要的参数
// invoke 是目标方法的返回值,如果没有返回值则是 null
Object invoke = method.invoke(target, args);
// 调用后增强
after();
return invoke;
}
private void before() {
log.info("目标调用前");
}
private void after() {
log.info("目前调用后");
}
}
在这段代码中,定义了一个实现了
InvocationHandler
接口的类UserServiceInvocationHandler
,以便动态代理调用目标对象时,可以在不修改目标对象的情况下添加一些额外的逻辑。首先,我们定义了一个
target
字段,它是被代理的目标对象。然后,在构造函数中,我们将目标对象作为参数传递给代理对象。这样可以确保代理对象可以访问目标对象的所有方法和属性。
接下来,我们实现了
invoke
方法,这是InvocationHandler
接口的核心方法。在这个方法中,我们可以调用目标对象的方法,并在调用前后执行一些额外的逻辑。在这个方法中,我们首先执行了一个
before
方法。这个方法表示在调用目标方法之前执行的逻辑。接下来,我们通过反射机制调用目标对象的方法,使用
method.invoke(target, args)
语句,其中target
是目标对象的实例,method
是目标方法的实例,args
是目标方法需要的参数。这个语句会返回目标方法的返回值。最后,我们执行了一个
after
方法,它表示在调用目标方法之后执行的逻辑。在这个方法中,我们可以执行一些清理、日志记录、结果处理等操作。在上述代码中,
UserServiceInvocationHandler
类实现了一些增强的逻辑,可以在调用目标方法之前和之后执行。我们可以使用它来动态代理任何实现了接口的类。
3、测试
public static void main(String[] args) {
// 创建目标对象
UserService userService = new UserServiceImpl();
// 创建回调处理器
InvocationHandler handler = new UserServiceInvocationHandler(userService);
// 可以通过当前类的获取一个类加载器
ClassLoader loader = Main.class.getClassLoader();
// 获取目标对象实现的所有接口的 Class
Class[] interfaces = UserServiceImpl.class.getInterfaces();
// 通过 jdk 的 Proxy 类,动态创建代理对象
// newProxyInstance 方法需要提供三个参数
// 参数一:需要提供一个类加载器去动态创建出来的代理字节码
// 从而实例化一个代理对象
// 参数二:目标对象所实现的所有接口的 Class ,因为 jdk 动态代理
// 是一定要根据接口来创建一个代理对象,这个代理对象
// 会自动实现这些接口
// 参数三:自定义的回调处理器
// 返回的就是已经创建好的对象
UserService proxy = (UserService) Proxy.newProxyInstance(loader, interfaces, handler);
// 调用代理对象的任何方法,都会去调用回调处理器的 invoke 方法
// 来完成代理的调用
proxy.add();
}
运行结果
实现了使用动态代理创建代理对象并调用代理对象的方法,并在调用代理对象方法前后做了增强。
注意:JDK 的动态代理要求目标对象一定要有实现接口。
三、cglib 动态代理
1、什么是 cglib 动态代理
CGLIB(Code Generation Library)是一个基于 ASM(Java 字节码操作框架)的代码生成库,用于在运行时对字节码进行增强和动态生成代理对象。与 JDK 动态代理一样,CGLIB 动态代理也是一种常见的动态代理实现方式。
CGLIB 动态代理是通过继承目标类来生成代理对象。在运行时,CGLIB 会通过动态生成一个目标类的子类,并将代理逻辑添加到子类中的方法中,在调用目标方法时,实际上是调用了子类中重写的代理方法。因此,CGLIB 动态代理不要求目标对象实现接口,而是直接生成目标类的子类。
2、通过一个简单的案例来了解 cglib
1)在 demo 包下新建 Father、Son、Main 类
Father 类
@Slf4j
public class Father {
public void add() {
log.info("Hello Father.");
}
}
Son 类
@Slf4j
public class Son extends Father {
/**
* 重写父类的方法从而进行代理
*/
@Override
public void add() {
before();
// 调用父类的方法
super.add();
after();
}
private void before(){
log.info("调用目标对象方法前....");
}
private void after(){
log.info("调用目标对象方法后.....");
}
}
Main 类(测试)
public class Main {
public static void main(String[] args) {
Father f = new Son();
f.add();
}
}
运行结果
这段代码是一个对父类的方法进行增强的例子,通过子类继承父类并重写父类方法的方式,实现了代理的过程。
具体来说,Son 类继承了 Father 类,并重写了其中的 add() 方法。在重写的 add() 方法中,先调用了 before() 方法进行一些前置处理,再调用了 super.add(),即执行了父类的 add() 方法,最后再调用了 after() 方法进行一些后置处理。
在 Main 类中,实例化了一个 Son 对象,向上转型为 Father 类型,然后调用其 add() 方法。由于 Son 重写了 Father 中的 add() 方法并添加了增强逻辑,因此在执行 add() 方法时会先调用增强逻辑,再执行原始的父类方法。
总之,这段代码通过继承和重写父类方法的方式实现了对父类方法的代理,进而可以在代理逻辑中添加前置处理和后置处理等操作。
2)看图分析
CGLIB是针对类来实现代理的,它的原理是对指定目标类生成一个子类,并覆盖其中的方法实现增强 。CGLIB像是一个拦截器,在调用我们的代理类方法时,代理类(子类)会去找到目标类(父类),此时它会被一个方法拦截器所拦截,在拦截器中才会去实现方法的调用。并且还会对方法进行行为增强。这是模拟,在使用 cglib 不需要写代理类,由 cglib 动态生成。
3、在 study 包下新建一个 Userservice 类、UserServiceMethodInterceptor 拦截器、Main 测试类
UserService 类
/**
* @Date 2023-10-11
* @Author qiu
* 不实现任何接口,因此无法使用 jdk 的动态代理,
* 此时因该使用 cglib 代理框架来为目标对象
* 动态创建一个子类对象来实现代理
*/
@Slf4j
public class UserService {
public void add(){
log.info("添加用户....");
}
}
UserServiceMethodInterceptor 拦截器
/**
* @Date 2023-10-11
* @Author qiu
* cglib 要求编写一个方法拦截器用于调用目标对象的方法
* 本质上和 jdk 的 InvocationHandler 的作用是一样的
* 需要实现 MethodInterceptor 接口
*/
@Slf4j
public class UserServiceMethodInterceptor implements MethodInterceptor {
/**
* 回调处理方法(等同于 InvocationHandler 的 invoke 方法)
* @param proxy 运行时创建的代理对象(其实就是目标对象的子类)
* @param method 目标对象的方法
* @param args 对象方法所需要的参数
* @param methodProxy 代理对象的方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("调用目标对象方法前.....");
// 调用子类的代理方法
Object retrunVal = methodProxy.invokeSuper(proxy, args);
log.info("调用目标对象方法后.....");
return retrunVal;
}
}
Main 测试类
public static void main(String[] args) {
// 创建代理生成器
Enhancer enhancer = new Enhancer();
// 告诉代理生成器需要代理的父类
enhancer.setSuperclass(UserService.class);
// 设置方法拦截器(回调处理器)
enhancer.setCallback(new UserServiceMethodInterceptor());
// 创建代理对象(运行时动态创建的子类对象就是代理独享)
UserService service = (UserService) enhancer.create();
// 调用代理对象的方法
service.add();
}
这段代码演示了使用 CGLIB 动态代理的过程。
首先,定义了一个实现了 MethodInterceptor 接口的 UserServiceMethodInterceptor 类,该类负责处理代理逻辑。在 intercept() 方法中,先输出日志表示调用目标对象方法前,然后通过 methodProxy.invokeSuper(proxy, args) 调用代理对象的父类方法,即目标对象的方法,最后再输出日志表示调用目标对象方法后,最终返回方法的返回值。
在 Main 方法中,首先创建了一个 Enhancer 对象,用于生成代理对象。然后使用 enhancer.setSuperclass(UserService.class) 指定需要代理的父类,这里是 UserService 类。接下来,通过 enhancer.setCallback(new UserServiceMethodInterceptor()) 设置方法拦截器为 UserServiceMethodInterceptor 实例,即指定了代理对象的方法拦截器。最后,调用 enhancer.create() 方法生成代理对象,并将其强制转换为 UserService 类型。
最终,在调用 service.add() 方法时,会触发代理逻辑。即先输出日志表示调用目标对象方法前,然后执行目标对象的 add() 方法,最后再输出日志表示调用目标对象方法后。
总之,这段代码演示了使用 CGLIB 动态代理生成代理对象,并在代理逻辑中添加了前置处理和后置处理的操作。
注意:使用 cglib 代理框架来为目标对象 , 动态创建一个子类对象来实现代理。
四、jdk 动态代理和 cglib 动态代理的区别
JDK动态代理和CGLIB动态代理是两种常见的实现动态代理的方式,它们有以下区别:
基于的原理:
- JDK动态代理是基于接口的代理。JDK提供的java.lang.reflect.Proxy类动态地创建了一个实现指定接口的代理类对象,并通过该代理类对象来代理目标对象的方法调用。因此,只能代理实现了接口的类。
- CGLIB动态代理是基于继承的代理。CGLIB库通过创建目标类的子类来进行代理,并重写父类中的方法来实现代理。因此,可以代理没有实现接口的类。
生成代理对象的方式:
- JDK动态代理是在运行时通过反射动态创建代理类的字节码,并使用类加载器将其加载进内存,然后通过构造器创建代理对象。
- CGLIB动态代理则是在运行时通过Enhancer类创建代理类的子类,并在子类中重写父类的方法来实现代理。
性能表现:
- 由于JDK动态代理是基于接口的代理,生成的代理类相对较小,所以在方法调用的性能上更加优于CGLIB动态代理。
- CGLIB动态代理在代理过程中绕过了对接口的依赖,因此对类的代理相对更加灵活和强大。但由于生成的代理类是目标类的子类,所以在性能上略逊于JDK动态代理。
注意:如果目标对象实现了接口,建议使用JDK动态代理;如果目标对象没有实现接口或者需要对类进行代理,可以使用CGLIB动态代理。需要根据具体的场景和需求来选择适合的代理方式。
五、使用动态代理的好处
使用动态代理的好处主要有以下几点:
隐藏目标对象的实现细节,提高安全性:代理对象可以屏蔽目标对象的实现细节,防止外部非法访问和修改目标对象,从而提高安全性。
降低系统耦合度:代理对象将目标对象和代理逻辑分离开来,客户端透明地访问代理对象而不需要知道目标对象的具体实现,从而降低了系统中各个模块之间的耦合度,增加了系统的灵活性和扩展性。
提供额外的功能:代理对象可以在方法执行前、中、后插入一些额外的逻辑,比如日志记录、性能检测、权限控制、事务管理等,从而增强了系统的功能和可维护性。
减少重复代码:如果多个对象的代理实现类中存在一些重复的代码,使用动态代理可以将这些重复代码封装到通用的代理逻辑中,减少代码冗余。
使用JDK和CGLIB动态代理的好处分别如下:
使用JDK动态代理的好处:
- 目标对象必须要有接口,符合面向接口编程的设计思想。
- JDK动态代理是Java自带的功能,不需要额外的库依赖。
- JDK动态代理基于接口,生成的代理类相对较小,在性能上更加优于CGLIB动态代理。
使用CGLIB动态代理的好处:
- 可以代理没有实现接口的类,比如私有方法、static方法等。
- CGLIB动态代理不需要目标对象实现接口,所以可以对类进行代理,使用更加灵活和强大。
- CGLIB动态代理更加适合对单例对象进行代理,因为JDK动态代理每次都要重新生成代理类,而CGLIB动态代理只需在第一次需要代理时生成代理类。