[SSM][11][面向切面编程]

第 11 章 面向切面编程

11.1 一个简单的约定游戏

11.2 SpringAOP的基本概念

11.2.1 AOP的概念和使用原因

先看一个不使用SpringAOP的例子,这里使用MyBatis框架,完成扣减一个产品的库存并且新增一笔交易记录的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void savePurchaseRecord(Long productId, PurchaseRecord record){
SqlSession sqlSession = null;
try{
sqlSession = SqlSessionFactoryUtils.openSqlSession();
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
Product product = productMapper.getRole(productId);
if(product.getStock() >= record.getQuantity()){
product.setStock(product.getStock() - record.getQuantity());
productMapper.update(product);
PurchaseRecordMapper purchaseRecordMapper = sqlSession.getMapper(PurchaseRecordMapper.class);
purchaseRecordMapper.save(record);
sqlSession.commit();
}
}catch(Exception e){
e.printStackTrace();
sqlSession.rollBack();
}finally{
if(sqlSession != null){
sqlSession.close();
}
}
}
  • 这里的购买交易的产品和购买记录都在try...catch...finally...语句中
  • 业务流程中穿插着事务的提交和回滚
  • 并不是一个很好的设计

针对上一个例子的缺点,SpringAOP希望开发者能写成下面例子的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
private ProductMapper productMapper = null;
@Autowired
private PurchaseRecordMapper purchaseRecordMapper = null;
//.....
@Transactional
public void updateRoleNote(Long productId, PurchaseRecord record){
Product product = productMapper.getRole(productId);
if(product.getStock() >= record.getQuantity()){
product.setStock(product.getStock() - record.getQuantity());
productMapper.update(product);
purchaseRecordMapper.save(record);
}
}
  • 这段代码除了一个注解@Transactional,没有任何打开、关闭数据库资源或者提交回滚事务的代码,却能完成与上面例子相同的功能
  • 这段代码主要集中在业务处理上,而不是数据库事务和资源控制,这就是AOP的魅力

接下来我们探讨SpringAOP是如何做到这点的。

AOP可以通过动态代理模式,带来管控各个对象操作的切面环境,管理包括日志、数据库事务等操作,让我们拥有在反射原有对象方法之前、正常返回之后、异常返回之后插入自己代码的能力,有时我们甚至可以取代原有对象方法。

下面还是以数据库事务的例子来做说明

  • 首先我们来了解以下正常SQL的逻辑步骤

    1. 通过数据库连接池来获得数据库连接资源,并进行一定的设置工作
    2. 执行对应的SQL语句,对数据进行操作
    3. 如果SQL执行过程中发生异常,回滚事务
    4. 如果SQL执行过程中没有发生异常,最后提交事务
    5. 关闭连接资源
  • SQL逻辑步骤的流程图如下

  • 而作为AOP,完全可以根据这个流程做一定的封装,然后通过动态代理技术,将代码织入到对应的流程中,我们完全可以进行如下设计

    1. 打开获取数据连接在before方法中完成
    2. 执行SQL,通过反射机制调用业务逻辑
    3. 如果SQL执行过程中发生异常,回滚事务,如果SQL执行过程中没有发生异常,提交事务,关闭连接资源
  • 更重要的,对于数据库事务这种通用的操作来说,SpringAOP已经提供了一些通用的拦截器来处理,并不需要开发者自己实现。开发者只需要修改配置,就可以定制想要的功能。这就是@Transactional标签要做的,当方法标注为@Transcational标签时,方法启用数据库事务功能。如下图所示

  • 通过这种方式,达成了约定优于配置的原则,使得开发者更关注于业务逻辑本身,而不是资源的控制。

11.2.2 面向切面编程的术语

这一节来解释一些AOP的常用术语

1. 切面

切面就是在一个怎么样的环境中工作,它可以定义后面需要介绍的通知、切点和引入等内容,然后SpringAOP会将其定义的内容织入到约定的流程中,在动态代理中可以把它理解为拦截器,比如类RoleInterceptor就是一个切面类。而常见的比如说,数据库事务就可以理解为一个切面

2 通知

通知是切面的方法,与动态代理中,拦截器的方法类似,通知有如下种类:

  • 前置通知(before): 在动态代理反射原有对象方法前执行的通知
  • 后置通知(after): 在动态代理反射原有对象方法后执行的通知
  • 返回通知(afterRuturning):在动态代理反射原有对象方法正常返回后执行的通知
  • 异常通知(afterThrowing):在动态代理反射原有对象方法产生异常后执行的通知
  • 环绕通知(around):它可以取代当前被拦截对象的方法
3 引入

引入允许我们在现有的类里添加自定义的类和方法

4 切点

用来告诉SpringAOP在什么时候启动拦截并织入对应的流程中

5 连接点

连接点就是需要拦截器拦截的方法,比如例子中的savePurchaseRecord就是一个连接点

6 织入

织入是一个生成代理对象并将切面内容放入到流程中的过程

AOP的流程图如下所示

11.2.3 Spring对AOP的支持

SpringAOP是一种基于方法拦截的AOP。在Spring中有4种方法去实现AOP的拦截

  • 使用ProxyFactoryBean和对应的接口实现AOP
  • 使用XML配置AOP
  • 使用@AspectJ注解驱动切面
  • 使用AspectJ注入切面

11.3 使用@AspectJ注解开发SpringAOP

11.3.1 选择连接点

Spring是方法级别的AOP框架,所以连接点只能是某个类下的某个方法。用动态代理来理解,就是要拦截哪个方法织入对应的AOP通知。

例子:我们先定义一个接口,和一个实现类,然后将实现类里的方法作为连接点

1
2
3
4
5
package ComponentDemo;

public interface RoleService {
public void printRoleInfo(Role role);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package ComponentDemo;

import org.springframework.stereotype.Component;

@Component(value = "roleServiceImpl")
public class RoleServiceImpl implements RoleService {
@Override
public void printRoleInfo(Role role) {
System.out.println("id = " + role.getId());
System.out.println("roleName = " + role.getRoleName());
System.out.println("note = " + role.getNote());
}
}

11.3.2 创建切面

Spring中使用@Aspect注解一个类,那么这个类就会被看作切面,就相当于动态代理中的拦截器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package ComponentDemo;


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

@Aspect
@Component(value = "roleAspect")
public class RoleAspect{
@Before("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))")
public void before(){
System.out.println("before......");
}

@After("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))")
public void after(){
System.out.println("after......");
}

@AfterReturning("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))")
public void afterReturning(){
System.out.println("afterReturning......");
}

@AfterThrowing("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))")
public void afterThrowing(){
System.out.println("afterThrowing......");
}
}

11.3.3 定义切点

毕竟不是所有方法都需要使用AOP编程,所以我们的程序要有判断是否启用切面的功能,这也就是切点的定义,确定对于哪些调用启用切面。

如上面的例程所示,Spring是通过@Before这类注解后的正则表达式来判断切点的,例如下面这个表达式execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))

  • execution: 表示执行方法的时候触发
  • *:代表任意返回类型的方法
  • ComponentDemo.RoleServiceImpl: 代表类的全限定名
  • printRoleInfo: 是被拦截的方法名称
  • (..): 任意的参数

进一步讨论这个正则表达式,它还可以配置如下内容

AspectJ 描述
arg() 规定连接点匹配参数为指定类型的方法
@args() 规定连接点匹配指定注解标注的方法
execution 匹配连接点的执行方法
this() 规定连接点匹配AOP代理的Bean
target 规定连接点匹配被代理对象为指定的类型
@target() 规定连接点匹配特定的执行对象,这些对象要符合指定的注解类型
within() 规定连接点匹配指定的包
@within() 规定连接点匹配指定的类型
@annotation 规定匹配带有指定注解的连接点

11.3.4 测试AOP

这一节来编写程序测试AOP是否生效

首先需要进行Spring Bean的配置

1
2
3
4
5
6
7
8
9
package ComponentDemo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"ComponentDemo"})
public class PojoConfig {
}
  • @EnableAspectJAutoProxy代表启用AspectJ框架的自动代理,这时Spring才会生成动态代理对象,进而使用AOP

然后编写程序的主入口即可

1
2
3
4
5
6
7
8
9
10
11
12
13
package ComponentDemo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(PojoConfig.class);
Role role = ctx.getBean(Role.class);
RoleService roleService = ctx.getBean(RoleService.class);
roleService.printRoleInfo(role);
((AnnotationConfigApplicationContext) ctx).close();
}
}

运行结果如下

1
2
3
4
5
6
before......
id = 1
roleName = role_name_1
note = role_note_1
after......
afterReturning......

11.3.5 环绕通知

环绕通知是SpringAOP中最强大的通知,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大,又灵活。但是由于强大,它的可控制性不那么强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。

例如

1
2
3
4
5
6
7
8
9
10
@Around("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))")
public void around(ProceedingJoinPoint jp){
System.out.println("around before......");
try{
jp.proceed();
}catch(Throwable e){
e.printStackTrace();
}
System.out.println("around after......");
}

11.3.6 织入

织入是生成代理对象并将切面内容放入约定流程的过程。使用JDK动态代理时,必须拥有接口,而使用CGLib 则不需要,于是Spring就提供了一个规则:当类的实现存在接口的时候,Spring将提供JDK动态代理。而当类不存在接口的时候没有办法使用JDK动态代理,Spring会采用CGLIB来生成代理对象。

11.3.7 给通知传递参数

有时我们希望给各类通知传递参数,可以使用如下方法

1
2
3
4
@Before("execution(* ComponentDemo.RoleServiceImpl.printRoleInfo(..))" + "&& args(role)")
public void before(Role role){
System.out.println("before......" + role.getRoleName());
}
  • 在切点的表达式中,加入了参数的定义,这样便可以传递参数

11.3.8 引入

11.4 使用XML配置开发SpringAOP

方法大致和使用注解类似,这里不详细说明

11.5 经典SpringAOP应用程序

11.6 多个切面

Spring支持使多个切面按照指定的顺序运行,这时候可以使用注解@Order

1
2
3
4
@Aspect
@Order(1)
public class Aspect1{
}
1
2
3
4
@Aspect
@Order(2)
public class Aspect2{
}
1
2
3
4
@Aspect
@Order(3)
public class Aspect2{
}

Spring底层是通过责任链来处理多个切面的,如下图

11.7 小结

AOPSpring两大核心内容之一,通过AOP可以将一些比较公用的代码抽取出来,进而减少开发者的工作量