SpringBoot-切面AOP实现统一逻辑处理
最近在做接口的统一逻辑处理问题的时候学习了一下AOP,觉得很有帮助,故在此整理总结一下,希望对大家有所帮助。
AOP概述
AOP(Aspect Oriented Programming),面向切面思想,与IOC(控制反转)、DI(依赖注入)组成Spring的三大核心思想。既然是核心,那肯定是重要的。那么他为什么重要,以及在实际应用场景中我们可以用它来做什么呢?
不知道大家在开发过程中有没有遇到过这样的系统性需求:统计,权限检验,日志记录等等。可能很多人想到的就是在每一个调用方法的业务逻辑中都写一遍检验或者记录日志或者统计,例如我们需要在调用方法前进行一下权限的校验,在方法结束以后记录一下操作的日志,示意图如下:
这样做也的确是可以满足我们的需求,但是在实际的维护过程中非常不利,有多少业务操作,就要写多少重复的校验和日志记录代码,存在大量的冗余代码,这显然是无法接受的。如果你稍微觉得不妥,在原来的基础上进行一次优化,利用面向对象的思想,将这些冗余的代码抽离出来做成一个公共的方法,示意图如下:
这样的确是可以解决代码冗余和可维护性的问题,但是我们在使用的时候依然需要手动的调用这些公共的方法,看起来也有点不妥。那么有没有一种更好的方法,我们不需要每次都去调用,在调用方法前后就会自动帮我们进行对应的操作呢?答案是肯定的,用AOP就可以,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
AOP体系
AOP的体系大致如下图:
接下来对各个概念做一下解释:
Aspect
切面,包含Pointcut和Advice。在使用AOP进行切面定义的时候,在类上加上@Aspect即可定义一个切面类。例如我们需要定义一个日志切面LogAspect,则可如下定义,@Component 注解表示该类交给 Spring 来管理。
/*** @Author likangmin* @create 2020/11/24 17:03*/
//定义切面类LogAspect
@Aspect
@Component
public class LogAdvice {}
Pointcut
切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面),分为execution(系统注解,可以用路径表达式指定哪些类织入切面)方式和annotation(自定义注解,以指定被哪些注解修饰的代码织入切面)方式。在实际使用中,用@Pointcut注解定义一个切面,即某件事情的入口,如记录操作日志的入口,切入点定义了事件触发时机。
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
public class LogAspect{/*** 定义一个切面,拦截 com.kmli.aopexe.controller 包和子包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}
}
以上表示拦截com.kmli.aopexe.controller 包和子包下的所有方法,进入该包和子包下的所有方法都需要进行记录日志的操作。前面说了两种注解的方式,接下里解释一下两种方式中表达式的具体含义:
execution方式
以代码中execution(* com.kmli.aopexe.controller….(…))为例:
- 第一个 *号的位置:表示返回值类型,星号表示所有类型;
- com.kmli.aopexe.controller…: 表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包;
- 第二个 * 号的位置:表示类名,* 表示所有类;
- *(…):星号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
annotation方式
这种方式是针对某个注解来定义切面,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}
使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping不同注解有各种特定处理逻辑的场景。
Advice
处理,包括处理时机(在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等)和处理内容(要做什么事,比如校验权限和记录日志)。
处理内容没有什么需要说明,主要看看处理时机的几个注解:
@Before
使用@Before 注解指定的方法在切面切入目标方法之前执行,这个时候我们可以做一些处理,如记录当前调用日志,获取用户请求的URL以及用户的IP等等信息。还是以上面的日志记录为例介绍一下使用方法(参数JointPoint对象很重要):
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Slf4j
public class LogAdvice {/*** 定义一个切面,拦截 com.mutest.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定义的切面方法之前执行该方法* @param joinPoint jointPoint*/@Before("pointCut()")public void doBefore(JoinPoint joinPoint) {log.info("====doBefore方法进入了====");// 获取签名Signature signature = joinPoint.getSignature();// 获取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 获取即将执行的方法名String funcName = signature.getName();log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);// 也可以用来记录一些信息,比如获取请求的 URL 和 IPServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 获取请求 URLString url = request.getRequestURL().toString();// 获取请求 IPString ip = request.getRemoteAddr();log.info("用户请求的url为:{},ip地址为:{}", url, ip);log.info("====doBefore方法结束了====");}
}
@After
和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,同样可以做和@Before一样的处理:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Slf4j
public class LogAdvice {/*** 定义一个切面,拦截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在上面定义的切面方法之后执行该方法* @param joinPoint jointPoint*/@After("pointCut()")public void doAfter(JoinPoint joinPoint) {log.info("==== doAfter 方法进入了====");Signature signature = joinPoint.getSignature();String method = signature.getName();log.info("方法{}已经执行完", method);log.info("==== doAfter 方法结束了====");}
}
@Around
用于修饰Around增强处理,可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法,之所以具有这样的特性主要是因为使用了用ProceedingJoinPoint参数的procedd()方法才会执行目标方法,这就是@Around增强处理可以完全控制目标方法执行时机、如何执行的关键,如果程序没有调用ProceedingJoinPoint的proceed方法,则目标方法不会执行。我们在使用@Around时,对应的方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参),正如上面所说ProceedingJoinPoint.procedd()的重要性。
使用它不仅可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值,具体实现方法是调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参,需要注意的是如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
注意:
@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就
能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用
Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。
具体看一下使用的示例:
首先定义一个测试接口类TestController,定义方法getGroupList用于检验权限:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@RestController
@RequestMapping(value = "/permission")
public class TestController {@RequestMapping(value = "/check", method = RequestMethod.POST)@PermissionsAnnotation()public JSONObject getGroupList(@RequestBody JSONObject request) {return JSON.parseObject("{"message":"SUCCESS","code":200,"data":" + request + "}");}
}
然后我们定义一个切面类:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Order(1)
public class PermissionAdvice {@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")private void permissionCheck() {}@Around("permissionCheck()")public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("===================开始增强处理===================");//获取请求参数,详见接口类Object[] objects = joinPoint.getArgs();Long id = ((JSONObject) objects[0]).getLong("id");String name = ((JSONObject) objects[0]).getString("name");// 修改入参JSONObject object = new JSONObject();object.put("id", 8);object.put("name", "kmli");objects[0] = object;// 将修改后的参数传入return joinPoint.proceed(objects);}
}
使用PostMan进行调用,传入参数{“id”:-5,“name”:“admin”},返回如下:
{"code":200,"data":{"name":"kmli","id":"8"},"message":"SUCCESS"}
从结果可以看出,@Around截取到了接口的入参,并使接口返回了切面类中的结果。
@AfterReturning
@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理。还是以日志为例:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Slf4j
public class LogAdvice {/*** 定义一个切面,拦截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强* @param joinPoint joinPoint* @param result result*/@AfterReturning(pointcut = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, Object result) {Signature signature = joinPoint.getSignature();String classMethod = signature.getName();log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);// 实际项目中可以根据业务做具体的返回值增强log.info("对返回参数进行业务上的增强:{}", result + "需要增加的信息");}
}
但是在使用@AfterReturning 注解时需要注意的是返回的值必须和参数保持一致,否则会报错。
@AfterThrowing
当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。还是以日志为例:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Slf4j
public class LogAdvice {/*** 定义一个切面,拦截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定义的切面方法执行抛异常时,执行该方法* @param joinPoint jointPoint* @param ex ex*/@AfterThrowing(pointcut = "pointCut()", throwing = "ex")public void afterThrowing(JoinPoint joinPoint, Throwable ex) {Signature signature = joinPoint.getSignature();String method = signature.getName();// 处理异常的逻辑log.info("执行方法{}出错,异常为:{}", method, ex);}
}
Joint point
连接点,是程序执行的一个点,一个方法的执行或者一个异常的处理都是一个连接点。在 Spring AOP 中,一个连接点总是代表一个方法执行。
Weaving
织入,通过动态代理,在目标对象方法中执行处理内容的过程。
AOP使用
对AOP进行理论和参数进行分析之后,接下来我们使用AOP进行一下实际的操作。首先在使用AOP之前需要在项目的POM文件中引入AOP的相关依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
还有一个版本属性,不写就自动为最新的版本。既然我们之前以日志和权限进行说明,所以我们还是拿这两个进行示例说明。可能跟上面的代码会有重复的地方。先来看我们的第一个需求:所有的get请求被调用前需要记录信息"get请求的advice触发了"。则我们定义切面类如下:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
public class LogAdvice {// 定义一个切点:所有被GetMapping注解修饰的方法会织入advice@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")private void logAdvicePointcut() {}// Before表示logAdvice将在目标方法执行前执行@Before("logAdvicePointcut()")public void logAdvice(){// 这里只是一个示例,你可以写任何处理逻辑System.out.println("get请求的advice触发了");}
}
然后在测试类中写post类的接口:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@RestController
@RequestMapping(value = "/aop")
public class AopController {@PostMapping(value = "/getTest")public JSONObject aopTest() {return JSON.parseObject("{"message":"SUCCESS","code":200}");}@PostMapping(value = "/postTest")public JSONObject aopTest2(@RequestParam("id") String id) {return JSON.parseObject("{"message":"SUCCESS","code":200}");}
}
项目启动后,使用postMan调用http://localhost:8085/aop/getTest,会发现控制台打印了:get请求的advice触发了,当调用http://localhost:8085/aop/postTest时,没有打印任何信息。
下面我们将问题复杂化一些,我们的需求是自定义一个注解,要求只要是标注了自定义注解的方法都需要对参数进行检验,这个可以怎么做呢?
首先自定义一个注解,对于自定义注解这里不做过多解释,不太懂的伙伴可以百度查看,不复杂。我们自定义注解类PermissionsAnnotation:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation{}
然后创建一个切面用于参数的校验:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@Aspect
@Component
@Order(1)
public class PermissionAdvice {// 定义一个切面,括号内写入第1步中自定义注解的路径@Pointcut("@annotation(com.kmli.demo.annotation.PermissionAnnotation)")private void permissionCheck() {}@Around("permissionCheck()")public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {Object[] objects = joinPoint.getArgs();Long id = ((JSONObject) objects[0]).getLong("id");String name = ((JSONObject) objects[0]).getString("name");// id小于0则抛出非法id的异常if (id < 0) {return JSON.parseObject("{"message":"illegal id","code":403}");}return joinPoint.proceed();}
}
然后在测试类中写接口:
/*** @Author likangmin* @create 2020/11/24 17:03*/
@RestController
@RequestMapping(value = "/permission")
public class TestController {@RequestMapping(value = "/check", method = RequestMethod.POST)// 自定义注解@PermissionsAnnotation()public JSONObject getGroupList(@RequestBody JSONObject request) {return JSON.parseObject("{"message":"SUCCESS","code":200}");}@RequestMapping(value = "/check2", method = RequestMethod.POST)public JSONObject getGroupList2(@RequestBody JSONObject request) {return JSON.parseObject("{"message":"SUCCESS","code":200}");}
}
然后再利用postMan进行测试,输入http://localhost:8085/permission/check,传入参数{“id”:1,“name”:“kmli”},则正常返回:
{"code":200,"message":"SUCCESS"}
然后我们传入参数{“id”:1,“name”:“kmli”},则返回:
{"code":403,"message":"illegal id"}
然后我们使用同样的参数,调用http://localhost:8085/permission/check2,不论哪种传参都是正常返回。
到这里可能有人会问,如果我一个接口想设置多个切面类进行校验怎么办?如果我想切面1在切面2之前执行怎么办呢?这些切面的执行顺序如何管理?很简单,一个自定义的AOP注解可以对应多个切面类,这些切面类执行顺序由@Order注解管理,该注解后的数字越小,所在切面类越先执行。
以上就是要讲的AOP的全部内容,通过几个简单的例子就可以感受到,AOP的便捷之处。如果有不足的地方还请大家指出,感谢大家的阅读。
SpringBoot-切面AOP实现统一逻辑处理相关推荐
- SpringBoot切面AOP打印请求和响应日志
1.说明 Spring Boot微服务对外开放的Restful接口, 为了方便定位问题, 一般需要记录请求日志和响应日志, 而在每个接口中开发日志代码是非常繁琐的, 本文介绍使用Spring的切面AO ...
- springboot切面AOP拦截父类或接口中标记注解的方法
一.注解的继承性回顾 被@Inherited元注解标注的注解标注在类上的时候,子类可以继承父类上的注解. 注解未被@Inherited元注解标注的,该注解标注在类上时,子类不会继承父类上标注的注解. ...
- SpringBoot之AOP面向切面编程
什么是AOP AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续,是软件开 ...
- java 切面 不执行,解决springboot的aop切面不起作用问题(失效的排查)
检查下springboot的启动类是否开启扫描 @springbootapplication @componentscan(basepackages = {"com.zhangpu.spri ...
- aop springboot 传入参数_java相关:springboot配置aop切面日志打印过程解析
java相关:springboot配置aop切面日志打印过程解析 发布于 2020-3-31| 复制链接 摘记: 这篇文章主要介绍了springboot配置aop切面日志打印过程解析,文中通过示例代码 ...
- springboot aop + logback + 统一异常处理 打印日志
springboot aop + logback + 统一异常处理 打印日志 参考文章: (1)springboot aop + logback + 统一异常处理 打印日志 (2)https://ww ...
- springboot之aop切面获取请求
springboot之aop切面获取请求 项目场景: 在学习springboot的博客开发中,通过aop切面,对博客中的操作进行记录 问题描述: 问题: 在切面方法中,无法获取请求的参数和类名,方法, ...
- SpringBoot之AOP面向切面编程实例
目录 1.引入pom依赖 2.切入点表达式 --组成 --逻辑运算符 --通配符 --范例 3. 启动类配置 4.通知类型 4.1 @Before : 标注当前方法作为前置通知 4.1.1 创建自定义 ...
- SpringBoot的AOP是默认开启的,不需要加注解@EnableAspectJAutoProxy____听说SpringAOP 有坑?那就来踩一踩
@Aspect @Component public class CustomerServiceInterceptor {@Before("execution(public * org.exa ...
最新文章
- GDOI2018记录
- zookeeperclient设置监听
- Unity3D基础API之Vector3
- ultraedit连接UNIX
- 跨链Cosmos(8)同构跨链交易流程
- 【企业管理】如何降低内部成本
- Oracle超出最大连接数问题及解决
- android studio turn off hyperv,Android Studio 无法运行模拟器
- 七种设计原则(二)单一职责原则
- 使用Axure制作App原型的尺寸设置
- 清除浮动的方法总结CSS实现水平垂直居中方法总结
- [Swift]LeetCode890. 查找和替换模式 | Find and Replace Pattern
- 在计算机应用领域中媒体是指,在计算机中,媒体是指什么
- 根据日志统计出每个用户在站点所呆时间最长的前2个的信息
- visual studio 2015中的webapi生成helpPage,页面不显示方法说明问题解决
- PDF文档转换平台的核心技术-开源解决方案
- 离线 维基百科 android,iPhone上的离线维基百科(附安装方法)
- com词根词缀_词根词缀记忆大全---经典详细的总结
- CST仿真指导 | 设置基本单位Units
- 【行业了解】天眼查、企查查、启信宝、爱企查
热门文章
- [专栏目录]-optee/TEE/TA笔记-目录
- VTS工具测试指定的testcase函数(以VtsHalKeymasterV4_0TargetTest为例)
- [ARM-assembly]-ARM交叉编译器下编译的各个镜像的反汇编文件分析
- 基础知识——密码学笔记(一)
- Python编程实现粒子群算法(PSO)详解
- 【安全技术】红队之windows信息收集思路
- MySQL GROUP BY:分组查询
- SSD 通俗易懂介绍
- 使用Statement接口实现增,删,改操作
- 1094 The Largest Generation (25 分)【难度: 一般 / 树的遍历】