200 Success
200 Success 博客
200 Success
常用标签
组员

一、写在前面

我们首先来通过一个银行转账的小案例来介绍一下数据库中的事务,并引出动态代理的概念。

1.1 功能需求

模拟一个银行转账功能,能实现A用户向B用户转账指定金额。

1.2 需求实现

在这里,accountDao即持久层中类的对象,能实现基本的增删改查,但在此处不是重点,就不一一列出;transfer是accountService即业务逻辑层中类的方法。

    public void transfer(String sourceName, String targetName, float money) {
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);
        source.setMoney(source.getMoney() - money);
        target.setMoney(target.getMoney() + money);
        accountDao.updateAccount(source);
        accountDao.updateAccount(target);
    }

在这里我们用单元测试类对方法功能进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
    @Autowired
    private IAccoutService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer("aaa","bbb",100f);
    }
}

测试结果显示通过了,数据库中也发生了改变,完成了需求。(原本aaa和bbb都是1000元)

<center></center>

但是,问题并没有那么简单,有没有想过如果在tranfer()方法中发生了异常,情况将会是怎么样?

  public void transfer(String sourceName, String targetName, float money) {
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);
        source.setMoney(source.getMoney() - money);
        target.setMoney(target.getMoney() + money);
        accountDao.updateAccount(source);
        int i = 1/0;            //在这里模拟转账过程会出现的异常
        accountDao.updateAccount(target);
    }

我们再次执行以下测试类的测试方法。测试方法显示失败了,因为有异常抛出。

<center></center>

我们再看一下数据库中的数据。aaa的钱是减少了,但是bbb的钱却没有增加。

<center></center>

为了解决这个问题,需要引入事务,下面将介绍一下事务。

二、事务的概念

事务(Transaction)是用户定义的一个数据库操作序列, 这些操作要么全做, 要么全不做, 是一个不可分割的工作单位。

例如, 在关系数据库中, 一个事务可以是一条 SQL 语句、 一组SQL 语句或整个程序。

在 SQL 中, 定义事务的语句一般有三条:

  • BEGIN TRANSACTION
  • COMMIT
  • ROLLBACK

事务通常是以 BEGIN TRANSACTION 开始, 以 COMMIT 或 ROLLBACK 结束。

  • 事务正常:用COMMIT ,提交事务的所有操作。
  • 事务异常:用ROLLBACK ,系统将事务中对数据库的所有己完成的操作全部撤销, 回滚到事务开始时的状态。

三、事务的实现

我们需要把转账transfer方法弄成一个事务,使其达到要么你aaa和bbb的钱都更新了,要么你都别更新的效果。

在Dao中,一个Dao方法就是一个数据库连接Connection,因此我们需要把以下四个连接归一,设置执行的四次Dao方法都是同一个连接。

<center></center>

为了达到这个目的,我们把线程和数据库Connection绑定在一起,这样就实现当前一个线程只有一个Connection了,然后Dao执行SQL语句时,也使用这个SQL语句。

  • 声明一个ConnectionUtils类,他用于从数据源中获取一个连接,并且实现和线程的绑定。
public class ConnectionUtils {
    
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 拿一个连接,并且和线程绑定
     * @return
     */
    public Connection getThreadConnection() {
        try {
            //1.先从ThreadLocal上获取
            Connection conn = tl.get();
            //2.判断当前线程上有没有连接
            if (conn == null) {
                //3.从数据源获取一个连接,并存入ThreadLocal
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程上的连接
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 把连接和线程解绑
     */
    public void release() {
        tl.remove();
    }
}
  • 接下来就是声明一个事务的管理类,达到上面BEGIN TRANSACTION,COMMIT,ROLLBACK的效果。
/**
 * 和事务管理相关的工具类(这是一个通知) 包含了 开启事务 提交事务 回滚事务 释放连接     
 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 提交事务
     */
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 回滚事务
     */
    public void rollback() {
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 释放连接
     */
    public void release() {
        try {
            connectionUtils.getThreadConnection().close();     //把连接还回了连接池中
            connectionUtils.release();       //把线程和连接解绑 恢复到最开始的状态 互不相关
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  • 接下来就是给业务层中所有的方法加上BEGIN TRANSACTION,COMMIT,ROLLBACK的功能了。这里仅以transfer方法为例,效果如下。
    @Override
    public boolean transfer(String sourceName, String targetName, float money) {
        try {
            //1.开启事务
            transactionManager.beginTransaction();
            //2.执行操作
            boolean flag = false;
            Account source = accountDao.findAccountByName(sourceName);
            Account target = accountDao.findAccountByName(targetName);
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            accountDao.updateAccount(source);
            int i = 1 / 0;        //模拟转账过程中发生异常
            accountDao.updateAccount(target);
            //3.提交事务
            transactionManager.commit();
            //4.返回结果
            return flag;
        } catch (Exception e) {
            //5.回滚操作
            transactionManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放连接
            transactionManager.release();
        }

    }

但是如果每个方法都引入这么一大串重复代码业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了。

再者,试想一下,如果我们此时提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码,况且这还只是一个业务层实现类,而实际的项目中这种业务层实现类可能有十几个甚至几十个。

接下来我们引入动态代理的概念。

四、 动态代理的概念

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式。即通过代理对象访问目标对象,这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

代理可分为静态代理动态代理两种,但是关于他们的区别我还没有搞懂... 等我搞懂了再更新这部分内容吧。

来讲一下动态代理。

  • 特点:字节码随用随创建,随用随加载。
  • 作用:不修改源码的基础上,对方法增强。
  • 分类:基于接口的动态代理 和 基于子类的动态代理

基于接口的动态代理
涉及类:Proxy
提供者:JDK官方
如何创建代理对象:使用Proxy类中的newProxyInstance方法
创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数:
ClassLoader:类加载器。他是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法
Class[]:字节码数组,他是用于让代理对象和被代理对象有相同方法。固定写法
InvocationHandler:用于提供增强的代码,他是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写。

在这里拿生产者,经销商,消费者的关系做个案例。其中proxyProducer是“生产者”,producer是“经销商”。

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();   //生产者
        //返回的代理对象
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(), new InvocationHandler() {
                    /**
                     * 作用:执行被代理对象的任何接口方法都会经过该方法
                     * @param proxy     代理对象的引用
                     * @param method    当前执行的方法
                     * @param args      当前执行方法所需的参数
                     * @return          和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue = null;
                        //1.获取方法执行的参数
                        Float money = (Float)args[0];
                        //2.判断当前方法是不是销售
                        if("sellProduct".equals(method.getName())){
                            //用来执行某个的对象的目标方法  经销商赚差价!!!  赚了0.2的原价
                            returnValue = method.invoke(producer,money * 0.8f);     
                        }
                        //-------------没有任何增强------------
                        //return method.invoke(producer,args);
                        return returnValue;
                    }
                });
        //模拟消费者从经销商买东西
        proxyProducer.sellProduct(10000f);
    }
}

基于子类的动态代理
涉及类:Enhancer
提供者:第三方cglib库
如何创建代理对象:使用Enhancer类中的create方法
创建代理对象的要求:被代理类不能是最终final类
create方法的参数:
Class:字节码。它是用于指定被代理对象的字节码。
Callback:用于提供增强的代码。他是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写。我们一般写的都是该接口的子接口实现类:MethodInterceptor。

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             *          以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
             * @param methodProxy       当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue = null;
                //1.获取方法执行的参数
                Float money = (Float)args[0];
                //2.判断当前方法是不是销售
                if("sellProduct".equals(method.getName())){
                    returnValue = method.invoke(producer,money * 0.8f);
                }
                //-------------没有任何增强------------
                //return method.invoke(producer,args);
                return returnValue;
            }
        });
        cglibProducer.sellProduct(1200f);
    }
}

五、 动态代理的实现

要想在不修改业务层方法的代码基础上增强方法(在本案例中值增加事务支持),需要先声明一个代理类,实现对业务层方法的增强。

在实际调用业务层方法时,并不实例化业务层的对象,而是通过代理工厂获取代理类对象来实现对增强方法的调用。下面是增强代理类。

/**
 * 用于创建service对象的代理工厂类
 */
public class BeanFactory {

    private IAccountService accountService;
    private TransactionManager transactionManager;

    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    public final void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    /**
     * 获取service代理对象
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(), new InvocationHandler() {
                    Object returnValue = null;

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        try {
                            //1.开启事务
                            transactionManager.beginTransaction();
                            //2.执行操作
                            returnValue = method.invoke(accountService, args);
                            //3.提交事务
                            transactionManager.commit();
                            //4.返回结果
                            return returnValue;
                        } catch (Exception e) {
                            //5.回滚操作
                            transactionManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.释放连接
                            transactionManager.release();
                        }
                    }
                });
    }
}
//调用测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    @Qualifier("proxyAccountService")
    private IAccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer("aaa", "bbb", 100f);
    }
}

XML配置就不放出了,很容易理解。

为了简化对动态代理的支持,引入Spring的AOP(面向切面编程)。

六、Spring中的AOP

面向切面编程( Aspect Oriented Programming ,AOP)。简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强 。

作用:在程序运行期间,不修改源码对已有方法进行增强。

优势:减少重复代码,提高开发效率,维护方便。

6.1 AOP的相关术语

  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点。拿业务逻辑层来说,IAccountService接口中的的所有方法就是连接点。
  • Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。就是要加强的方法,因为不一定要对所有方法都加强。
  • Advice(通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。通知的类型: 前置通知,后置通知,异常通知,最终通知,环绕通知。 就是要在委托类基础上被增强的内容。

<center></center>

  • Aspect(切面):是切入点和通知的结合。就是要拿来被加强的类,像上面的TransactionManager类就是个切面。

6.2 Spring中基于XML的AOP配置

  • XML配置
<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置spring的ioc,把service交给spring管理-->
    <bean id="accountService" class="org.leslie.service.impl.AccountServiceImpl"></bean>

    <!--spring中基于xml的aop配置步骤
        1.把通知bean也交给spring管理
        2.使用aop:config标签表明开始aop的配置
        3.使用aop:aspect标签表面开始配置切面
            id:给切面提供一个唯一标识
            ref:指定通知类bean的ID
        4.在aop:aspect标签的内部使用对应标签来配置通知的类型
            aop:before:标识前置通知
                method属性:标识那个方法是前置通知
                pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层哪些方法增强

            切入点表达式的写法:
                关键字:execution(表达式)
                表达式:
                    访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)
                标准的表达式写法:
                    public void org.leslie.service.impl.AccountServiceImpl.saveAccount()
                访问修饰符可以省略:
                    void org.leslie.service.impl.AccountServiceImpl.saveAccount()
                返回值可以使用通配符,表示返回值
                    * org.leslie.service.impl.AccountServiceImpl.saveAccount()
                包名可以使用通配符,表示任意包。有几级包就需要写几个*.
                    * *.*.*.*.AccountServiceImpl.saveAccount()
                包名可以使用..表示当前包及其子包
                    * *..AccountServiceImpl.saveAccount()
                类名和方法名都可以使用*来实现通配
                    * *..*.*()
                参数类别:
                    可以直接写数据类型:
                        基本类型直接写         int
                        引用类型写 包名.类名的方法   java.lang.String
                    可以使用通配符表示任意类型,但是必须有参数
                    可以使用..表示有误参数均可,有参数可以是任意类型
                全通配写法:
                    * *..*.*(..)

                 实际开发中切入点表达式的通常写法:
                    切到业务层实现类下的所有方法
                        * org.leslie.service.impl.*.*(..)
    -->

    <!--配置日志类-->
    <bean id="logger" class="org.leslie.utils.Logger"></bean>
    
    <!--配置aop-->
    <aop:config>
        <!--切入点表达式
                写在aop:aspect标签中 只能当前标签用
                他还可以写在aop:aspect标签外面  但是要在aop:aspect切面上面
        -->
        <aop:pointcut id="pt1" expression="execution(* org.leslie.service.impl.*.*(..))"/>
        <!--开始配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--前置通知:切入点方法执行之前·-->
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
            <!--后置通知:切入点方法正常执行之后 与异常通知二选一·-->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
            <!--异常通知:切入点方法执行产生异常之后·-->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
            <!--最终通知:无论执行结果·-->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
            <!--环绕通知   注释笔记看logger类-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>
  • 切面类
package org.leslie.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 用于记录日志的工具类,包含了公共代码
 */
public class Logger {

    /**
     * 前置通知
     */
    public void beforePrintLog(){
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog(){
        System.out.println("后置通知");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog(){
        System.out.println("异常通知");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog(){
        System.out.println("最终通知");
    }

    /**
     * 环绕通知
     *  问题:
     *      当我们配置了环绕通知之后,切入点方法没有执行,而执行方法执行了
     *  分析:
     *      通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知
     *      有明确的切入点方法调用,而我们没有
     *  解决:
     *      Spring为我们提供了一个接口ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法,
     *      该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
     *  Spring中的环绕通知:
     *      他是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
     * @return
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object returnValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("前置通知");
            returnValue = pjp.proceed();      //明确调用业务层方法  即切入点方法
            System.out.println("后置通知");
            return returnValue;
        } catch (Throwable throwable) {
            System.out.println("异常通知");
            throw new RuntimeException(throwable);
        }finally {
            System.out.println("最终通知");
        }
    }
}

6.3 Spring中基于注解的AOP配置

如果使用注解模式配置AOP,四个通知的执行顺序会有问题。

  • 正常:前置通知,切入点方法,后置通知/异常通知,最终通知。
  • 实际:前置通知,切入点方法,最终通知,后置通知/异常通知。

最后两个是颠倒顺序,不符合要求,因此建议使用环绕通知,可以自定义顺序。

还有XML需要开启注解AOP的支持。

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
package org.leslie.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 用于记录日志的工具类,包含了公共代码
 */
@Component("logger")
@Aspect
public class Logger {

    @Pointcut("execution(* org.leslie.service.impl.*.*(..))")
    private void pt1(){};

    /**
     * 前置通知
     */
    //@Before("pt1()")
    public void beforePrintLog(){
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     */
    //@AfterReturning("pt1()")
    public void afterReturningPrintLog(){
        System.out.println("后置通知");
    }

    /**
     * 异常通知
     */
    //@AfterThrowing("pt1()")
    public void afterThrowingPrintLog(){
        System.out.println("异常通知");
    }

    /**
     * 最终通知
     */
    //@After("pt1()")
    public void afterPrintLog(){
        System.out.println("最终通知");
    }

    /**
     * 环绕通知
     * @return
     */
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object returnValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("前置通知");
            returnValue = pjp.proceed();      //明确调用业务层方法  即切入点方法
            System.out.println("后置通知");
            return returnValue;
        } catch (Throwable throwable) {
            System.out.println("异常通知");
            throw new RuntimeException(throwable);
        }finally {
            System.out.println("最终通知");
        }
    }
}
Java
访问: 77 次

添加新评论

icon_biggrin.pngicon_neutral.pngicon_twisted.pngicon_arrow.pngicon_eek.pngicon_smile.pngicon_sad.pngicon_cool.pngicon_evil.pngicon_mrgreen.pngicon_exclaim.pngicon_surprised.pngicon_razz.pngicon_rolleyes.pngicon_wink.pngicon_cry.pngicon_confused.pngicon_lol.pngicon_mad.pngicon_question.pngicon_idea.pngicon_redface.png