本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
详细介绍了JDK动态代理和CGLIB动态代理的区别,并且给出了实现案例。
1 动态代理
动态代理:通过程序动态生成代理类,该代理类不是我们自己定义的。而是由程序自动生成。
有两种动态代理的实现方式,分别是JDK动态代理和CGLIB动态代理。它们的区别如下:
-
基于接口的动态代理:
- 提供者:JDK官方的Proxy类。
- 要求:被代理类最少实现一个接口, 如果没有则不能使用。
- 借助java内部的反射机制,动态产生一个实现和目标对象所属相同接口的代理类。然后通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法并进行代理,因此只能代理接口的方法。
-
基于子类的动态代理:
- 提供者:第三方的CGLIB,如果报asmxxxx异常,需要导入asm.jar。
- 要求:被代理类不能是用final修饰的类(最终类),可以拥有子类。
- 借助asm机制,会把被目标对象的class文件加载进来,修改其字节码生成一个继承了目标对象的子类,通过重写业务方法进行代理,因此需要代理的方法也不能是private/final/static的。
2 JDK动态代理
在Java中java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,是我们实现动态代理的核心,通过使用这个类和接口就可以生成动态代理对象。
2.1 Proxy类
public class Proxy
extends Object
implements Serializable
位于java.lang.reflect包。Proxy提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。
Proxy类中的newProxyInstance方法创建动态代理类对象:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
loader:一个classloader对象,定义了由哪个实例、类、接口的classloader对象对生成的代理类进行加载。一般是被代理对象(类\接口)的classloader
interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。如果为代理类提供的接口是真实对象实现的接口数组,这样代理对象就能像真实对象一样调用接口中的所有方法。
h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler(调用处理程序)对象上,并最终由其调用。
即当我通过代理对象来调用方法的时候,实际就是委托由其关联到的h对象的invoke方法中来调用,并不是自己来真实调用,而是通过代理的方式来调用的。
通过 Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。
2.2 InvocationHandler接口
public interface InvocationHandler
位于java.lang.reflect包。InvocationHandler 是代理实例的调用处理程序 实现的接口。
每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被分派到调用处理程序的invoke方法。即invoke方法定义了代理对象的额外的行为!
每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:
Object invoke(Object proxy,Method method,Object[] args)
proxy:代理对象实例,其所属的类为com.sun.proxy.$Proxy0,它的类加载器与被代理类的加载器一致,这也应证了newProxyInstance第一个参数loader的作用,即代理类是由传入的类加载器加载的!
method:要调用某个被代理对象真实的方法的Method对象,当时要使用真实对象方法的时,一般调用Object o=method.invoke(真实对象实例,args);
args:代理对象调用某个方法时所传递的实际参数数组!
返回:从代理实例的方法调用返回的值。即代理对象调用方法时的返回值,对某方法没有进行代理一般返回method.invoke的返回值!
2.3 JDK动态代理的案例
笔记本生产厂家具备的基本要求:
* 对生产厂家要求的接口
*/
public interface IProducer {
/**
* 销售
* @param money
*/
void saleProduct(float money);
/**
* 售后
* @param money
*/
void afterService(float money);
}
某个笔记本生产厂家:
* 一个生产者
*/
public class Producer implements IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money) {
System.out.println("厂家销售产品,并拿到钱:" + money);
}
/**
* 售后
* @param money
*/
public void afterService(float money) {
System.out.println("厂家提供售后服务,并拿到钱:" + money);
}
}
消费者购买产品:实际上现在消费者都是去代理商手上购买产品的,并且售后等大都找的代理!
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。让代理对象实现和被代理对象相同的接口!固定写法。
* InvocationHandler:用于提供具体代理方式的代码
* 它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
/** 执行被代理对象的任何接口方法都会经过该方法,真正执行代理行为的地方
* @param proxy 代理对象的引用
* @param method 当前执行的方法对象
* @param args 当前执行方法所需的参数
* @return 和被代理对象的方法有相同的返回值
*/
(proxy, method, args1) -> {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float) args1[0];
//2.判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
if (money <= 5000) {
returnValue = method.invoke(producer, money * 0.9f);
} else if (money > 5000) {
returnValue = method.invoke(producer, money * 0.8f);
} else if (money >= 10000) {
returnValue = method.invoke(producer, money * 0.6f);
}
}
return returnValue;
}
);
//一万元的笔记本,代理商获取40%的利润
proxyProducer.saleProduct(10000f);
//八千元的笔记本,代理商获取20%的利润
proxyProducer.saleProduct(8000f);
//四千元的笔记本,代理商获取10%的利润
proxyProducer.saleProduct(4000f);
}
}
CGLIB动态代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的ASM字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类和方法进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), (MethodInterceptor)
//提供增强的代码
(o, method, objects, methodProxy) -> {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float) objects[0];
//2.判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
if (money <= 5000) {
returnValue = method.invoke(producer, money * 0.9f);
} else if (money > 5000) {
returnValue = method.invoke(producer, money * 0.8f);
} else if (money >= 10000) {
returnValue = method.invoke(producer, money * 0.6f);
}
}
return returnValue;
}
);
//一万元的笔记本,代理商获取40%的利润
cglibProducer.saleProduct(10000f);
//八千元的笔记本,代理商获取200%的利润
cglibProducer.saleProduct(8000f);
//四千元的笔记本,代理商获取40%的利润
cglibProducer.saleProduct(4000f);
}
}
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。因此,在Spring Boot中默认使用CGLIB动态代理。
同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。