一文理解Spring中的AOP

75 阅读13分钟

文章目录


1. 概念

面向切面编程(Aspect Oriented Programming,AOP)指通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以实现对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。

AOP依赖于动态代理在程序运行期间,不修改源码就可以实现对已有方法的增强。这样的做法减少了重复代码的编写、提高了开发效率,以及后期维护更加方便。


2. 引入

在前面基于XML和注解的Ioc学习中,对于数据库表的操作执行的都是单条SQL语句,而且在代码中并没有看到有关事务的相关操作,说明Spring自动执行了事务管理操作。但是如果执行的是类似于转账这样的操作,此时事务的管理就相当重要的。

假设此时的account表为:

mysql> select * from account;
+----+----------+-------+
| id | name     | money |
+----+----------+-------+
|  1 | Forlogen |  1000 |
|  2 | Kobe     |  1000 |
|  3 | James    |  1000 |
+----+----------+-------+
3 rows in set (0.00 sec)

2.1 导入依赖

首先导入的包含Spring相关的依赖,如mysql驱动、dbUtils、数据源c3p0和Junit等

<?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>org.example</groupId>
    <artifactId>SpringProxyAccount</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.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>8.0.20</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>
</project>

2.2 创建实体类

public class Account implements Serializable {
    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 +
                '}';
    }
}

2.3 持久层

首先编写持久层接口,接口中只包含查询所有、根据名字查询和账户更新三个和转账相关的方法:

public interface IAccountDao {

    List<Account> findAll();

    Account findByName(String name) throws SQLException;

    void updateAccount(Account account);
}

编写接口的实现类,这是使用QueryRunner的query()update()来执行相关的SQL语句。同时添加QueryRunner对象的setr()来方便后面Spring的Ioc的依赖注入。

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

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

    public Account findByName(String name){
        try {
            final List<Account> accounts = runner.query( "select * from account where name = ?",
                    new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.size() == 0){
                return null;
            }
            if(accounts.size() > 1){
                throw new RuntimeException("!-----");
            }
            return accounts.get(0);
        } catch (Throwable t){
            throw new RuntimeException(t);
        }
    }

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

2.3 业务层

业务层只完成一个操作,那就是转账。但是它需要使用到持久层来操作数据库,所以接口中持有IAccountDao的对象。首先编写业务层接口:

public interface IAccountService {

    List<Account> findAll();
    
    void transfer(String sourceName, String targetName, Float money);
}

然后编写接口的实现类,同样添加IAccountDao对象的set()来方便依赖注入:

public class IAccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;
    
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAll() {
        try {
            List<Account> accounts = accountDao.findAll();
            return accounts;
        }catch (Exception e){
            txManager.rollback();
            throw new RuntimeException(e);
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {
       try{
           Account source = accountDao.findByName(sourceName);
           Account target = accountDao.findByName(targetName);
           source.setMoney(source.getMoney() - money);
           target.setMoney(target.getMoney() + money);

           accountDao.updateAccount(source);
           accountDao.updateAccount(target);
       } catch (Throwable t){
           throw new RuntimeException(t);
       } 
    }
}

2.4 bean.xml

在bean.xml中添加Spring相关的约束、相关的<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">

    <bean id="accountService" class="dyliang.service.impl.IAccountServiceImpl">
        <property name="accountDao" ref="accountDao" ></property>
    </bean>

    <bean id="accountDao" class="dyliang.dao.impl.IAccountImpl">
        <property name="runner" ref="runner"></property>
    </bean>

    <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.cj.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/sql_store?serverTimezone=GMT"></property>
    <property name="user" value="root"></property>
    <property name="password" value="1234"></property>
</bean>
</beans>

2.5 测试类

使用Junit来执行单元测试:

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

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

    @Test
    public void testTransfer(){
        System.out.println("----------------------");
        List<Account> all = as.findAll();
        for (Account account : all) {
            System.out.println(account);
        }
        as.transfer("Forlogen","Kobe",100f);

        System.out.println("----------------------");
        List<Account> all1 = as.findAll();
        for (Account account : all1) {
            System.out.println(account);
        }
    }
}

执行testTransfer()发现,可以转账成功,好像程序是可以的?但如果在转账操作的执行过程中出现了异常,那么执行完测试后发现Forlogen中钱减了,但是Kobe中钱并没有增加,这显然是不正常的。

这是因为在transfer()中执行了四次持久层中的方法,虽然Spring提供了对于单条语句的事务管理,但此时每次方法的执行都是独立的事务,它们之间无法满足事务的一致性。

public void transfer(String sourceName, String targetName, Float money) {
       try{
           Account source = accountDao.findByName(sourceName);
           Account target = accountDao.findByName(targetName);
           source.setMoney(source.getMoney() - money);
           target.setMoney(target.getMoney() + money);

           accountDao.updateAccount(source);
           accountDao.updateAccount(target);
       } catch (Throwable t){
           throw new RuntimeException(t);
       } 
    }

那么我们就需要将对于数据库的多条操作和事务联系起来,这里使用ThreadLoacal类来实现数据库连接和线程的绑定。

public class ConnectionUtils {

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

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

    public Connection getThreadConnection(){
        try{
            Connection conn = tl.get();
            if (conn == null){
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            return conn;
        } catch (Throwable t){
            throw new RuntimeException(t);
        }
    }

    public void removeConnection(){
        tl.remove();
    }
}

从源码实现理解ThreadLocal和InheritableThreadLocal

然后使用和线程相关的连接来编写自己的事务管理类,类中的方法包括事务开启、事务提交、事务回滚和连接的归还。

public class TransactionManager {

    private ConnectionUtils connectionUtils;

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

    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch(Exception e){
            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();
        }
    }
}

然后修改持久层接口和业务层接口的实现类:

public class IAccountImpl implements IAccountDao {

    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

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

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

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

    public Account findByName(String name){
        try {
            final List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account where name = ?",
                    new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.size() == 0){
                return null;
            }
            if(accounts.size() > 1){
                throw new RuntimeException("!-----");
            }
            return accounts.get(0);
        } catch (Throwable t){
            throw new RuntimeException(t);
        }
    }

    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);
        }
    }
}

执行执行的query()为:

public <T> T query(Connection conn, String sql, ResultSetHandler<T> rsh) throws SQLException {
        return this.query(conn, false, sql, rsh, (Object[]) null);
    }

继续修改

public class IAccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;
    private TransactionManager txManager;

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAll() {
        try {
            txManager.beginTransaction();
            List<Account> accounts = accountDao.findAll();
            txManager.commit();
            return accounts;
        }catch (Exception e){
            txManager.rollback();
            throw new RuntimeException(e);
        }finally {
            txManager.release();
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {
       try{
           txManager.beginTransaction();

           Account source = accountDao.findByName(sourceName);
           Account target = accountDao.findByName(targetName);
           source.setMoney(source.getMoney() - money);
           target.setMoney(target.getMoney() + money);

           accountDao.updateAccount(source);
           accountDao.updateAccount(target);

           txManager.commit();
       } catch (Throwable t){
           txManager.rollback();
           throw new RuntimeException(t);
       } finally {
           txManager.release();
       }
    }
}

此时,在业务层的方法实现中就加入了自定义的事务管理。同样也需要在bean.xml文件中体现这些更新:

<bean id="accountService" class="dyliang.service.impl.IAccountServiceImpl">
    <property name="accountDao" ref="accountDao" ></property>
    <property name="txManager" ref="txManager"></property>
</bean>

<bean id="txManager" class="dyliang.utils.TransactionManager">
    <property name="connectionUtils" ref="connectionUtils"></property>
</bean>

<bean id="connectionUtils" class="dyliang.utils.ConnectionUtils">
    <property name="dataSource" ref="dataSource"></property>
</bean>

<bean id="accountDao" class="dyliang.dao.impl.IAccountImpl">
    <property name="runner" ref="runner"></property>
    <property name="connectionUtils" ref="connectionUtils"></property>
</bean>

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

最后执行测试方法发现,不仅可以在正常情况下完成转账,而且当转账中间出现异常后也能正常的回滚操作。但现在程序中仍然存在问题:

  • 在业务层每个和数据库相关的方法中都要添加和事务相关的方法,重复代码多
  • 当事务类中的方法名改变时,需要修改每一个使用到该方法的地方,操作繁琐

那如何解决这个问题,使得事务的操作可以应用到每个方法,同时又可以避免上述的问题呢?动态代理!

Java中的代理和代理模式

这里我们使用的是基于接口的Proxy类的动态代理方式,同理也可以是使用基于子类的cglib代理。使用动态代理的目的是得到AccoutService的增强代理类,此时的增强操作就是关于事务的管理。

public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager txManager;

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

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public IAccountService getAccountService(){
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object rtValue = null;
                        try{
                            txManager.beginTransaction();
                            // 根据方法名执行指定的方法
                            rtValue= method.invoke(accountService, args);
                            txManager.commit();
                            return rtValue;
                        }
                        catch (Exception e){
                            txManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            txManager.release();
                        }
                    }
                });
    }
}

这样当事务类中的方法名改变时,我们只需要修改一个地方即可。最后在bean.xml中添加如下内容:

<bean id="proxyAccountService" 
      factory-bean="beanFactory" factory-method="getAccountService"></bean>

<bean id="beanFactory" class="dyliang.factory.BeanFactory">
	<property name="accountService" ref="accountService"></property>
	<property name="txManager" ref="txManager"></property>
</bean>

此时,测试类中的IAccountServie对象的 @Qualifier标签的内容为@Qualifier("proxyAccountService"),表示此时使用的是代理类

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

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

    @Test
    public void testTransfer(){
        System.out.println("----------------------");
        List<Account> all = as.findAll();
        for (Account account : all) {
            System.out.println(account);
        }
        as.transfer("Forlogen","Kobe",100f);

        System.out.println("----------------------");
        List<Account> all1 = as.findAll();
        for (Account account : all1) {
            System.out.println(account);
        }
    }
}

执行单元测试,发现它依然可以很好的完成转账操作,说明使用动态代理实现事务管理是可行的。

Account{id=1, name='Forlogen', money=1000.0}
Account{id=2, name='Kobe', money=1000.0}
Account{id=3, name='James', money=1000.0}
----------------------
Account{id=1, name='Forlogen', money=900.0}
Account{id=2, name='Kobe', money=1100.0}
Account{id=3, name='James', money=1000.0}

3. 基于XML的AOP

3.1 相关概念

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

3.2 环境配置

导入依赖:

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

更新约束:

<?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">
</beans>

3.3 案例

这里通过一个简单的案例来了解一下如果通过xml文件来实现AOP的配置。假设此时业务层接口为:

public interface IAccountService {
    void saveAccount();

    void updateAccount(int i);

    int  deleteAccount();
}

接口的实现类为:

public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("saveAccount...");
    }

    public void updateAccount(int i) {
        System.out.println("updateAccount...");
    }

    public int deleteAccount() {
        System.out.println("deleteAccount...");
        return 0;
    }
}

这里没有其他多余的操作。然后编写一个Logger类表示日志打印:

public class Logger {
    public void logging(){
        System.out.println("logging...");
    }
}

最后需要做的就是编写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">

    <bean id="accountService" class="dyliang.service.impl.AccountServiceImpl"></bean>
    <bean id="logger" class="dyliang.utils.Logger"></bean>
</beans>

那么如何配置AOP呢?首先先看一下和AOP配置相关的标签:

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

    <aop:config>
    	<!-- 配置的代码都写在此处 --> 
    </aop:config>
    
  • aop:aspect:用于配置切面,其中属性包含:

  • id:给切面提供一个唯一标识
  • ref:引用配置好的通知类bean的id
<aop:aspect id="txAdvice" ref="txManager">
	<!--配置通知的类型要写在此处--> 
</aop:aspect>
  • aop:pointcut:用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。属性包含:
  • expression:用于定义切入点表达式
  • id:用于给切入点表达式提供一个唯一标识
<aop:pointcut expression="..." id="pt"/>
  • before 作用: 用于配置前置通知。指定增强的方法在切入点方法之前执行 属性:
  • method:用于指定通知类中的增强方法名称
  • ponitcut-ref:用于指定切入点的表达式的引用
  • poinitcut:用于指定切入点表达式

执行时间点: 切入点方法执行之前执行。

<aop:before method="beginTransaction" pointcut-ref="pt"/>
  • aop:after-returning:作用: 用于配置后置通知 属性:
  • method:指定通知中方法的名称。
  • pointct:定义切入点表达式
  • pointcut-ref:指定切入点表达式的引用

执行时间点: 切入点方法正常执行之后,它和异常通知只能有一个执行。

<aop:after-returning method="commit" pointcut-ref="pt"/>
  • aop:after-throwing 作用: 用于配置异常通知 属性:
  • method:指定通知中方法的名称
  • pointct:定义切入点表达式
  • pointcut-ref:指定切入点表达式的引用

执行时间点: 切入点方法执行产生异常后执行,它和后置通知只能执行一个。

<aop:after-throwing method="rollback" pointcut-ref="pt"/>
  • aop:after作用: 用于配置最终通知 属性:
  • method:指定通知中方法的名称
  • pointct:定义切入点表达式
  • pointcut-ref:指定切入点表达式的引用

执行时间点: 无论切入点方法执行时是否有异常,它都会在其后面执行。

<aop:after method="release" pointcut-ref="pt"/>
  • aop:around: 作用: 用于配置环绕通知 属性:
  • method:指定通知中方法的名称
  • pointct:定义切入点表达式
  • pointcut-ref:指定切入点表达式的引用

它是spring框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。

注意: 通常情况下,环绕通知都是独立使用的。

<aop:config> 
    <aop:pointcut expression="" id="pt"/> 
    <aop:aspect id="txAdvice" ref="txManager"> 
        <!-- 配置环绕通知 --> 
        <aop:around method="transactionAround" pointcut-ref="pt"/> 
    </aop:aspect> 
</aop:config>

更新Logger类:

public class Logger {

    public  void beforePrintLog(){
        System.out.println("beforePrintLog...");
    }

    public  void afterReturningPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    public  void afterThrowingPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    public  void afterPrintLog(){
        System.out.println("afterPrintLog...");
    }

    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();
            System.out.println("before...");
            rtValue = pjp.proceed(args); 
            System.out.println("after return...");
            return rtValue;
        }catch (Throwable t){
            System.out.println("after throwing...");
            throw new RuntimeException(t);
        }finally {
            System.out.println("after...");
        }
    }
}

案例中使用AOP配置如下:

<aop:config>
    <!--配置切面-->
    <aop:aspect id="log" ref="logger">
        <aop:pointcut id="pt" expression="execution(* dyliang.service.impl.*.*(..))"/>
        <!--
        <aop:before method="logging" pointcut="execution(* dyliang.service.impl.*.*(..))"></aop:before>-->

        <!-- 配置前置通知:在切入点方法执行之前执行
        <aop:before method="beforePrintLog" pointcut-ref="pt" ></aop:before>-->

        <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个
        <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt"></aop:after-returning>-->

        <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个
        <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt"></aop:after-throwing>-->

        <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
        <aop:after method="afterPrintLog" pointcut-ref="pt"></aop:after>-->

        <!-- 配置环绕通知 详细的注释请看Logger类中-->
        <aop:around method="aroundPringLog" pointcut-ref="pt"></aop:around>
    </aop:aspect>
</aop:config>

执行测试方法:

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

        as.saveAccount();
        System.out.println("-----------");
        as.updateAccount(1);
        System.out.println("-----------");
        as.deleteAccount();
    }
}

通过输出可以看出,AOP配置成功!

before...
saveAccount...
after return...
after...
-----------
before...
updateAccount...
after return...
after...
-----------
before...
deleteAccount...
after return...
after...

4. 基于注解的AOP

下面针对于使用注解来配置AOP,此时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"
       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创建容器时要扫描的包-->
    <context:component-scan base-package="dyliang"></context:component-scan>

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

然后只关注和AOP配置相关的注解:

package dyliang.utils;

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

@Component("logger")
@Aspect
public class Logger {
    
    @Pointcut("execution(* dyliang.service.impl.*.*(..))")
    public void pt(){}

    @Before("pt()")
    public void beforePrintLog(){
        System.out.println("beforePrintLog...");
    }

    @AfterReturning("pt()")
    public void afterReturningPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    @AfterThrowing("pt()")
    public void afterThrowingPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    @After("pt()")
    public void afterPrintLog(){
        System.out.println("afterPrintLog...");
    }

    @Around("pt()")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();
            System.out.println("before...");
            rtValue = pjp.proceed(args);  
            System.out.println("after return...");
            return rtValue;
        }catch (Throwable t){
            System.out.println("after throwing...");
            throw new RuntimeException(t);
        }finally {
            System.out.println("after...");
        }
    }
}

其中涉及到的注解有:

  • @Aspect:表明当前类是一个切面类

  • @Before: 把当前方法看成是前置通知。 属性:

    • value:用于指定切入点表达式,还可以指定切入点表达式的引用
  • @AfterReturning作用: 把当前方法看成是后置通知。 属性:

    • value:用于指定切入点表达式,还可以指定切入点表达式的引用
  • @AfterThrowing: 把当前方法看成是异常通知。 属性:

    • value:用于指定切入点表达式,还可以指定切入点表达式的引用
  • @After: 把当前方法看成是最终通知。 属性:

    • value:用于指定切入点表达式,还可以指定切入点表达式的引用
  • @Around: 把当前方法看成是环绕通知。 属性:

    • value:用于指定切入点表达式,还可以指定切入点表达式的引用。
  • @Pointcut: 指定切入点表达式 属性:

    • value:指定表达式的内容

如果完全不用xml配置,可以创建配置类代替bean.xml:

@Configuration
@ComponentScan(basePackages="dyliang") 
@EnableAspectJAutoProxy 
public class SpringConfiguration { }
  • @Configuration:表示当前类为配置类
  • @ComponentScan(basePackages="..."):表明Spring创建ioc容器时应该扫描的包
  • @EnableAspectJAutoProxy:启用基于注解的AOP配置

5. 切入点表达式的写法

关键字:execution(表达式),表达式访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)

  • 标准的表达式写法:public void dyliang.service.impl.AccountServiceImpl.saveAccount()

  • 访问修饰符可以省略:void dyliang.service.impl.AccountServiceImpl.saveAccount()

  • 返回值可以使用通配符,表示任意返回值:

    * dyliang.service.impl.AccountServiceImpl.saveAccount()
    
  • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*. :

    * *.*.*.*.AccountServiceImpl.saveAccount())
    
  • 包名可以使用…表示当前包及其子包

    * *..AccountServiceImpl.saveAccount()
    
  • 类名和方法名都可以使用*来实现通配:

    * *..*.*()
    
  • 参数列表:

    • 直接写数据类型

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

       * *..*.*(..)