Spring(二):AOP 面向切面编程

目录

1. 转账案例

1.1 基础功能

1.2 传统事务

2. Proxy 优化转账案例

2.1 JDK 动态代理方式

2.2 CGLIB 动态代理方式

3. 初识 AOP

3.1 什么是 AOP

3.2 AOP 底层实现

3.3 AOP 相关术语

3.4 AOP 开发明确事项

3.5 小结

4. 基于 XML 的 AOP 开发

4.1 快速入门

4.2 XML 配置 AOP 详解

4.2.1 切点表达式

4.2.2 通知类型

4.3 小结

5. 基于注解的 AOP 开发

5.1 快速入门

5.2 注解配置 AOP 详解

5.2.1 切点表达式

5.2.2 通知类型

5.2.3 纯注解配置

5.3 总结

6. AOP 优化转账案例

6.1 xml 配置实现

6.2 注解配置实现


1. 转账案例

需求:使用 spring 框架整合 DBUtils 技术,实现用户转账功能

1.1 基础功能

步骤分析:

  1. 创建 java 项目,导入坐标
  2. 编写 Account 实体类
  3. 编写 AccountDao 接口和实现类
  4. 编写 AccountService 接口和实现类
  5. 编写 spring 核心配置文件
  6. 编写测试代码

1 . 创建 java 项目,导入坐标

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.15</version>
    </dependency>

    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.6</version>
    </dependency>

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

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>

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

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

2. 编写 AccountDao 接口和实现类

public interface AccountDao {
    /**
     * 转出操作
     * @param outUser
     * @param money
     */
    void out(String outUser,Double money);

    /**
     * 转入操作
     * @param inUser
     * @param money
     */
    void in(String inUser,Double money);
}
@Repository("AccountDao")
public class AccountDaoImpl implements AccountDao {
    @Autowired
    private QueryRunner queryRunner;

    @Override
    public void out(String outUser, Double money) {
        String sql = "update account set money = money - ? where name = ?";
        try {
            queryRunner.update(sql, money, outUser);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    @Override
    public void in(String inUser, Double money) {
        String sql = "update account set money = money + ? where name = ?";
        try {
            queryRunner.update(sql, money, inUser);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

3. 编写 AccountService 接口和实现类

public interface AccountService {
        /**
         * 转账方法
         */
        void transfer(String outUser,String inUser,Double money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    /**
     * 转账方法
     */
    @Override
    public void transfer(String outUser, String inUser, Double money) {
        // 编写了事务相关代码
        // 调用了减钱方法
        accountDao.out(outUser,money);

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

        // 调用了加钱方法
        accountDao.in(inUser,money);
    }
}

4. 编写 spring 核心配置文件

<?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"
       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/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
">
    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.zm"/>
    <!-- 引入 properties -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 配置 DataSource -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 配置 queryRunner -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>
</beans>

5. 编写测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:applicationContext.xml"})
public class AccountServiceTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer("tom", "jerry", 100d);
    }
}

问题分析
上面的代码事务在 Dao 层,转出转入操作都是一个独立的事务,但实际开发,应该把业务逻辑控制在一个事务中,所以应该将事务挪到 Service 层。

1.2 传统事务

步骤分析:

  1. 编写线程绑定工具类
  2. 编写事务管理器
  3. 修改 service 层代码
  4. 修改 dao 层代码

1. 编写线程绑定工具类


  /*
     连接工具类:从数据源中获取一个连接,并且将获取到的连接与线程进行绑定
     ThreadLocal : 线程内部的存储类,可以在指定的线程内存储数据 key:threadLocal(当前线程)   
                                                            value:任意类型的值 Connection

  */

@Component
public class ConnectionUtils {
    @Autowired
    private DataSource dataSource;

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

    /**
     * 获取当前线程上绑定连接:如果获取到的连接为空,那么就要从数据源中获取连接,并且放到 ThreadLocal 中(绑定到当前线程)
     */
    public Connection getThreadConnection() {
        // 1.先从 ThreadLocal 上获取连接
        Connection connection = threadLocal.get();
        // 2.判断当前线程中是否是有 Connection
        if(connection == null){
            // 3.从数据源中获取一个连接,并且存入 ThreadLocal 中
            try {
                
                connection = dataSource.getConnection();
                threadLocal.set(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return  connection;
    }

    /**
     * 解除当前线程的连接绑定
     */
    public void  removeThreadConnection(){
        threadLocal.remove();
    }
}

2. 编写事务管理器

@Component("transactionManager")
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 开启事务
     */
    public void beginTransaction(){
        // 获取 connection 对象
        Connection connection = connectionUtils.getThreadConnection();
        try {
            // 开启了一个手动事务
            connection.setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

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

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

    /**
     * 释放资源
     */
    public void release(){
        // 将手动事务改回成自动提交事务
        Connection connection = connectionUtils.getThreadConnection();
        try {
            connection.setAutoCommit(true);
            // 将连接归还到连接池
            connectionUtils.getThreadConnection().close();
            // 解除线程绑定
            connectionUtils.removeThreadConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

3. 修改 service 层代码

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Autowired
    private TransactionManager transactionManager;

    @Override
    public void transfer(String outUser, String inUser, Double money) {
        try {
            // 1.开启事务
            transactionManager.beginTransaction();

            // 2.业务操作
            // 编写了事务相关代码
            // 调用了减钱方法
            accountDao.out(outUser, money);
            // 模拟出错
            // int i= 1/0;
            // 调用了加钱方法
            accountDao.in(inUser, money);

            // 3.提交事务
            transactionManager.commit();
        } catch (Exception e) {
            // 4.回滚事务
            transactionManager.rollback();
            e.printStackTrace();
        } finally {
            // 5.释放资源
            transactionManager.release();
        }
    }
}

4. 修改 Dao 层代码

@Repository("AccountDao")
public class AccountDaoImpl implements AccountDao {
    @Autowired
    private QueryRunner queryRunner;

    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 转出操作
     */
    @Override
    public void out(String outUser, Double money) {
        String sql = "update account set money = money - ? where name = ?";
        try {
            queryRunner.update(connectionUtils.getThreadConnection(), sql, money, outUser);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    /**
     * 转入操作
     */
    @Override
    public void in(String inUser, Double money) {
        String sql = "update account set money = money + ? where name = ?";
        try {
            queryRunner.update(connectionUtils.getThreadConnection(), sql, money, inUser);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

问题分析
上面代码,通过对业务层改造,已经可以实现事务控制了,但是由于添加了事务控制,也产生了一个新的问题: 业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了,违背了面向对象的开发思想。


2. Proxy 优化转账案例

可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强。这样就不会对业务层产生影响,解决了耦合性的问题。

常用的动态代理技术:

  • JDK 代理,基于接口的动态代理技术 - 利用拦截器(必须实现 invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理,从而实现方法增强

  • CGLIB 代理,基于父类的动态代理技术 - 动态生成一个要代理的子类,子类重写要代理的类的所有不是 final 的方法;在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强

2.1 JDK 动态代理方式

去掉 AccountServiceImpl 的事务控制代码

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Override
    public void transfer(String outUser, String inUser, Double money) {
        accountDao.out(outUser, money);
        // 模拟出错
        int i= 1/0;
        accountDao.in(inUser, money);
    }
}

JDK 工厂类

@Component
public class JDKProxyFactory {
    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager transactionManager;

    /**
     * 采用 JDK 动态代理技术来生成目标类的代理对象
     * ClassLoader loader : 类加载器:借助被代理对象获取到类加载器
     * Class<?>[] interfaces : 被代理类所需要实现的全部接口
     * InvocationHandler h : 当代理对象调用接口中的任意方法时,那么都会执行 InvocationHandler 中 invoke 方法
     */
    public AccountService createAccountServiceJdkProxy() {
        return (AccountService) Proxy.newProxyInstance(
                accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * @param proxy 当前的代理对象引用
                     * @param method 被调用的目标方法的引用
                     * @param args 被调用的目标方法所用到的参数
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) {
                        try {
                            if ("transfer".equalsIgnoreCase(method.getName())) {
                                System.out.println("JDK Proxy: Pre-Enhance ...");
                                transactionManager.beginTransaction();

                                // 让被代理对象原方法执行
                                method.invoke(accountService, args);

                                System.out.println("JDK Proxy: Post-Enhance ...");
                                transactionManager.commit();
                            } else {
                                method.invoke(accountService, args);
                            }
                        } catch (Exception e) {
                            transactionManager.rollback();
                            e.printStackTrace();
                        } finally {
                            transactionManager.release();
                        }

                        return null;
                    }
                });
    }

}

测试代码

@Autowired
private JDKProxyFactory jdkProxyFactory;

@Test
public void testTransferProxyJDK(){
    // 当前返回的实际上是 AccountService 的代理对象 proxy
    AccountService accountServiceJDKProxy = jdkProxyFactory.createAccountServiceJDKProxy();
    // 代理对象 proxy 调用接口中的任意方法时,都会执行底层的 invoke 方法
    accountServiceJDKProxy.transfer("tom", "jerry", 100d);
}

2.2 CGLIB 动态代理方式

CGLIB 工厂类

@Component
public class CglibProxyFactory {
    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager transactionManager;

    /**
     * 编写 cglib 对应的 API 来生成代理对象进行返回
     * 参数 1 : 目标类的字节码对象
     * 参数 2:  动作类,当代理对象调用目标对象中原方法时,那么会执行 intercept 方法
     */
    public AccountService createAccountServiceCglibProxy() {
        return (AccountService) Enhancer.create(accountService.getClass(), new MethodInterceptor() {
            /**
             * @param o 代表生成的代理对象
             * @param method 调用目标方法的引用
             * @param objects 方法入参
             * @param methodProxy 代理方法
             */
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) {
                try {
                    System.out.println("CGLIB Proxy: Pre-Enhance ...");
                    // 手动开启事务:调用事务管理器类中的开启事务方法
                    transactionManager.beginTransaction();

                    method.invoke(accountService, objects);

                    System.out.println("CGLIB Proxy: Post-Enhance ...");
                    transactionManager.commit();
                } catch (Exception e) {
                    // 手动回滚事务
                    transactionManager.rollback();
                    e.printStackTrace();
                } finally {
                    // 手动释放资源
                    transactionManager.release();
                }

                return null;
            }
        });
    }
}

3. 初识 AOP

3.1 什么是 AOP

AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程。

AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

优势:

  1. 在程序运行期间,在不修改源码的情况下对方法进行功能增强
  2. 逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
  3. 减少重复代码,提高开发效率,便于后期维护

3.2 AOP 底层实现

实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring 通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

3.3 AOP 相关术语

Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。

在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:

* Target(目标对象):代理的目标对象;被代理类;

* Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类;生成的代理对象;

* Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点;在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点;可以被拦截增强的方法;

* Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义;即真正被拦截增强的方法;

* Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知分类:前置通知、后置通知、异常通知、最终通知、环绕通知 - 一种可以通过代码的方式来手动控制的类型;即增强的业务逻辑;

* Aspect(切面):是切入点和通知(引介)的结合;

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

3.4 AOP 开发明确事项

开发阶段

  1. 编写核心业务代码(目标类的目标方法) 切入点
  2. 把公用代码抽取出来,制作成通知(增强功能方法) 通知
  3. 在配置文件中,声明切入点与通知间的关系,即切面

运行阶段(Spring 框架自动完成)

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

底层代理实现

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

  • 当 bean 实现接口时,会用 JDK 代理模式
  • 当 bean 没有实现接口,用 cglib 实现( 可以强制使用 cglib(在 spring 配置中加入 <aop:aspectj-autoproxy proxy-target-class=”true”/>)

3.5 小结

  • aop - 面向切面编程
  • aop 底层实现:基于 JDK 的动态代理和基于 Cglib 的动态代理
  • aop 的重点概念:
       Pointcut(切入点):真正被增强的方法
       Advice(通知/ 增强):封装增强业务逻辑的方法
       Aspect(切面):切点 + 通知
       Weaving(织入):将切点与通知结合,产生代理对象的过程

4. 基于 XML 的 AOP 开发

4.1 快速入门

步骤分析:

  1. 创建 java 项目,导入 AOP 相关坐标
  2. 创建目标接口和目标实现类(定义切入点)
  3. 创建通知类及方法(定义通知)
  4. 将目标类和通知类对象创建权交给 spring
  5. 在核心配置文件中配置织入关系,及切面
  6. 编写测试代码

1. 创建 java 项目,导入 AOP 相关坐标

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.11</java.version>
    <maven.compiler.source>1.11</maven.compiler.source>
    <maven.compiler.target>1.11</maven.compiler.target>
</properties>

<dependencies>
    <!-- 导入 spring 的 context 坐标,context 依赖 aop -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <!-- aspectj 的织入(切点表达式需要用到该 jar 包) -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>
    <!-- spring 整合 junit -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

2. 创建目标接口和目标实现类

public interface AccountService {
    /**
     * 目标方法:(切入点:要进行拦截增强的方法)
     */
    void transfer();
}
public class AccountServiceImpl implements AccountService {
    @Override
    public void transfer() {
        System.out.println("转账方法执行了....");
        //int i = 1/0;
    }
}

3. 创建通知类

public class MyAdvice {
    public void before(){
        System.out.println("前置通知执行了....");
    }
}

4. 将目标类和通知类对象创建权交给 spring

<!-- 目标类交给 IOC 容器 -->
<bean id="accountServcie" class="com.zm.service.impl.AccountServiceImpl"/>

<!-- 通知类交给 IOC 容器 -->
<bean id="myAdvice" class="com.zm.advice.MyAdvice"/>

5. 在核心配置文件中配置织入关系,及切面
导入 AOP 命名空间

<?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="accountServcie" class="com.zm.service.impl.AccountServiceImpl"/>
    
    <bean id="myAdvice" class="com.zm.advice.MyAdvice"/>
    
    <aop:config>
        <aop:aspect ref="myAdvice">
            <aop:before method="before" pointcut="execution(public void com.zm.service.impl.AccountServiceImpl.transfer())"/>
        </aop:aspect>
    </aop:config>

</beans>

6. 编写测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:applicationContext.xml"})
public class AccountServiceTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer();
    }
}

4.2 XML 配置 AOP 详解

4.2.1 切点表达式

表达式语法:

excution( [修饰符]  返回值类型  包名.类名.方法名(参数))
  • 访问修饰符可以省略
  • 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
  • 包名与类名之间一个点.代表当前包下的类,两个点 ..表示当前包及其子包下的类
  • 参数列表可以使用两个点..表示任意个数,任意类型的参数列表

例子:

  • execution([修饰符] 返回值类型 包名.类名.方法名(参数))
    execution(public void com.zm.service.impl.AccountServiceImpl.transfer(java.lang.String))

  • 访问修饰符可以省略
    execution(void com.zm.service.impl.AccountServiceImpl.transfer(java.lang.String))

  • 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
    execution(* *.*.*.*.*.*(java.lang.String))

  • 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
    execution(* *..*.*(java.lang.String))

  • 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表
    execution(* *..*.*(..))

  • 其实,比较常用的写法还是:execution(* com.zm.service.impl.AccountServiceImpl.*(..))

切点表达式抽取:
当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式。

public class MyAdvice {
    public void before(){
        System.out.println("前置通知执行了....");
    }

    public void afterReturning(){
        System.out.println("后置通知执行了....");
    }

    public void afterThrowing(){
        System.out.println("异常通知执行了....");
    }

    public void after(){
        System.out.println("最终通知执行了....");
    }




    /**
     * 环绕通知 (环绕通知建议单独使用,使用该通知时将上面的四个通知代码删掉)
     * @param pjp Proceeding JoinPoint - 正在执行的连接点:切点
     */
    public Object around(ProceedingJoinPoint pjp){
        Object proceed = null;
        try {
            System.out.println("前置通知执行了");
            // 切点方法执行
            proceed = pjp.proceed();
            System.out.println("后置通知执行了");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("异常通知执行了");
        }finally {
            System.out.println("最终通知执行了");
        }

        return proceed;
    }
}

<aop:config>
    <!-- 抽取的切点表达式 -->
    <aop:pointcut id="myPointcut" expression="execution(* com.zm.service.impl.AccountServiceImpl.*(..))"/>
    <!-- 配置切面:切入点 + 通知 -->
    <aop:aspect ref="myAdvice">
        <aop:before method="before" pointcut-ref="myPointcut"/>
        <aop:after-returning method="afterReturning" pointcut-ref="myPointcut"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
        <aop:after method="after" pointcut-ref="myPointcut"/>
        <!-- <aop:around method="around" pointcut-ref="myPointcut"/> -->
    </aop:aspect>
</aop:config>

4.2.2 通知类型

通知的配置语法:

<aop:通知类型 method=“通知类中方法名” pointcut=“切点表达式"></aop:通知类型>
名称标签说明
前置通知<aop:before>用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知<aop:afterReturning>用于配置后置通知。指定增强的方法在切入点方法之后执行
异常通知<aop:afterThrowing>用于配置异常通知;指定增强的方法出现异常后执行
最终通知<aop:after>用于配置最终通知;无论切入点方法执行时是否有异常,都会执行
环绕通知<aop:around>用于配置环绕通知;开发者可以手动控制增强代码在什么时候执行

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

4.3 小结

  • aop 织入的配置
    <aop:config>
         <aop:aspect ref=“通知类”>
               <aop:before method=“通知方法名称” pointcut=“切点表达式"></aop:before>
         </aop:aspect>
    </aop:config>

  • 通知的类型
    前置通知、后置通知、异常通知、最终通知
    环绕通知

  • 切点表达式
    execution([修饰符] 返回值类型 包名.类名.方法名(参数))


5. 基于注解的 AOP 开发

5.1 快速入门

步骤分析:

  1. 创建 java 项目,导入 AOP 相关坐标
  2. 创建目标接口和目标实现类(定义切入点)
  3. 创建通知类(定义通知)
  4. 将目标类和通知类对象创建权交给 spring
  5. 在通知类中使用注解配置织入关系,升级为切面类
  6. 在配置文件中开启组件扫描和 AOP 的自动代理
  7. 编写测试代码

1. 创建 java 项目,导入 AOP 相关坐标

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.11</java.version>
    <maven.compiler.source>1.11</maven.compiler.source>
    <maven.compiler.target>1.11</maven.compiler.target>
</properties>

<dependencies>
    <!-- 导入 spring 的 context 坐标,context 依赖 aop -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <!-- aspectj 的织入 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>
    <!-- spring 整合 junit -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

2. 创建目标接口和目标实现类

public interface AccountService {
    void transfer();
}
public class AccountServiceImpl implements AccountService {
    @Override
    public void transfer() {
        System.out.println("转账方法执行了....");
    }
}

3. 创建通知类

public class MyAdvice {
    public void before(){
        System.out.println("前置通知执行了....");
    }
}

4. 将目标类和通知类对象创建权交给 spring

@Service
public class AccountServiceImpl implements AccountService {
    @Override
    public void transfer() {
        System.out.println("转账方法执行了....");
    }
}

@Component
public class MyAdvice {
    ...
}

5. 在通知类中使用注解配置织入关系,升级为切面类

@Component
@Aspect // 升级为切面类:配置切入点和通知的关系
public class MyAdvice {
    @Before("execution(* com.zm.service.impl.AccountServiceImpl.*(..))")
    public void before(){
        System.out.println("前置通知执行了....");
    }
}

6. 在配置文件中开启组件扫描和 AOP 的自动代理

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

    <!-- 开启 IOC 注解扫描 -->
    <context:component-scan base-package="com.zm"/>

    <!-- aop 的自动代理:采用动态代理完成织入增强,并且生成代理;proxy-target-class="true" 表示强制使用 cglib 动态代理-->
    <aop:aspectj-autoproxy proxy-target-class="false"/>

</beans>

7. 编写测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer(){
        accountService.transfer();
    }

}

5.2 注解配置 AOP 详解

5.2.1 切点表达式

切点表达式的抽取

@Component
@Aspect // 升级为切面类:配置切入点和通知的关系
public class MyAdvice {

    @Pointcut("execution(* com.zm.service.impl.AccountServiceImpl.*(..))")
    public void myPoint(){

    }

    @Before("MyAdvice.myPoint()")
    public void before(){
        System.out.println("前置通知执行了....");
    }

    @AfterReturning("MyAdvice.myPoint()")
    public void afterReturning(){
        System.out.println("后置通知执行了....");
    }

    @AfterThrowing("MyAdvice.myPoint()")
    public void afterThrowing(){
        System.out.println("异常通知执行了....");
    }

    @After("MyAdvice.myPoint()")
    public void after(){
        System.out.println("最终通知执行了....");
    }



    /**
     * 环绕通知 (环绕通知建议单独使用,使用该通知时将上面的四个通知代码删掉)
     * @param pjp Proceeding JoinPoint - 正在执行的连接点:切点
     */
    @Around("MyAdvice.myPoint()")
    public Object around(ProceedingJoinPoint pjp){
        Object proceed = null;
        try {
            System.out.println("前置通知执行了");
            // 切点方法执行
            proceed = pjp.proceed();
            System.out.println("后置通知执行了");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("异常通知执行了");
        }finally {
            System.out.println("最终通知执行了");
        }

        return proceed;
    }

}

5.2.2 通知类型

通知的配置语法:@通知注解(“切点表达式")

名称标签说明
前置通知@Before用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知@AfterReturning用于配置后置通知。指定增强的方法在切入点方法之后执行
异常通知@AfterThrowing用于配置异常通知;指定增强的方法出现异常后执行
最终通知@After用于配置最终通知;无论切入点方法执行时是否有异常,都会执行
环绕通知@Around用于配置环绕通知;开发者可以手动控制增强代码在什么时候执行

注意
当使用注解方式将前四个通知组合在一起时(xml方式没有这个bug),会出现一个 Spring 的执行顺序的 Bug,错误的执行顺序如下:

@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing)

如果单独使用环绕通知 @Around 注解则不会有这个 Bug,执行顺序如下:

@Before -> @AfterReturning(如果有异常:@AfterThrowing)-> @After

5.2.3 纯注解配置

去掉 applicationContext.xml 配置文件,增加 Spring核心配置类:SpringConfig 配置类

@Configuration
@ComponentScan("com.zm")
@EnableAspectJAutoProxy // 开启 AOP 的自动代理,替代 xml 配置的 <aop:aspectj-autoproxy />
public class SpringConfig {
}

修改测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer(){
        accountService.transfer();
    }

}

5.3 总结

  • 使用 @Aspect 注解,标注切面类
  • 使用 @Before 等注解,标注通知方法
  • 使用 @Pointcut 注解,抽取切点表达式
  • 配置 aop 自动代理 <aop:aspectj-autoproxy/> 或 @EnableAspectJAutoProxy

6. AOP 优化转账案例

依然使用前面的转账案例,将两个代理工厂对象直接删除,改为 spring 的 AOP 思想来实现

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

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.zm"/>

    <!-- 引入 properties,加载jdbc配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置 DataSource -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 queryRunner -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>



    <!-- AOP 配置 -->
      <aop:config>
          <!-- 1.切点表达式 -->
          <aop:pointcut id="myPointcut" expression="execution(* com.zm.service.impl.AccountServiceImpl.*(..))"/>
          <!-- 2.切面配置 -->
          <aop:aspect ref="transactionManager">
              <aop:before method="beginTransaction" pointcut-ref="myPointcut"/>
              <aop:after-returning method="commit"  pointcut-ref="myPointcut"/>
              <aop:after-throwing method="rollback" pointcut-ref="myPointcut"/>
              <aop:after method="release" pointcut-ref="myPointcut"/>
          </aop:aspect>
      </aop:config>

</beans>

事务管理器(通知)

package com.zm.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * 事务管理器工具类:包含:开启事务、提交事务、回滚事务、释放资源
 *      Spring AOP 的通知类
 *
 */
@Component("transactionManager")
public class TransactionManager {

    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 开启事务
     */
    public void beginTransaction(){
        // 获取 connection 对象
        Connection connection = connectionUtils.getThreadConnection();
        try {
            // 开启了一个手动事务
            connection.setAutoCommit(false);
            System.out.println("开启事务");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commit(){
        Connection connection = connectionUtils.getThreadConnection();
        try {
            connection.commit();
            System.out.println("提交事务");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback(){
        Connection connection = connectionUtils.getThreadConnection();
        try {
            connection.rollback();
            System.out.println("回滚事务");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放资源
     */
    public void release(){
        // 将手动事务改回成自动提交事务
        Connection connection = connectionUtils.getThreadConnection();
        try {
            connection.setAutoCommit(true);
            // 将连接归还到连接池
            connectionUtils.getThreadConnection().close();
            // 解除线程绑定
            connectionUtils.removeThreadConnection();
            System.out.println("释放资源");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

}

6.2 注解配置实现

配置文件

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

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.zm"/>

    <!-- 引入 properties -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置 DataSource -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 queryRunner -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>

    <!-- 开启 AOP 的自动代理 -->
    <aop:aspectj-autoproxy/>

</beans>

事务管理器(通知)

@Component("transactionManager")
@Aspect // 表明该类为切面类
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;

    @Around("execution(* com.zm.service.impl.AccountServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws SQLException {
        Object proceed = null;

        try {
            // 开启手动事务
            System.out.println("开启事务");
            connectionUtils.getThreadConnection().setAutoCommit(false);

            // 切入点方法执行
            proceed = pjp.proceed();

            // 手动提交事务
            System.out.println("提交事务");
            connectionUtils.getThreadConnection().commit();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            // 手动回滚事务
            System.out.println("回滚事务");
            connectionUtils.getThreadConnection().rollback();
        } finally {
            System.out.println("释放资源");
            // 将手动事务恢复成自动事务
            connectionUtils.getThreadConnection().setAutoCommit(true);
            // 将连接归还到连接池
            connectionUtils.getThreadConnection().close();
            // 解除线程绑定
            connectionUtils.removeThreadConnection();
        }

        return proceed;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值