Spring Aop该如何使用
AOP(Aspect OrientedProgramming),即面向切面编程。本文介绍了AOP的相关概念和术语,用业务场景演示了Spring Aop的使用方式。希望本文对你轻松使用Spring Aop有所帮助。
一 什么是AOP
AOP(Aspect OrientedProgramming),面向切面编程,通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP中模块化的关键单元是类,而AOP中模块化的单元是方面,即处理过程中某个步骤或阶段。
举个例子,项目中对关键操作需要记录日志,无非是简单地插表操作。但是如果在每个接口中都重复调用或实现,不仅浪费时间,还将项目变得不那么清爽。这时,面向切面编程就该出场了。
利用AOP,可以对项目中边缘业务进行隔离,降低无关业务逻辑耦合度,提高代码复用率和开发效率。一般用于日志记录、性能统计、权限管理、事务控制,异常处理
等场景。
二 AOP相关术语
首先来看看AOP的一些术语:
- 切面(Aspect) :一个关注点。Spring中以注解@Aspect标记在类上,声明一个切面类。
- 连接点(Joinpoint) :在程序执行过程中某个特定的点,比如调用某方法。
- 通知(Advice) :定义了切面动作和执行时机,如业务逻辑之外的事务、日志等。
- 切点(Pointcut) :定义了执行通知的具体地点,是连接点的全集或子集;Spring中使用AspectJ切入点语法。
- 引入(Introduction) :就是把切面用到目标类中去。在不改变现有类或方法代码的情况下,为其添加新的属性或行为。
- 目标对象(Target Object) :被一个或者多个切面所通知的对象。Spring AOP是通过运行时代理实现的,因此目标对象是一个被代理类对象。
- AOP代理(AOP Proxy) :AOP框架创建的对象,用来实现通知的执行。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
- 织入(Weaving) :把切面连接到其它应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时、类加载时或运行时完成。Spring AOP框架在运行时完成织入。
三 Spring AOP
Spring最核心的两个功能是Ioc和Aop,即控制反转和面向切面编程。
如官网介绍,AOP在spring中应用如下:docs.spring.io/spring-fram…
即:AOP在Spring框架中用于:
- 提供声明性企业服务。这类服务中最重要的是声明性事务管理。
- 让用户实现自定义方面,用AOP补充他们对OOP的使用。
3.1 试用Spring AOP
以下代码可访问:github.com/kqcaihong/a…
3.1.1 准备工作
创建spring boot项目,引入如下依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring整合aspectj来实现aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
添加启动类及配置application.properties。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MoreApplication {
public static void main(String[] args) {
SpringApplication.run(MoreApplication.class, args);
}
}
spring.application.name=aop-demo
server.port=8010
添加User
类。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
创建一个UserController
类,模拟用户管理业务。
import com.learn.more.entiry.User;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
@RequestMapping("/user")
@RestController
public class UserController implements InitializingBean {
// 生成ID
private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
// 模拟数据库来保存记录
private static final Map<Long, User> USER_MAP = new ConcurrentHashMap<>();
// 查询所有用户
@GetMapping("/queryAll")
public List<User> queryAll() {
return USER_MAP.values().stream().sorted(Comparator.comparingLong(User::getId)).collect(Collectors.toList());
}
// 添加一个用户
@PostMapping("/add")
public User addByParam(@RequestParam String name, @RequestParam int age) {
User user = new User(name, age);
user.setId(ID_GENERATOR.incrementAndGet());
USER_MAP.put(user.getId(), user);
return user;
}
// 初始化一条记录
@Override
public void afterPropertiesSet() {
User bob = new User(ID_GENERATOR.incrementAndGet(), "Bob", 33);
USER_MAP.put(bob.getId(), bob);
}
}
3.1.2 定义切面
创建AopTest
类(交由spring容器管理),使用@Aspect
注解声明它是一个切面类,可以在类中定义多个切面。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AopTest {
}
3.1.3 定义切点
切点通过@Pointcut
注解和切点表达式
定义。Spring切面最小粒度是方法级别
,而execution表达式
声明了一个方法要作为切点需要满足的条件。
execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
- *表示不限制
- 两个点表示任意参数列表
- 在多个表达式之间
- 使用 ||, or表示 或
- 使用 &&,and表示 与
- 使用not,!表示 非
在AopTest
类中增加切点:controller包下任何类中public方法
// controller包下任意类中public方法,都是切点
@Pointcut("execution(public com.learn.more.controller.*.*(..))")
public void pointcut() {
}
3.1.4 定义通知
spring中有5类通知,对应5个用于方法上的注解:方法本身就是切面动作,注解则声明了执行时机。
- @Before:在切点方法之前执行。
- @After:在切点方法之后执行
- @AfterReturning:切点方法返回后执行
- @AfterThrowing:切点方法抛异常执行
- @Around:属于环绕增强,能控制在切点执行前和执行后,执行自定义逻辑
在AopTest
类中增加以下通知。
@Before("pointcut()")
public void beforeAdvice() {
log.info("before advice...");
}
@After("pointcut()")
public void afterAdvice() {
log.info("after advice...");
}
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before");
try {
Object result = proceedingJoinPoint.proceed();
log.info("around result: {}", result);
return result;
} catch (Throwable t) {
log.error("around error: ", t);
throw t;
} finally {
log.info("around after");
}
}
3.1.5 测试
使用postman访问GET http://localhost:8010/user/queryAll
。
运行结果如下。
3.2 业务场景——记录日志
假设项目已经上线运行,现在需要保存API接口访问记录。如果我们去修改每一个接口,添加保存日志逻辑,是不是让人很抓狂。现在,使用Aop来轻松实现这个功能。
首先,定义一个LogRecord
实体类。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class LogRecord {
private String startTime;
// 消时,单位:毫秒
private Long elapsedTime;
private String uri;
// 请求类型
private String method;
private String remoteIp;
private Object parameter;
private Object result;
}
3.2.1 创建切面
定义一个切面,将Controller中接口作为切点,用打印console日志模拟保存数据库操作。
/**
* Controller接口日志切面
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 用于入参、结果序列化
public static final ObjectMapper MAPPER = new ObjectMapper();
static {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
MAPPER.registerModule(javaTimeModule);
}
// controller包下任意类中public方法,都是切点
@Pointcut("execution(public * com.learn.more.controller.*.*(..))")
public void log() {
}
@Around("log()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - startTime;
LogRecord logRecord = new LogRecord();
logRecord.setStartTime(format(startTime));
logRecord.setElapsedTime(elapsedTime);
// 从RequestContextHolder中获取request信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
logRecord.setRemoteIp(request.getRemoteUser());
logRecord.setUri(request.getRequestURI());
logRecord.setMethod(request.getMethod());
Method method = resolveMethod(joinPoint);
Map<String, Object> parameterMap = getParameter(method, joinPoint.getArgs());
logRecord.setParameter(parameterMap);
logRecord.setResult(result);
// 用打印 模拟保存数据库
log.info(MAPPER.writeValueAsString(logRecord));
return result;
}
// 解析切点对应的Method
private Method resolveMethod(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?> targetClass = point.getTarget().getClass();
return getDeclaredMethod(targetClass, signature.getName(), signature.getMethod().getParameterTypes())
.orElseThrow(() -> new MethodNotFoundException(signature.getMethod().getName()));
}
private Optional<Method> getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return Optional.of(clazz.getDeclaredMethod(name, parameterTypes));
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (Objects.nonNull(superClass)) {
return getDeclaredMethod(superClass, name, parameterTypes);
}
}
return Optional.empty();
}
// 获取请求参数
private Map<String, Object> getParameter(Method method, Object[] args) {
Parameter[] parameters = method.getParameters();
Map<String, Object> map = new HashMap<>();
// 只记录springMVC框架注解标记参数
for (int i = 0; i < parameters.length; i++) {
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
PathVariable pathVariable = parameters[i].getAnnotation(PathVariable.class);
String key = parameters[i].getName();
if (Objects.nonNull(requestBody) || Objects.nonNull(requestParam) || Objects.nonNull(pathVariable)) {
map.put(key, args[i]);
}
}
return map;
}
// 格式化时间
private String format(long milliseconds) {
return format(new Date(milliseconds));
}
private String format(Date date) {
Instant instant = date.toInstant();
LocalDateTime time = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
return FORMATTER.format(time);
}
}
3.2.2 运行结果
用postman执行POST http://localhost:8010/user/add?name=Tom&age=25
,运行结果及格式化后日志如下。
{
"startTime": "2024-06-09 21:43:07",
"elapsedTime": 18,
"uri": "/user/add",
"method": "POST",
"remoteIp": null,
"parameter": {
"name": "Tom",
"age": 25
},
"result": {
"id": 2,
"name": "Tom",
"age": 25
}
}
再用postman执行GET http://localhost:8010/user/queryAll
,结果如下。
{
"startTime": "2024-06-09 22:04:54",
"elapsedTime": 1,
"uri": "/user/queryAll",
"method": "GET",
"remoteIp": null,
"parameter": {},
"result": [
{
"id": 1,
"name": "Bob",
"age": 33
},
{
"id": 2,
"name": "Tom",
"age": 25
}
]
}
转载自:https://juejin.cn/post/7378046072952635426