Spring AOP

基础概念

AOP 是一种面向切面的编程思想,通俗来讲,这里假如我们有多个方法。

@Component
public class Demo {
public void say1() {
System.out.println("say1~~~~~~~");
} public void say2() {
System.out.println("say2~~~~~~~");
} public void say3() {
System.out.println("say3~~~~~~~");
}
}

此时,如果我们要在每个方法执行完毕后,再输出一句话,则需要在每个方法里面都再加一个方法。

    public void say1() {
System.out.println("say1~~~~~~~");
System.out.println("XX say good!!!");
} public void say2() {
System.out.println("say2~~~~~~~");
System.out.println("XX say good!!!");
} public void say3() {
System.out.println("say3~~~~~~~");
System.out.println("XX say good!!!");
}

这种方式,就会显得代码十分的冗余且不够优雅。


我们想一下,该实现的逻辑是在我们要在每个方法后面(切点)实现一个差不多的逻辑(切面实现),通过类似于下图所示的方式,将和主要业务无关的代码抽离出来,实现代码的解耦。


类似于下图所示的方式:

Spring 实现

首先,我们在一个 Spring Web 程序中,引入 spring-aop 的相关 jar 包。

     <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后,我们构建一个切面类,在该类里面,我们来定义要切入的点,以及切入后该做什么。

@Aspect
@Component
public class LogAspect {
@After("execution(public * say*(..))")
public void saveLog() {
System.out.println("XX say good!!!");
}
}

在这里,首先我们用 @Aspect 来声明这是一个切面,然后用 @Component 来让 Spring 容器可以扫描到该类。


紧接着,我们定义一个方法 saveLog() ,该方法的目的是在执行完 say1() 后,可以输出一条日志,所通过的方式便是注解: @After("execution(public * say*(..))")

有关于 aop 可以使用的注解,已经注解里配置的切点表达式,在后续再进行展开。

最后,我们在启动类上加上 @EnableAspectJAutoProxy 即可。


最后的实现效果,如下所示:

概念详解

切面

Aspect,要抽象出来的横跨多个地方的功能。

连接点

Joinpoint,定义在应用程序流程的何处插入切面进行执行。

切入点

Pointcut,一组连接点的集合。


其实在 AOP 中,这些概念点并不重要,重要是理解,以及如何在实战中进行演练。

可用切面

  • before:先执行拦截代码,如果拦截代码错误,目标代码不执行;
  • after:先执行目标代码,无论目标代码执行正确与否,都会执行拦截代码;
  • afterReturning:和after不同的是,只有目标代码正确返回,才会执行拦截代码;
  • afterThrowing:和after不同的是,只有目标代码抛出异常,才会执行拦截代码;
  • around:能完全控制代码执行,并可以在目标代码前后,任意执行拦截代码。

切点表达式

execution

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
  • motifiers-pattern?:修饰符,public、protect、private、*(所有类型);
  • ret-type-pattern:返回值;
  • declaring-type-pattern?:类路径匹配;
  • name-pattern:方法名,支持*,_占位符;
  • param-pattern:参数匹配,..代表所有参数类型;
  • throws-pattern?异常类型匹配

其中?代表该项是可选项。


另外切点表达式是可以组合的,用 || 或 && 可以进行逻辑组合。(不止是 execution,也可以跟其他的切点表达式进行组合)

// 匹配所有方法,无法使用
execution(* *(..))
// 匹配所有 com.demo 包下的公有的,返回值为void的,方法名是say为前缀的,参数随意的方法
execution(public void com.demo say*(..))

@annotation

当执行的方法上有指定的注解,则算是匹配成功。


我认为该方式会更加的灵活些,在下面的实战演练中,我用的就是该方式,其拦截规则可以充分自定义,且可以在注解中,定义一些自己需要的值,然后在切面中进行使用。

args

用来匹配方法参数的。

  • args():匹配不带参数的方法;
  • args(type(String)):匹配一个参数,且类型为String的方法;
  • args(..):匹配任意参数方法;
  • args(String,..):匹配任意参数方法,但第一个参数类型是String的方法;
  • args(..,String):匹配任意参数方法,但最后一个参数类型是String的方法;

该方法其实就是 execution 的变种形式,了解即可。

@args

也是用来匹配方法参数的,但是其匹配的逻辑是方法参数带有执行注解的方法。


其他方法,如 within、this、target、@target、@within、bean 不多做介绍了,平常用的也不多,以后有兴趣,或者在实际使用中有所涉及,再进行补充。

实战演练

在实战中,我们通过注解的方式来进行切入,

定义注解

/**
* 操作行为注解,通过该注解获取数据详情
*
* @author iceWang
* @date 2020/9/10
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperatorAnnotation {
String bodyType(); String operatorType();
}

在这里,我们定义一个注解,后续在要拦截的方法上,加上该注解即可。


其中 bodyType 代表我们要操作的实体类型,OperatorType 代表我们要操作的行为类型。

业务逻辑

 @OperatorAnnotation(bodyType = LogAspect.BODY_TYPE_COMPANY, operatorType = LogAspect.OPERATOR_TYPE_DELETE)
public String deleteCompany(String companyUniqueId) {
Optional.of(companyMapper.deleteCompany(companyUniqueId))
.filter(result -> result > 0)
.orElseThrow(() -> new IllegalArgumentException("无法删除,请稍后再试!"));
return companyUniqueId;
}

因为个人原因,这里我们只展示一部分代码——根据 id 删除公司,定义实体类型为 company,操作类型为删除,为后续插入日志做数据铺垫。

切面定义

@Aspect
@Component
public class LogAspect {
public static final String BODY_TYPE_COMPANY = "company";
public static final String OPERATOR_TYPE_DELETE = "delete"; @AfterReturning(value = "@annotation(OperatorAnnotation)", returning = "result")
public void saveOperatorLog(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
OperatorAnnotation operatorAnnotation = methodSignature.getMethod().getAnnotation(OperatorAnnotation.class);
String bodyType = operatorAnnotation.bodyType();
String operatorType = operatorAnnotation.operatorType(); if (bodyType.contains(BODY_TYPE_COMPANY) && operatorType.contains(OPERATOR_TYPE_DELETE)) {
saveOperatorLog(bodyType, operatorType, result);
return;
}
} /**
* 返回日志操作实体类
* @param bodyType
* @param operatorType
* @return
*/
private Operator getOperator(String bodyType, String operatorType) {
return Operator.builder()
.bodyType(bodyType)
.operatorType(operatorType)
.createTime(LocalDateTime.now())
.build();
} /**
* 保存日志操作实体类
* @param bodyType
* @param operatorType
* @param result
*/
private void saveOperatorLog(String bodyType, String operatorType, Object result) {
Operator operator = getOperator(bodyType, operatorType);
operator.setOperatorUser(mdUserInfo.getPhone());
operator.setBody(result.toString());
operatorMapper.insert(operator);
}
}

在切面中,首先,我们用反射的方式来获取方法上的注解,通过注解获取实际的操作实体类型和操作类型,然后根据不同的实体类型和操作类型,执行不同的方法,将日志插入数据库中。

文章在公众号「iceWang」第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

最新文章

  1. callback的实现
  2. smb
  3. Android IOS WebRTC 音视频开发总结(五六)-- 如何测试网络性能?
  4. 一天一个Java基础——反射
  5. Redis_基本类型介绍和指令___3
  6. Ubuntu安装secureCRT
  7. HDU 1027 - Ignatius and the Princess II
  8. DataTables warning 错误警告
  9. JS firebug小技巧
  10. logistic回归和probit回归预测公司被ST的概率(应用)
  11. 第二章 JavaScript核心语法
  12. 【代码笔记】Web-CSS-CSS样式列表(url)
  13. oracle如何调试存储过程
  14. XtraBackup备份出现&quot;Can't locate Digest/MD5.pm in @INC&quot;
  15. C/C++:函数调用规则__stdcall,__cdecl,__pascal,__fastcall
  16. 第二个Sprint冲刺第六天
  17. 交叉编译环境以及开发板上-/bin/sh: ./hello: not found 转载自 http://blankboy.72pines.com
  18. vim for python
  19. 葡萄城报表介绍:Java 报表
  20. flutter控件之CheckBox

热门文章

  1. Css 设置超过再两行显示省略号
  2. node mssql 无法连接sql server
  3. 学习Python(新手教程链接)
  4. 快速搭建一个Vue-cli项目(简单到爆炸)
  5. Mybatis-plus 实体类新增属性,使用实体类执行sql操作时忽略该属性 注解
  6. Mybatis 循环删除/插入
  7. Hitool打开出现failed to create the java virtual machine
  8. linux 基本命令整理--转
  9. Windows servers 2008 环境下,CA证书服务器搭建。
  10. SpringBoot2搭建基础架构——开源软件诞生4