前言
本篇文章主要涉及两个知识点
- Java中基于接口和子类的动态代理实现
- 动态代理死循环的出现原因以及解决方法
动态代理的概念和作用
· 动态代理是SpringAOP的实现方式,因此要深入理解SpringAOP就必须要深入理解动态代理机制。
· 什么是代理:谈动态的代理,不得不谈代理概念,而动态代理就是在运行阶段创建代理对象(通过字节码创建,十分有效率)。代理可以理解成中介的意思,当我们买电脑的时候不去电脑的生产厂商买,而是去淘宝买的时候,这里的淘宝就是代理,其代理的对象就是电脑厂商。当有了代理之后,用户一般就只和代理交互了。
· 代理最大的两个作用就是:1.在不改变原来对象的代码上,对该对象进行增强。2.业务层的对象只需要考虑业务逻辑,而不必考虑其他的逻辑。举个简单的例子:我们在操作数据库的时候,都需要进行事务的管理,而事务的逻辑和业务层的逻辑显然是不同的,因此可以用代理的模式去实现两个逻辑的分离。
· 本博客的案例就是:用动态代理的方式去增强业务层的方法,实现业务层的事务管理。
· 要看死循环问题的朋友请戳:动态代理的死循环问题
前期准备
动态代理的实现方式
· 动态代理有两种实现方式:
- 第一种是JDK提供的基于接口的动态代理,要求被代理的类必须至少实现一个接口
- 第二种是第三方cglib提供的基于子类的动态代理,要求被代理类不能被final修饰(因为被final修饰的类不能被继承)导入cglib依赖(asm包)。
编写必要的类
- 1.业务层接口和实现类:该类的编写与SpringIOC实战(xml+注解)中StudentService一模一样。这里就不贴了。
使用Spring注入数据源和QuerryRunner,配置文件如下:
1 |
|
2.线程绑定获取连接的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package com.memoforward.utils;
import...
public class ConnectionUtils {
DataSource ds;
private ThreadLocal<Connection> tl = new ThreadLocal<>();
public Connection getConnection(){
Connection conn = tl.get();
if(conn == null) {
try {
conn = ds.getConnection();
tl.set(conn);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return conn;
}
public void removeThread(){
tl.remove();
}
}3.有关事务管理的类
1 | package com.memoforward.utils; |
使用动态代理实现业务管理
不使用代理如何实现?
- 如果不使用动态代理,业务层的代码是这样写的(注入了txManager):
1 |
|
- 我们希望在业务层只实现业务层的逻辑,即:我们希望只写这样的代码:
1 |
|
· 这就需要我们使用动态代理的技术,在不改变源码的基础上对Service类进行增强了。
基于接口的动态代理的实现
- 1.使用Proxy类中的newProxyInstance静态方法来创建代理,被代理类至少要实现是一个接口。
- 2.该方法有三个参数一个返回值:
参数 | 作用 | 如何构造 |
---|---|---|
Classloader | 用于加载代理对象的字节码,与被代理对象的类加载器相同 | 被代理对象.getClass().getClassLoader() |
Class[] | 用于让代理对象实现被代理对象的所有方法 | 被代理对象.getClass().getInterfaces() |
InvocationHandle接口对象 | 用代理对象对原对象的方法进行增强 | 实现该接口对象的invoke方法来对原方法进行增强(一般用匿名内部类的方式实现) |
返回值 return | 返回一个Object对象,需要强转成被代理的对象类型。 |
\ |
· 其中,invoke方法有三个入参,分别是:
参数 | 含义 |
---|---|
Object proxy | 代理对象的引用 |
Method method | 通过字节码获得的需要被增强的方法的引用 |
Object[] args | 被增强的方法的入参 |
值得注意的是: 通过method.invoke调用的方法,始终会返回一个Object类型。也就是说,如果原方法返回void就放回null,如果原方法放回基本类型,就返回包装类。
- 实现动态的ServiceProxy类
1 | package com.memoforward.proxy; |
- 为了观测到业务层的方法被执行,将业务层代码改为:
1 |
|
- 测试:注入代理的对象的bean之后就可以直接使用了
1 | "classpath:applicationContext.xml") (locations = |
- 测试结果如下:可见事务管理被执行了,而业务层的代码并没有改变
1 | 开启事务... |
基于子类的动态代理实现
· 基于接口的代理要求被代理类必须实现至少一个接口,多多少少有些不方便。因此才有了这种基于接口的代理实现。
· 其实,创建代理的方式和基于接口的代理步骤极为相似:
- 1.使用Enhancer类的create静态方法创建代理对象
- 2.该方法有三个参数一个返回值
参数 | 作用 | 如何构造 |
---|---|---|
Class | 获得代理对象的字节码,有了字节码被代理类的所有信息都能得到 | 被代理对象.getClass() |
Callback接口对象 | 用代理对象对原对象的方法进行增强 | 一般实现其子类接口MethodIntereptor方法拦截器(实现有intercept方法) |
返回值 return | 返回一个Object对象,需要强转成被代理的对象类型。 | / |
· 其中,intercept方法有三个入参,分别是:
参数 | 含义 |
---|---|
Object o | 代理对象的引用 |
Method method | 通过字节码获得的需要被增强的方法的引用 |
Object[] objects | 被增强的方法的入参 |
MethodProxy methodProxy | 代理对象的方法对象,用来执行父类(即被代理的对象)的方法 |
关于动态代理的死循环问题
· 我们看到基于子类的动态代理在实现拦截的时候,拦截方法多了一个入参:MethodProxy。这个方法从作用上讲,是和Method一样的:
· 1. Method method是被代理对象的方法字节码对象。使用方法是:method.invoke(被代理对象,方法参数)
· 2.MethodProxy methodProxy是代理对象的方法字节码对象。使用方法是:methodProxy.invokeSuper(代理对象,方法参数)
使用methodProxy有两点好处:
· 1.不需要给代理对象传入被代理对象,效率更高。
· 2.不会出现死循环的问题。
· 第一点无需解释了,invoke方法的入参就说明了这个问题。主要是第二点:让我们来回顾一下,什么时候态代理会出现死循环的问题?答:在实现拦截器的时候,又调用了代理对象的方法。 这是什么意思呢?用刚才基于接口的动态代理为例,如果我在inovke拦截放法中增加proxy.toString() 这一句话:
1 | Proxy.newProxyInstance(xxx, xxx, |
· 就立刻会出现死循环的问题。为什么呢?答案其实很简单: 因为,代理对象是没有自己的方法的,它的所有方法都是基于被代理对象,而调用代理对象方法的时候,都会经过拦截器方法。因此,如果在拦截器中再调用代理对象的方法,就会再次进入拦截器,这样就形成了死循环。
· 而基于子类的动态实现,是构建一个继承被代理对象的对象来实现代理的,因此其可以使用代理对象父类的方法(就是被代理对象)而不必经过拦截器,这就是上面所用的invokeSuper方法,用这种方法既可以不用注入被代理对象,又避免了死循环的问题,非常推荐使用!!
· 但是这个方法有一个细节:不能用代理对象去使用没有在被代理对象中声明的方法,即使这个方法是其父类的,比如toString方法。即:如果代理对象想运行诸如toString这种方法,应当在被代理类中重写toString。因为:如果使用了父类的toString方法,methodProxy会自动去找父类Object,于是又生成了一次Object类的代理对象。语言比较枯燥,具体如下图:
· 可见,toString方法会被执行两次,两个输出的都不是同一个值,一个是根据Object的字节码输出的值,一个是根据被代理对象的字节码生成的值。因此,如果要使用原被代理对象父类的方法,则这个方法至少被增强两次。值得注意的是:在第8步中,有可能走到final也有可能走到exception。在本案例中,会在第7步时会抛出异常,因为在第6步执行完之后,该线程的连接被释放了,于是当方法执行完后,事务提交时会再申请一个没有被开启事务的链接(因为新的链接autoCommit默认是true),因此提交会失败。
生成代理对象的类
· 因为,基于子类的动态代理不需要接口,所以我们让StudentServiceImpl不再实现StudentService接口,从而直接获得StudentServicImpl对象(其实就是因为我懒了,不想再写一个类了… )。生成代理类的工厂如下:
1 | package com.memoforward.cglib; |
- 测试类注入新的代理对象并运行:
1 | "classpath:applicationContext.xml") (locations = |
- 测试结果如下:成功!
1 | 开启事务... |
总结
· 动态代理还是比工厂模式难很多的,但是这种面向切面的变成方式确实简化了重复无用的劳动,十分有趣。看到上面的两种实现方法,虽然大同小异,但是第三方的cglib肯定是要比原JDK的方法要先进一些的(不然这个第三方还有什么存在的必要 ),而Spring的AOP也使用cglib来进行动态代理的。
· 其实在写动态代理的时候,我们就已经感觉到了,虽然理解起来不是很难,但是写起来确实是很复杂啊,所以Spring用配置的方式来简化了我们的代码量,可谓功德无量。下一篇博客,我就会简单的讲解一下SpringAOP的使用。
交流
请联系邮箱:chenxingyu@bupt.edu.cn