Spring学习笔记第三天,AOP的相关概念、Spring中的AOP、Spring中基于XML配置的AOP和基于注解配置的AOP

9 篇文章 0 订阅
5 篇文章 0 订阅

1. 第1章 AOP 的相关概念[理解]

demo代码

1.1案例分析

我们在昨天的day02_02account_xml_ioc 这个Module下进行分析其中的问题。首先我们创建一个Module命名为 day03_01account ,然后把 day02_02account_xml_ioc 这个Module里src/下的所有文件都复制到我们新建的Module的src下。然后我们参考 day02_02account_xml_ioc 这个Module里的pom.xml文件来修改我们新建的pom.xml文件。
下面我们修改一下 IAccountService.java 文件,增加一个新的接口。

public interface IAccountService {
    /**
     * 转账
     * @param sourceName 转出账户的名称
     * @param targetName 转入账户的名称
     * @param money      转账金额
     */
    void transfer(String sourceName, String targetName, Float money);
}

然后在 AccountServiceImpl.java这个类里实现一下这个方法。

public class AccountServiceImpl implements IAccountService {
	...
    public void transfer(String sourceName, String targetName, Float money) {
        //1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //3.转出账户的存款减去转账金额
        source.setMoney(source.getMoney() - money);
        //4.转入账户的存款加转账金额
        target.setMoney(target.getMoney() + money);
        //5.更新转出账户
        accountDao.updateAccount(source);
        //6.更新转入账户
        accountDao.updateAccount(target);
    }
}

这时你会发现我们的 IAccountDao.java 接口里少了一个 findAccountByName 方法,下面我们就把这个方法添加上。

public interface IAccountDao {
	...
    /**
     * 根据名称查询账户
     * @param accountName 账户名称
     * @return 如果有唯一的一个结果就返回,如果没有结果就返回null
     * 如果结果集超过一个就跑出异常
     */
    Account findAccountByName(String accountName);
}

下面我们在AccountDaoImpl.java里实现一下这个方法。

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

然后修改一下测试类 AccountServiceTest.java 把无关的内容都删除掉。

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

    @Test
    public void testTransfer() {
    	//aaa账户给bbb账户转账100元。
        as.transfer("aaa", "bbb", 100f);
    }
}

我们运行这个测试方法之前,数据库account表里的内容如下
在这里插入图片描述
我们来运行一下这个测试方法,运行之后数据库表的内容如下,可以看出正常运行了。
在这里插入图片描述
虽然我们的程序运行正常,但是里面存在一些问题,如果在AccountServiceImpl的transfer方法里运行了一半出错了,那数据库里的信息就不一致了。我们来修改一下AccountServiceImpl的transfer方法。添加 int i = 1 / 0; 语句模拟运行出错。

public class AccountServiceImpl implements IAccountService {
	...
    public void transfer(String sourceName, String targetName, Float money) {
        //1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //3.转出账户的存款减去转账金额
        source.setMoney(source.getMoney() - money);
        //4.转入账户的存款加转账金额
        target.setMoney(target.getMoney() + money);
        //5.更新转出账户
        accountDao.updateAccount(source);

        int i = 1 / 0;//模拟运行出错

        //6.更新转入账户
        accountDao.updateAccount(target);
    }
}

下面我们在来运行一下上面的测试方法。运行之后数据库表的内容如下
在这里插入图片描述
从上图中可以看出,aaa账户的存款减少了但是bbb账户的金额没有增加。这是一个很严重的问题。
在这里插入图片描述
究其原因就是在这个方法中存在多个连接,每和数据库交互一次就要获取一个连接,这些连接可能不是同一个(我们使用了连接池的技术),所以就没法保证数据的一致。当一个操作有多次和数据库交互获取多个连接,就没法进行事务控制。我们应该让这个几个和数据库交互的动作要发生就一起发生,要不发生就都不发生,才能保证数据的一致。我们知道每一个连接都可以保证事务的一致性,但是多个连接一起使用就不可以,所以我们应该在这一个方法中只有一个连接存在。这样我们才可以通过事务来保证数据一致。

添加事务管理的工具类并分析连接和线程绑定

为了解决上面的这个问题我们新建一个数据库的连接工具类 ConnectionUtils.java

/*
 * 连接的工具类,从数据源中获取一个连接并且实现和一个线程的绑定
 */
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    private DataSource dataSource;
	// 这个方法用于spring的IOC依赖注入
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection() {
        //1.先从ThreadLocal上获取
        Connection conn = tl.get();
        try {
            //2.判断当前线程上是否有连接
            if (conn == null) {
                //3.从数据源中获取一个连接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程上的连接
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 把连接和线程解绑
     */
    public void removeConnection() {
        tl.remove();
    }
}

然后我们在添加一个事务管理的工具类 TransactionManager.java

/**
 * 和事务管理相关的工具类,包含了:开启事务、提交事务、回滚事务和释放连接
 */
public class TransactionManager {
    private ConnectionUtils connectionUtils;
	//这个方法用于spring的IOC依赖注入
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开始事务
     */
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 提交事务
     */
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    /**
     * 释放连接
     */
    public void release() {
        try {
            connectionUtils.getThreadConnection().close(); // 把连接返回连接池中
            connectionUtils.removeConnection();//把连接和线程进行解绑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在工具类TransactionManager的 release()方法中在close()连接之后要把连接从线程中移除出去,因为,我们的服务器也会使用线程池技术,当我们对线程对象调用close()方法不是真正的把线程关闭,还是把线程还回了线程池中。所以在close()连接之后要把连接从线程中移除出去,避免下一个任务使用线程时会存在一个数据库连接在线程里。
下面来修改一下AccountDaoImpl.java,使我们在使用 QueryRunner 执行sql的时候,使用外面传给了Connection,而不是使用QueryRunner里的。

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

	//这个方法为了进行数据注入
    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }
	//这个方法为了进行数据注入
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    public List<Account> findAllAccount() {
        try {
            return runner.query(connectionUtils.getThreadConnection(), "select * from account;", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountById(Integer accountId) {
        try {
            return runner.query(connectionUtils.getThreadConnection(), "select * from account where id = ?;", new BeanHandler<Account>(Account.class), accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void saveAccount(Account account) {
        try {
            runner.update(connectionUtils.getThreadConnection(), "insert into account (name, money) values(?,?)", account.getName(), account.getMoney());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void updateAccount(Account account) {
        try {
            runner.update(connectionUtils.getThreadConnection(), "update account set name = ?, money = ? where id = ?;", account.getName(), account.getMoney(), account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            runner.update(connectionUtils.getThreadConnection(), "delete from account where id = ?;", accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

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

然后我们就来改造一下AccountServiceImpl.java,让这个类的方法都处在一个事务中,事务的控制就应该在业务层。

/**
 * 账户的业务层实现类
 * 事务控制应该都是在业务层的
 */
public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;
    private TransactionManager txManager;
    //这个方法用于spring的IOC依赖注入
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }
    //这个方法用于spring的IOC依赖注入
    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public List<Account> findAllAccount() {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            List<Account> accounts = this.accountDao.findAllAccount();
            //3.提交事务
            txManager.commit();
            //4.返回结果
            return accounts;
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }

    public Account findAccountById(Integer accountId) {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            Account account = this.accountDao.findAccountById(accountId);
            //3.提交事务
            txManager.commit();
            //4.返回结果
            return account;
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }

    public void saveAccount(Account account) {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            this.accountDao.saveAccount(account);
            //3.提交事务
            txManager.commit();
            //4.返回结果
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }

    public void updateAccount(Account account) {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            this.accountDao.updateAccount(account);
            //3.提交事务
            txManager.commit();
            //4.返回结果
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            this.accountDao.deleteAccount(accountId);
            //3.提交事务
            txManager.commit();
            //4.返回结果
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //1.开启事务
            txManager.beginTransaction();
            //2.执行操作
            //2.1.根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2.根据名称查询转入账户
            Account target = accountDao.findAccountByName(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.提交事务
            txManager.commit();
            //4.返回结果
        } catch (Exception e) {
            //5.回滚操作
            txManager.rollback();
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            txManager.release();
        }
    }
}

然后我们来修改一下bean.xml,把需要数据的bean对象配置好

<?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.itheima.service.impl.AccountServiceImpl">
        <!-- 使用set方法注入 dao对象-->
        <property name="accountDao" ref="accountDao"></property>
        <!--注入事务管理器-->
        <property name="txManager" ref="transactionManager"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 使用set方法注入 QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置 QueryRunner 对象,因为 bean 对象 默认是单例对象,所以有可能发生线程安全问题。所以让scope设置为prototype-->
    <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/spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 配置 Connection 的工具类 ConnectionUtils ,这个工具类使用之后就不会在使用runner里的连接了-->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

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

修改好之后我们就来运行一下测试类AccountServiceTest.java里的测试方法testTransfer,控制台输出如下,出现了这个异常。
在这里插入图片描述
异常出现了,但是又会出现上面的那个数据不一致的问题吗,我们来看看数据库表里的内容,从下图可以看出,数据库里的内容没有发生变化,这正是我们想要的,事务控制成功了,说明我们成功解决了上面的那个问题。
在这里插入图片描述
经过上面的一顿操作已经解决了上面的问题,但是又有个问题出现了,就是这个写太复杂了看着太乱了,尤其是那个bean.xml依赖注入很乱。
下面就来解决这个问题。动态代理有来喽

1.2 代理分析

1.2.1 代理模式的讲解

代理模式参考这里

1.2.2 使用动态代理实现事务的控制

首先我复制一份 AccountServiceImpl.java 复制到原来的位置命名为AccountServiceImpl_OLD。因为我们要使用动态代理来进行事务的控制,让 AccountServiceImpl来做被代理对象,所以AccountServiceImpl里就不需要在使用TransactionManager做事务控制了。AccountServiceImpl.java修改如下

public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;
    //这个方法用于spring的IOC依赖注入
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        return this.accountDao.findAllAccount();
    }

    public Account findAccountById(Integer accountId) {
        return this.accountDao.findAccountById(accountId);
    }

    public void saveAccount(Account account) {
        this.accountDao.saveAccount(account);
    }

    public void updateAccount(Account account) {
        this.accountDao.updateAccount(account);
    }

    public void deleteAccount(Integer accountId) {
        this.accountDao.deleteAccount(accountId);
    }

    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
        //2.1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(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);
    }
}

然后我们在com.itheima.factory里创建一个BeanFactory.java用于代理 AccountService。BeanFactory内容如下

/**
 * 用于创建Service的代理对象的工厂
 */
public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager txManager;

    // 用于Spring的IOC依赖注入
    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    //这个方法用于spring的IOC依赖注入
    // 因为这个 txManager 对象要在匿名内部类里使用,所以要使用final修饰
    public final void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    //获取Service的代理对象
    public IAccountService getAccountService() {
        IAccountService service = (IAccountService) Proxy.newProxyInstance(this.accountService.getClass().getClassLoader(), this.accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    // 添加事务的支持,让所有和数据库交互的方法全部经过这里
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object returnValue = null;
                        try {
                            //1.开启事务
                            txManager.beginTransaction();
                            //2.执行操作
                            returnValue = method.invoke(accountService, args);
                            //3.提交事务
                            txManager.commit();
                            //4.返回结果
                            return returnValue;
                        } catch (Exception e) {
                            //5.回滚操作
                            txManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.释放事务
                            txManager.release();
                        }
                    }
                });
        return service;
    }
}

最后我们在来修改一下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="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

    <!-- 配置beanFactory 这个得到的对象是代理的 accountService 的 service-->
    <bean id="beanFactory" class="com.itheima.factory.BeanFactory">
        <!--注入accountService-->
        <property name="accountService" ref="accountService"></property>
        <!--注入事务管理器-->
        <property name="txManager" ref="transactionManager"></property>
    </bean>

    <!-- 配置Service 这个是没有事务控制的service-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 使用set方法注入 dao对象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!-- 配置Service 这个是原来在service里做事务控制的类-->
    <bean id="accountService_OLD" class="com.itheima.service.impl.AccountServiceImpl_OLD">
        <!-- 使用set方法注入 dao对象-->
        <property name="accountDao" ref="accountDao"></property>
        <!--注入事务管理器-->
        <property name="txManager" ref="transactionManager"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 使用set方法注入 QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置 QueryRunner 对象,因为 bean 对象 默认是单例对象,所以有可能发生线程安全问题。所以让scope设置为prototype-->
    <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/spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 配置 Connection 的工具类 ConnectionUtils -->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

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

我们还需要修改一下AccountServiceTest.java,因为项目IOC容器里有三个AccountService,我们这里要使用有动态代理的那个service来进行数据的注入。

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

    @Autowired
    @Qualifier("proxyAccountService")//使用有动态代理的那个service来进行数据的注入
    IAccountService as;

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

我们运行一下这个测试方法,控制台输出如下,可以看出动态代理成功了。
在这里插入图片描述
我们在来查看一下数据库,说明事务控制也成功了。
在这里插入图片描述

1.3 AOP 概述

1.3.1 什么是 AOP

AOP: 全称是 Aspect Oriented Programming 即: 面向切面编程。 百度百科中的解释如下
在这里插入图片描述
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

  • AOP 的作用及优势
  • 作用:
    • 在程序运行期间,不修改源码对已有方法进行增强。
  • 优势:
    • 减少重复代码
    • 提高开发效率
    • 维护 方便
  • AOP 的实现方式
    • 使用动态代理技术

1.3.2 AOP 的具体应用

参考这里 Java中的代理模式和装饰者模式,还有我们上面提到的动态代理进行事务控制。

2. Spring 中的 AOP[掌握]

我们学习 spring 的 aop,就是通过配置的方式,实现上一章节的功能 使用动态代理来进行事务控制。

2.1 AOP 相关术语

  • Joinpoint(连接点):

    • 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。我所理解的连接点就是那些在调用时经过动态代理的 invoke 这个方法的方法。比如我上一章节最后使用动态代理来进行事务控制时,service里的所用的方法都叫连接点。
  • Pointcut(切入点):

    • 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。我们使用动态代理的目的就是对方法进行增强,但是我们使用动态代理时,并不是所用的方法都需要被增强,所以被增强的方法就叫切入点,
      比如,我们的 Iservice 接口中新定义了一个 test()方法,但是我们在使用动态代理时,遇到这个方法就直接结束掉,如下。
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         if ("test".equals(method.getName())) { // 这个方法没有增强就不叫切入点
              return method.invoke(args);
          }
          // 经过下面的方法都进行了增强,所以经过下面的方法都叫切入点
          Object returnValue = null;
          try { // 下面对方法增强(比如开启事务、提交事务、回滚事务、释放事务等)就是 Advice(通知/增强)
              //1.开启事务
              txManager.beginTransaction();
              //2.执行操作
              returnValue = method.invoke(accountService, args); // 之前的操作叫 前置通知,之后的叫后置通知
              //3.提交事务
              txManager.commit();
              //4.返回结果
              return returnValue;
          } catch (Exception e) { // 这里的叫异常通知
              //5.回滚操作
              txManager.rollback();
              throw new RuntimeException(e);
          } finally { // 这里的叫 最终通知
              //6.释放事务
              txManager.release();
          }
      }
    }
    

所以就有有一句话要记住,所有的切入点都是连接点,但不是所有的连接点都是切入点。

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

学习 spring 中的 AOP 要明确的事

  • a、开发阶段(我们做的)
    • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。把公用代码抽取出来,制作成通知。(开发阶段最后再做): AOP 编程人员来做。在配置文件中,声明切入点与通知间的关系,即切面。
  • b、运行阶段( Spring 框架完成的)
    • Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

2.2 基于 XML 的 AOP 配置

2.2.1 基于 XML 的 AOP 配置入门案例

首先我们新建一个Module命名为 day03_03soringAOP这个Module用于演示基于XML的AOP的配置
首先在pom.xml添加中添加需要使用的坐标。

<?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.itheima</groupId>
    <artifactId>day03_03springAOP</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <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>
</project>

然后添加一个IAccountService的接口,这个接口里有三种方法,第一种无参无返回值、第二种有参无返回值、第三种无参有返回值。

/**
 * 账户的业务层接口
 */
public interface IAccountService {
    /**
     * 模拟保存账户
     */
    void saveAccount();

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

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

然后在定义一个这个接口的实现类AccountServiceImpl

/**
 * 账户的业务层实现类,主要是模拟service的操作
 */
public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("执行了保存");
    }

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

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

然后在在utils包里定义一个公共类Logger

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

写好了公共类之后,下面我们就来创建一下bean.xml,并且配置aop,首先在spring的官方说明文档中找到带有aop的约束。

  • spring中基于xml的AOP配置步骤
  1. 把通知类也交给spring来管理
  2. 使用aop:config标签表明开始AOP的配置
  3. 使用aop:aspect标签表明配置切面
    • id属性:是给切面提供一个唯一的标识
    • ref属性:是指定通知类bean的id
  4. 在aop:aspect标签内部使用对应标签来配置通知的类型
    • 我们现在示例是让printLog方法在切入点方法执行之前执行;所以是前置通知
    • aop:before:表示配置前置通知
      • method属性:用于指定Logger类中的哪个方法是前置通知
      • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
        • 切入点表达式的写法:
          • 关键字:execution(表达式)
          • 表达式:
            • 访问修饰f符 返回值 包名.包名…类名.方法名(参数列表)
            • 标准的表达式写法:
              public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
<?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对象配置进来 -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
    <!--spring中基于xml的AOP配置步骤
        1.把通知类也交给spring来管理
        2.使用aop:config标签表明开始AOP的配置
        3.使用aop:aspect标签表明配置切面
            id属性:是给切面提供一个唯一的标识
            ref属性:是指定通知类bean的id
        4.在aop:aspect标签内部使用对应标签来配置通知的类型
            我们现在示例是让printLog方法在切入点方法执行之前执行;所以是前置通知
            aop:before:表示配置前置通知
                method属性:用于指定Logger类中的哪个方法是前置通知
                pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

            切入点表达式的写法:
                关键字:execution(表达式)
                表达式:
                    访问修饰f符  返回值 包名.包名...类名.方法名(参数列表)
                标准的表达式写法:
                    public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
      -->

    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

在这里插入图片描述
然后在test.java里的创建一个测试类com.itheima.test.AOPTest.java如下

/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }
}

这个Module的结构如下
在这里插入图片描述

2.2.2切入点表达式

  • 关键字:execution(表达式)
  • 表达式:
    • 访问修饰f符 返回值 包名.包名…类名.方法名(参数列表)
  • 标准的表达式写法:
    public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
    
  • 访问修饰符可以省略:
    void com.itheima.service.impl.AccountServiceImpl.saveAccount()
    
  • 返回值可以使用通配符,表示任意返回值:
    * com.itheima.service.impl.AccountServiceImpl.saveAccount()
    
  • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*:
    * com.itheima.service.impl.AccountServiceImpl.saveAccount()
    * *.*.*.*.AccountServiceImpl.saveAccount()
    
  • 包名可以使用 … 表示当前包及其子包
    * *..AccountServiceImpl.saveAccount()
    
  • 类名和方法名都可以使用*来实现统配
    * *..*.*()
    
  • 参数列表:
    • 可以直接写数据类型:
      • 基本类型直接写名称
      • 引用类型写包名.类名的方式 如 java.lang.String int
        * *..*.*() 表示没有参数的方法
        * *..*.*(int) 表示有一个int型的参数
        * *..*.*(java.lang.String) 表示有一个String型的参数
        * *..*.*(java.lang.String, int) 表示有一个int型的参数和一个int型的参数
        
    • 可以使用通配符表示任意参数类型,但是方法必须要有参数
    • 可以使用 … 表示有无参数均可,有参数可以是任意类型
  • 全统配写法(在实际开发中不推荐使用,因为这样会扫描所有的包下的所有的类和所有的方法,因为整个项目的所有方法都是匹配的)
    * *..*.*(..)
    
  • 实际开发中切入点表达式的通常写法:
    要切入到业务层实现类下的所有方法
    * com.itheima.service.impl.*.*(..)
    

2.2.3 AOP各种通知的XML配置

我们新建一个Module命名为day03_04adviceType,然后我们把上一个名为day03_springAOP的Module里的src下的所有内容都拷贝到当前的module的src下。然后我们修改一下Logger.java

/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
public class Logger {
    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了...");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了...");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了...");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog() {
        System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了...");
    }
}

修改一下测试类里的main方法

/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }
}

关于AOP各种通知的XML配置请见下面的这个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的IOC,把service对象配置进来 -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--前置通知,在切入点方法执行之前执行-->
            <aop:before method="beforePrintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
            <!--后置通知,在切入点方法正常执行之后执行。它和异常通知永远只能执行一个-->
            <aop:after-returning method="afterReturningPrintLog"
                                 pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-returning>
            <!--异常通知,在切入点方法执行产生异常之后执行,相当于 catch 块里的代码。它和后置通知永远只能执行一个-->
            <aop:after-throwing method="afterThrowingPrintLog"
                                pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-throwing>
            <!--最终通知,无论切入点方法是否正常执行它都会执行,就是在 finally 块里的代理-->
            <aop:after method="afterPrintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

我们在编写通知处的代码时发现每个地方都要写切入点表达式,在这里的每个切入点表达式都一样,那能不能把这个切入点表达式只写一遍,然后都去引入这个呢?答案当然是可以的。在 aop:aspect 这个标签里存在一个 aop:pointcut 标签,我们可以把切入点表达式写在这个标签里面,然后在需要使用切入点表达式的地方直接引用这个标签即可。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的IOC,把service对象配置进来 -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <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>
            <!--异常通知,在切入点方法执行产生异常之后执行,相当于 catch 块里的代码。它和后置通知永远只能执行一个-->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
            <!--最终通知,无论切入点方法是否正常执行它都会执行,就是在 finally 块里的代理-->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>

            <!--配置切入点表达式,id属性用于指定表达式的唯一标识。expression属性用于指定表达式的内容
                注意:此标签写在 aop:aspect 标签内部只能在当前切面使用。
                它还可以写在 aop:aspect 标签外面,此时就变成了所有的切面可用
            -->
            <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
        </aop:aspect>
    </aop:config>
</beans>

但是这样写还有一个问题就是 aop:pointcut 标签只能写在 aop:aspect 标签内部只能在当前切面使用,在其他的切面里就不能使用了。还有一个办法,正如在上面的注释中写的, aop:pointcut 标签还可以写在 aop:aspect 标签外面,此时就变成了所有的切面可用了。这样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的IOC,把service对象配置进来 -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式,id属性用于指定表达式的唯一标识。expression属性用于指定表达式的内容
                注意:此标签写在 aop:aspect 标签内部只能在当前切面使用。
                它还可以写在 aop:aspect 标签外面,此时就变成了所有的切面可用
            -->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.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>
            <!--异常通知,在切入点方法执行产生异常之后执行,相当于 catch 块里的代码。它和后置通知永远只能执行一个-->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
            <!--最终通知,无论切入点方法是否正常执行它都会执行,就是在 finally 块里的代理-->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

2.2.4 AOP的最后一种通知:环绕通知

在切面 aop:aspect 标签里除了上面我们讲到的前置通知、后置通知、异常通知、最终通知的标签之外,还有一个标签,就是 aop:around 标签标示环绕通知。我们首先修改一下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的IOC,把service对象配置进来 -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置环绕通知-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

然后我们在 Logger 类中创建一个 aroundPrintLog方法如下。

package com.itheima.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
public class Logger {
    ...
    public void aroundPrintLog() {
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...");
    }
}

下面我们来运行一下这个main方法,控制台输出如下,表明值调用了环绕通知的方法,但是没有调用我 业务方法。
在这里插入图片描述
这是为什么呢?还记得上面讲的什么是环绕通知吗啊?就是下图里的这个invoke这个方法,在动态代理里面这个就是环绕通知。
在这里插入图片描述
在来看一下我们使用Spring的AOP的环绕通知,这里少了很多东西,比如前置通知、对切入点方法的调用、后置通知、异常通知、最终通知等。

public void aroundPrintLog() {
   System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...");
}
  • 环绕通知
    • 问题:
      当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
    • 分析:
      通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点调用,而我们的代码中没有
    • 解决方法:
      Spring框架为我们提供的一个接口:ProceedingJoinPoint。该接口有一个proceed(),此方法就相当于明确调用切入点方法。
      该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类工我们使用。
    • Spring中的环绕通知:
      它是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

下面就来改变一下这个方法。

public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue;
        try {
            Object[] args = pjp.getArgs();
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了..."); // 写在这里表示前置通知
            rtValue = pjp.proceed(args);// 明确调用业务层方法
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了..."); // 写在这里表示后置通知
            return rtValue;
        } catch (Throwable throwable) { // 这里要使用Throwable来拦截异常
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了..."); // 写在这里表示异常通知
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了..."); // 写在这里表示最终通知
        }
    }

2.3 基于注解的 AOP 配置

2.3.1 使用AOP注解来配置

基于注解的AOP实现起来非常的简单。首先我们来新创建一个Module命名为day03_05annotationAOP,然后配置好pom.xml导入相应的jar包,因为我们的代码和上面额基本一样,所以我们把day03_04adviceType这个Module里的src下的所有文件都拷贝到我们新建的Module的src下。
首先我们来修改一下bean.xml文件,因为我们要使用注解的方式来配置AOP,那我们也把ioc也改成使用注解的方式(注意bean.xml的约束内容的改变,因为需要扫描包,所以要引入 context约束)

<?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"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置Spring创建IOC容器时要扫描的包-->
    <context:component-scan base-package="com.itheima"/>

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

然后给class加上IOC的注解

/**
 * 账户的业务层实现类
 */
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
	···
}
/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
@Component("logger") // 不属于MVC三层架构里的任何一种,就使用Component注解
public class Logger {
	···
}

下面就来配置AOP的注解

/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
    /**
     * 这个方法用于表示切入点表达式
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1() {
    }

    /**
     * 前置通知
     */
//    @Before("pt1()") // 引用切入点表达式时必须要写pt1后面的()
    public void beforePrintLog() {
        System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了...");
    }

    /**
     * 后置通知
     */
//    @AfterReturning("pt1()")
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了...");
    }

    /**
     * 异常通知
     */
//    @AfterThrowing("pt1()")
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了...");
    }

    /**
     * 最终通知
     */
//    @After("pt1()")
    public void afterPrintLog() {
        System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了...");
    }

    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue;
        try {
            Object[] args = pjp.getArgs();
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...前置"); // 写在这里表示前置通知
            rtValue = pjp.proceed(args);// 明确调用业务层方法
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...后置"); // 写在这里表示后置通知
            return rtValue;
        } catch (Throwable throwable) { // 这里要使用Throwable来拦截异常
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...异常"); // 写在这里表示异常通知
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...最终"); // 写在这里表示最终通知
        }
    }
}

我们首先把环绕通知的注解注释掉,打开其他通知的注解,运行一下我们的测试类AOPTest里的main方法,控制台输出如下,可以看出最终通知和后置通知的调用顺序颠倒了,和使用xml配置的不太一样。不符合我们的要求。
在这里插入图片描述
下面我们来打开环绕通知的注解,注释掉其他通知的注解。然后运行一下我们的测试类AOPTest里的main方法,控制台输出如下,可以看出这种才是我们需要的,因为环绕通知里的代码是我们自己写的,所以它的调用顺序我们可以自己控制。使用注解配置AOP时推荐使用环绕通知
在这里插入图片描述

2.3.2 基于注解的切入点

在上一节2.3.1中我们对想要进行代理的方法是使用切入点表达式的方式去匹配方法,这个方式不够灵活,如果我们只想对某一个类中的某一个方法进行aop的话,在使用上面的这种形式虽然可能达到我们的目的,但是会对其他的方法也进行代理不够优雅,怎么办呢?spring提供了另一个方式,就是使用注解的方式。
我们先创建一个新的Module命名为day03_06annotationAdvice,然后把day03_05annotationAOP这个Module中src下的所有文件都拷贝到我们新建的Module的src下。
下面我们就新建一个注解LoggerAnnotation用于代理标识,代码如下

package com.itheima.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用于标识需要记录日志的注解,
 * ElementType.METHOD 表示这个注解可以放在方法上
 * ElementType.TYPE  表示这个注解可以放到 类、接口上
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggerAnnotation {
}
2.3.2.1 切入点注解Pointcut中@annotation的使用

下面我们修改一下Logger,代码如下

package com.itheima.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
    /**
     * 这个方法用于表示切入点表达式,要代理的方法通过切入点表达式解析得到
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1() {
    }

    /**
     * 这里使用使用 @annotation 标识要使用哪个注解来进行代理
     * 这里 @annotation() 里填写注解的名字
     * 要代理的方法上必须要写该注解
     * 注意 这种方法只能对该注解写在方法起作用,只能对写该注解的方法进行代理
     * 注意 如果注解和当前的这个切面类不在同一个包里就要写全这个注解的路径
     *     如果注解和当前的这个切面类在同一个包里就不用写全这个注解的路径,只填注解名就行
     */
    @Pointcut("@annotation(com.itheima.annotation.LoggerAnnotation)")
    private void ptAnnotation() {
    }

    /*
     * 环绕通知
     */
    //@Around("pt1()")
    @Around("ptAnnotation()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue;
        try {
            Object[] args = pjp.getArgs();
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...前置"); // 写在这里表示前置通知
            rtValue = pjp.proceed(args);// 明确调用业务层方法
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...后置"); // 写在这里表示后置通知
            return rtValue;
        } catch (Throwable throwable) { // 这里要使用Throwable来拦截异常
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...异常"); // 写在这里表示异常通知
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...最终"); // 写在这里表示最终通知
        }
    }
}

然后在修改一下IAccountService的实现类AccountServiceImpl,代码如下

package com.itheima.service.impl;

import com.itheima.annotation.LoggerAnnotation;
import com.itheima.service.IAccountService;
import org.springframework.stereotype.Service;

/**
 * 账户的业务层实现类
 */
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Override
    @LoggerAnnotation // 增加这个注解用于代理的方法标识
    public void saveAccount() {
        System.out.println("执行了保存");
    }

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

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

下面我们来修改下一测试类AOPTest,代码如下

package com.itheima.test;
import com.itheima.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
        as.deleteAccount();
    }
}

执行一下测试类AOPTest中的main方法,结果如下,由此可见使用LoggerAnnotation注解也达到了代理方式。没有使用注解的deleteAccount方法就没有被代理。
在这里插入图片描述

2.3.2.2 切入点注解Pointcut中@within的使用

通过上面的测试可以看出要进行代理的方法都需要使用自定义的注解LoggerAnnotation,如果我们想要一个类中的所有方法都进行代理的话就要在该类中所有的方法都使用该注解,不怎么优雅。有什么方法可以在类上使用一个注解然后就可以对该类中的所有方法进行代理呢?答案当然是肯定的,spring已经考虑到这个需求了。下面我们就来介绍切入点注解Pointcut中@within的使用。
因为代码类似,所以我们还在day03_06annotationAdvice这个Module中进行测试。
下面我们再修改一下切面类Logger,代码如下

package com.itheima.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 用于记录日志的工具类,它里面提共了公共的方法
 */
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
    /**
     * 这个方法用于表示切入点表达式,要代理的方法通过切入点表达式解析得到
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1() {
    }

    /**
     * 这里使用使用 @annotation 标识要使用哪个注解来进行代理
     * 这里 @annotation() 里填写注解的名字
     * 要代理的方法上必须要写该注解
     * 注意 这种方法只能对该注解写在方法起作用,只能对写该注解的方法进行代理
     * 注意 如果注解和当前的这个切面类不在同一个包里就要写全这个注解的路径
     *     如果注解和当前的这个切面类在同一个包里就不用写全这个注解的路径,只填注解名就行
     */
    @Pointcut("@annotation(com.itheima.annotation.LoggerAnnotation)")
    private void ptAnnotation() {
    }

    /**
     * 这里 @within 与 @annotation 作用类似
     * 唯一不同的是作用在类上,对应的注解放到类上面,就可以把这个类里的所有的方法都代理
     * 要代理的类上必须要写该注解。
     * 如果注解放在方法上,是不会代理的。
     */
    @Pointcut("@within(com.itheima.annotation.LoggerAnnotation)")
    private void ptWithin() {
    }

    /*
     * 环绕通知
     */
    //@Around("pt1()")
    //@Around("ptAnnotation()")
    @Around("ptWithin()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue;
        try {
            Object[] args = pjp.getArgs();
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...前置"); // 写在这里表示前置通知
            rtValue = pjp.proceed(args);// 明确调用业务层方法
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...后置"); // 写在这里表示后置通知
            return rtValue;
        } catch (Throwable throwable) { // 这里要使用Throwable来拦截异常
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...异常"); // 写在这里表示异常通知
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了...最终"); // 写在这里表示最终通知
        }
    }
}

下面我们再创建一个IAccountService的测试类AccountServiceImpl1用于测试,代码如下

package com.itheima.service.impl;

import com.itheima.annotation.LoggerAnnotation;
import com.itheima.service.IAccountService;
import org.springframework.stereotype.Service;
/**
 * 用与测试在类上添加一个注解,就对该类中所有方法进行代理
 */
@LoggerAnnotation
@Service("accountServiceImpl1")
public class AccountServiceImpl1 implements IAccountService {
    @Override
    public void saveAccount() {
        System.out.println("AccountServiceImpl1 执行了保存");
    }

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

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

我们再来修改一下测试类AOPTest,代码如下

package com.itheima.test;

import com.itheima.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
        as.deleteAccount();
        System.out.println();

        IAccountService as1 = (IAccountService) ac.getBean("accountServiceImpl1");
        as1.saveAccount();
        as1.deleteAccount();
    }
}

执行测试类AOPTest中的main方法结果如下,使用切入点注解Pointcut中@within的使用对配置指定注解的类里的所有方法都进行了代理,指定的注解只放在方法上,该方法就不会代理。
在这里插入图片描述

2.3.2.3 扩展切入点注解Pointcut中表达式标签

在实际开发中我们大部分都是使用上面介绍了这三种情况,但是spring提供了更多的使用方式,表达式标签共有10种,其中这么对哦表达式又可以组合使用组成更多的使用姿势。
使用表达式的形式进行匹配的标签

execution:用于匹配方法执行的连接点
within:用于匹配指定类型内的方法执行
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法

使用自定义注解的形式进行匹配的标签

@within:用于匹配所以持有指定注解类型内的方法
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation:用于匹配当前执行方法持有指定注解的方法

除了这里10种标签的使用方式外,pointcut还支持引用的使用
有时,我们可以将切入专门放在一个类中集中定义。其他地方可以通过引用的方式引入其他类中定义的切入点。
例如有如下切面类,该类中定义了两个切入点表达式方法

package com.javacode2018.aop.demo9.test14;
import org.aspectj.lang.annotation.Pointcut;
public class AspectPcDefine {
    @Pointcut("bean(bean1)")
    public void pc1() {
    }
    @Pointcut("bean(bean2)")
    public void pc2() {
    }
}

如果我们想在其他的切面类中也进行拦截代理同样的切入点表达式的方法,就可以使用引用的方式,如下

package com.javacode2018.aop.demo9.test14;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspect14 {
    @Pointcut("com.javacode2018.aop.demo9.test14.AspectPcDefine.pc1()")
    public void pointcut1() {
    }
}

除此之外,pointcut还可以组合使用,Pointcut定义时,还可以使用&&、||、!运算符。

  • &&:多个匹配都需要满足
  • ||:多个匹配中只需满足一个
  • !:匹配不满足的情况下

如下所示

@Pointcut("bean(bean1) || bean(bean2)") //匹配bean1或者bean2
@Pointcut("@target(Ann1) && @Annotation(Ann2)") //匹配目标类上有Ann1注解并且目标方法上有Ann2注解
@Pointcut("@target(Ann1) && !@target(Ann2)") // 匹配目标类上有Ann1注解但是没有Ann2注解

具体使用姿势可以参考这里

2.3.3 不使用 XML 的配置方式

配置 SpringConfiguration 类

@Configuration
@ComponentScan(basePackages="com.itheima")
@EnableAspectJAutoProxy // 配置Spring开启注解AOP的支持
public class SpringConfiguration {
}

参考
不同版本的spring说明文档
spring的官方说明文档
Java中的代理模式和装饰者模式
AOP百度百科
@Pointcut 的常用方式:execution,within,this,target,args,@within,@target,@args,@annotation等介绍

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值