Spring Boot之面向切面编程:Spring AOP
前言
来啦老铁!
笔者学习Spring Boot有一段时间了,附上Spring Boot系列学习文章,欢迎取阅、赐教:
- 5分钟入手Spring Boot;
- Spring Boot数据库交互之Spring Data JPA;
- Spring Boot数据库交互之Mybatis;
- Spring Boot视图技术;
- Spring Boot之整合Swagger;
- Spring Boot之junit单元测试踩坑;
- 如何在Spring Boot中使用TestNG;
- Spring Boot之整合logback日志;
- Spring Boot之整合Spring Batch:批处理与任务调度;
- Spring Boot之整合Spring Security: 访问认证;
- Spring Boot之整合Spring Security: 授权管理;
- Spring Boot之多数据库源:极简方案;
- Spring Boot之使用MongoDB数据库源;
- Spring Boot之多线程、异步:@Async;
- Spring Boot之前后端分离(一):Vue前端;
- Spring Boot之前后端分离(二):后端、前后端集成
- Spring Boot之前后端分离(三):登录、登出、页面认证
之前在刚学习Spring Boot的时候有看到AOP,还是挺容易的,但没有实践一下,而近期由于某些原因,几次被问及AOP,作为系统学习Spring Boot的咱们,当然不能落下Spring AOP!
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,是面向对象编程的补充,它提供了另外一种思路来实现应用系统的公共服务。AOP采用“横切”技术,解剖已封装的对象,将这种公共服务封装到一个可重用的模块中,这模块称之为“Aspect”,即“切面”。“切面”可降低系统代码冗余,降低模块间的耦合度,提升系统的可维护性。
AOP常见的使用场景:
1. 日志功能;
采用AOP之后,不需要在每一处功能中添加日志收集代码,而是在切面中统一完成这一步骤,提升了编程速度和代码整洁度!
2. 业务方法调用的权限管理;
采用AOP在处理权限管理,我们不用在所有业务代码处判断用户是否有权限调用此方法,而是在切面中统一完成这一步骤,减少了这种非核心业务的代码!
3. 数据库事务的管理;
采用AOP可以统一在执行数据库前先开启事务,在执行完成后提交事务,若执行出错,则回滚事务等。
4. 缓存方面;
我们可采用AOP技术,统一对数据进行缓存,在下次调用时,如果参数、条件等未变,则直接获取数据,而不再调取应用方法。
5. 等。
AOP有点拦截的感觉!
AOP有一些术语:
- Aspect;
- Joint point;
- Pointcut;
- Advice;
- AOP proxy;
- Weaving;
这些术语对我们理解、实践AOP没有太大阻碍,请自行脑补哈,我们直接上代码开始Demo!
项目代码已上传Git Hub仓库,欢迎取阅:
整体步骤
- 创建AOP演示项目;
- 引入AOP依赖;
- 创建演示用API;
- 编写AOP切面类;
- 验证AOP代码织入效果;
1. 创建AOP演示项目;
Spring Boot项目创建可参考文章:5分钟入手Spring Boot,此处不再介绍。
2. 引入AOP依赖;
在项目pom.xml中添加spring-boot-starter-aop依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
记得安装一下依赖:
mvn install -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true
3. 创建演示用API;
项目内创建controller包,包内创建一个controller,如HelloWorldController.java,HelloWorldController内创建一个用于演示用的API:
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 10/22/2020
*/
@RestController
public class HelloWorldController {
@GetMapping("/api/hello")
public String sayHello(@RequestParam String user) {
return "Hello " + user;
}
}
此处特地写了一个需要参数的API,我将把API处理过程进行横切,在API请求前后做一些系统级别的操作,但不影响业务过程。
4. 编写AOP切面类;
在项目内创建config包,在包内创建一个config类,如AOPConfig.java,在AOPConfig编写如下代码:
package com.github.dylanz666.config;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @author : dylanz
* @since : 10/22/2020
*/
@Configuration
@Aspect
public class AOPConfig {
@Around("@within(org.springframework.web.bind.annotation.RestController)")
public Object simpleAop(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
System.out.println("client ip:" + request.getRemoteAddr());
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("args:" + Arrays.asList(args));
Object object = proceedingJoinPoint.proceed();
System.out.println("return: " + object);
return object;
}
}
稍微解读一下:
1). @Aspect,声明了这个类是个切面类;
2). @Around,声明了一个表达式,描述了要织入的目标特性;
比如本例@within表示目标类型带有注解,且其注解类型为 org.springframework.web.bind.annotation.RestController(如果API的注解用的是@Conrtoller,则此处为 org.springframework.stereotype.Controller),这样系统内所有RestController方法(Rest API,也即带有@RestController注解的controller类中的方法)被调用的时候,都会执行@Around注解的方法,也就是本例的simpleAop方法;
除了@Around(方法执行前后织入代码),还有@Before、@After、@AfterReturning、@AfterThrowing,他们均分别表示该织入代码用于执行方法前、执行方法后、方法返回后、方法抛出异常后,如:
@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void before(JoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
System.out.println("args:" + Arrays.asList(args));
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
System.out.println("client ip:" + request.getRemoteAddr());
}
我们在执行方法前打印请求参数和客户端ip;
3). 除了Around注解的方法可以传ProceedingJionPoint类型的参数外,其余的几个都不能传ProceedingJionPoint类型的参数;
4). simpleAop(名字任意)是用来织入的代码,我们可以利用参数ProceedingJoinPoint提供的方法,来对请求前后进行系统级别操作。
例如本例在接收到API请求还未执行业务代码时将客户端ip、请求参数打印出来,然后在业务代码执行完成后未返回给客户端前,将返回结果先打印出来;
5). 通常当切面代码执行完后,我们需要继续执行应用代码,并将返回对象正常返回,Object object = proceedingJoinPoint.proceed();就是为了完成这一过程;
6). 除了@within这种切面目标匹配表达式外,Spring AOP还提供了多种可选的表达式及表达式组合:
(1). within();
(2). @within;
(3). execution(),如:
- execution(public * *(...));
- execution(* set*(...));
- execution(public set*(...));
- execution(public com.xyz.service..set(...));
(4). target();
(5). @target;
(6). args();
(7). @args();
(8). @annotation();
(9). this();
(10). @Transactional;
等,读者可自行展开学习!
5. 验证AOP代码织入效果;
1). 项目整体结构:

2). 启动项目:

3). 访问API:
(手机在局域网内访问我们的应用路径:http://192.168.0.101:8080/api/hello?user=dylanz)

4). 后端执行切面代码:

我们可以看到,在API执行前,打印了手机的ip地址:192.168.0.100,同时打印了请求参数值:dylanz,在API对应的方法执行后,打印方法返回的Hello dylanz字符串给客户端,然后将该字符串传给客户端,之后我们便能在手机客户端看到Hello dylanz字符串!
不难看出,我们可以将这些打印换成日志打印,就能全局收集详细的信息!
或者也可在切面中做一些缓存操作、数据库事务方面的行为等。
至此,我们完成了一个简单的Spring AOP案例,整个过程简单而不失灵活,灵活而不失优雅,有没有?
如果本文对您有帮助,麻烦点赞+关注!
谢谢!