Spring 学习笔记 事务 动态代理 AOP

一、转账业务存在的问题

首先新建项目,选择 webapps 骨架,在 pom.xml 中导入 jar 包依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.jia</groupId>
  <artifactId>AOPDemo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>AOPDemo Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>commons-dbutils</groupId>
      <artifactId>commons-dbutils</artifactId>
      <version>1.4</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>

    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>AOPDemo</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

接下来把账户 Account 相关的代码拷贝过来:
在这里插入图片描述
在 resources 包中新建 bean.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: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">

<!--使用注解,需要告知Spring 在创建容器时需要扫描的包-->
<context:component-scan base-package="com.jia">
</context:component-scan>


    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="jdpy1229jiajia"></property>
    </bean>


</beans>

在这里插入图片描述

在业务类接口中新增转账方法:

 /**
     * 转账
     * @param sourceName 转出账户
     * @param targetName 转入账户
     * @param money 转账金额
     */
    void transfer(String sourceName,String targetName,float money);

转账需要 6 个步骤:

 1.根据名称查询转入账户
 2.根据名称查询转出账户
 3.转出账户减钱
 4.转入账户加钱
 5.更新转出账户
 6.更新转入账户

因此需要在 IAccountDao 中新建一个 根据名称查询用户的方法:

 /**
     * 根据名称查询账户
     * @param accountName
     * @return 如果有唯一结果就返回,如果没有结果就返回 null
     *         如果结果集个数超过 1 ,就抛异常
     */
    Account findAccountByNames(String accountName);

实现:

 @Override
    public Account findAccountByNames(String accountName) {
        try{
            List<Account> accounts=runner.query("select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
        if(accounts==null||accounts.size()==0)
            return null;
        if(accounts.size()>1)
            throw new RuntimeException("结果集不唯一,数据有问题");
        return accounts.get(0);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

完善转账方法:

   @Override
    public void transfer(String sourceName,String targetName,float money)
    {
        //1.根据名称查询转入账户
        Account source=accountDao.findAccountByNames(sourceName);
       //2.根据名称查询转出账户
        Account target=accountDao.findAccountByNames(targetName);
        //3.转出账户减钱
        source.setMoney(source.getMoney()-money);
        //4.转入账户加钱
        target.setMoney(target.getMoney()+money);
        //5.更新转出账户
        accountDao.updateAccount(source);
        //6.更新转入账户
        accountDao.updateAccount(target);
    }

测试转账方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccoutServiceTest {

 @Autowired
    private IAccountService as;
    
        @Test
    public void testTransfer()
        {
           as.transfer("aaa","bbb",100);
        }
    }

转账成功:
在这里插入图片描述
但是如果在更新转入与转出之间加上 1/0:

//5.更新转出账户
        accountDao.updateAccount(source);
        
        int i=1/0;
        
        //6.更新转入账户
        accountDao.updateAccount(target);

执行测试会报异常,而数据 不满足一致性原则:
在这里插入图片描述

    这是因为转账的步骤中1.2.5.6 各获取了一次连接,这样相当于有 4 次 Connection,各自是一次事务,这样在 5 和 6 之间出现了异常,会导致 5 提交了 而 6 未提交。解决方法——使用 ThreadLocal 对象把 Connectrion 和当前线程绑定,从而使一个线程中只有一个能控制事务的对象,这样多次操作就使用一个事务,可以保证一致性、原子性。
    在 utils 包下新建一个 ConnectionUtils ,用于获取连接,并实现和线程的绑定:

package com.jia.utils;

//连接的工具类,它用于从数据源中获取一个连接,
//并且实现和线程的绑定
public class ConnectionUtils
{
    public ConnectionUtils()
    {}

    private ThreadLocal<Connection> tl=new ThreadLocal<>();

    //由 Spring 容器注入,需要提供 set 方法
    private DataSource dataSource;
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程的连接
     * @return
     */
    public Connection getThreadConnection()  {
        //1.先从 ThreadLocal 上获取
        Connection connection=tl.get();
        //2.判断当前线程上是否有连接
        if(connection==null)
        {     
        //3.从数据源中获取连接,并且绑定到线程上,即存在 ThreadLocal 中
            try {
                connection=dataSource.getConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            tl.set(connection);
        }
        //返回当前线程上的连接
        return connection;
    }

    接下来写管理事务的工具类:

package com.jia.utils;

//和事务管理相关的工具类
//包含 开启事务、提交事务、回滚事务、释放连接
public class TranscationManager {

    //由 Spring 容器注入,需要提供 set 方法
    private ConnectionUtils connectionUtils;
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    //开启事务
    public void beginTranscation()
    {
        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();
            
/*
需要在 ConnectionUtils 类中提供 removeConnection 方法:
 public void removeConnection()
    {
        tl.remove();
    }
*/
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

接下来修改 AccountServiceImpl 类中的代码,如:

 @Override
    public List<Account> findAllAccount() {
        try {
           //1.开启事务
            transcationManager.beginTranscation();
            //2.执行操作
            List<Account> accounts=accountDao.findAllAccount();
            //3.提交事务
            transcationManager.commit();
            //4.返回结果
            return accounts;
        } catch (Exception e)
        {
            //5.回滚操作
           transcationManager.rollback();
        }
        finally {
            //6.释放连接
            transcationManager.release();
        }
        return null;
        }

转账的方法修改为:

@Override
public void transfer(String sourceName,String targetName,float money)
    {
        try {
        //1.开启事务
        transcationManager.beginTranscation();
        //2.执行操作
        //2.1 根据名称查询转入账户
        Account source = accountDao.findAccountByNames(sourceName);
        //2.2 根据名称查询转出账户
        Account target = accountDao.findAccountByNames(targetName);
        //2.3 转出账户减钱
        source.setMoney(source.getMoney() - money);
        //2.4 转入账户加钱
        target.setMoney(target.getMoney() + money);
        //2.5 更新转出账户
        accountDao.updateAccount(source);
        int i = 1 / 0;
        //2.6 更新转入账户
        accountDao.updateAccount(target);    
        //3.提交事务
        transcationManager.commit();
    }
    catch (Exception e)
    {
        //4.回滚事务
        transcationManager.rollback();
       e.printStackTrace();
    }
    finally {
        //5.释放连接
        transcationManager.release();
      }
    }

现在不希望从 QueryRunner 中获取连接了,所以需要把 “注入数据源” 去掉:

<!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--需要去掉:注入数据源>
        <property name="dataSource" ref="dataSource"> </property-->
    </bean>

然后把

  //由 Spring 容器注入,需要提供 set 方法
  private ConnectionUtils connectionUtils;
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

每个方法都需要传入 线程绑定的连接作为参数,如:

runner.query(connectionUtils.getThreadConnection(),
"select * from accounts",new BeanListHandler<Account>(Account.class));

在 bean.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
        
  <!--配置Service-->
    <bean id="accountService" class="com.jia.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao">
        </property>

        <!--注入事务管理器-->
        <property name="transcationManager" ref="transcationManager">
        </property>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.jia.dao.impl.AccountDaoImpl">
        <!--注入QueryRunner-->
        <property name="runner" ref="runner"></property>
        <!--注入 ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
    
    <!--配置 Connection 工具类-->
    <bean id="connectionUtils" class="com.jia.utils.ConnectionUtils">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"> </property>
    </bean>

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

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="jdpy1229jiajia"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="transcationManager" class="com.jia.utils.TranscationManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

进行测试:

 @Autowired
    private IAccountService as;

        @Test
    public void testTransfer()
        {        as.transfer("aaa","bbb",200);  }

测试通过,而且 5 和 6 之间有运算异常,而执行结果是符合预期的,进行了事务控制:
在这里插入图片描述

二、动态代理

这样写的话,方法之间的依赖性很强,而且代码重复度很高,这是我们不希望看到的,应当改变,复习一下动态代理:

  • 动态代理的特点:
        字节码随用随创建,随用随加载

  • 动态代理的作用:
        不修改源码的基础上,增强方法

    新建工程 DynamicProxy ,不选择骨架。
提供生产者接口:

  package com.jia.proxy;

public interface IProducer {
    //出售产品
    public void saleProduct(float money);

    //提供售后服务
    public void afterService(float money);
}

实现接口:

package com.jia.proxy;

//生产者
public class Producer implements IProducer {
    //出售产品
    public void saleProduct(float money)
    {
        System.out.println("销售产品,并拿到钱:"+money);
    }
    //提供售后服务
    public void afterService(float money)
    {
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

消费者:

  • 动态代理的分类:
1.基于接口的

涉及的类
    Proxy
提供者
    JDK 官方
创建代理对象
使用 Proxy 类中的 newProxyInstance 方法,
    要求:被代理类至少实现一个接口,否则不能使用
    newProxyInstance 的参数:
①ClassLoader 类加载器
    用于加载代理对象字节码,和被代理对象使用相同的类加载器
②Class[ ] 字节码数组
    用于让代理对象和被代理对象有相同方法
③InvocationHandler 用于提供增强的代码
    让写如何代理的,一般写该接口的实现类,通常情况下都是匿名内部类

package com.jia.proxy;

//模拟一个消费者
public class Client {

    public static void main(String[] args) {
        final Producer producer=new Producer();
     //消费者付10000万,就可以购得电脑
     producer.saleProduct(10000);

IProducer proxyProducer=(IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {

            //执行被代理对象的任何接口方法都会经过该方法,即该方法有拦截的功能
            /**
             *
             * @param proxy 代理对象的引用,一般不用
             * @param method 当前执行的方法
             * @param args 当前执行方法所需的参数
             * @return 和被代理对象有相同的返回值
             * @throws Throwable
             */
public Object invoke(Object proxy, Method method, Object[] args) 
throws Throwable {
                //提供增强的代码
                Object returnValue=null;
               //1.获取方法执行的参数
                Float money=(Float)args[0];

                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName()))

                 //经销商拿到两成利润,对于生产者来说,只能拿到八成的钱
                 returnValue=method.invoke(producer,money*(float)0.8);
                return returnValue;
            }
        });
proxyProducer.saleProduct(10000);
    }
}

运行结果:
在这里插入图片描述
那如果类没有实现接口的话,就应当考虑基于子类的动态代理,需要第三方库支持,导入坐标:

  <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_3</version>
        </dependency>
2.基于子类的

涉及的类:
    Enhoncer
提供者:
    第三方 cglib 库
创建代理对象:
使用 Enhoncer 类的 create 方法
    要求:被代理类不能是 final 类
Enhoncer 的 create 方法,有 2 个参数:
①class 类型:
    指定被代理对象的指定字节码
②callback 类型:
    用于实现方法的增强,一般是该接口的子接口实现类:
MethodInterceptor

    package com.jia.cglib;

//模拟一个消费者
public class Client {
    public static void main(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
             */
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object returnValue=null;
                //1.获取方法执行的参数
                Float money=(Float)objects[0];

                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName()))

                    //对于生产者来说,只能拿到八成的钱
                 returnValue=method.invoke(producer,money*(float)0.8);
                return returnValue;
            }
        });
cglibproducer.saleProduct(12000);
  }
}

运行结果:
在这里插入图片描述
    之前说 Connection 的 close 方法不能真正关闭连接,只是把连接还回池中,那就可以使用动态代理的方式,对 该方法进行增强,把它添加回池里。

    回到转账业务中,需要先提供一个 BeanFactory——即 用于创建 Service 的代理对象的工厂:

 package com.jia.factory;

public class BeanFactory {
    //由 Spring 容器注入,需要提供 set 方法
    private TranscationManager transcationManager;
    public final void setTranscationManager(TranscationManager transcationManager) {
        this.transcationManager = transcationManager;
    }


    //由 Spring 容器注入,需要提供 set 方法
    private IAccountService accountService;
    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 获取 Service 代理对象
     * @return
     */
    public IAccountService getAccountService()
    {
    return  (IAccountService)  Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.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 rtValue=null;
                        try {
                        //1.开启事务
                        transcationManager.beginTranscation();
                        //2.执行操作
                       method.invoke(accountService,args);
                        //3.提交事务
                        transcationManager.commit();
                        //4.返回结果
                        return rtValue;
                    } catch (Exception e)
                    {
                        //5.回滚操作
                        transcationManager.rollback();
                    } finally {
                        //6.释放连接
                        transcationManager.release();
                    }
                  return null;}
                });
    }
}

    那么 AccountServiceImpl 类中也就不再需要 TranscationManager 类型的引用 ,将它及 set 方法删除即可。因为已经都由代理对象控制了,这样事务控制和业务层的方法就进行了真正的分离。
    接下来修改 bean.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--使用注解,需要告知Spring 在创建容器时需要扫描的包-->
<!--context:component-scan base-package="com.jia">
</context:component-scan-->

    <!--配置代理的Service,使用实例工厂的方式进行配置-->
    <bean id="proxyAccountService" factory-bean="beanFactory"
          factory-method="getAccountService">
    </bean>

<!--配置 BeanFactory-->
    <bean id="beanFactory" class="com.jia.factory.BeanFactory">
        <!--注入Service-->
        <property name="accountService" ref="accountService">
        </property>
        <!--注入事务管理器-->
        <property name="transcationManager" ref="transcationManager">
        </property>
    </bean>

    <!--配置Service-->
    <bean id="accountService" class="com.jia.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao">
        </property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.jia.dao.impl.AccountDaoImpl">
        <!--注入QueryRunner-->
        <property name="runner" ref="runner"></property>
        <!--注入 ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
        
    <!--配置 Connection 工具类-->
    <bean id="connectionUtils" class="com.jia.utils.ConnectionUtils">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"> </property>
    </bean>

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

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="jdpy1229jiajia"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="transcationManager" class="com.jia.utils.TranscationManager">
<property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

</beans>

编写测试方法,因为这时 bean.xml 中配置了两个类型相同的 IAccountService 的实现类,所以需要使用 @Qualifier 注解指定名称 :

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

        @Test
    public void testTransfer()
        {
           as.transfer("aaa","bbb",400);
        }

测试通过。

三、AOP

AOP 的作用和优势:

  • 作用:
    在程序的运行期间,不修改源码对已有方法进行增强。
  • 优势:
    减少重复代码
    提高开发效率
    维护方便

Spring 的 AOP 是通过配置的方式实现功能。

AOP 相关概念
  • 连接点:
    那些被拦截到的点,在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。比如转帐项目中,业务层的所有方法都是连接点:
    在这里插入图片描述

  • 切入点:
    那些被增强的方法,所有的切入点都是连接点,但是连接点不一定是切入点

  • 通知(/增强):
    拦截到连接点所要做的事情。
    通知的类型:
    前置通知,后置通知,异常通知,最终通知,环绕通知。

    比如 该项目中,动态代理的 invoke 方法中会拦截被代理对象的所有方法,拦截到方法后 需要 开启事务、执行操作、提交事务 等事务支持。

    在 invoke 方法中,在 method.invoke 之前执行的就是前置通知,在 method.invoke 之后执行的就是 后置通知, catch( )中的是是 异常通知,finally 中的是 最终通知。

    环绕通知:是指整个invoke 方法在执行。环绕通知中会有明确的切入点方法调用。
在这里插入图片描述

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

  • target 目标对象:
    被代理的对象 。

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

切面:
切入点和通知的结合。

学习 Spring 中的 AOP 要明确的方法:
1. 开发阶段
编写核心业务代码(开发主线)
把公用代码抽取出来,制作成通知,(开发阶段最后再做)
在配置文件中,声明切入点 和 通知之间的关系,即切面。
2. 运行阶段(Spring 框架完成的)
Spring 框架监控切入点方的执行。一旦监控到切入点方法被执行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

新建一个模板 SpringAOP:
先在 pom.xml 中导入 jar 包依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>

    <!--解析切入点表达式-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.7</version>
    </dependency>

</dependencies>

提供一个业务层接口,分别写 无返回值无参、无返回值有参、有返回值无参 的 三个方法:

package Service;
/*
* 账户的业务层接口 
 */

public interface IAccountService1 {
    
    //模拟保存账户
    void saveAccount();
    
    //模拟更新账户
    void updateAccount(int i);
    
    //删除账户
    int deleteAccount();
}

简单实现这个接口:

package Service;

//账户的业务层实现类
public class AccountServiceImpl1 implements IAccountService1 {

    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("执行了更新");
    }

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

在 utils 包中新建 Logger 类,用于记录日志的工具类,提供公共的代码:

package Utils;

/*
用于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {

    //用于打印日志,计划让其在切入点方法执行之前执行
    //(切入点 即 业务方法)
    public void printLog()
    {
        System.out.println("Logger 类中的 prinLOg 方法开始记录日志");
    }
}

写 bean.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 中基于XML的AOP配置步骤-->

    <!--1、把通知Bean 也交给 Spring来管理-->
    <!-- 配置 Srping 的 IOC ,把 service 对象配置进来-->
    <bean id="accountService" class="Service.impl.AccountServiceImpl1"></bean>
    <!-- 配置Logger类 -->
    <bean id="logger" class="Utils.Logger"></bean>


    <!-- 2、使用aop:config标签表明开始AOP的配置-->
    <!--配置AOP-->
    <aop:config>


    <!--3、使用aop:aspect标签表明配置切面
             id属性:是给切面提供一个唯一标识,这里是日志的通知
             ref属性:是指定通知类bean的Id。-->
        <aop:aspect id="logAdvice" ref="logger">


    <!--- 4、在aop:aspect标签的内部使用对应标签来配置通知的类型
            我们现在示例是让 printLog 方法在切入点方法执行之前之前,所以是前置通知
            前置通知 使用 aop:before:表示配置前置通知
            method属性:用于指定 Logger 类中哪个方法是前置通知
            pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强-->
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
           
            <aop:before method="printLog" pointcut="execution( public void Service.impl.AccountServiceImpl1.saveAccount( )))"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>
  • 切入点表达式的写法:
    关键字:execution (表达式)
    表达式是由以下部分组成:
 访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)

如:标准的表达式写法:

  public void Service.impl.AccountServiceImpl.saveAccount( ) 

这时编写测试类,看效果:

package com.jia.test;

public class aopTest {
    //1.获取容器
ApplicationContext ac=new ClassPathXmlApplicationContext("bean1.xml");

    //2.获取对象
    IAccountService1 as=(IAccountService1) ac.getBean("accountService");

    @Test
    public void test()
    {
        //3.执行方法,记录日志
        as.saveAccount();
    }
}

运行结果:
在这里插入图片描述
如果在测试方法中新增:

  as.deleteAccount();
  as.updateAccount(1);

因为在切点表达式子中只有
pointcut="execution( public void Service.impl.AccountServiceImpl1.saveAccount( )))"
所以只有 saveAccount 方法会被增强,运行结果如下:
在这里插入图片描述
那如果想对所有方法进行增强,就要考虑 切入点表达式除了标准写法之外,其他写法:

  • 全通配写法
 - *..*.*(..)

这样的话所有方法之前都进行了增强。
在这里插入图片描述

  • 访问修饰符可以省略
 void Service.impl.AccountServiceImpl1.saveAccount( )
  • 返回值可以使用通配符,表示任意返回值
    如:
 - Service.impl.AccountServiceImpl1.saveAccount( )
  • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 *
   *.*.*.AccountServiceImpl1.saveAccount())
  • 包名可以使用, ..表示当前包及其子包
 - *..AccountServiceImpl1.saveAccount()
  • 类名和方法名都可以使用*来实现通配
* *..*.*( )
 <!--类名和方法名都可以使用通配符,这时无参数的方法会被增强-->
 <aop:before method="printLog" pointcut="execution(* *..*.*( ))"></aop:before>

在这里插入图片描述

  • 参数列表:
    (1)可以直接写数据类型:
    基本类型直接写名称 int
    引用类型写包名.类名的方式 java.lang.String
    (2)可以使用通配符表示任意类型,但是必须有参数
    (3)可以使用…表示有无参数均可,有参数可以是任意类型

✨ 实际开发中切入点表达式的通常写法:✨
切到业务层实现类下的所有方法

* Service.impl.*.*(..)

四种常用的通知类型:
       新建一个工程 AOPAdviceType,不使用 骨架,并把之前的 Service 包 和 Utils 包下的内容拷贝过来,先在 pom.xml 中导入依赖:

  <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
    </dependencies>

编写 Logger 类:

package Utils;

/*
用于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {
    //前置通知
    public void BeforePrintLog()
    {
        System.out.println("Logger 类中的 BeforePrintLog()");
    }
    //后置通知
    public void AfterReturningPintLog()
    {
        System.out.println("Logger 类中的 AfterPintLog()");
    }      
    //异常通知
    public void AfterThrowingPrintLog()
    {
        System.out.println("Logger 类中的 AfterThrowingPrintLog()");
    }
    //最终通知
    public void finalPrintLog()
    {
        System.out.println("Logger 类中的finalPrintLog()");
    }
}

编写测试方法:

package com.jia.test;

public class AOPTest {
    //1.获取容器
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");

    //2.获取对象
    IAccountService1 as=(IAccountService1) ac.getBean("accountService");

    @Test
    public void test()
    {
        //3.执行方法,记录日志
        as.saveAccount();
    }
}

在 bean.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">

    <!-- 配置 Srping 的 IOC ,把 service 对象配置进来-->
    <bean id="accountService" class="Service.impl.AccountServiceImpl1"></bean>
    <!-- 配置Logger类 -->
    <bean id="logger" class="Utils.Logger"></bean>


    <!--配置AOP-->
    <aop:config>
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知-->
            <aop:before method="BeforePrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:before>

            <!--配置后置通知-->
            <aop:after-returning method="AfterReturningPintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after-returning>

            <!--配置异常通知-->
            <aop:after-throwing method="AfterThrowingPrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after-throwing>

            <!--配置最终通知-->
            <aop:after method="finalPrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

运行结果:
在这里插入图片描述
在 saveAccount() 方法的实现中新增 int i=1/0;,看看异常通知的执行情况:
在这里插入图片描述
后置通知和异常通知只能执行一个。
环绕通知:
在 bean.xml 中配置切入点表达式,这样之前的 pointcut="execution(* Service.impl.*.*(..))" 就都可以替换成 pointcut-ref="pt1"

    <!--配置切入点表达式
            id 属性用于指定表达式的唯一标准
            expression 属性用于指定表达式内容
            此标签写在 aop:aspect 标签之中,只能在当前切面使用
            它还可以写在 aop:aspect 标签之外,就是所有标签可用
            -->
            <aop:pointcut id="pt1" 
            expression="execution(* Service.impl.*.*(..))"></aop:pointcut>

     如果把它写在标签之外,需要注意顺序:

 <!--配置AOP-->
    <aop:config>

        <!--顺序要求在此,不能再 aop:config 外部-->
        <aop:pointcut id="pt1" expression="execution(* Service.impl.*.*(..))"></aop:pointcut>

        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知-->
            <aop:before method="BeforePrintLog" pointcut-ref="pt1"></aop:before>
            ... ...

在 bean.xml 中配置 环绕通知:

<!--配置环绕通知-->
<aop:around method="arroundPrintLog" pointcut-ref="pt1">

在 Logger 类中新增:

  //环绕通知
    public void arroundPrintLog()
    {
        System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
    }

执行后只看到输出:
在这里插入图片描述
    通过与 动态代理中的环绕通知代码 对比,发现:动态代理的环绕通知有明确的切入点。解决方法:Spring 框架提供了一个接口:ProceedingJoinPoint ,该接口有 proceed( )方法,此方法就相当于明确调用切入点方法,可以作为环绕通知的方法参数在程序执行时,Spring 框架会为我们提供该接口的实现类供我们使用。✨环绕通知是 Spring 框架为我们提供的 可以在代码中手动控制增强方法何时执行的一种方式✨:

  //环绕通知
    public Object arroundPrintLog(ProceedingJoinPoint pjp)
    
    {
       
        Object rtValue=null;
        //明确调用业务层方法,即 切入点方法
        try {
            
            //得到方法执行所需的参数
            Object[] args=pjp.getArgs();
            
            /* 
            *写在 proceed 方法前就表示它是前置通知
            System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
            */
            
            rtValue=pjp.proceed(args);
            
            /* 
            *写在 proceed 方法后就表示它是后置通知
            System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
            */
            
            return rtValue;
        } catch (Throwable throwable) {
            
            
            /* 
            *写在 catch 中就表示它是异常通知
            System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
            */
            
            throwable.printStackTrace();
        }
        finally {
            /* 
            *写在 finally 中就表示它是最终通知
            System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
            */
        }
       
        
        return rtValue;
    }

Spring 基于注解的 AOP 配置:
    新建 项目,在 pom.xml 中导入 spring-context 和 aspectjweaver 依赖,并把之前 src 包中的内容都拷贝过来。
需要在 bean.xml 头中添加如下,并配置 Spring 创建容器时需要扫描的包:

  xmlns:context="http://www.springframework.org/schema/context"
     http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">
 <!--配置 Spring 创建容器时要扫描的包-->
    <context:component-scan base-package="com.jia">
    </context:component-scan>

<!--配置 Spring 开启注解 AOP 的支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

不需要在 bean.xml 中配置,而是直接使用注解的方式:

//账户的业务层实现类
@Service("accountService")
public class AccountServiceImpl1 implements IAccountService1 
package com.jia.Utils;

/*
用于记录日志的工具类,它里面提供了公共的代码
 */
@Component("logger")
//表示当前类是个切面类
@Aspect
public class Logger {

    @Pointcut("execution(* com.jia.Service.impl.*.*(..))")
    private void pt1()
    {}

    //前置通知
    @Before("pt1()")
    public void BeforePrintLog()
    {
        System.out.println("前置通知:Logger 类中的 BeforePrintLog()");
    }

    @AfterReturning("pt1()")
    //后置通知
    public void AfterReturningPintLog()
    {
        System.out.println("后置通知:Logger 类中的 AfterPintLog()");
    }

    @AfterThrowing("pt1()")
    //异常通知
    public void AfterThrowingPrintLog()
    {
System.out.println("异常通知:Logger 类中的AfterThrowingPrintLog()");
    }
    
   @After("pt1()")
    //最终通知
    public void finalPrintLog()
    {
        System.out.println("最终通知:Logger 类中的 finalPrintLog()");
    }

}

运行结果:
在这里插入图片描述
如果放开环绕通知的注释:

 @Around("pt1()")
    //环绕通知
    public Object arroundPrintLog(ProceedingJoinPoint pjp)

执行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值