-->

Spring框架-第三篇

2020-02-27 09:36发布

第一章:动态代理

1.1-转账案例

1.1.1-需求

账户A向账户B转账100元

  • 账户A减100
  • 账户B加100

1.1.2-数据库脚本

CREATE DATABASE  IF NOT EXISTS db1
USE db1
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(40),
money FLOAT
)CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO account(NAME,money) VALUES('A',1000);
INSERT INTO account(NAME,money) VALUES('B',1000);

1.1.3-环境搭建

Maven引入依赖包

  <dependencies>
    <!--junit-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <!--dbUtils-->
    <dependency>
      <groupId>commons-dbutils</groupId>
      <artifactId>commons-dbutils</artifactId>
      <version>1.4</version>
    </dependency>
    <!--c3p0-->
    <dependency>
      <groupId>com.mchange</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.5.2</version>
    </dependency>
    <!--spring-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.3.RELEASE</version>
    </dependency>
    <!--spring-test-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.3.RELEASE</version>
    </dependency>
    <!--mysql-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.26</version>
    </dependency>
  </dependencies>

实体类

/*
	账户实体类
*/
public class Account {
  private int id;
  private String name;
  private float money;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public float getMoney() {
    return money;
  }

  public void setMoney(float money) {
    this.money = money;
  }

  @Override
  public String toString() {
    return "Account{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", money=" + money +
            '}';
  }
}

持久层接口和实现类

接口

public interface IAccountDao {

  /**
   * 根据id查询一个数据
   * @param id
   * @return
   */
  Account findOne(int id);

  /**
   * 更新
   * @param account
   */
  void update(Account account);

}

实现类

public class AccountDaoImpl implements IAccountDao {
  private QueryRunner runner = null;
  public void setRunner(QueryRunner runner) {
    this.runner = runner;
  }

  @Override
  public Account findOne(int id) {
    try{
      String sql = "select * from account where id=?";
      return runner.query(sql,new BeanHandler<>(Account.class),id);
    }catch (Exception e){
      e.printStackTrace();
    }
    return null;
  }


  @Override
  public void update(Account account) {
    try{
      String sql = "UPDATE account SET NAME=?,money=? WHERE id=?";
      runner.update(sql,account.getName(),account.getMoney(),account.getId());
    }catch (Exception e){
      e.printStackTrace();
    }
  }


}

业务层接口和实现类

接口

public interface IAccountServices {
  /**
   * 根据id查询一个数据
   * @param id
   * @return
   */
  Account findOne(int id);

  /**
   * 更新
   * @param account
   */
  void update(Account account);


  /**
   * 【转账】
   * @param idOne 转账账户
   * @param idTwo 收账账户
   * @param money 转账钱数
   */
  void transfer(int idOne,int idTwo,float money);
}

实现类

public class AccountServicesImpl implements IAccountServices {
  private IAccountDao dao = null;
  public void setDao(IAccountDao dao) {
    this.dao = dao;
  }
  @Override
  public Account findOne(int id) {
    return dao.findOne(id);
  }
  @Override
  public void update(Account account) {
    dao.update(account);
  }
  @Override
  public void transfer(int idOne, int idTwo, float money) {
     // 根据id查找转账账户A
      Account one = dao.findOne(idOne);
      // 根据id查找收账账户B
      Account two = dao.findOne(idTwo);
      // 转账账户减钱
      one.setMoney(one.getMoney()-money);
      // 收账账户加钱
      two.setMoney(two.getMoney()+money);
      // 更新转账账户
      dao.update(one);
      // 更新收账账户
      dao.update(two);
  }

}

Spring的IOC配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.services.impl.AccountServicesImpl">
        <property name="dao" ref="accountDao"></property>
    </bean>
    <!--配置IAccountDao对象-->
    <bean id="accountDao" class="cn.lpl666.dao.impl.AccountDaoImpl">
        <property name="runner" ref="queryRunner"></property>
    </bean>
    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="c3p0"></constructor-arg>
    </bean>
    <!--配置数据源-->
    <bean id="c3p0" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入连接数据库必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/db1"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

1.1.4-测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class ClientTest {
  // 获取IAccountServices对象
  @Autowired
  private IAccountServices services = null;

  /**
   * 转账测试
   */
  @Test
  public void transfer(){
    services.transfer(1,2,100);
  }
}

1.1.5-问题分析

问题:若在转账的过程中发生异常(如突然宕机),则可能会造成数据库数据错误(数据库会出现脏数据)

异常伪代码

  @Override
  public void transfer(int idOne, int idTwo, float money) {
     // 根据id查找转账账户A
      Account one = dao.findOne(idOne);
      // 根据id查找收账账户B
      Account two = dao.findOne(idTwo);
      // 转账账户减钱
      one.setMoney(one.getMoney()-money);
      // 收账账户加钱
      two.setMoney(two.getMoney()+money);
      // 更新转账账户
      dao.update(one);
      int i = 10 / 0; // 【此处会产生异常】
      // 更新收账账户
      dao.update(two);
  }
// 结果:账户A减了钱,但账户B没有增加钱

原因

  • 本次业务多次操作数据库,但不是同步操作。
  • 同步操作,就是若出现异常,本次业务中的所有数据库操作都失效,这样在数据库中就不会现脏数据。

解决方案:事务操作

1.2-事务处理转账案例

需求、数据库脚本同上

1.2.1-环境搭建

实体类、Maven工程同上

数据库连接工具类

目的,在同一线程中保证数据库连接对象一致。

/**
 * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
 */
public class ConnectionUtils {
  private ThreadLocal<Connection> td = new ThreadLocal<>();
  private DataSource dataSource;
  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  /**
   * 获取当前线程上的连接对象
   * @return
   */
  public Connection getThreadConnection(){
    try {
      // 从当前线程获取连接
      Connection connection = td.get();
      // 检测是否存在
      if(connection==null){
        // 若不存在,则从数据源对象中获取一个连接
        connection = dataSource.getConnection();
        // 存入当前线程线程中
        td.set(connection);
      }
      return connection;
    }catch (Exception e){
      e.printStackTrace();
    }
    return null;
  }
  /**
   * 把连接对象和线程解绑
   */
  public void removeConnection(){
    td.remove();
  }
}

事务管理工具类

目的,封装事务操作(开启事务、提交事务、事务回滚)

/**
 * 事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
 */
public class TransactionManager {
  private ConnectionUtils connectionUtils;
  public void setConnectionUtils(ConnectionUtils connectionUtils) {
    this.connectionUtils = connectionUtils;
  }

  /**
   * 开启事务
   */
  public void beginTransaction(){
    try {
      connectionUtils.getThreadConnection().setAutoCommit(false);
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  /**
   * 提交事务
   */
  public void commit(){
    try {
      connectionUtils.getThreadConnection().commit();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  /**
   * 回滚事务
   */
  public void rollback(){
    try {
      connectionUtils.getThreadConnection().rollback();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  /**
   * 释放连接
   */
  public void  release(){
    try {
      connectionUtils.getThreadConnection().close();
      connectionUtils.removeConnection();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

持久层接口和实现类

接口

public interface IAccountDao {

  /**
   * 根据id查询一个数据
   * @param id
   * @return
   */
  Account findOne(int id);

  /**
   * 更新
   * @param account
   */
  void update(Account account);

}

实现类

public class AccountDaoImpl implements IAccountDao {
  private QueryRunner runner = null;
  public void setRunner(QueryRunner runner) {
    this.runner = runner;
  }
  // 数据库连接对象
  private ConnectionUtils connectionUtils;
  public void setConnectionUtils(ConnectionUtils connectionUtils) {
    this.connectionUtils = connectionUtils;
  }


  @Override
  public Account findOne(int id) {
    try{
      String sql = "select * from account where id=?";
      return runner.query(connectionUtils.getThreadConnection(),sql,new BeanHandler<>(Account.class),id);
    }catch (Exception e){
      e.printStackTrace();
    }
    return null;
  }


  @Override
  public void update(Account account) {
    try{
      String sql = "UPDATE account SET NAME=?,money=? WHERE id=?";
      runner.update(connectionUtils.getThreadConnection(),sql,account.getName(),account.getMoney(),account.getId());
    }catch (Exception e){
      e.printStackTrace();
    }
  }


}

业务层接口和实现类

接口

public interface IAccountServices {
  /**
   * 根据id查询一个数据
   * @param id
   * @return
   */
  Account findOne(int id);

  /**
   * 更新
   * @param account
   */
  void update(Account account);


  /**
   * 转账
   * @param idOne 转账账户
   * @param idTwo 收账账户
   * @param money 转账钱数
   */
  void transfer(int idOne,int idTwo,float money);
}

实现类

public class AccountServicesImpl implements IAccountServices {
  private IAccountDao dao = null;
  public void setDao(IAccountDao dao) {
    this.dao = dao;
  }
  // 事务管理对象
  private TransactionManager transactionManager = null;
  public void setTransactionManager(TransactionManager transactionManager) {
    this.transactionManager = transactionManager;
  }

  @Override
  public Account findOne(int id) { ;
    try{
      transactionManager.beginTransaction();
      Account account =  dao.findOne(id);
      transactionManager.commit();
      return account;
    }catch (Exception e){
      transactionManager.rollback();
      e.printStackTrace();
    }finally {
      transactionManager.release();
    }
    return null;
  }


  @Override
  public void update(Account account) {
    try{
      transactionManager.beginTransaction();
      dao.update(account);
      transactionManager.commit();
    }catch (Exception e){
      transactionManager.rollback();
      e.printStackTrace();
    }finally {
      transactionManager.release();
    }
  }


  @Override
  public void transfer(int idOne, int idTwo, float money) {
    try{
      // 开启事务
      transactionManager.beginTransaction();
      // 根据id查找转账账户
      Account one = dao.findOne(idOne);
      // 根据id查找收账账户
      Account two = dao.findOne(idTwo);
      // 转账账户减钱
      one.setMoney(one.getMoney()-money);
      // 收账账户加钱
      two.setMoney(two.getMoney()+money);
      // 更新转账账户
      dao.update(one);
      //int i = 2/0; //异常,转账失败,需要事务回滚
      // 更新收账账户
      dao.update(two);
      // 提交事务
      transactionManager.commit();
    }catch (Exception e){
      // 事务回滚
      transactionManager.rollback();
      e.printStackTrace();
    }finally {
      // 释放连接
      transactionManager.release();
    }
  }

}

Spring框架IOC配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.services.impl.AccountServicesImpl">
        <property name="dao" ref="accountDao"></property>
        <property name="transactionManager" ref="transactionManager"></property>
    </bean>
    <!--配置IAccountDao对象-->
    <bean id="accountDao" class="cn.lpl666.dao.impl.AccountDaoImpl">
        <property name="runner" ref="queryRunner"></property>
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
    <!--配置数据源-->
    <bean id="c3p0" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入连接数据库必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/db1"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
    <!--配置ConnectionUtils-->
    <bean id="connectionUtils" class="cn.lpl666.utils.ConnectionUtils">
        <!--注入数据源-->
        <property name="dataSource" ref="c3p0"></property>
    </bean>
    <!--配置TransactionManager-->
    <bean id="transactionManager" class="cn.lpl666.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

1.2.2-测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class ClientTest {
  // 获取IAccountServices对象
  @Autowired
  private IAccountServices services = null;

  /**
   * 转账测试
   */
  @Test
  public void transfer(){
    services.transfer(1,2,100);
  }
}

1.2.3-问题分析

  • 问题:在业务层中,多个业务中都用到了事务,此时事务操作重复,代码臃肿。
  • 原因:事务操作重复
  • 解决方案:使用动态代理增强方法,实现隔离业务方法中的事务操作,简化业务代码。

1.3-动态代理概述

原理:利用反射机制在运行时创建代理类。

特点:字节码即用即创建。

作用:可以不修改源码的基础上增强方法。

分类:①-基于接口的动态代理;②基于子类的动态代理

1.4-基于接口的动态代理

####1.4.1-概述

  • 涉及的类:Proxy
  • 提供者:JDK官方
  • 如何创建代理对象:使用Proxy类中的newProxyInstance方法
  • 创建代理对象的要求:被代理的类需要实现至少一个接口,否则无法使用。
  • newProxyInstance方法的参数
    • 参数1:ClassLoader loader,类加载器,用于加载代理对象的字节码,和被代理对象使用相同的加载器
    • 参数2:Class<?>[] interfaces,字节码数组,用于让代理对象和被代理对象有相同的方法。
    • 参数3:InvocationHandler 启用处理,用于提供增强代码。
      • 如何写代理代码,一般都是写一个该接口的实现类,通常情况下使用匿名内部类,用此接口的类都是谁用谁写

1.4.2-代码演示

需求

对Producer对象方法中的sellProduct方法增强(在不修改源码的基础上),增强业务是扣除20%的费用。

代码演示

IProducer接口

public interface IProducer {
  /**
   * 销售产品
   * @param money
   */
  void sellProduct(double money);

  /**
   * 售后服务
   * @param money
   */
  void sellService(double money);
}

Producer实现类

package cn.lpl666.proxy;
public class Producer implements IProducer {

    @Override
    public void sellProduct(double money) {
        System.out.println("销售产品:消费"+money);
    }

    @Override
    public void sellService(double money) {
        System.out.println("售后服务:消费" + money);
    }
}

测试-实现方法的增强

package cn.lpl666.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

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;
                double money = (double) args[0] * 0.8;
                if (method.getName().equals("sellProduct")) {
                  returnValue = method.invoke(producer, money);
                }
                return returnValue;
              }
            }
    );

    proxyProducer.sellProduct(10000);
  }
}

1.5-基于子类的动态代理

1.5.1-概述

  • 涉及的类:Enhancer
  • 提供者:第三方cglib库
  • 如何创建代理对象:使用Enhancer类中的create方法
  • 创建代理对象的要求:被代理的类不能是最终类
  • create方法的参数:
    • Class class,字节码,用于指定被代理对象的字节码
    • Callback callback,回调,用于实现增强方法
      • 如何写代理代码,一般都是写一个该接口的实现类(通常使用其子接口MethodInterceptor的实现类),通常情况下使用匿名内部类,用此接口的类都是谁用谁写

1.5.2-代码演示

Maven引入cglib库

  <dependencies>
    <!--cglib-->
    <!-- https://mvnrepository.com/artifact/cglib/cglib -->
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.3.0</version>
    </dependency>
  </dependencies>

代码

Producer类

public class Producer  {
    
    public void sellProduct(double money) {
        System.out.println("销售产品:消费"+money);
    }
    
    public void sellService(double money) {
        System.out.println("售后服务:消费" + money);
    }
}

测试

public class Client {
  public static void main(final String[] args) {
    final Producer producer = new Producer();
    Producer cglibProducer =(Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
      /**
       *
       * @param o  代理对象的引用
       * @param method  执行的方法
       * @param objects 执行的方法的参数
       * @param methodProxy  当前执行方法的代理对象
       * @return
       * @throws Throwable
       */
      @Override
      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object returnValue = null;
        double money = (double) objects[0] * 0.8;
        if (method.getName().equals("sellProduct")) {
          returnValue = method.invoke(producer, money);
        }
        return returnValue;
      }
    });
    cglibProducer.sellProduct(14000);
  }
}

1.6-动态代理简化转账案例

针对本章1.2中转账案例因事务重复操作而造成的代码臃肿问题,可以使用动态代理简化操作

1.6.1-环境搭建

业务层实现类代码

可以发现以下业务代码中,只有业务操作,而没有事务操作。业务层代码变得简洁干净不再臃肿。

public class AccountServicesImpl implements IAccountServices {
  private IAccountDao dao = null;
  public void setDao(IAccountDao dao) {
    this.dao = dao;
  }

  @Override
  public Account findOne(int id) {
    return dao.findOne(id);
  }

  @Override
  public void update(Account account) {
    dao.update(account);
  }


  @Override
  public void transfer(int idOne, int idTwo, float money) {
// 根据id查找转账账户
    Account one = dao.findOne(idOne);
    // 根据id查找收账账户
    Account two = dao.findOne(idTwo);
    // 转账账户减钱
    one.setMoney(one.getMoney() - money);
    // 收账账户加钱
    two.setMoney(two.getMoney() + money);
    // 更新转账账户
    dao.update(one);
    // int i = 2/0; //异常,转账失败,需要事务回滚
    // 更新收账账户
    dao.update(two);
  }

}

工厂类,提供业务层对象

因为要对象业务层对象中的方法实现增强

public class BeanFactory {
  // 账户业务层对象
  private IAccountServices accountServices;
  public final void setAccountServices(IAccountServices accountServices) {
    this.accountServices = accountServices;
  }
  // 事务管理对象
  private TransactionManager transactionManager;
  public void setTransactionManager(TransactionManager transactionManager) {
    this.transactionManager = transactionManager;
  }
  // 提供创建账户业务对象,内部实现调用对象方法时的增强
  public IAccountServices getAccountServices(){
    // 创建IAccountServices的代理对象,在业务方法执行时添加事务操作
    return (IAccountServices) Proxy.newProxyInstance(accountServices.getClass().getClassLoader(), accountServices.getClass().getInterfaces(), new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object returnValue = null;
        try{
          // 开启事务
          transactionManager.beginTransaction();
          // 处理业务
          returnValue=method.invoke(accountServices,args);
          // 提交事务
          transactionManager.commit();
        }catch (Exception e){
          // 事务回滚
          transactionManager.rollback();
          e.printStackTrace();
        }finally {
          // 释放连接
          transactionManager.release();
        }
        return returnValue;
      }
    });
  }
}

Spring框架的IOC配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置IAccountService对象-->
    <bean id="factoryIAccountService" factory-bean="beanFactory" factory-method="getAccountServices"></bean>
    <!--配置BeanFactory-->
    <bean id="beanFactory" class="cn.lpl666.factory.BeanFactory">
        <!--注入事务管理对象-->
        <property name="transactionManager" ref="transactionManager"></property>
        <!--注入AccountService对象-->
        <property name="accountServices" ref="accountService"></property>
    </bean>
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.services.impl.AccountServicesImpl">
        <property name="dao" ref="accountDao"></property>
    </bean>
    <!--配置IAccountDao对象-->
    <bean id="accountDao" class="cn.lpl666.dao.impl.AccountDaoImpl">
        <property name="runner" ref="queryRunner"></property>
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
    <!--配置数据源-->
    <bean id="c3p0" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入连接数据库必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/db1"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
    <!--配置ConnectionUtils-->
    <bean id="connectionUtils" class="cn.lpl666.utils.ConnectionUtils">
        <!--注入数据源-->
        <property name="dataSource" ref="c3p0"></property>
    </bean>
    <!--配置TransactionManager-->
    <bean id="transactionManager" class="cn.lpl666.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

1.6.2-测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class ClientTest {
  // 获取IAccountServices对象
  @Autowired
  @Qualifier("factoryIAccountService")
  private IAccountServices services = null;

  /**
   * 转账测试
   */
  @Test
  public void transfer(){
    services.transfer(1,2,100);
  }
}

第二章:Spring中的AOP

2.1-什么是AOP

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

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

2.2-AOP和OOP的区别

AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。

上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。

同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。

换而言之,OOD/OOP面向名词领域,AOP面向动词领域。

2.3-AOP相关术语

  • Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
  • Pointcut(切入点): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
  • Advice(通知/增强): 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
    • 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
  • Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。
  • Target(目标对象): 代理的目标对象。
  • Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。 spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
  • Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
  • Aspect(切面): 是切入点和通知(引介)的结合。

2.4-AOP开发步骤

2.4.1-开发阶段

  • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
  • 把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
  • 在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。

2.4.2-运行阶段

运行阶段由Spring 框架完成的

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

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

2.5-基于XML配置实现AOP

2.5.1-快速入门

需求

在业务层方法调用之前,实现记录日志操作(不修改业务层源代码的情况下)

Maven引入依赖包

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>
    <!--spring-context-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.3.RELEASE</version>
    </dependency>
    <!-- 【rg.aspectj/aspectjweaver】 -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.5</version>
    </dependency>

  </dependencies>

业务层接口和实现类

接口

public interface IAccountService {
  /**
   * 模拟保存账户
   */
  void saveAccount();

  /**
   * 模拟更新账户
   * @param i
   */
  void updateAccount(int i);

  /**
   * 删除账户
   * @return
   */
  int  deleteAccount();
}

实现类

public class AccountServiceImpl implements IAccountService {
  @Override
  public void saveAccount() {
    System.out.println("执行:保存了账户");
  }

  @Override
  public void updateAccount(int i) {
    System.out.println("执行:更新了账户,账户id:" + i);
  }

  @Override
  public int deleteAccount() {
    System.out.println("执行:删除了账户");
    return 0;
  }
}

日志工具类,提供切入点的公共代码

/**
 * 用于记录日志的工具类,提供了切入点的公共代码
 */
public class Logger {
  /**
   * 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务方法)
   */
  public  void printLog(){
    System.out.println("Logger类中的printLog方法开始记录日志了...");
  }
}

配置步骤

       【实现步骤】
            1. 把通知Bean也交给spring来管理
            2. 使用aop:config标签表明开始AOP的配置
            3. 使用aop:aspect标签表明配置切面
                 id属性:是给切面提供一个唯一标识
                 ref属性:是指定通知类bean的Id。
            4. 在aop:aspect标签的内部使用对应标签来配置通知的类型
               我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
               aop:before:表示配置前置通知
                    method属性:用于指定Logger类中哪个方法是前置通知
                    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
            5.切入点表达式的写法:
                关键字:execution(表达式)
                表达式:访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)
                标准的表达式写法:
                    public void cn.lpl666.service.impl.AccountServiceImpl.saveAccount()
                访问修饰符可以省略
                    void cn.lpl666.service.impl.AccountServiceImpl.saveAccount()
                返回值可以使用通配符,表示任意返回值
                    * cn.lpl666.service.impl.AccountServiceImpl.saveAccount()
                包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
                    * *.*.*.*.AccountServiceImpl.saveAccount())
                包名可以使用..表示当前包及其子包
                    * *..AccountServiceImpl.saveAccount()
                类名和方法名都可以使用*来实现通配
                    * *..*.*()
                参数列表:
                    可以直接写数据类型:
                        基本类型直接写名称           int
                        引用类型写包名.类名的方式   java.lang.String
                    可以使用通配符表示任意类型,但是必须有参数
                    可以使用..表示有无参数均可,有参数可以是任意类型
                全通配写法:
                    * *..*.*(..)

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

Spring框架配置AOP

<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger对象-->
    <bean id="logger" class="cn.lpl666.utils.Logger"></bean>
    <!--AOP配置-->
    <aop:config>
        <!--切面配置-->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(* cn.lpl666.service.impl.*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

测试

public class ClientTest {
  public static void main(String[] args) {
    // 创建容器对象
    ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    // 创建业务层对象
    IAccountService accountService = (IAccountService)ac.getBean("accountService");
    accountService.saveAccount();
    accountService.deleteAccount();
  }
}

执行结果

Logger类中的printLog方法开始记录日志了...
执行:保存了账户
Logger类中的printLog方法开始记录日志了...
执行:删除了账户

2.5.2-AOP配置标签

aop:config:

作用:用于声明开始 aop 的配置

<aop:config>
	<!-- 配置的代码都写在此处 -->
</aop:config>

使用 aop:aspect 配置切面

作用: 用于配置切面。

属性:

  • id:给切面提供一个唯一标识。
  • ref:引用配置好的通知类 bean 的 id。
<!--切面配置-->
<aop:aspect id="logAdvice" ref="logger">
    <!--配置通知的类型要写在此处-->
</aop:aspect>

使用 aop:pointcut 配置切入点表达式

作用: 用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。

属性:

  • expression:用于定义切入点表达式。
  • id:用于给切入点表达式提供一个唯一标识
<aop:pointcut id="serviceImpl" expression="execution(* cn.lpl666.service.impl.*.*(..))"></aop:pointcut>

2.5.3-通知类型

概述

代码演示

日志工具类,提供切入点的公共代码

/**
 * 用于记录日志的工具类,提供了切入点的公共代码
 */
public class Logger {
  /**
   * 前置通知
   */
  public  void beforePrintLog(){
    System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 后置通知
   */
  public  void afterReturningPrintLog(){
    System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 异常通知
   */
  public  void afterThrowPrintLog(){
    System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 最终通知
   */
  public  void afterPrintLog(){
    System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
  }

}

SpringAOP配置

<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger对象-->
    <bean id="logger" class="cn.lpl666.utils.Logger"></bean>
    <!--AOP配置-->
    <aop:config>
        <aop:pointcut id="serviceImpl" expression="execution(* cn.lpl666.service.impl.*.*(..))"></aop:pointcut>
        <!--切面配置-->
        <aop:aspect id="logAdvice" ref="logger">
              <!--
				【通知类型标签的属性】
                    method:指定通知中方法的名称。
                    pointct:定义切入点表达式
                    pointcut-ref:指定切入点表达式的引用
			-->
            <!-- 前置通知,并且建立通知方法和切入点方法的关联-->
            <aop:before method="beforePrintLog" pointcut-ref="serviceImpl"></aop:before>
            <!-- 后置通知,并且建立通知方法和切入点方法的关联-->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="serviceImpl"></aop:after-returning>
            <!-- 异常通知,并且建立通知方法和切入点方法的关联-->
            <aop:after-throwing method="afterThrowPrintLog" pointcut-ref="serviceImpl"></aop:after-throwing>
            <!-- 最终通知,并且建立通知方法和切入点方法的关联-->
            <aop:before method="afterPrintLog" pointcut-ref="serviceImpl"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

测试

public class ClientTest {
  public static void main(String[] args) {
    ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    IAccountService accountService = (IAccountService)ac.getBean("accountService");
    accountService.saveAccount();
  }
}

运行结果

前置通知:Logger类中的printLog方法开始记录日志了...
最终通知:Logger类中的printLog方法开始记录日志了...
执行:保存了账户
后置通知:Logger类中的printLog方法开始记录日志了...

2.5.4-环绕通知

Logger日志的工具类,提供了切入点的公共代码-环绕通知

/**
 * 用于记录日志的工具类,提供了切入点的公共代码
 */
public class Logger {
  /**
   * 环绕通知
   * 问题:当配置了环绕通知,通知方法执行了,但切面方法没有执行
   * 分析:通过对以往动态代理的对比,动态代理的环绕通知中有调用切面的方法,而此方法没有调用
   * 解决:
   *      Spring框架提供了一个接口,ProceedingJoinPoint。该接口中有一个方法proceed(),此方法相当于明确调用切入点的方法。
   *      该接口可以作为环绕通知方法的参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用
   */
  public void aroundPrintLog(ProceedingJoinPoint pro){
    try{
      Object[] args = pro.getArgs(); // 得到执行方法所需要的参数
      System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
      pro.proceed(args); // 调用业务层方法
      System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
    }catch (Throwable throwable){
      System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
    }finally {
      System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
    }
  }


}

Spring框架配置

<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--配置IAccountService对象-->
    <bean id="accountService" class="cn.lpl666.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger对象-->
    <bean id="logger" class="cn.lpl666.utils.Logger"></bean>
    <!--AOP配置-->
    <aop:config>
        <aop:pointcut id="serviceImpl" expression="execution(* cn.lpl666.service.impl.*.*(..))"></aop:pointcut>
        <!--切面配置-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--环绕通知-->
            <aop:around method="aroundPrintLog" pointcut-ref="serviceImpl"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

测试

public class ClientTest {
  public static void main(String[] args) {
    ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    IAccountService accountService = (IAccountService)ac.getBean("accountService");
    accountService.saveAccount();
  }
}

结果

前置通知:Logger类中的printLog方法开始记录日志了...
执行:保存了账户
后置通知:Logger类中的printLog方法开始记录日志了...
最终通知:Logger类中的printLog方法开始记录日志了...

2.6-基于注解实现AOP

2.6.1-实现方式1

业务层实现类代码

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
  @Override
  public void saveAccount() {
    System.out.println("执行:保存了账户");
    // int i = 10/0;
  }

  @Override
  public void updateAccount(int i) {
    System.out.println("执行:更新了账户,账户id:" + i);
  }

  @Override
  public int deleteAccount() {
    System.out.println("执行:删除了账户");
    return 0;
  }
}

Logger工具类实现,提供了切入点的公共代码

/**
 * 用于记录日志的工具类,提供了切入点的公共代码
 */
@Component("logger")
@Aspect // 表示这是一个切面类
public class Logger {
  // 切面类所关联的对应包的表达式
  @Pointcut("execution(* cn.lpl666.service.impl.*.*(..))")
  public void serviceImpl(){}
  /**
   * 前置通知
   */
  @Before("serviceImpl()")
  public  void beforePrintLog(){
    System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 后置通知
   */
  @AfterReturning("serviceImpl()")
  public  void afterReturningPrintLog(){
    System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 异常通知
   */
  @AfterThrowing("serviceImpl()")
  public  void afterThrowPrintLog(){
    System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 最终通知
   */
  @After("serviceImpl()")
  public  void afterPrintLog(){
    System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
  }
  //@Around("serviceImpl()")
  public void aroundPrintLog(ProceedingJoinPoint pro){
    try{
      Object[] args = pro.getArgs(); // 得到执行方法所需要的参数
      System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
      pro.proceed(args); // 调用业务层方法
      System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
    }catch (Throwable throwable){
      System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
    }finally {
      System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
    }
  }


}

Spring框架xml配置

<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 告知 spring 创建容器时要扫描的包 -->
    <context:component-scan base-package="cn.lpl666"/>
    <!--Spring开启注解AOP-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

测试类

public class ClientTest {
  public static void main(String[] args) {
    ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    IAccountService accountService = (IAccountService)ac.getBean("accountService");
    accountService.saveAccount();
  }
}

执行结果

前置通知:Logger类中的printLog方法开始记录日志了...
执行:保存了账户
最终通知:Logger类中的printLog方法开始记录日志了...
后置通知:Logger类中的printLog方法开始记录日志了...

2.6.2-实现方式2-纯注解

移除xml配置文件

Logger工具类

/**
 * 用于记录日志的工具类,提供了切入点的公共代码
 */
@Component("logger")
@Aspect // 表示这是一个切面类
public class Logger {
  // 切面类所关联的对应包的表达式
  @Pointcut("execution(* cn.lpl666.service.impl.*.*(..))")
  public void serviceImpl(){}
  /**
   * 前置通知
   */
  @Before("serviceImpl()")
  public  void beforePrintLog(){
    System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 后置通知
   */
  @AfterReturning("serviceImpl()")
  public  void afterReturningPrintLog(){
    System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 异常通知
   */
  @AfterThrowing("serviceImpl()")
  public  void afterThrowPrintLog(){
    System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 最终通知
   */
  @After("serviceImpl()")
  public  void afterPrintLog(){
    System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
  }
  /**
   * 环绕通知
   * 问题:当配置了环绕通知,通知方法执行了,但切面方法没有执行
   * 分析:通过对以往动态代理的对比,动态代理的环绕通知中有调用切面的方法,而此方法没有调用
   * 解决:
   *      Spring框架提供了一个接口,ProceedingJoinPoint。该接口中有一个方法proceed(),此方法相当于明确调用切入点的方法。
   *      该接口可以作为环绕通知方法的参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用
   */
  //@Around("serviceImpl()")
  public void aroundPrintLog(ProceedingJoinPoint pro){
    try{
      Object[] args = pro.getArgs(); // 得到执行方法所需要的参数
      System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
      pro.proceed(args); // 调用业务层方法
      System.out.println("后置通知:Logger类中的printLog方法开始记录日志了...");
    }catch (Throwable throwable){
      System.out.println("异常通知:Logger类中的printLog方法开始记录日志了...");
    }finally {
      System.out.println("最终通知:Logger类中的printLog方法开始记录日志了...");
    }
  }


}

Spring配置类

@Configuration    // 表示该类是配置类
@ComponentScan("cn.lpl666")  // spring容器要扫描的包
@EnableAspectJAutoProxy   // 启用Aop注解
public class SpringConfiguration {
}

测试

public class App 
{
    public static void main(String[] args) {
        
        // 创建容器对象
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        // 创建业务对象
        IAccountService accountService = (IAccountService)ac.getBean("accountService");
        accountService.saveAccount();
    }
}

2.6.3-注解步骤

第一步:把通知类也使用注解配置

第二步:在通知类上使用@Aspect 注解声明为切面

第三步:在增强的方法上使用注解配置通知

  • @Before("切入点表达式")
  • @AfterReturning("切入点表达式")
  • @AfterThrowing("切入点表达式")
  • @After("切入点表达式")
  • @Around("切入点表达式")

第四步:切入点表达式注解

  • @Pointcut
    • 作用:指定切入点表达式
    • 属性:value,指定表达式的内容
  // 切面类所关联的对应包的表达式
  @Pointcut("execution(* cn.lpl666.service.impl.*.*(..))")
  public void serviceImpl(){}
  /**
   * 前置通知
   */
  @Before("serviceImpl()") 
  public  void beforePrintLog(){
    System.out.println("前置通知:Logger类中的printLog方法开始记录日志了...");
  }
标签: