AOP面向切面

image

AOP 是什么?

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

AOP和OOP

AOP 与 OOP 字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象
运行时动态织入一些扩展功能或控制对象执行

AOP 应用场景分析?

实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在
编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助 AOP 进行实现。

AOP 就是要基于 OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如 AOP 应用于项目中的日志处理,事务处理,
权限处理,缓存处理等等。

Spring AOP 应用原理分析(先了解)?

Spring AOP 底层基于代理机制(动态方式)实现功能扩展:
1) 假如目标对象(被代理对象)实现接口,则底层可以采用 JDK 动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
2) 假如目标对象(被代理对象)没有实现接口,则底层可以采用 CGLIB 代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)
Spring AOP 原理分析,如图-3 所示:
image
说明:Spring boot2.x 中 AOP 现在默认使用的 CGLIB 代理,假如需要使用 JDK 动态
代理可以在配置文件(applicatiion.properties)中进行如下配置:

spring.aop.proxy-target-class=false

Spring 中 AOP 相关术语分析

▪ 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect 声明)。
▪ 通 知 (Advice): 在 切 面 的 某 个 特 定 连 接 点 上 执 行 的 动 作 ( 扩 展 功 能 ) , 例 如around,before,after 等。
▪ 连接点(joinpoint):程序执行过程中某个特定的点,一般指向被拦截到的目标方法。
▪ 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集 合。

连接点与切入点定义如图-4 所示:
image
说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。

Spring AOP 快速实践

业务描述

基于项目中的核心业务,添加简单的日志操作,借助 SLF4J 日志 API 输出目标方法的执行时长。(前提,不能修改目标方法代码-遵循 OCP 原则)

项目创建及配置

创建 maven 项目或在已有项目基础上添加 AOP 启动依赖:


 org.springframework.boot
 spring-boot-starter-aop

说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵守 java 规范的 class 文件。

扩展业务分析及实现

将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长,
其关键代码如下:

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 注解描述的方法参数。

当我们切入点引入不正确时,会出现如图所示错误:
image

业务切面测试实现

启动项目测试或者进行单元测试,其中 Spring Boot 项目中的单元测试代码如下:

@SpringBootTest
public class AopTests {
@Autowired
private SysUserService userService;
@Test
public void testSysUserService() {
PageObject po=
userService.findPageObjects("admin",1);
System.out.println("rowCount:"+po.getRowCount());
} }

对于测试类中的 userService 对象而言,它有可能指向 JDK 代理,也有可能指向 CGLIB代理,具体是什么类型的代理对象,要看 application.yml 配置文件中的配置

应用总结分析

在业务应用,AOP 相关对象分析,如图-5 所示:
image

扩展业务织入增强分析

基于 JDK 代理方式实现

假如目标对象有实现接口,则可以基于 JDK 为目标对象创建代理对象,然后为目标对象
进行功能扩展,如图-6 所示:
image

基于 CGLIB 代理方式实现

假如目标对象没有实现接口(当然实现了接口也是可以的),可以基于 CGLIB 代理方式
为目标对象织入功能扩展,如图-7 所示:
image

Spring AOP 编程增强

通知类型

在基于 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;
 } } }
切入点表达式增强

Spring 中通过切入点表达式定义具体切入点,其常用 AOP 切入点表达式定义及说明:
image

bean 表达式(重点)

bean 表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:
▪ bean("userServiceImpl")指定一个 userServiceImpl 类中所有方法。
▪ bean("*ServiceImpl")指定所有后缀为 ServiceImpl 的类中所有方法。
说明:bean 表达式内部的对象是由 spring 容器管理的一个 bean 对象,表达式内部的
名字应该是 spring 容器中某个 bean 的 name。
缺陷:不能精确到具体方法,也不能针对于具体模块包中的方法做切入点设计

within 表达式(了解)

within 表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:
▪ within("aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法。
▪ within("aop.service.*") 指定当前目录下的所有类的所有方法。
▪ within("aop.service..*") 指定当前目录以及子目录中类的所有方法。
within 表达式应用场景分析:
1)对所有业务 bean 都要进行功能增强,但是 bean 名字又没有规则。
2)按业务模块(不同包下的业务)对 bean 对象进行业务功能增强。

execution 表达式(了解)

execution 表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:
语法:execution(返回值类型 包名.类名.方法名(参数列表))。 ▪ execution(void aop.service.UserServiceImpl.addUser())匹配 addUser 方法。
▪ execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为
String 的 addUser 方法。
▪ execution( aop.service...*(..)) 万能配置

@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> findObjects() {
….
return list; }

第四步:进行部门模块的访问测试,分析其结果.

切面优先级设置实现

切面的优先级需要借助@Order 注解进行描述,数字越小优先级越高,默认优先级比较低。例如:
定义日志切面并指定优先级。

@Order(1)
@Aspect
@Component
public class SysLogAspect {
…}

定义缓存切面并指定优先级:

@Order(2)
@Aspect
@Component
public class SysCacheAspect {
… }

说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤器链、拦截器链,其执行分析如图-9 所示:
image

关键对象与术语总结

Spring 基于 AspectJ 框架实现 AOP 设计的关键对象概览,如图-10 所示:
image

重难点分析

▪ AOP 是什么,解决了什么问题,应用场景?
▪ AOP 编程基本步骤及实现过程(以基于 AspectJ 框架实现为例)。
▪ AOP 编程中的核心对象及应用关系。(代理对象,切面对象,通知,切入点)
▪ AOP 思想在 Spring 中的实现原理分析。(基于代理方式进行扩展业务的织入)
▪ AOP 编程中基于注解方式的配置实现。(@Aspect,@PointCut,@Around,...)

FAQ 分析

▪ 什么是 OCP 原则(开闭原则)?
▪ 什么是 DIP 原则 (依赖倒置)?
▪ 什么是单一职责原则(SRP)?
▪ Spring 中 AOP 的有哪些配置方式?(XML,注解)
▪ Spring 中 AOP 的通知有哪些基本类型?(5 种)
▪ Spring 中 AOP 是如何为 Bean 对象创建代理对象的?(JDK,CGLIB)
▪ Spring 中 AOP 切面的执行顺序如何指定?(@Order)

Bug 分析

image

你可能感兴趣的