目录
@Pointcut("@annotation(xx)") 拦截拥有指定注解的方法
Spring AOP 注解概述
1、Spring 的 AOP 功能除了在配置文件中配置一大堆的配置,比如切入点、表达式、通知等等以外,使用注解的方式更为方便快捷,特别是 Spring boot 出现以后,基本不再使用原先的 beans.xml 等配置文件了,而都推荐注解编程。
@Aspect | 切面声明,标注在类、接口(包括注解类型)或枚举上。 |
@Pointcut | 切入点声明,即切入到哪些目标类的目标方法。既可以用 execution 切点表达式, 也可以是 annotation 指定拦截拥有指定注解的方法. value 属性指定切入点表达式,默认为 "",用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式 |
@Before | 前置通知, 在目标方法(切入点)执行之前执行。 value 属性绑定通知的切入点表达式,可以关联切入点声明,也可以直接设置切入点表达式 注意:如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知 -> 异常通知。 |
@After | 后置通知, 在目标方法(切入点)执行之后执行 |
@AfterReturning | 返回通知, 在目标方法(切入点)返回结果之后执行. pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 "" |
@AfterThrowing | 异常通知, 在方法抛出异常之后执行, 意味着跳过返回通知 pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 "" 注意:如果目标方法自己 try-catch 了异常,而没有继续往外抛,则不会进入此回调函数 |
@Around | 环绕通知:目标方法执行前后分别执行一些代码,类似拦截器,可以控制目标方法是否继续执行。 通常用于统计方法耗时,参数校验等等操作。 |
正常流程:【环绕通知-前】-> 【前置通知】-> 【返回通知】-> 【后置通知】->【环绕通知-后】。
2、上面这些 AOP 注解都是位于如下所示的 aspectjweaver 依赖中:
3、对于习惯了 Spring 全家桶编程的人来说,并不是需要直接引入 aspectjweaver 依赖,因为 spring-boot-starter-aop 组件默认已经引用了 aspectjweaver 来实现 AOP 功能。换句话说 Spring 的 AOP 功能就是依赖的 aspectjweaver !
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
4、AOP 底层是通过 Spring 提供的的动态代理技术实现的,在运行期间动态生成代理对象,代理对象方法执行时进行增强功能的介入,再去调用目标对象的方法,从而完成功能的增强。主要使用 JDK 动态代理与 Cglib 动态代理。
5、所以如果目标类不是 Spring 组件,则无法拦截,如果是 类名.方法名 方式调用,也无法拦截。
@Aspect 快速入门
1、@Aspect 常见用于记录日志、异常集中处理、权限验证、Web 参数校验、事务处理等等
2、要想把一个类变成切面类,只需3步:
1)在类上使用 @Aspect 注解使之成为切面类 2)切面类需要交由 Sprign 容器管理,所以类上还需要有 @Service、@Repository、@Controller、@Component 等注解 |
3、AOP 的含义就不再累述了,下面直接上示例:
/**
* 切面注解 Aspect 使用入门
* 1、@Aspect:声明本类为切面类
* 2、@Component:将本类交由 Spring 容器管理
* 3、@Order:指定切入执行顺序,数值越小,切面执行顺序越靠前,默认为 Integer.MAX_VALUE
*
* @author wangMaoXiong
* @version 1.0
* @date 2020/8/20 19:22
*/
@Aspect
@Order(value = 999)
@Component
public class AspectHelloWorld {
private static final Logger LOG = LoggerFactory.getLogger(AspectHelloWorld.class);
/**
* @Pointcut :切入点声明,即切入到哪些目标方法。value 属性指定切入点表达式,默认为 ""。
* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
* <p>
* 切入点表达式常用格式举例如下:
* - * com.wmx.aspect.EmpService.*(..)):表示 com.wmx.aspect.EmpService 类中的任意方法
* - * com.wmx.aspect.*.*(..)):表示 com.wmx.aspect 包(不含子包)下任意类中的任意方法
* - * com.wmx.aspect..*.*(..)):表示 com.wmx.aspect 包及其子包下任意类中的任意方法
* </p>
* value 的 execution 可以有多个,使用 || 隔开.
*/
@Pointcut(value =
"execution(* com.wmx.hb.controller.DeptController.*(..)) " +
"|| execution(* com.wmx.hb.controller.EmpController.*(..))")
private void aspectPointcut() {
}
/**
* 前置通知:目标方法执行之前执行以下方法体的内容。
* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* <br/>
* * @param joinPoint:提供对连接点处可用状态和有关它的静态信息的反射访问<br/> <p>
* * * Object[] getArgs():返回此连接点处(目标方法)的参数,目标方法无参数时,返回空数组
* * * Signature getSignature():返回连接点处的签名。
* * * Object getTarget():返回目标对象
* * * Object getThis():返回当前正在执行的对象
* * * StaticPart getStaticPart():返回一个封装此连接点的静态部分的对象。
* * * SourceLocation getSourceLocation():返回与连接点对应的源位置
* * * String toLongString():返回连接点的扩展字符串表示形式。
* * * String toShortString():返回连接点的缩写字符串表示形式。
* * * String getKind():返回表示连接点类型的字符串
* * * </p>
*/
@Before(value = "aspectPointcut()")
public void aspectBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
Object target = joinPoint.getTarget();
Object aThis = joinPoint.getThis();
JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();
SourceLocation sourceLocation = joinPoint.getSourceLocation();
String longString = joinPoint.toLongString();
String shortString = joinPoint.toShortString();
LOG.debug("【前置通知】" +
"args={},signature={},target={},aThis={},staticPart={}," +
"sourceLocation={},longString={},shortString={}"
, Arrays.asList(args), signature, target, aThis, staticPart, sourceLocation, longString, shortString);
}
/**
* 后置通知:目标方法执行之后执行以下方法体的内容,不管目标方法是否发生异常。
* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
*/
@After(value = "aspectPointcut()")
public void aspectAfter(JoinPoint joinPoint) {
LOG.debug("【后置通知】kind={}", joinPoint.getKind());
}
/**
* 返回通知:目标方法返回后执行以下代码
* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
* returning 属性:通知签名中要将返回值绑定到的参数的名称,默认为 ""
*
* @param joinPoint :提供对连接点处可用状态和有关它的静态信息的反射访问
* @param result :目标方法返回的值,参数名称与 returning 属性值一致。无返回值时,这里 result 会为 null.
*/
@AfterReturning(pointcut = "aspectPointcut()", returning = "result")
public void aspectAfterReturning(JoinPoint joinPoint, Object result) {
LOG.debug("【返回通知】,shortString={},result=", joinPoint.toShortString(), result);
}
/**
* 异常通知:目标方法发生异常的时候执行以下代码,此时返回通知不会再触发
* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
* throwing 属性:与方法中的异常参数名称一致,
*
* @param ex:捕获的异常对象,名称与 throwing 属性值一致
*/
@AfterThrowing(pointcut = "aspectPointcut()", throwing = "ex")
public void aspectAfterThrowing(JoinPoint jp, Exception ex) {
String methodName = jp.getSignature().getName();
if (ex instanceof ArithmeticException) {
LOG.error("【异常通知】" + methodName + "方法算术异常(ArithmeticException):" + ex.getMessage());
} else {
LOG.error("【异常通知】" + methodName + "方法异常:" + ex.getMessage());
}
}
/**
* 环绕通知
* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚
* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "aspectPointcut()")
public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkRequestParam(joinPoint);
StopWatch stopWatch = StopWatch.createStarted();
LOG.debug("【环绕通知】执行接口开始,方法={},参数={} ", joinPoint.getSignature(), Arrays.asList(joinPoint.getArgs()).toString());
//继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
//如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走.
Object proceed = joinPoint.proceed(joinPoint.getArgs());
stopWatch.stop();
long watchTime = stopWatch.getTime();
LOG.debug("【环绕通知】执行接口结束,方法={}, 返回值={},耗时={} (毫秒)", joinPoint.getSignature(), proceed, watchTime);
return proceed;
}
/**
* 参数校验,防止 SQL 注入
*
* @param joinPoint
*/
private void checkRequestParam(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args == null || args.length <= 0) {
return;
}
String params = Arrays.toString(joinPoint.getArgs()).toUpperCase();
String[] keywords = {"DELETE ", "UPDATE ", "SELECT ", "INSERT ", "SET ", "SUBSTR(", "COUNT(", "DROP ",
"TRUNCATE ", "INTO ", "DECLARE ", "EXEC ", "EXECUTE ", " AND ", " OR ", "--"};
for (String keyword : keywords) {
if (params.contains(keyword)) {
LOG.warn("参数存在SQL注入风险,其中包含非法字符 {}.", keyword);
throw new RuntimeException("参数存在SQL注入风险:params=" + params);
}
}
}
}
如上所示在不修改原来业务层代码的基础上,就可以使用 AOP 功能,在目标方法执行前后或者异常时都能捕获然后执行。
在线演示源码:src/main/java/com/wmx/hb/aop/AspectHelloWorld.java · 汪少棠/hb - Gitee.com
execution 切点表达式 拦截指定类的方法
1、@Pointcut 切入点声明注解,以及所有的通知注解都可以通过 value 属性或者 pointcut 属性指定切入点表达式。
2、切入点表达式通过 execution 函数匹配连接点,语法:execution([方法修饰符] 返回类型 包名.类名.方法名(参数类型) [异常类型])
- 访问修饰符可以省略;
- 返回值类型、包名、类名、方法名可以使用星号*代表任意;
- 包名与类名之间一个点.代表当前包下的类,两个点..表示当前包及其子包下的类;
- 参数列表可以使用两个点..表示任意个数,任意类型的参数列表;
3、切入点表达式的写法比较灵活,比如:* 号表示任意一个,.. 表示任意多个,还可以使用 &&、||、! 进行逻辑运算,不过实际开发中通常用不到那么多花里胡哨的,掌握以下几种就基本够用了。
4、特别注意:当明确指定了切入的类时,类必须存在,否则启动报错,此时可以在类名前后加上*号表示模糊包含。
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(Integer)) | 匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,且带有一个 Integer 类型参数。 |
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(*)) | 匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,且带有一个任意类型参数。 |
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(..)) | 匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,参数不限。 |
execution(* grp.basic3.se.service.SEBasAgencyService3.editAgencyInfo(..)) || execution(* grp.basic3.se.service.SEBasAgencyService3.adjustAgencyInfo(..)) | 匹配 editAgencyInfo 方法或者 adjustAgencyInfo 方法 |
@Pointcut("(execution(* grp.basic3..*Controller*.*(..)) && !execution(* grp.basic3.BaseExceptionController*.*(..)))") | 匹配 grp.basic3包及其子包下面名称包含 'Controller' 类中的全部方法,但是排除掉其中的以 BaseExceptionController 开头的类。 |
execution(* com.wmx.aspect.EmpService.*(..)) | 匹配 com.wmx.aspect.EmpService 类中的任意方法 |
execution(* com.wmx.aspect.*.*(..)) | 匹配 com.wmx.aspect 包(不含子包)下任意类中的任意方法 |
execution(* com.wmx.aspect..*.*(..)) | 匹配 com.wmx.aspect 包及其子包下任意类中的任意方法 |
execution(* grp.pm..*Controller.*(..)) | 匹配 grp.pm 包下任意子孙包中以 "Controller" 结尾的类中的所有方法 |
* com.wmx..*Controller*.*(..)) | com.wmx 包及其子包下面类名包含'Controller'的任意类中的任意方法 |
* com.wmx.*.controller.*.*(..)) | 第一二层包名为 com.wmx ,第三层包名任意,第4层包名为 controller 下面的任意类中的任意方法 |
@Pointcut("@annotation(xx)") 拦截拥有指定注解的方法
/**
* @Pointcut :切入点声明,即切入到哪些目标方法。
* execution:可以用于指定具体类中的具体方法
* annotation:匹配拥有指定注解的方法; 只匹配实现类中有注解的方法,不会匹配接口中的注解方法; 如果注解是在类上,而不是方法上,并不会匹配类中的全部方法.
* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
* @annotation 中的路径表示拦截特定注解
*/
@Pointcut("@annotation(com.wmx.annotation.RedisLock)")
public void redisLockPC() {
}
具体参考: src/main/java/com/wmx/annotation/RedisLockAspect.java · 汪少棠/jpaTransactional - Gitee.com
环绕通知 实现开关目标方法
1、比如某个方法只有管理员才有权限执行,而普通用户是没有权限
2、比如不符合条件的时候,需要终止(跳过)目标方法的执行
3、比如一个组件(Component)专门用于做校验,里面的方法是否校验可以配置在数据库中,当配置为启用时,则继续校验,否则不校验。
/**
* 环绕通知
* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed() 会抛异常.
* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响业务层方法的事务回滚
* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
*/
@Around(value = "aspectPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
Object target = joinPoint.getTarget();
System.out.println("环绕通知=" + signature);
System.out.println("环绕通知=" + target);
// 是否继续校验
boolean validation = true;
if (validation) {
// 校验通过后执行目标方法
// 继续下一个切面通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
// 如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走
return joinPoint.proceed(joinPoint.getArgs());
} else {
// 校验未通过时,不继续往后走,直接返回。
// 可以返回提示信息,但是必须保证返回的参数类型与目标方法的返回值类型一致,否则类型转换异常。
// 也可以直接抛异常。
return null;
}
}
src/main/java/com/wmx/hb/aop/BasicDataRuleAop.java · 汪少棠/hb - Gitee.com
@Aspect 切面不生效原因
确保切面类被Spring管理:在切面类上添加 @Service、@Repository、@Controller、@Component 等注解 |
检查路径设置:确保切面类被 @ComponentScan 注解扫描到。即有没有被Spring容器管理。可以使用 @PostConstruct注解测试。 |
检查切面表达式:确保切面表达式正确无误,能够匹配到目标方法。 |
特别注意: 比如定义了一个 AOP 切面(@Pointcut)拦截 ServiceA 中的方法 B,当从其他类调用方法 B 时(比如 Controller 层),会正常切入拦截,而从本类其他方法中调用方法 B 时,无法切入拦截,因为此时默认并不是通过代理对象调用的,而是直接通过 this 对象来调的。可以参考@EnableAspectJAutoProxy注解。 |