1 Spring AOP简介
1.1 AOP 概述
1.1.1 AOP 是什么?
AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。如图-1所示:
图-1
AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。
1.1.2 AOP 应用场景分析
实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现。
AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。如图-2所示:
图-2
思考:现有一业务,在没有AOP编程时,如何基于OCP原则实现功能扩展?
1.1.3 Spring AOP 应用原理分析(先了解)
Spring AOP底层基于代理机制(动态方式)实现功能扩展:
-
假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
-
假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。
Spring AOP 原理分析,如图-3所示:
图-3
说明:Spring boot2.x 中AOP现在默认使用的CGLIB代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties)中进行如下配置:
spring.aop.proxy-target-class=false
1.2 Spring 中AOP 相关术语分析
▪ 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。
▪ 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。
▪ 连接点(joinpoint):程序执行过程中某个特定的点,一般指向被拦截到的目标方法。
▪ 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。
连接点与切入点定义如图-4所示:
图-4
说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。
2 Spring AOP快速实践
2.1 业务描述
基于项目中的核心业务,添加简单的日志操作,借助SLF4J日志API输出目标方法的执行时长。(前提,不能修改目标方法代码-遵循OCP原则)
2.2 项目创建及配置
创建maven项目或在已有项目基础上添加AOP启动依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ 是一个面向切面的框架,他定义了AOP的一些语法,有一个专门的字节码生成器来生成遵守java规范的class文件。
2.3 扩展业务分析及实现
2.3.1 创建日志切面类对象
将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长,其关键代码如下:
package com.cy.pj.common.aspect;
@Aspect
@Slf4j
@Component
public class SysLogAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doLogPointCut() {}
@Around("doLogPointCut()")
public Object around(ProceedingJoinPoint jp)throws Throwable{
try {
log.info("start:{}"+System.currentTimeMillis());
Object result=jp.proceed();//最终会调用目标方法
log.info("after:{}"+System.currentTimeMillis());
return result;
}catch(Throwable e) {
log.error("after:{}",e.getMessage());
throw e;
}
}
}
说明:
▪ @Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。
▪ @Pointcut注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的名字。
▪ @Around注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。
▪ ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。只能用于@Around注解描述的方法参数。
当我们切入点引入不正确时,会出现如图所示错误:
2.3.2 业务切面测试实现
启动项目测试或者进行单元测试,其中Spring Boot项目中的单元测试代码如下:
@SpringBootTest
public class AopTests {
@Autowired
private SysUserService userService;
@Test
public void testSysUserService() {
PageObject<SysUserDeptVo> po=
userService.findPageObjects("admin",1);
System.out.println("rowCount:"+po.getRowCount());
}
}
对于测试类中的userService对象而言,它有可能指向JDK代理,也有可能指向CGLIB代理,具体是什么类型的代理对象,要看application.yml配置文件中的配置.
2.3.3 应用总结分析
在业务应用,AOP相关对象分析,如图-5所示:
图-5
2.4 扩展业务织入增强分析
2.4.1 基于JDK代理方式实现
假如目标对象有实现接口,则可以基于JDK为目标对象创建代理对象,然后为目标对象进行功能扩展,如图-6所示:
图-6
说明:假如目标对象类型没有实现接口,则不允许使用JDK代理。
2.4.2 基于CGLIB代理方式实现
假如目标对象没有实现接口(当然实现了接口也是可以的),可以基于CGLIB代理方式为目标对象织入功能扩展,如图-7所示:
图-7
说明:目标对象实现了接口也可以基于CGLIB为目标对象创建代理对象。但是目标对象类型假如使用了final修饰,则不可以使用CGBLIB。
3 Spring AOP编程增强
3.1 切面通知应用增强
3.1.1 通知类型
在基于Spring AOP编程的过程中,基于AspectJ框架标准,spring中定义了五种类型的通知(通知-Advice描述的是一种扩展业务),它们分别是:
▪ @Before。(目标方法执行之前执行)
▪ @AfterReturning。(目标方法成功结束时执行)
▪ @AfterThrowing。(目标方法异常结束时执行)
▪ @After。(目标方法结束时执行)
▪ @Around.(重点掌握,目标方法执行前后都可以做业务拓展)(优先级最高)
说明:在切面类中使用什么通知,由业务决定,并不是说,在切面中要把所有通知都写上。代码实践分析如下:
package com.cy.pj.common.aspect;
@Component
@Aspect
public class SysTimeAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doTime(){}
@Before("doTime()")
public void doBefore(){
System.out.println("time doBefore()");
}
@After("doTime()")
public void doAfter(){
System.out.println("time doAfter()");
}
/*核心业务正常结束时执行 说明:假如有after,先执行after,再执行returning*/
@AfterReturning("doTime()")
public void doAfterReturning(){
System.out.println("time doAfterReturning");
}
/*核心业务出现异常时执行说明:假如有after,先执行after,再执行Throwing/
@AfterThrowing("doTime()")
public void doAfterThrowing(){
System.out.println("time doAfterThrowing");
}
@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp)
throws Throwable{
System.out.println("doAround.before");
try{
Object obj=jp.proceed();
System.out.println("doAround.after");
return obj;
}catch(Throwable e){
System.out.println(e.getMessage());
throw e;
}
}
}
3.2 切入点表达式增强
Spring中通过切入点表达式定义具体切入点,其常用AOP切入点表达式定义及说明:
表-1 Spring AOP 中切入点表达式说明
指示符 | 作用 |
---|---|
bean | 用于匹配指定bean对象的所有方法 |
within | 用于匹配指定包下所有类内的所有方法 |
execution | 用于按指定语法规则匹配到具体方法 |
@annotation | 用于匹配指定注解修饰的方法 |
3.2.1 bean表达式(重点)
bean表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:
▪ bean(“userServiceImpl”)指定一个userServiceImpl类中所有方法。
▪ bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法。
说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。
缺陷:不能精确到具体方法,也不能针对于具体模块包中的方法做切入点设计
3.2.2 within表达式(了解)
within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:
▪ within(“aop.service.UserServiceImpl”)指定当前包中这个类内部的所有方法。
▪ within(“aop.service.*”) 指定当前目录下的所有类的所有方法。
▪ within(“aop.service…*”) 指定当前目录以及子目录中类的所有方法。
within表达式应用场景分析:
1)对所有业务bean都要进行功能增强,但是bean名字又没有规则。
2)按业务模块(不同包下的业务)对bean对象进行业务功能增强。
3.2.3 execution表达式(了解)
execution表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:
语法:execution(返回值类型 包名.类名.方法名(参数列表))。
▪ execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
▪ execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。
▪ execution(* aop.service….(…)) 万能配置。
3.2.4 @annotation表达式(重点)
@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析
▪ @annotation(anno.RequiredLog) 匹配有此注解描述的方法。
▪ @annotation(anno.RequiredCache) 匹配有此注解描述的方法。
其中:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会在执行此方法时进行日扩展操作。
定义一Cache相关切面,使用注解表达式定义切入点,并使用此注解对需要使用cache的业务方法进行描述,代码分析如下:
第一步:定义注解RequiredCache
package com.cy.pj.common.annotation;
/**
- 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredCache {
//...
}
第二步:定义SysCacheAspect切面对象。
package com.cy.pj.common.aspect;
@Aspect
@Component
public class SysCacheAspect {
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
public void doCache() {}
@Around("doCache()")
public Object around(ProceedingJoinPoint jp)
throws** Throwable{**
System.out.println("Get data from cache");
Object obj=jp.proceed();
System.out.println("Put data to cache");
return obj;
}
}
第三步:使用@RequiredCache注解对特定业务目标对象中的查询方法进行描述(这里以部门模块的查询方法为例)。
@RequiredCache
@Override
public List<Map<String, Object>> findObjects() {
….
return list;
}
第四步:进行部门模块的访问测试,分析其结果.
3.3 切面优先级设置实现
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。例如:
定义日志切面并指定优先级。
@Order(1)
@Aspect
@Component
public class SysLogAspect {
…
}
定义缓存切面并指定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}
说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤器链、拦截器链,其执行分析如图-9所示:
图-9
3.4 关键对象与术语总结
Spring 基于AspectJ框架实现AOP设计的关键对象概览,如图-10所示:
图-10
总结
重难点分析
▪ AOP 是什么,解决了什么问题,应用场景?
▪ AOP 编程基本步骤及实现过程(以基于AspectJ框架实现为例)。
▪ AOP 编程中的核心对象及应用关系。(代理对象,切面对象,通知,切入点)
▪ AOP 思想在Spring中的实现原理分析。(基于代理方式进行扩展业务的织入)
▪ AOP 编程中基于注解方式的配置实现。(@Aspect,@PointCut,@Around,…)
▪ AOP 编程中基于注解方式的事务控制。(@Transactional)
▪ AOP 编程中异步操作的实现?(@EnableAsync,@Async,配置)
▪ AOP 编程中的缓存应用?(@EnableCaching,@Cacheable,@CacheEvict)
FAQ分析
▪ 什么是OCP原则(开闭原则)?
▪ 什么是DIP原则 (依赖倒置)?
▪ 什么是单一职责原则(SRP)?
▪ Spring 中AOP的有哪些配置方式?(XML,注解)
▪ Spring 中AOP 的通知有哪些基本类型?(5种)
▪ Spring 中AOP是如何为Bean对象创建代理对象的?(JDK,CGLIB)
▪ Spring 中AOP切面的执行顺序如何指定?(@Order)
▪ Spring 单体架构项目中事务的控制要通过Connection对象实现,?
▪ Spring 如何保证一个线程一个Connection对象?借助ThreadLocal实现.?
▪ 多个事务并发执行时可能会出现什么问题?(脏读,不可重复读,幻影读)
▪ 你了解事务的隔离级别吗?知道具体的应用场景吗?
▪ ……
Bug分析
▪ 切入点应用错误,如图-17所示:
图-17
问题分析:检查切入点的引入是否丢掉了"()".