如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:
- 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
静态代理:
在运行前就已经写好接口代码
public interface Hello {
void morning();
}
public class HelloWorld implements Hello {
@Override
public void morning() {
System.out.println("hello world");
}
}
public class Main {
public static void main(String[] args) {
Hello hello = new HelloWorld();
hello.morning();
}
}
动态代理
public interface Hello {
void morning();
}
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() { // handler 接口
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("morning")){
System.out.println("hello world");
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 类加载器
new Class[]{Hello.class}, // 需要动态代理的类
handler // handler
);
hello.morning();
}
}
静态代理的缺点
静态代理可通过 代理模式 来实现
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
动态代理
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。
其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是 Applet
- 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
- 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
- 从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
目前常见的实现方式
为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:
- 通过实现接口的方式 -> JDK动态代理
- 通过继承类的方式 -> CGLIB动态代理
JDK动态代理
public interface Hello {
void morning();
}
public interface World {
void change();
}
public class HelloWord implements Hello,World {
@Override
public void morning() {
System.out.println("hello world");
}
@Override
public void change() {
System.out.println("change world");
}
}
public class Handler implements InvocationHandler {
private Object target;
public Handler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target,args);
after();
return result;
}
private void before(){
System.out.println("before");
}
private void after(){
System.out.println("after");
}
}
public class Main {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
// 1. 创建被代理的对象,UserService接口的实现类
HelloWord helloWord = new HelloWord();
// 2. 获取对应的 ClassLoader
ClassLoader classLoader = helloWord.getClass().getClassLoader();
// 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
Class[] interfaces = helloWord.getClass().getInterfaces();
// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
// 这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
InvocationHandler logHandler = new Handler(helloWord);
/*
5.根据上面提供的信息,创建代理对象 在这个过程中,
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
Hello p1 = (Hello) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
p1.morning();
World p2 = (World) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
p2.change();
}
}
Handler类为我们的代理处理类,在该类中我们只需要把需要对每个方法进行的增强操作编写好,对所有的方法都适用。
在动态代理的使用中有三个流程
- 获取实现类的类加载器,既ClassLoader classLoader = helloWord.getClass().getClassLoader();
- 获取实现类所有实现的接口 Class[] interfaces = helloWord.getClass().getInterfaces();
- 获取Handler代理处理类 InvocationHandler logHandler = new Handler(helloWord);
Handler类需要实现 java.lang.reflect.InvocationHandler
然后通过进行调用
Hello p1 = (Hello) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
调用后,Java实际上是会把这个类给生成出来,生成一个HelloProxy的类文件
- HelloProxy 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
- 由于 HelloProxy继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器
- 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
- 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
- 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 Handler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑
Cglib动态代理
Cglib是一种基于继承的代理方式,由于Java是单继承的,JDK代理在实现上继承了Proxy类,所以只能实现接口代理
CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法啊,这样也可以保证代理类拥有目标类的同名方法;
public class Base {
public void operate() {
System.out.println("Base 实现");
}
}
public class CglibProxyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object object = methodProxy.invokeSuper(o, objects);
after();
return object;
}
public void before() {}
public void after() {}
}
public static void main(String[] args) {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
enhancer.setSuperclass(Base.class);
//设置回调函数
enhancer.setCallback(new CglibProxyInterceptor());
//这里的creat方法就是正式创建代理类
Dog proxyDog = (Dog)enhancer.create();
//调用代理类的方法
proxyDog.operate();
}
Cglib通过继承的方式实现代理,并继承其特有的Factory接口,通过重写父类方法的办法调用父类方法 super.xxx();
所有非final的方法都会进入handler的intercept方法是
当调用代理对象的方法时,首先会尝试转发给MethodInterceptor.intercept()方法,如果没有MethodInterceptor就执行父类的方法方法
public class Base$$EnhancerByCGLIB$$fbca2ec6 extends Base implements Factory
两者的区别
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy 的优势:
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
- 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
- 代码实现简单。
基于类似 cglib 框架的优势:
- 无需实现接口,达到代理类无侵入
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能
面试题
描述动态代理的几种实现方式?分别说出相应的优缺点
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object
- 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
- 缺点:不同的接口要有不同的代理类实现,会很冗余
JDK 动态代理:
- 为了解决静态代理中,生成大量的代理类造成的冗余;
- JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,
- JDK的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象
- JDK动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口
- 优点:解决了静态代理中冗余的代理实现类问题。
- 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。因为Java是单继承的
CGLIB 代理:
- 由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;
- CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。
- 实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。
- 但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
- 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
- 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
- 缺点:技术实现相对难理解些。