天天看点

Java两种动态代理实战+动态代理死循环的解释一、动态代理的概念和作用二、前期准备四、总结

文章目录

  • 一、动态代理的概念和作用
  • 二、前期准备
    • 2.1 动态代理的实现方式
    • 2.2 编写必要的类
    • 三、使用动态代理实现业务管理
    • 3.1 不使用代理如何实现?
    • 3.2 基于接口的动态代理的实现
    • 3.3 基于子类的动态代理实现
      • 3.3.1 关于动态代理的死循环问题
      • 3.3.2 生成代理对象的类
  • 四、总结

一、动态代理的概念和作用

·  动态代理是SpringAOP的实现方式,因此要深入理解SpringAOP就必须要深入理解动态代理机制。

·  什么是代理:谈动态的代理,不得不谈代理概念,而动态代理就是在运行阶段创建代理对象(通过字节码创建,十分有效率)。代理可以理解成中介的意思,当我们买电脑的时候不去电脑的生产厂商买,而是去淘宝买的时候,这里的淘宝就是代理,其代理的对象就是电脑厂商。当有了代理之后,用户一般就只和代理交互了。

·  代理最大的两个作用就是:1.在不改变原来对象的代码上,对该对象进行增强。2.业务层的对象只需要考虑业务逻辑,而不必考虑其他的逻辑。举个简单的例子:我们在操作数据库的时候,都需要进行事务的管理,而事务的逻辑和业务层的逻辑显然是不同的,因此可以用代理的模式去实现两个逻辑的分离。

·  本博客的案例就是:用动态代理的方式去增强业务层的方法,实现业务层的事务管理。

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

二、前期准备

2.1 动态代理的实现方式

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

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

2.2 编写必要的类

  • 1.业务层接口和实现类:该类的编写与SpringIOC实战(xml+注解)中StudentService一模一样。这里就不贴了。

    使用Spring注入数据源和QuerryRunner,配置文件如下:

<?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.线程绑定获取连接的工具类
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.有关事务管理的类
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();
        }
    }
}
           

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

3.1 不使用代理如何实现?

  • 如果不使用动态代理,业务层的代码是这样写的(注入了txManager):
@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();
        }
    }
           
  • 我们希望在业务层只实现业务层的逻辑,即:我们希望只写这样的代码:
@Override
    public List<Student> findAllStudents() throws SQLException {
        return stuDao.findAllStudents();
    }
           

· 这就需要我们使用动态代理的技术,在不改变源码的基础上对Service类进行增强了。

3.2 基于接口的动态代理的实现

  • 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类
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();
                        }
                    }
                });
    }
}
           
  • 为了观测到业务层的方法被执行,将业务层代码改为:
@Override
    public List<Student> findAllStudents() throws SQLException {
        System.out.println("方法被执行了....");
        return stuDao.findAllStudents();
    }
           
  • 测试:注入代理的对象的bean之后就可以直接使用了
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestSpringAnnotation extends AbstractTestNGSpringContextTests {
    @Autowired
    @Qualifier("stuServiceProxy")
    StudentService stuServiceProxy;
    
    @Test
    public void testProxy() throws SQLException {
       stuServiceProxy.findAllStudents();
    }
}
           
  • 测试结果如下:可见事务管理被执行了,而业务层的代码并没有改变
开启事务...
方法被执行了....
提交事务...
释放连接...
           

3.3 基于子类的动态代理实现

·  基于接口的代理要求被代理类必须实现至少一个接口,多多少少有些不方便。因此才有了这种基于接口的代理实现。

·  其实,创建代理的方式和基于接口的代理步骤极为相似:

  • 1.使用Enhancer类的create静态方法创建代理对象
  • 2.该方法有三个参数一个返回值
参数 作用 如何构造
Class 获得代理对象的字节码,有了字节码被代理类的所有信息都能得到 被代理对象.getClass()
Callback接口对象 用代理对象对原对象的方法进行增强 一般实现其子类接口MethodIntereptor方法拦截器(实现有intercept方法)
返回值 return 返回一个Object对象,需要强转成被代理的对象类型。 -

·  其中,intercept方法有三个入参,分别是:

参数 含义
Object o 代理对象的引用
Method method 通过字节码获得的需要被增强的方法的引用
Object[] objects 被增强的方法的入参
MethodProxy methodProxy 代理对象的方法对象,用来执行父类(即被代理的对象)的方法

~

3.3.1 关于动态代理的死循环问题

·  我们看到基于子类的动态代理在实现拦截的时候,拦截方法多了一个入参:MethodProxy。这个方法从作用上讲,是和Method一样的:

· 1. Method method是被代理对象的方法字节码对象。使用方法是:method.invoke(被代理对象,方法参数)

· 2.MethodProxy methodProxy是代理对象的方法字节码对象。使用方法是:methodProxy.invokeSuper(代理对象,方法参数)

使用methodProxy有两点好处:

· 1.不需要给代理对象传入被代理对象,效率更高。

· 2.不会出现死循环的问题。

·  第一点无需解释了,invoke方法的入参就说明了这个问题。主要是第二点:让我们来回顾一下,什么时候态代理会出现死循环的问题?答:在实现拦截器的时候,又调用了代理对象的方法。 这是什么意思呢?用刚才基于接口的动态代理为例,如果我在inovke拦截放法中增加proxy.toString() 这一句话:

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类的代理对象。语言比较枯燥,具体如下图:

Java两种动态代理实战+动态代理死循环的解释一、动态代理的概念和作用二、前期准备四、总结

· 可见,toString方法会被执行两次,两个输出的都不是同一个值,一个是根据Object的字节码输出的值,一个是根据被代理对象的字节码生成的值。因此,如果要使用原被代理对象父类的方法,则这个方法至少被增强两次。值得注意的是:在第8步中,有可能走到final也有可能走到exception。在本案例中,会在第7步时会抛出异常,因为在第6步执行完之后,该线程的连接被释放了,于是当方法执行完后,事务提交时会再申请一个没有被开启事务的链接(因为新的链接autoCommit默认是true),因此提交会失败。

3.3.2 生成代理对象的类

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

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();
                }
            }
        });
    }
}
           
  • 测试类注入新的代理对象并运行:
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestSpringAnnotation extends AbstractTestNGSpringContextTests {
    @Autowired
    @Qualifier("stuServiceProxy02")
    StudentServiceImpl stuServiceProxy02;
    
    @Test
    public void testProxy02() throws SQLException {
        stuServiceProxy02.findAllStudents();
    }
}
           
  • 测试结果如下:成功!
开启事务...
方法被执行了....
提交事务...
释放连接...
           

四、总结

·  动态代理还是比工厂模式难很多的,但是这种面向切面的变成方式确实简化了重复无用的劳动,十分有趣。看到上面的两种实现方法,虽然大同小异,但是第三方的cglib肯定是要比原JDK的方法要先进一些的(不然这个第三方还有什么存在的必要 ),而Spring的AOP也使用cglib来进行动态代理的。

·  其实在写动态代理的时候,我们就已经感觉到了,虽然理解起来不是很难,但是写起来确实是很复杂啊,所以Spring用配置的方式来简化了我们的代码量,可谓功德无量。下一篇博客,我就会简单的讲解一下SpringAOP的使用。