动态代理
(文章中有很多其他文章的引用)
首先讲代理模式
定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用
代理模式中,是需要代理对象和目标对象实现同一个接口
目的:不改变目标对象方法的情况下对方法进行增强
代理模式大概结构如下:
动态代理
为什么叫动态代理,因为在运行期间动态生成的。
有两种,jdk 动态代理,cglib 动态代理。
jdk 要基于接口实现,cglib 通过子类来实现
jdk 动态代理
实现动态代理的方法:
Proxy.newProxyInstance() 中有三个参数
第一个是类加载器,用来动态加载动态生成的类的字节码。
第二个是一个class 数组,是接口中的方法。接口中可以实现多个方法,所以把接口中所有方法的实现一遍。jdk 的动态代理既然想调用用户端method1,那么通过技术手段,通过反射get 到接口,生成一个类,实现了所有接口中所有方法的类,是动态生成的Proxy。
然后如何加载,那么使用类加载器。使用被代理类的加载器。
第三个参数是一个Handler,即用户想调用A 的method1,那么不直接给出A,为了把一段公共代码脱离出来,所以不能给出A。这个时候生成一个代理类。Handler 指导如何生成一个代理类。(可以理解为代理类的模板)。第三个参数其实就是“代理逻辑实现类”,这个类的编写要去实现java.lang.reflect.InvocationHandler 接口。
为啥叫动态代理,因为是要动态生成的。
上代码
一个接口。
public interface UserDao {
public Integer addUser();
public void editUser();
}
接口的实现类,也就是要执行“动态代理”的目标类。
public class UserDaoImpl implements UserDao {
@Override
public Integer addUser() {
System.out.println("调用add 方法");
return 1;
}
@Override
public void editUser() {
System.out.printf("调用edit 方法");
}
}
模拟一个切面Aspect,里面是对目标类的增强的两个操作,一个是检查权限,一个是日志记录。
public class MyAspect {
public void check_permission() {
System.out.println("检查权限");
}
public void log() {
System.out.println("日志记录...");
}
}
动态代理相关的Handler 实现,即如何把增强的逻辑写入到目标类中。(这个就是动态代理逻辑类)
// 此类 决定我们如何动态去生成或者编织我们的代理类 实现接口的方法 如何被我们修改的
public class MyInvocationHandler implements InvocationHandler {
// 我们要编织的对象(这里是要通过写的那个Aspect 来做一部分"模仿切面操作")
// 或者可以理解为我们要进行动态代理操作的类(就是不改变这个类的原有属性和方法,去为这个类添加一些额外的功能)
private Object object;
// 构造方法
// 实现我们要编织的对象(在这个例子里就是要用那个Aspect 来进行一个"伪切面")
public MyInvocationHandler(Object object) {
this.object = object;
}
/**
* @param proxy 代理类的实例对象。动态生成的实例类的对象
* @param method 用户请求的方法
* @param args 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 对于动态代理类的方法,如何修改。就在这个invoke 方法里进行修改。
MyAspect myAspect = new MyAspect();
myAspect.check_permission();
Object result = method.invoke(object, args);
myAspect.log();
// 对于这个result 的理解。
// 举例来说,比如说调用要进行动态代理的对象"UserDaoImpl" 里的addUser() 方法,是有个返回值的,
// 对应上面method.invoke 方法,返回结果就是result。
return result;
}
}
测试:
public class JdkTest {
public static void main(String[] args) {
// 1. 原始代理对象(目标对象)
UserDao userDao = new UserDaoImpl();
// 2. 调用如何织入我们的代理类 实现哪些接口是如何被我们修改的(就是为目标类中的方法进行增强的逻辑)
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(userDao);
// 3. 通过jdk proxy 的代理方法 动态生成了原始类实例实现接口的动态代理类 返回的是动态代理类的实例
UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader()
, userDao.getClass().getInterfaces()
, new MyInvocationHandler(userDao));
// 调用一下这个方法进行验证
Integer result = userDaoProxy.addUser();
System.out.println(result);
userDaoProxy.editUser();
// 注意这里,输出是UserDaoImpl
System.out.println(userDaoProxy);
// 输出"true",发现userDaoProxy 就是Proxy 的子类。是基于刚才那个模板生成的。
// 因为java 中,每个类只能有一个父类,所以是Proxy 的类,就不能是其他类的子类了。
// 所以是对接口进行"动态编织"
System.out.println(userDaoProxy instanceof Proxy);
}
}
输出结果:
检查权限
调用add 方法
日志记录...
1
检查权限
调用edit 方法日志记录...
CGLib动态代理
上面代理方式,目标对象UserServiceimpl 实现了一个接口。
如果只是一个普通的类,没有实现任何接口,该如何进行代理呢?这就引出了CGLib动态代理。
CGLib动态代理也叫子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。
要用cglib代理,需要导入相应的包,好在spring已经集成了它,引入spring即可。
上代码
代理类
public class CGLibProxy implements MethodInterceptor{
private Object target;
// 或者定义函数签名为:
// public Object getProxy(Object target) {}
public Object bind(Object target) {
this.target = target;
// CGLib enhancer 增强类对象
Enhancer enhancer = new Enhancer();
// 设置增强类型
enhancer.setSuperclass(target.getClass());
// 设置回调函数
//(定义代理逻辑对象为当前对象,要求当前对象实现MethodInterceptor 方法)
enhancer.setCallback(this);
// 创建并返回子类对象
return enhancer.create();
}
/**
* @param obj
* @param method
* @param args
* @param proxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object object = proxy.invoke(obj, args);
System.out.println("打印日志");
return object;
// Object result = proxy.invokeSuper(proxy, args);
// System.err.println("调用真实对象后");
// result result;
}
}
测试类:
public class CGLibProxyTest {
public static void main(String [] args) {
CGLibProxy cgLibProxy = new CGLibProxy();
UserService userService = (UserService) cgLibProxy.bind(new UserServiceImpl());
userService.addUser();
userService.updateUser();
}
}
参考:https://juejin.cn/post/6844903853813563405
jdk 动态代理
- 问题1:为什么 JDK 动态代理要基于接口实现?而不是基于继承来实现?
- 问题2:JDK 动态代理中,目标对象调用自己的另一个方法,会经过代理对象么?
问题1:
动态代理对象是在运行期间产生的。动态代理对象继承了Proxy 类(真正的Porxy 类,而不是代号),并且实现了被代理类的接口。因为不能多继承,所以jdk 动态代理需要基于接口来实现。
生成的代理对象里面调用了invocationHandler 的invoke 的方法,而被代理类要实现invocationHandler 方法,所以就相当于调用了被代理类的需要被增强的方法
问题2:目标对象调用自己的另一个方法,会经过代理对象吗
问题的意思是,如果原被代理类中有个方法A(),还有个方法B(),A 中调用了B,会走代理对象吗
不会,因为相当于this.B(),而this 是指原来的对象,所以B() 方法没有得到增强。
本篇文章了解了 JDK 动态代理的使用,通过分析 JDK 动态代理生成对象的 class 文件,解决了两个问题:
- 问题1:为什么 JDK 动态代理要基于接口实现?而不是基于继承来实现?
- 解答:因为 JDK 动态代理生成的对象默认是继承
Proxy
,Java 不支持多继承,所以 JDK 动态代理要基于接口来实现。 - 问题2:JDK 动态代理中,目标对象调用自己的另一个方法,会经过代理对象么?
- 解答:内部调用方法使用的对象是目标对象本身,被调用的方法不会经过代理对象。
我们知道了 JDK 动态代理内部调用是不走代理对象的。那对于 @Transactional 和 @Async 等注解不起作用是不是就搞清楚为啥了?
- 因为 @Transactional 和 @Async 等注解是通过 Spring AOP 来进行实现的,如果动态代理使用的是 JDK 动态代理,那么在方法的内部调用该方法中其它带有该注解的方法,由于此时调用的不是动态代理对象,所以注解失效。
以上参考:掘金
cglib 动态代理
CGLIB 动态代理生成代理类的子类,并且实现了 Factory 接口,底层进行字节码的增强,然后生成一个新的子类。调用方法就直接调用,不需要再通过反射的方式调用。
spring boot 2.0 默认使用cglib 动态代理
cglib 代理的目标类调用自己的另一个方法,也是经过代理对象的。
CGLIB 代理的目标类的方法满足下面条件任意一点,就不会被代理:
- 使用 final 修饰的方法
- 使用 private 类型的方法
- 使用包访问权限
1、目标对象不能被final关键字修饰,因为被final关键字修饰的对象是不可继承的。 2、目标对象的方法如果是final/staic,那么该方法是不会被拦截(不能被增强),即不会执行目标对象额外的业务方法。
参考:掘金
spring 利⽤动态代理实现 AOP,如果 Bean 实现了接⼝就使⽤ JDK 代理,否则使⽤ CGLib 代理。
静态代理:代理对象持有被代理对象的引⽤,调⽤代理对象⽅法时也会调⽤被代理对象的⽅法,但是会 在被代理对象⽅法的前后增加其他逻辑。需要⼿动完成,在程序运⾏前就已经存在代理类的字节码⽂件,代理类和被代理类的关系在运⾏前就已经确定了。 缺点是⼀个代理类只能为⼀个⽬标服务,如果要服务多种类型会增加⼯作量。
动态代理:动态代理在程序运⾏时通过反射创建具体的代理类,代理类和被代理类的关系在运⾏前是不确定的。动态代理的适⽤性更强,主要分为 JDK 动态代理和 CGLib 动态代理。
JDK 动态代理:通过类的⽅法获取⼀个动态代理对象,需要传⼊三个参数,被代理对象的类加载器、被代理对象实现的接⼝,以及⼀个器来指明具体的逻辑,相⽐静态代理的优势是接⼝中声明的所有⽅法都被转移到调⽤处理的⽅法集中处理。
CGLib 动态代理:JDK 动态代理要求实现被代理对象的接⼝,⽽ CGLib 要求继承被代理对象,如果⼀个类是 final 类则不能使⽤ CGLib 代理。两种代理都在运⾏期⽣成字节码,JDK 动态代理直接写字节码,⽽ CGLib 动态代理使⽤ ASM 框架写字节码,ASM 的⽬的是⽣成、转换和分析以字节数组表示的已编译 Java 类。 JDK 动态代理调⽤代理⽅法通过反射机制实现,⽽ GCLib 动态代理通过 FastClass 机制直接调⽤⽅法,它为代理类和被代理类各⽣成⼀个类,该类为代理类和被代理类的⽅法分配⼀个 int 参数,调⽤⽅法时可以直接定位,因此调⽤效率更⾼。
另外参考:
讲动态代理的:Java 动态代理作用是什么? - 知乎
代理类的使用方法:
整个动态代理类的生成过程可以归纳为以下一个步骤。
1.根据接口信息,新生成一个代理类的.java文件
2.根据.java,编译生成.class文件
3.classloader读取class文件信息到jvm
4.新建对象,设置InvocationHandler参数。