07.Spring中的AOP【面向切面编程】

Spring中的AOP【面向切面编程】

就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

优点:
  1. 在程序运行期间,不修改源代码下对已有的方法进行增强。
  2. 减少重复代码
  3. 提高开发效率
  4. 维护方便

一、动态代理回顾

1.) 动态代理的特点

  1. 字节码随用随创建,随用随加载
  2. 与静态代理的区别是,静态代理是字节码一上来就创建好,并完成加载。
  3. 装饰者模式就是静态代理的一种体现。

2.) 动态代理的两种方式

1.基于接口的动态代理

  1. 提供者:JDK官方的Proxy类。
  2. 要求:被代理类最少实现一个接口。
  3. 详情/案例

2.基于子类的动态代理

  1. 提供者:第三方的CGLib,如果报asmxxx异常,需要导入asm.jar包
  2. 要求:被代理类不能用final修饰类(最终类)

3.) 使用CGLib的Enhancer类创建代理

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/** 被代理类 */
class Consumer{
    public Object buySomething(double money) {
        System.out.println("buy one 'GirlFriend'");
        return "GirlFriend";
    }
}

/** 代理类 */
public class MyEnhancer {

    public static Consumer getConsumer(final Consumer consumer) {
        /**
         * 被代理的类consumer不能是最终类
         * 再被代理时需要被声明称final类型*/

        Consumer poxyConsumer = (Consumer) Enhancer.create(
                consumer.getClass(), /** 被代理对象的字节码 */
                new MethodInterceptor() { /** 如何代理实现的匿名内部类,推荐使用lambda表达式 */
                @Override
                public Object intercept(
                        Object o, /** 代理对象本身 */
                        Method method, /** 被代理对向内被代理的方法对象 */
                        Object[] objects, /** 被代理类的参数列表 */
                        MethodProxy methodProxy /** 指定当前方法的代理方法应用,这里就是intercept方法对应的方法对象 */
                ) throws Throwable {

                    String methodName = method.getName();
                    if("buySomething".equals(methodName)) {
                        Double money = (Double) objects[0];
                        System.out.println("中间商拿了 " + money * 0.2 + "的回扣");
                        return method.invoke(consumer, money * 0.8);
                    }

                    return method.invoke(consumer, objects);
                }
                });
        return poxyConsumer;
    }
}

/** 测试类 */
class Main{
    public static void main(String[] args) {
        Consumer consumer = MyEnhancer.getConsumer(new Consumer());
        System.out.println(consumer.buySomething(1000));
    }
}

二、Spring中AOP的细节

1). AOP相关术语

1.Joinpoint 连接点

所谓连接点是指那些被拦截到的点。在spring中,这些点指的是【需要被代理的方法】,因为spring只支持方法类型的连接点

2.Pointcut 切入点

所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。【被代理方法中被增强的方法

3.Advice 通知/增强

所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。【增强的逻辑

  • 通知的类型:【增强的位置
    • 前置通知 切入点前【执行被代理方法前】
    • 后置通知 切入点后
    • 异常通知 出现异常的位置
    • 最终通知 finally中的逻辑
    • 环绕通知 整个代理对象的 invoke 方法(在环绕通知中,有明确的切入点方法调用,也就是被代理对象的方法执行,原始的业务逻辑)

通知的类型.jpg

4.Introduction 引介【现阶段了解】

引介是一种特殊的通知,在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。

5.Target 目标对象

代理的目标对向,也就是被代理的对象

Weaving 织入

把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

Proxy 代理

一个类被AOP织入增强后,就产生一个结果代理类。就是产生的代理对象

Aspect 切面

是切入点【被增强的方法】和通知【增强的逻辑方法】(引介)的结合。

2). 学习spring中的AOP要明确的事

1. 开发阶段【我们要做的】

  1. 编写核心业务代码(开发主线):要求属性业务需求。
  2. 将公共代码抽取出来,制作成通知。(开发阶段最后再做):
  3. 再配置文件中,声明切入点与通知间的关系及切面:

2. 运行阶段【Spring框架完成的】

Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

三、使用动态代理阐述spring的AOP功能

模拟一个转账业务,利用c3p0数据库连接池技术作为数据源、数据库连接对象与当前线程绑定、基于接口的动态代理技术、spring的IOC控制反转与DI依赖注入的方式实现代理类实现事务的支持。

1). 项目结构


动态代理实现转账事务操作.png

2). maven坐标 pom.xml

使用jdk1.8 打包方式为jar


    
        commons-dbutils
        commons-dbutils
        1.4
    
    
        commons-logging
        commons-logging
        1.2
    
    
        mysql
        mysql-connector-java
        5.1.41
    
    
        org.springframework
        spring-beans
        5.0.3.RELEASE
    
    
        org.springframework
        spring-context
        5.0.3.RELEASE
    
    
        org.springframework
        spring-core
        5.0.3.RELEASE
    
    
        org.springframework
        spring-expression
        5.0.3.RELEASE
    
    
        org.springframework
        spring-aop
        5.0.3.RELEASE
    
    
        org.springframework
        spring-test
        5.0.2.RELEASE
        test
    
    
        junit
        junit
        4.12
        test
    
    
        c3p0
        c3p0
        0.9.1.2
    

3). 工具类代码

1. ConnectionUtils 获取连接对象

ConnectionUtils对象的创建交给spring,放入spring容器中,属性有【ThreadLocal类】(类似于map集合,但是很大的区别,内部维护的key为当前访问的当前线程,value为数据库连接对象Connection,每个线程调用getThreadConnection将得到同一个连接Connection,以支持事务);【DataSource数据源】(依靠spring依赖注入)。

package com.itheima.utils;

import javax.sql.DataSource;
import java.sql.Connection;

public class ConnectionUtils {
    /**
     ThreadLocal对象内维护了一个键值对,类似于Map集合,但是
     不是Map,key为当前访问线程,value为自定义泛型,每个不同的
     线程拿到的value对象是不同的,同一个线程拿到的value对象是相同的
     这个机制很好的满足的事务对Connection对应唯一的要求!
     */
    private ThreadLocal threadLocal = new ThreadLocal();

    private DataSource dataSource;

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

    public  Connection  getThreadConnection(){
        try {
            // 从本地线程中获取连接对象并返回
            Connection connection = threadLocal.get();
            if(connection == null){

                //第一次调用该方法时当前线程中并未有connection对象
                //从连接池获取一个连接
                connection = dataSource.getConnection();

                //将从连接池获取的连接和当前的线程绑定
                threadLocal.set(connection);
            }
            // 返回当前线程的连接
            return  connection;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void removeConnection(){
        //将连接对象和当前线程解绑
        threadLocal.remove();
    }
}

2. TransactionManager 事务处理工具类

TransactionManager类 也交给spring去创建,成员变量为 ConnectionUtils,依靠spring的依赖注入,以获得与当前线程绑定的数据库连接对象。

package com.itheima.utils;
import java.sql.SQLException;
/** 和事务相关的工具类,包含了开启事务、提交事务、回滚事务和释放连接 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

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

    /** 开启事务 */
    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
            System.out.println("开启事务 --- 前置通知");
        } catch (SQLException e) { e.printStackTrace(); }
    }

    /** 提交事务 */
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
            System.out.println("提交事务 --- 后置通知");
        } catch (SQLException e) { e.printStackTrace(); }
    }

    /** 回滚事务 */
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
            System.out.println("回滚事务 --- 异常通知");
        } catch (SQLException e) { e.printStackTrace(); }
    }

    /** 释放资源 */
    public void release(){
        try {
            // 首先:归还连接对象到连接池
            connectionUtils.getThreadConnection().close();

            // 然后:将当前的连接和当前线程解绑
            connectionUtils.removeConnection();
            System.out.println("归还连接对象回连接池,将连接对象与当前线程解绑 --- 最终通知");

        } catch (SQLException e) { e.printStackTrace(); }
    }
}

4). domain中的javaBean Account

标准的javaBean,这里省略了get/set/toString方法,如需使用,请自行加上!

package com.itheima.domain;
public class Account {
    private Integer id;
    private String name;
    private Float money;
    ...
  • 数据库支持:mysql --- spring_01
CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(40) DEFAULT NULL,
  `money` float DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

/*Data for the table `account` */
insert  into `account`(`id`,`name`,`money`) values (2,'bbb',1100),(3,'ccc',1000),(4,'ddddd',99890);

5). dao持久层代码

持久层与数据大交道,有两个成员属性,都交给spring进行DI依赖注入【QueryRunner】不与数据源绑定,执行时依赖单独的连接对象进行增删改查以支持事务,【ConnectionUtils】负责提供与线程绑定的数据源。

1. AccountDao 接口

package com.itheima.dao;
import com.itheima.domain.Account;
import java.sql.SQLException;
import java.util.List;
public interface AccountDao {

    /** 根据id查询账户的余额 */
    public Account queryMoney(Integer id) throws SQLException;

    /** 根据id更新账户的余额 */
    public int updateMoney(Integer id, Float money) throws SQLException;
}

2. AccountDaoImpl实现了类

package com.itheima.dao.impl;
import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import com.itheima.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.SQLException;
import java.util.List;
public class AccountDaoImpl implements AccountDao {

    private QueryRunner queryRunner;
    private ConnectionUtils connectionUtils;

    public void setQueryRunner(QueryRunner queryRunner) {
        this.queryRunner = queryRunner;
    }

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

    /** @事务
     * 根据id查询账户的余额,将异常显示抛出 */
    @Override
    public Account queryMoney(Integer id) throws SQLException {
        return queryRunner.query(connectionUtils.getThreadConnection(),"select money from account where id = ?",
                new BeanHandler(Account.class), id);
    }

    /** @事务
     * 根据id更新账户的余额, 将异常显示抛出 */
    @Override
    public int updateMoney(Integer id, Float money) throws SQLException {
        return queryRunner.update(connectionUtils.getThreadConnection(),"update account set money = ? where id = ?", money, id);
    }
}

6). service业务层代码

提供转账业务操作

1. AccountService接口

package com.itheima.service;
public interface AccountService {
    /** 转账业务 */
    public boolean transfer(Integer fromId, Integer toId, Float money);
}

2. AccountServiceImpl 实现类

package com.itheima.service.impl;

import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import com.itheima.service.AccountService;

public class AccountServiceImpl implements AccountService {

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /** @事务
     * 转账业务 */
    @Override
    public boolean transfer(Integer fromId, Integer toId, Float money) {
        // 1. 查询原账户余额
        Account fromMoney = null;
        try {
            fromMoney = accountDao.queryMoney(fromId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (fromMoney.getMoney() < money) {
            throw new RuntimeException("账户余额不足,无法完成转账");
        }
        // 2. 查询被转入账户的余额
        Account toMoney = null;
        try {
            toMoney = accountDao.queryMoney(toId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // 3. 扣除原账户的money
        try {
            accountDao.updateMoney(fromId, fromMoney.getMoney() - money);
        } catch (Exception e) {
            throw new RuntimeException("扣除账户余额失败");
        }

        // 【*】显示抛出一个异常。【测试事务的原子性】
        //int a = 8 / 0;

        // 4. 添加被转入账户的余额
        try {
            accountDao.updateMoney(toId, toMoney.getMoney() + money);
        } catch (Exception e) {
            throw new RuntimeException("添加账户余额失败");
        }
        return true;
    }
}

7). spring核心配置文件applicationContext.xml

进行IOC控制反转、DI依赖注入、获取数据源等核心操作




    
    
        
    

    
        
        
    

    
    

    
    
        
        
    

    
    
        
    

    
    
        
    

    
    
        
        
        
        
    

    
    

8). 测试文件MyTest

测试转账案例,使用junit4框架进行测试

package com.itheima.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class MyTest {
    @Autowired
    private ApplicationContext ac;

    /** 转账测试 */
    @Test
    public void testTransfer() {
        AccountService service = (AccountService)ac.getBean("proxyAccountService");
        boolean res = service.transfer(4, 2, 10F);
        System.out.println(res);
    }
}
  1. 成功测试结果 成功完成转账
开启事务 --- 前置通知
提交事务 --- 后置通知
事务提交成功!
归还连接对象回连接池,将连接对象与当前线程解绑 --- 最终通知
true
  1. 显示抛出异常 事务成功回滚

将 AccountServiceImpl 实现类中【*】 下面的代码去掉前面的双斜杠注释

...
开启事务 --- 前置通知
...
Caused by: java.lang.ArithmeticException: / by zero
    at com.itheima.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:45)
    ... 37 more
回滚事务 --- 异常通知
事务提交失败,已经回滚!
归还连接对象回连接池,将连接对象与当前线程解绑 --- 最终通知
...

你可能感兴趣的:(07.Spring中的AOP【面向切面编程】)