什么是切面
专业术语解释:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
通俗解释
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。上图中红框圈住的部分就是面向切面编程。
使用@Aspect进行切面编程
注解说明
-
@Aspect:作用是把当前类标识为一个切面供容器读取
-
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
-
@Around:环绕增强,相当于MethodInterceptor
-
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
-
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
-
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
-
@After:final增强,不管是抛出异常或者正常退出都会执行
使用过程
1、引入maven依赖
<!--引入AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在完成了引入AOP依赖包后,不需要去做其他配置。AOP的默认配置属性中,spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy,不需要在程序主类中增加@EnableAspectJAutoProxy来启用。
2、新建一个类,在类上添加@Aspect和@Component 注解即可将一个类定义为切面类。Aspect 注解 使之成为切面类, @Component 注解 把切面类加入到IOC容器中
3、定义切入点
@Pointcut 代表这是一个切入点
方式一:利用execution() 表达式主体进行切入
/**
* 指定切入点表达式
* public * com.hkl.modules.*.controller..*(..))
*/
//此处的表达式主体代表所有的controller的方法
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut(){}
注解@Pointcut代表这是一个切入点
execution(): 表达式主体,execution切点函数
第一个*符号 表示返回值的类型任意;
com.example.demo.controller下边的所有类以及子包的类
.*表示包下的所有类,而…表示包、子孙包下的所有类。
.(…) 表示任何方法名,括号表示参数,两个点表示任何参数类型
方式二:利用@annotation()自定义注解进行切入
/**
* 指定注解切入
* @param @annotation(xxx):xxx是自定义注解的全路径
*/
@Pointcut("@annotation(com.sinosoft.springbootplus.datapermission.aspect.annotation.PermissionData)")
public void pointCut() {
}
4、通知
- 前置通知(Before advice):在某个连接点(Join point)之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。
- 返回后通知(After returning advice):在某个连接点(Join point)正常完成后执行的通知。例如,一个方法没有抛出任何异常正常返回。
- 抛出异常后通知(After throwing advice):在方法抛出异常后执行的通知。
- 后置通知(After(finally)advice):当某个连接点(Join point)退出的时候执行的通知(不论是正常返回还是发生异常退出)。
- 环绕通知(Around advice):包围一个连接点(Join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
Demo
定义注解
package com.sinosoft.springbootplus.datapermission.aspect.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
public @interface PermissionData {
/**
* 配置菜单的组件路径,用于数据权限
*/
String permissionId() default "";
}
定义切面类
package com.sinosoft.springbootplus.datapermission.aspect;
import com.sinosoft.springbootplus.datapermission.aspect.annotation.PermissionData;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component
@Aspect
@Slf4j
public class AopConfigure {
private long startTime = 0;
/**
* 指定切入点表达式
* public * com.hkl.modules.*.controller..*(..))
*/
@Pointcut("execution( * com.sinosoft.springbootplus.system.controller..*(..))")
public void getMethods() {
}
/**
* 指定注解切入
* @param @annotation(xxx):xxx是自定义注解的全路径
*/
@Pointcut("@annotation(com.sinosoft.springbootplus.datapermission.aspect.annotation.PermissionData)")
public void withAnnotationMethods() {
}
/***
* 方法执行之前切入控制层
* 表达式和注解方式同时满足才会切入
* @param joinPoint
*/
@Before(value = "getMethods() && withAnnotationMethods()")
public void doBefore(JoinPoint joinPoint){
//获取Servlet容器
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取request请求
HttpServletRequest request = attributes.getRequest();
//执行方法对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//判断切入的方法是否标记xxx注解
//boolean flag = method.isAnnotationPresent(xxx.class);
//CollUtil.toList(role).contains(userType);
/** 可做操作说明 start */
//1、鉴权、解析request请求对象中设置的属性
//2、反射解析注解、记录操作日志等
/** 可做操作说明 end */
log.info("测试切入{}成功,方法名:"+method.getName(), "@Before");
startTime = System.currentTimeMillis();
}
/***
* 方法执行之后切入控制层
* 表达式和注解方式同时满足才会切入
* @param joinPoint
*/
//@After(value = "getMethods() && withAnnotationMethods()")
public void doAfter(JoinPoint joinPoint) {
//业务操作同 @Before 方式
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
log.info("测试切入{}成功,是否包含注解:"+method.isAnnotationPresent(PermissionData.class), "@After");
log.info("注解中的属性值:"+method.getAnnotation(PermissionData.class).permissionId());
//log.info("执行方法耗时为:" + (System.currentTimeMillis() - startTime));
}
/**
* <p>环绕增强切入</p>
* 表达式和注解方式同时满足才会切入
* @author hkl
* @date 2022/11/9
*/
@Around(value = "getMethods() && withAnnotationMethods()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//业务操作同 @Before 方式
long startTime = System.currentTimeMillis();
//方法执行之前动作,等效于@Before
Object res = point.proceed();
//方法执行之后动作,等效于@After
MethodSignature methodSignature = (MethodSignature)point.getSignature();
Method method = methodSignature.getMethod();
log.info("测试切入{}成功,是否包含注解:"+method.isAnnotationPresent(PermissionData.class), "@Around");
log.info("注解中的属性值:"+method.getAnnotation(PermissionData.class).permissionId());
log.info("执行方法耗时(毫秒)为:" + (System.currentTimeMillis() - startTime));
return res;
}
}
在切面中指定路径的方法上增加注解
swagger中调用该接口,日志如下图所示: