技术总结(十九)
一、静态代理和动态代理的对比
一、概念
- 静态代理
- 静态代理是在编译时期就已经确定好代理类和被代理类的关系。代理类和被代理类需要实现相同的接口或者继承相同的抽象类。代理类会持有被代理类的一个引用,并且在代理类的方法中调用被代理类的对应方法,同时可以在调用前后添加一些额外的逻辑。
- 例如,假设有一个接口
Subject
,里面有一个方法doSomething()
。被代理类RealSubject
实现了Subject
接口,代理类ProxySubject
也实现了Subject
接口。ProxySubject
类中包含一个RealSubject
的实例,在ProxySubject
的doSomething
方法中,可以先进行一些日志记录或者权限检查等操作,然后调用RealSubject
的doSomething
方法。
- 动态代理
- 动态代理是在运行时动态地生成代理类。它不需要像静态代理那样在编译时期就定义好代理类,而是通过 Java 的反射机制在运行时创建代理对象。动态代理类通常是实现了
InvocationHandler
接口,通过Proxy
类的newProxyInstance
方法来生成代理对象。 - 比如,同样对于
Subject
接口和RealSubject
实现类,在使用动态代理时,可以通过一个InvocationHandler
的实现类来处理对RealSubject
的方法调用,在这个实现类的invoke
方法中,可以根据方法名等信息来决定是否添加额外的逻辑,以及如何调用真实的方法。
- 动态代理是在运行时动态地生成代理类。它不需要像静态代理那样在编译时期就定义好代理类,而是通过 Java 的反射机制在运行时创建代理对象。动态代理类通常是实现了
二、灵活性对比
- 静态代理
- 灵活性较差。因为每一个被代理类都需要有一个对应的代理类,如果接口增加了新的方法,那么代理类和被代理类都需要进行修改。例如,
Subject
接口新增了一个doAnotherThing
方法,那么RealSubject
和ProxySubject
都要实现这个新方法,并且在ProxySubject
的新方法中也要考虑是否添加额外逻辑等情况。
- 灵活性较差。因为每一个被代理类都需要有一个对应的代理类,如果接口增加了新的方法,那么代理类和被代理类都需要进行修改。例如,
- 动态代理
- 灵活性高。可以在运行时根据需要动态地生成代理对象,并且可以对不同的接口实现类使用同一个代理逻辑。只要这些类实现了相同的接口,就可以通过相同的
InvocationHandler
来处理。比如,有多个实现了Subject
接口的类,如RealSubject1
、RealSubject2
等,都可以通过同一个动态代理的InvocationHandler
来进行代理,在invoke
方法中可以根据传入的真实对象来灵活地处理方法调用。
- 灵活性高。可以在运行时根据需要动态地生成代理对象,并且可以对不同的接口实现类使用同一个代理逻辑。只要这些类实现了相同的接口,就可以通过相同的
三、性能对比
- 静态代理
- 性能相对较好。因为在编译时期已经确定了代理关系,在调用方法时,直接调用代理类的方法,不需要像动态代理那样通过反射等复杂机制来查找和调用方法。静态代理的方法调用类似于普通的对象方法调用,所以执行效率相对较高。
- 动态代理
- 性能稍差。由于动态代理是在运行时利用反射机制来生成代理对象和处理方法调用,反射操作本身会带来一定的性能开销。每次调用代理对象的方法时,都要经过
invoke
方法进行处理,包括方法查找、参数传递等操作,这些都会导致性能比静态代理稍低。
- 性能稍差。由于动态代理是在运行时利用反射机制来生成代理对象和处理方法调用,反射操作本身会带来一定的性能开销。每次调用代理对象的方法时,都要经过
四、代码复杂度对比
静态代理
- 代码结构相对简单,容易理解。代理类和被代理类的关系明确,代码编写和维护相对比较直观。例如,在实现
ProxySubject
代理RealSubject
时,代码逻辑主要集中在代理类中对被代理类方法的调用和额外逻辑的添加,层次比较清晰。
- 动态代理
- 代码复杂度较高。需要理解反射机制、
InvocationHandler
接口和Proxy
类的使用。特别是在InvocationHandler
的invoke
方法中,要正确地处理各种方法调用情况,包括参数传递、返回值处理等,对于初学者来说,理解和编写正确的动态代理代码有一定的难度。
- 代码复杂度较高。需要理解反射机制、
- z
二、动态代理的底层原理是什么?
- 反射机制基础
- 动态代理的底层核心是 Java 的反射机制。反射允许程序在运行时检查和修改类、接口、字段和方法等元素。在动态代理中,
java.lang.reflect.Proxy
类是关键。这个类提供了newProxyInstance
方法来创建动态代理对象。 - 例如,在动态代理的示例代码中,
Proxy.newProxyInstance
方法的参数包括类加载器(ClassLoader
)、接口数组(Class<?>[]
)和InvocationHandler
实例。 - 类加载器用于加载代理对象的字节码,它负责从类文件或其他资源中读取字节码并将其转换为
Class
对象。接口数组指定了代理对象要实现的接口,这决定了代理对象可以调用哪些方法。InvocationHandler
则是代理逻辑的核心处理程序。
- 动态代理的底层核心是 Java 的反射机制。反射允许程序在运行时检查和修改类、接口、字段和方法等元素。在动态代理中,
- InvocationHandler 接口
- 当通过动态代理对象调用方法时,实际上是调用了
InvocationHandler
接口的invoke
方法。invoke
方法有三个参数:Object proxy
(代理对象本身)、Method method
(被调用的方法对象)和Object[] args
(方法的参数数组)。 - 例如,在之前的动态代理代码中,
DynamicProxyHandler
类实现了InvocationHandler
接口。当调用代理对象的doSomething
方法时,实际上是调用了DynamicProxyHandler
类中的invoke
方法。在invoke
方法内部,可以根据method
参数获取被调用方法的信息,如方法名、参数类型等。 - 然后通过
method.invoke(target, args)
来调用真正的被代理对象(target
)的方法,并传入参数(args
)。这里的method.invoke
就是反射机制的应用,它在运行时根据方法对象来调用对应的方法。
- 当通过动态代理对象调用方法时,实际上是调用了
- 代理对象的生成
Proxy.newProxyInstance
方法在内部会根据传入的接口数组和其他信息,通过字节码生成技术动态地创建一个代理类的字节码。这个字节码在运行时被加载并实例化为代理对象。- 代理类实现了指定的接口,并且每个接口方法的实现逻辑都被转发到
InvocationHandler
的invoke
方法中。从外部看,代理对象就像是实现了目标接口的普通对象,可以直接调用接口中的方法。但在内部,这些方法调用都被重定向到invoke
方法进行处理,从而实现了代理逻辑的动态插入。 - 例如,对于一个实现了
Subject
接口的动态代理对象,当调用doSomething
方法时,JVM 会找到代理类中对应的方法实现(这个实现是由Proxy
类生成的),然后这个实现会调用InvocationHandler
的invoke
方法,由invoke
方法来决定如何处理这个doSomething
方法的调用,包括是否执行真正的被代理对象的doSomething
方法,以及在前后添加其他逻辑。
三、静态代理的底层原理是什么?
- 基于接口或抽象类的继承关系
- 静态代理要求代理类和被代理类实现相同的接口或者继承相同的抽象类。这是静态代理的基础架构。
- 例如,假设有接口
Subject
,被代理类RealSubject
和代理类ProxySubject
都实现了Subject
接口。这种接口的一致性使得代理类能够在逻辑上代替被代理类接收并处理方法调用。 - 从编译器的角度看,因为它们都遵循相同的接口契约,所以在代码中可以将代理对象和被代理对象相互替换使用,只要调用的是接口中定义的方法。
- 方法调用的委托机制
- 在静态代理中,代理类持有被代理类的一个引用。当外部对象调用代理类的方法时,代理类的方法内部会通过这个引用去调用被代理类的相应方法。
- 以之前提到的
Subject
接口为例,在ProxySubject
类中通常会有一个RealSubject
类型的成员变量,比如private RealSubject realSubject;
。在ProxySubject
的方法实现中,像doSomething
方法可能会这样写:
@Override
public void doSomething() {
// 可以在这里添加前置逻辑,如权限检查、日志记录等
System.out.println("Before calling real subject.");
realSubject.doSomething();
System.out.println("After calling real subject.");
// 也可以在这里添加后置逻辑
}
- 这样,代理类就像是一个中转站,它先处理自己的逻辑(如前置和后置操作),然后将方法调用委托给被代理类。这种委托机制是静态代理实现的核心,通过这种方式,代理类能够在不改变被代理类原有功能的基础上,为其添加额外的功能。
- 编译时确定代理关系
- 与动态代理不同,静态代理在编译时期就确定了代理类和被代理类的关系。编译器会根据代码中的定义生成代理类和被代理类的字节码,并且在字节码层面就已经明确了它们之间的调用关系。
- 例如,在上面的
Subject
接口及其实现类和代理类的例子中,当编译ProxySubject
和RealSubject
这两个类时,编译器会按照代码中的逻辑生成字节码。在运行时,这种字节码的结构就决定了代理类如何调用被代理类,并且不会再发生改变,除非重新编译代码。