精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(中)
攻破Java技术盲点之剖析动态代理的实现原理和开发指南
前提介绍
经历了上一篇文章内容:《精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(上)》,相信您对于Java原生的动态代理技术应该有了一定的认识和了解了,那么我们先来回顾一下对应的技术要点,看看您是否真正的认识了对应的技术原理了?
技术回顾
要回顾Java动态代理,需要考虑以下几个关键点:
-
动态代理的工作原理:动态代理通过在运行时动态创建代理对象来实现对目标对象的增强或修改。代理对象会拦截目标对象的所有方法调用,并在调用前后执行特定的逻辑。
-
接口的重要性:动态代理要求目标对象和代理对象都实现一个或多个相同的接口。这些接口定义了代理对象和目标对象可以调用的方法。通过这种方式,客户端代码可以像调用普通接口一样调用代理对象,而无需关心其实现细节。
-
实现动态代理的步骤:创建目标对象实现的所有接口的类,作为代理类的基础;在代理类中,使用
Proxy.newProxyInstance()
方法动态创建代理对象;在代理类中,重写目标对象实现的所有接口的方法,以便在调用这些方法时执行自定义逻辑。 -
代理模式的应用场景:动态代理适用于需要对目标对象进行增强或修改的场景。例如,在不修改目标对象的源代码的情况下,为对象添加日志记录、性能监控、事务处理等功能。
注意:使用动态代理时,需要确保目标对象和代理对象都实现了相同的接口,否则客户端代码将无法正确调用代理对象。另外,动态代理不适用于非接口方法,因为Java不支持多重继承,代理对象只能继承自一个类。
回顾问题分析
代理对象实现了什么接口
目标对象实现的接口即为所实现的接口,这与静态代理模式中代理对象实现的接口是相同的。此外,代理对象和目标对象都共有一个共同的接口,即它们都继承自同一个接口。因此,Proxy.newProxyInstance()
方法返回的类型就是该接口类型。
代理对象的方法体是什么
代理对象的方法体是拦截器中invoke方法的实现,用于拦截并处理代理对象的所有逻辑,控制是否执行目标对象的目标方法。这个方法通常包括对请求的处理、对目标方法的调用以及对结果的返回等操作。
通过代理对象的方法体,可以实现方法级别的拦截和过滤,以及对目标方法的调用进行扩展和增强。同时,代理对象的方法体还可以用于实现事务管理、日志记录、安全控制等高级功能。
Java动态代理为我们提供了非常灵活的代理机制,但Java动态代理是基于接口的,如果对象没有实现接口我们该如何代理呢?
CGLIB动态代理
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它具备强大的运行时字节码修改和动态生成能力。
CGLIB的原理
CGLIB需要指定父类和回调方法。当然cglib也可以与Java动态代理一样面向接口,因为本质是继承。
CGLIB通过继承方式实现代理,能够动态地生成子类,覆盖并重写目标对象的方法,这种能力使得CGLIB成为Spring框架中实现AOP(面向切面编程)的关键组件。
继承方式
这种动态生成和修改字节码的能力使得CGLIB在许多高级应用场景中,如AOP(面向切面编程)编程、动态代理、方法拦截等,都展现出了卓越的性能和灵活性。
通过使用CGLIB,开发人员可以在运行时动态地定义横切关注点,例如日志记录、事务管理、安全控制等,而无需修改原有的业务逻辑代码。
为什么要用CGLIB
选择CGLIB来实现动态代理的主要原因之一是Spring框架的使用。Spring框架广泛应用于企业级应用程序的开发,而CGLIB作为基于ASM的字节码生成库,与Spring框架紧密集成,提供了强大的运行时字节码修改和动态生成能力,以下是使用CGLIB实现动态代理的简单示例代码:
建立被代理的类
首先,创建一个简单的代理对象,并不需要实现复杂的接口,那么可以直接创建一个实现类对象作为目标类,然后通过CGLIB或其他字节码库动态地修改这个类的字节码,生成代理类。
/**
* 被代理的类
* 目标对象类
*/
public class TargetObject {
/**
* 目标方法(即目标操作)
*/
public void exec() {
System.out.println("exec");
}
}
cglib拦截器类
接下来,我们将实现一个MethodInterceptor。通过该实现,所有的方法调用将被转发到intercept()方法进行处理。
这种方法拦截器提供了一种机制,可以在方法调用之前和之后执行自定义逻辑,从而实现更高级的功能,如日志记录、事务管理、安全控制等。
/**
* 动态代理-拦截器
*/
public class MyInterceptor implements MethodInterceptor {
private Object target;//目标类
public MyInterceptor(Object target) {
this.target = target;
}
/**
* 返回代理对象
* 具体实现,暂时先不追究。
*/
public Object createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(this);//回调方法 拦截器
//设置代理对象的父类,可以看到代理对象是目标对象的子类。所以这个接口类就可以省略了。
enhancer.setSuperclass(this.target.getClass());
return enhancer.create();
}
}
上述代码中,我们通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑。
/**
* args 目标方法的参数
* method 目标方法
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
System.out.println("Before method invocation");
//调用目标类的目标方法
method.invokeSuper(this.target, objects);
System.out.println("After method invocation");
return null;
}
通过调用 MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是TargetObject 的具体方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很类似,都是方法调用的中转站。
测试类
在需要使用TargetObject时,我们可以通过CGLIB的动态代理机制来获取一个代理对象。通过这种方式,我们可以在运行时动态地创建代理对象,而无需修改TargetObject的源代码。
这个代理对象将继承自TargetObject,并且所有的方法调用都会被拦截并转发到指定的处理逻辑中。这种机制提供了极大的灵活性和可扩展性,使得我们可以在不改变原有业务逻辑的情况下,实现各种附加功能。
public class MainTest {
public static void main(String[] args) {
//目标对象
TargetObject target = new TargetObject();
//拦截器
MyInterceptor myInterceptor = new MyInterceptor(target);
//代理对象,调用cglib系统方法自动生成
//注意:代理类是目标类的子类。
TargetObject proxyObj = (TargetObject) myInterceptor.createProxy();
proxyObj.exec();
}
}
我们分析了一下,从文件数上来说,cglib比jdk实现的少了个接口类。因为cglib返回的代理对象是目标对象的子类。而jdk产生的代理对象和目标对象都实现了一个公共接口。
易错点:CGLIB的invoke和invokeSuper的区分
在CGLIB中,MethodProxy是一个非常重要的接口,它提供了对被代理方法的低级别访问。MethodProxy的invoke和invokeSuper方法都是用于调用目标方法的。
invoke方法
当你调用代理对象的某个方法时,这个方法会被转发到MethodProxy的invoke方法,invoke方法接受四个参数:被代理对象、被代理方法、被代理方法的参数以及一个MethodProxy对象。
在invoke方法中,你可以使用MethodProxy的getMethod方法来获取被代理方法的详细信息(如方法名、参数类型等),然后你可以决定是否转发该方法调用或者执行其他逻辑。
invokeSuper方法:
invokeSuper方法是MethodProxy接口中的一个重要方法,用于执行被代理方法的调用,这个方法的调用会触发对目标对象的实际方法调用,从而使得我们可以对目标方法进行拦截和增强。
使用invokeSuper,我们可以调用目标对象上的原始方法,并且可以在这个调用前后添加额外的逻辑。在CGLIB的动态代理中,通常在拦截器的intercept方法中使用MethodProxy.invokeSuper来调用目标方法,从而实现方法的拦截和增强。
注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不会,因为它是final方法,CGLIB无法代理。
TargetObject代理对象的类型信息
如果对CGLIB代理之后的对象类型进行深挖,可以看到如下信息:
class=class cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52
superClass=class lh.HelloConcrete
interfaces:
interface net.sf.cglib.proxy.Factory
invocationHandler=not java proxy class
-
看到使用CGLIB代理之后的对象类型是cglib.TargetObject E n h a n c e r B y C G L I B EnhancerByCGLIB EnhancerByCGLIBe3734e52,这是CGLIB动态生成的类型;父类是TargetObject,印证了CGLIB是通过继承实现代理;
-
同时实现了net.sf.cglib.proxy.Factory接口,这个接口是CGLIB自己加入的,包含一些工具方法。
final控制以及限制
既然是继承就不得不考虑final的问题。我们知道final类型不能有子类,所以CGLIB不能代理final类型,遇到这种情况会抛出类似如下异常:
java.lang.IllegalArgumentException: Cannot subclass final class cglib.TargetObject
同样的,final方法是不能重载的,所以也不能通过CGLIB代理,遇到这种情况不会抛异常,而是会跳过final方法只代理其他方法。如果你还对代理类cglib.TargetObject E n h a n c e r B y C G L I B EnhancerByCGLIB EnhancerByCGLIBe3734e52具体实现感兴趣,它大致长这个样子:
CGLIB代理类具体实现
public class TargetObject$$EnhancerByCGLIB$$e3734e52
extends TargetObject
implements Factory
{
...
private MethodInterceptor CGLIB$CALLBACK_0; // ~~
...
public final String exec()
{
...
MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;
if (tmp17_14 != null) {
// 将请求转发给MethodInterceptor.intercept()方法。
return (String)tmp17_14.intercept(this,
CGLIB$exec$0$Method,
new Object[] { },
CGLIB$exec$0$Proxy);
}
return super.exec();
}
...
}
上述代码我们看到,当调用代理对象的exec()方法时,首先会尝试转发给MethodInterceptor.intercept()方法,如果没有MethodInterceptor就执行父类的exec()。这些逻辑没什么复杂之处,但是他们是在运行时动态产生的,无需我们手动编写。
未完待续
由于篇幅过程,小编,会在下一章深入分一下cglib的底层实现以及生产详细的class类的内容,以及底层的细节原理,请《精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(下)》
归纳总结
动态代理是一种技术,它可以在运行时动态地创建代理对象,拦截对目标对象的调用并执行一些额外的逻辑。动态代理主要分为两种实现方式:JDK的动态代理和CGLIB的动态代理。
JDK的动态代理
- 代理对象和目标对象需要实现共同的接口。
- 拦截器(或处理器)必须实现InvocationHandler接口。
- JDK的动态代理主要适用于目标对象实现了某个接口的情况。
CGLIB的动态代理
- 代理对象是目标对象的子类。
- 拦截器必须实现MethodInterceptor接口。
- CGLIB不仅支持基于接口的代理,还支持基于类的代理。这意味着即使目标对象没有实现接口,仍然可以使用CGLIB创建代理对象。
如何选择JDK原生还是CGLIB动态代理
在实际应用中,选择JDK的动态代理还是CGLIB的动态代理取决于具体的需求和上下文。例如,如果目标对象已经有一个明确的接口定义,那么使用JDK的动态代理可能更加合适。而如果需要更灵活的字节码操作,或者目标对象没有明确的接口,那么CGLIB可能是一个更好的选择。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!