在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。上图中红框圈住的部分就是面向切面编程。
@Aspect:作用是把当前类标识为一个切面供容器读取
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After:final增强,不管是抛出异常或者正常退出都会执行
1、引入maven依赖
org.springframework.boot spring-boot-starter-aop
在完成了引入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、通知
定义注解
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));}/*** 环绕增强切入
* 表达式和注解方式同时满足才会切入* @author hkl* @date 2022/11/9*/@Around(value = "getMethods() && withAnnotationMethods()")public Object around(ProceedingJoinPoint point) throws Throwable {//业务操作同 @Before 方式long startTime = System.currentTimeMillis();//方法执行之前动作,等效于@BeforeObject res = point.proceed();//方法执行之后动作,等效于@AfterMethodSignature 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中调用该接口,日志如下图所示: