0%

Java两种动态代理实战+动态代理死循环的解释

前言

本篇文章主要涉及两个知识点

  1. Java中基于接口和子类的动态代理实现
  2. 动态代理死循环的出现原因以及解决方法

动态代理的概念和作用

·  动态代理是SpringAOP的实现方式,因此要深入理解SpringAOP就必须要深入理解动态代理机制。
·  什么是代理:谈动态的代理,不得不谈代理概念,而动态代理就是在运行阶段创建代理对象(通过字节码创建,十分有效率)。代理可以理解成中介的意思,当我们买电脑的时候不去电脑的生产厂商买,而是去淘宝买的时候,这里的淘宝就是代理,其代理的对象就是电脑厂商。当有了代理之后,用户一般就只和代理交互了。
·  代理最大的两个作用就是:1.在不改变原来对象的代码上,对该对象进行增强。2.业务层的对象只需要考虑业务逻辑,而不必考虑其他的逻辑。举个简单的例子:我们在操作数据库的时候,都需要进行事务的管理,而事务的逻辑和业务层的逻辑显然是不同的,因此可以用代理的模式去实现两个逻辑的分离。
·  本博客的案例就是:用动态代理的方式去增强业务层的方法,实现业务层的事务管理。

·  要看死循环问题的朋友请戳:动态代理的死循环问题

前期准备

动态代理的实现方式

·  动态代理有两种实现方式:

  1. 第一种是JDK提供的基于接口的动态代理,要求被代理的类必须至少实现一个接口
  2. 第二种是第三方cglib提供的基于子类的动态代理,要求被代理类不能被final修饰(因为被final修饰的类不能被继承)导入cglib依赖(asm包)。

编写必要的类

  • 1.业务层接口和实现类:该类的编写与SpringIOC实战(xml+注解)中StudentService一模一样。这里就不贴了。
    使用Spring注入数据源和QuerryRunner,配置文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.memoforward"/>

<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"/>

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///springioc"/>
<property name="user" value="root"/>
<property name="password" value="123"/>
</bean>
</beans>
  • 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
    27
    package com.memoforward.utils;

    import...

    @Component
    public class ConnectionUtils {
    @Autowired
    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
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.memoforward.utils;

import...

@Component
public class TransactionManager {
//注入连接工具
@Autowired
ConnectionUtils connUtils;
//开启事务
public void beginTransaction(){
Connection conn = connUtils.getConnection();
try {
conn.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交
public void commit(){
try {
connUtils.getConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
//回滚
public void rollback(){
try {
connUtils.getConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}

}
//释放连接
public void release(){
try{
connUtils.getConnection().close();
//线程解绑
connUtils.removeThread();
}catch (Exception e){
e.printStackTrace();
}
}
}

使用动态代理实现业务管理

不使用代理如何实现?

  • 如果不使用动态代理,业务层的代码是这样写的(注入了txManager):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public List<Student> findAllStudents() throws SQLException {
try{
txManager.beginTransaction();
List<Student> stuList = stuDao.findAllStudents();
txManager.commit();
return stuList;
}catch (Exception e){
txManager.rollback();
throw new RuntimeException(e);
}finally {
txManager.release();
}
}
  • 我们希望在业务层只实现业务层的逻辑,即:我们希望只写这样的代码:
1
2
3
4
@Override
public List<Student> findAllStudents() throws SQLException {
return stuDao.findAllStudents();
}

· 这就需要我们使用动态代理的技术,在不改变源码的基础上对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
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.memoforward.proxy;

import...

@Component
public class ServiceProxy {
@Autowired
StudentService stuService;

@Autowired
TransactionManager txManager;

//类似用<bean factory-bean>来创建bean
@Bean("stuServiceProxy")
public StudentService getStuServiceProxy(){
return (StudentService)Proxy.newProxyInstance(
stuService.getClass().getClassLoader(),
stuService.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
try{
//开启事务
txManager.beginTransaction();
//执行业务逻辑
obj = method.invoke(stuService,args);
//提交事务
txManager.commit();
return obj;
}catch(Exception e){
//异常回滚
txManager.rollback();
throw new RuntimeException(e);
}finally {
//释放连接
txManager.release();
}
}
});
}
}
  • 为了观测到业务层的方法被执行,将业务层代码改为:
1
2
3
4
5
@Override
public List<Student> findAllStudents() throws SQLException {
System.out.println("方法被执行了....");
return stuDao.findAllStudents();
}
  • 测试:注入代理的对象的bean之后就可以直接使用了
1
2
3
4
5
6
7
8
9
10
11
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestSpringAnnotation extends AbstractTestNGSpringContextTests {
@Autowired
@Qualifier("stuServiceProxy")
StudentService stuServiceProxy;

@Test
public void testProxy() throws SQLException {
stuServiceProxy.findAllStudents();
}
}
  • 测试结果如下:可见事务管理被执行了,而业务层的代码并没有改变
1
2
3
4
开启事务...
方法被执行了....
提交事务...
释放连接...

基于子类的动态代理实现

·  基于接口的代理要求被代理类必须实现至少一个接口,多多少少有些不方便。因此才有了这种基于接口的代理实现。
·  其实,创建代理的方式和基于接口的代理步骤极为相似:

  • 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
2
3
4
5
6
7
8
9
Proxy.newProxyInstance(xxx, xxx,
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
proxy.toString();
...
}
});

·  就立刻会出现死循环的问题。为什么呢?答案其实很简单: 因为,代理对象是没有自己的方法的,它的所有方法都是基于被代理对象,而调用代理对象方法的时候,都会经过拦截器方法。因此,如果在拦截器中再调用代理对象的方法,就会再次进入拦截器,这样就形成了死循环。
·  而基于子类的动态实现,是构建一个继承被代理对象的对象来实现代理的,因此其可以使用代理对象父类的方法(就是被代理对象)而不必经过拦截器,这就是上面所用的invokeSuper方法,用这种方法既可以不用注入被代理对象,又避免了死循环的问题,非常推荐使用!!
·  但是这个方法有一个细节:不能用代理对象去使用没有在被代理对象中声明的方法,即使这个方法是其父类的,比如toString方法。即:如果代理对象想运行诸如toString这种方法,应当在被代理类中重写toString。因为:如果使用了父类的toString方法,methodProxy会自动去找父类Object,于是又生成了一次Object类的代理对象。语言比较枯燥,具体如下图:
问题
· 可见,toString方法会被执行两次,两个输出的都不是同一个值,一个是根据Object的字节码输出的值,一个是根据被代理对象的字节码生成的值。因此,如果要使用原被代理对象父类的方法,则这个方法至少被增强两次。值得注意的是:在第8步中,有可能走到final也有可能走到exception。在本案例中,会在第7步时会抛出异常,因为在第6步执行完之后,该线程的连接被释放了,于是当方法执行完后,事务提交时会再申请一个没有被开启事务的链接(因为新的链接autoCommit默认是true),因此提交会失败。

生成代理对象的类

·  因为,基于子类的动态代理不需要接口,所以我们让StudentServiceImpl不再实现StudentService接口,从而直接获得StudentServicImpl对象(其实就是因为我懒了,不想再写一个类了… )。生成代理类的工厂如下:

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
27
28
29
30
31
32
33
34
35
package com.memoforward.cglib;
import...

@Component
public class StudentServiceProxy {
// 不再注入被代理对象
// @Autowired
// StudentServiceImpl stuService;

@Autowired
TransactionManager txManager;

@Bean("stuServiceProxy02")
public StudentServiceImpl createStuServiceProxy(){
return (StudentServiceImpl) Enhancer.create(StudentServiceImpl.class, new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object obj = null;
try{
txManager.beginTransaction();
// obj = method.invoke(stuService,objects);
//可以对比一下这两种方式的优劣
obj = methodProxy.invokeSuper(o,objects);
txManager.commit();
return obj;
}catch(Exception e){
txManager.rollback();
throw new RuntimeException(e);
}finally {
txManager.release();
}
}
});
}
}
  • 测试类注入新的代理对象并运行:
1
2
3
4
5
6
7
8
9
10
11
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestSpringAnnotation extends AbstractTestNGSpringContextTests {
@Autowired
@Qualifier("stuServiceProxy02")
StudentServiceImpl stuServiceProxy02;

@Test
public void testProxy02() throws SQLException {
stuServiceProxy02.findAllStudents();
}
}
  • 测试结果如下:成功!
1
2
3
4
开启事务...
方法被执行了....
提交事务...
释放连接...

总结

·  动态代理还是比工厂模式难很多的,但是这种面向切面的变成方式确实简化了重复无用的劳动,十分有趣。看到上面的两种实现方法,虽然大同小异,但是第三方的cglib肯定是要比原JDK的方法要先进一些的(不然这个第三方还有什么存在的必要 ),而Spring的AOP也使用cglib来进行动态代理的。
·  其实在写动态代理的时候,我们就已经感觉到了,虽然理解起来不是很难,但是写起来确实是很复杂啊,所以Spring用配置的方式来简化了我们的代码量,可谓功德无量。下一篇博客,我就会简单的讲解一下SpringAOP的使用。

交流

请联系邮箱:chenxingyu@bupt.edu.cn