Spring 中的切面 Aspect,这是 Spring 的一大优势。面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代。
AOP 的全称为 Aspect Oriented Programming,译为面向切面编程,是通过预编译方式和运行期动态代理实现核心业务逻辑之外的横切行为的统一维护的一种技术。AOP 是面向对象编程(OOP)的补充和扩展。 利用 AOP 可以对业务逻辑各部分进行隔离,从而达到降低模块之间的耦合度,并将那些影响多个类的公共行为封装到一个可重用模块,从而到达提高程序的复用性,同时提高了开发效率,提高了系统的可操作性和可维护性。
AOP 是 Spring 框架中的一个核心内容。在 Spring 中,AOP 代理可以用 JDK 动态代理或者 CGLIB 代理 CglibAopProxy 实现。Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成和管理,其依赖关系也由 IOC 容器负责管理。
在实际的 Web 项目开发中,我们常常需要对各个层面实现日志记录,性能统计,安全控制,事务处理,异常处理等等功能。如果我们对每个层面的每个类都独立编写这部分代码,那久而久之代码将变得很难维护,所以我们把这些功能从业务逻辑代码中分离出来,聚合在一起维护,而且我们能灵活地选择何处需要使用这些代码。
名词 | 概念 | 理解 |
切面(Aspect) | 切面类的定义,里面包含了切入点(Pointcut)和通知(Advice)的定义 | 首先要理解“切”字,需要把对象想象成一个立方体,每次实例化一个对象,对类定义中的成员变量赋值,就相当于对这个立方体进行了一个定义,定义完成之后,就等着被使用,等着被回收。 面向切面编程则是指,对于一个我们已经封装好的类,我们可以在编译期间或在运行期间,对其进行切割,把立方体切开,在原有的方法里面添加(织入)一些新的代码,对原有的方法代码进行一次增强处理。而那些增强部分的代码,就被称之为切面,如下面代码实例中的通用日志处理代码,常见的还有事务处理、权限认证等等。 |
切入点(PointCut) | 对连接点进行拦截的定义 | 要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。既要切哪些东西。 |
连接点(JoinPoint) | 被拦截到的点,如被拦截的方法、对类成员的访问以及异常处理程序块的执行等等,自身还能嵌套其他的 Joint Point。 | 知道了要切哪些方法后,剩下的就是什么时候切,在原方法的哪一个执行阶段加入增加代码,这个就是连接点。如方法调用前,方法调用后,发生异常时等等。 |
通知(Advice) | 拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。 | 通知被织入方法,该如何被增强。定义切面的具体实现。那么这里面就涉及到一个问题,空间(切哪里)和时间(什么时候切,在何时加入增加代码),空间我们已经知道了就是切入点中定义的方法,而什么时候切,则是连接点的概念。 |
目标对象(Target Object) | 切入点选择的对象,也就是需要被通知的对象。由于 Spring AOP 通过代理模式实现,所以该对象永远是被代理对象。 | 被一个或多个切面所通知的对象,即为目标对象。即业务逻辑本身。 |
AOP 代理对象(AOP Proxy Object) | Spring AOP 可以使用 JDK 动态代理或者 CGLIB 代理,前者基于接口,后者基于类。 | AOP代理是AOP框架所生成的对象,该对象是目标对象的代理对象。代理对象能够在目标对象的基础上,在相应的连接点上调用通知。 |
织入(Weaving) | 把切面应用到目标对象从而创建出AOP代理对象的过程。织入可以在编译期、类装载期、运行期进行,而 Spring 采用在运行期完成。 | 将切面切入到目标方法之中,使目标方法得到增强的过程被称之为织入。 |
注解 | 说明 |
@Aspect | 将一个 java 类定义为切面类 |
@Pointcut | 定义一个切入点,定义需要拦截的东西,即上下文中所关注的某件事情的入口,切入点定义了事件触发时机。可以是一个规则表达式(execution() 表达式,annotation() 表达式),比如下例中某个 package 下的所有函数,也可以是一个注解等 |
@Before | 在切入点开始处切入内容 |
@After | 在切入点结尾处切入内容 |
@AfterReturning | 在切入点 return 内容之后处理逻辑 |
@Around | 在切入点前后切入内容,并自己控制何时执行切入点自身的内容 |
@AfterThrowing | 用来处理当切入内容部分抛出异常之后的处理逻辑 |
@Order(100) | AOP 切面执行顺序,@Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行 |
其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都属于通(Advice)。
org.springframework.boot
spring-boot-starter-aop
接下来我们先看一个例子,利用AOP+Swagger注解实现日志记录功能,所有接口请求完成时,打印日志记录。
具体实现如下:
1、创建一个 AOP 切面类,只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现 advice:
@Aspect
@Slf4j
@Component
@Order(1)
public class WebLogAspect {
private ThreadLocal startTime = new Thre
// 定义一个切点:所有接口中被@ApiOperation 注解修饰的方法会织入advice
@Pointcut("@annotation(operation)")
public void logPointcut(ApiOperation operation
}
// Before 表示 before 将在目标方法执行前执行
@Before(value = "logPointcut(operation)", argN
public void before(JoinPoint joinPoint, ApiOperation operation) {
startTime.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String[] moduleName = getModuleName(joinPoint, operation);
log.info("[{}]-[{}]-start {}", moduleName[0], moduleName[1], request.getRequestURL().toString());
log.info("参数 : {}", Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "logPointcut(operation) ", argNames = "joinPoint,ret,operation")
public void after(JoinPoint joinPoint, Object ret, ApiOperation operation) {
// 处理完请求,返回内容
String[] moduleName = getModuleName(joinPoint, operation);
String retMesage = JSON.toJSONString(ret);
log.info("[{}]-[{}]-end [{}ms] 响应:{}", moduleName[0], moduleName[1], (System.currentTimeMillis() - startTime.get()), retMesage);
startTime.remove();
}
private String[] getModuleName(JoinPoint joinPoint, ApiOperation operation) {
Class> clazz = joinPoint.getTarget().getClass();
Api api = clazz.getAnnotation(Api.class);
String pInfo = Optional.ofNullable(api).map(Api::value).orElse(clazz.getName());
return new String[]{pInfo, operation.value()};
}
}
2、创建一个接口类,在方法上加上 swagger 的 @ApiOperation 的注解
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售卖渠道订单查询", notes = "售卖渠道订单查询")
public ApiResultResponse getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
3、项目启动后,请求该接口时,日志打印如下:
INFO n.b.z.c.l.WebLogAspect - [售卖渠道信息相关]-[售卖渠道资金池余额查询接口]-start http://localhost:8080/openapi/supplier/getSupplier
INFO n.b.z.c.l.WebLogAspect - 参数 : [SupplierAssetsPoolRequest(supplierId=8)]
INFO n.b.z.c.l.WebLogAspect - [售卖渠道信息相关]-[售卖渠道资金池余额查询接口]-end [136ms] 响应:{"apiResult":{"balance":2910},"retCode":"000000","retMsg":"成功","retState":"SUCCESS"}
下面将问题复杂化一些,该例的场景是:
1、自定义一个注解 SupplierCheck
2、创建一个切面类,切点设置为校验所有标注 SupplierCheck 的方法,截取到接口的参数,进行简单的 ip 白名单校验
3、将 SupplierCheck 标注在接口类上面的方法上
具体的实现步骤:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SupplierCheck {
/**
* 校验白名单
* 标识该方法是否需要校验白名单(默认校验),白话文就是说是否需要执行该校验白名单方法
*/
boolean checkIpWhite() default true;
}
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。可以指定多个位置(@Target({ElementType.METHOD, ElementType.FIELD})),语法如下:
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
类型 | 描述 |
ElementType.TYPE | 可以用于类、接口和枚举类型 |
ElementType.FIELD | 可以用于字段(包括枚举常量) |
ElementType.METHOD | 可以用于方法(controller上面的接口里它也是方法) |
ElementType.PARAMETER | 可以用于方法的参数 |
ElementType.CONSTRUCTOR | 可以用于构造函数 |
ElementType.LOCAL_VARIABLE | 可以用于局部变量 |
ElementType.ANNOTATION_TYPE | 可以用于注解类型 |
ElementType.PACKAGE | 可以用于包 |
ElementType.TYPE_PARAMETER | 可以用于类型参数声明(Java 8新增) |
ElementType.TYPE_USE | 可以用于使用类型的任何语句中(Java 8新增) |
该注解指定了被修饰的注解的生命周期,语法如下:
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
类型 | 描述 |
RetentionPolicy.SOURCE | 在源文件中有效(即源文件保留) |
RetentionPolicy.CLASS | 在class文件中有效(即class保留),不会被加载到JVM中 |
RetentionPolicy.RUNTIME | 在运行时有效(即运行时保留),会被加载到JVM中 |
@Documented 注解表示被它修饰的注解将被 javadoc 工具提取成文档。
@Inherited 注解表示被它修饰的注解具有继承性,即如果一个类声明了被 @Inherited 修饰的注解,那么它的子类也将具有这个注解。
只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里面实现第一步白名单校验逻辑:
@Aspect
@Order(3)
@Slf4j
@Component
public class SupplierAspect {
@Around("@annotation(supplierCheck)")
public Object around(ProceedingJoinPoint pjp, SupplierCheck supplierCheck) throws Throwable {
try {
Object obj = pjp.getArgs()[0];
// 业务逻辑
doCheck(obj, supplierCheck);
return pjp.proceed(pjp.getArgs());
} catch (BizException e) {
log.warn(e.getLogMsg());
return buildErrorMsg(e.getApiCode(), e.getApiMsg());
} catch (Exception e) {
log.error("SupplierAspect 调用失败 ", e);
return buildErrorMsg(ResponseCode.FAILED.code, ResponseCode.FAILED.desc);
}
}
private void doCheck(Object obj, SupplierCheck,supplierCheck) throws BizException {
AppAuthRequest authRequest = JSON.parseObject(JSON.toJSONString(obj), AppAuthRequest.class);
log.info("authRequest = {}", authRequest);
//白名单校验
if (supplierCheck.checkIpWhite()) {
// 白名单业务逻辑
}
}
3、创建接口类,并在目标方法上标注自定义注解 SupplierCheck :
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售卖渠道订单查询", notes = "售卖渠道订单查询")
@SupplierCheck
public ApiResultResponse getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
很简单,一个自定义的 AOP 注解可以对应多个切面类,这些切面类执行顺序由 @Order 注解管理,该注解后的数字越小,所在切面类越先执行。
比如上面接口中 @ApiOperation 增强的注解中(第一个实例介绍的)的 @Order(1),那么这个注解先执行,@SupplierCheck 白名单校验的注解的 @Order(3) 后面执行。