AOP使用
大约 7 分钟
1. 了解AOP
了解AOP
AOP是一种编程范式,它允许你在程序中横切关注点(cross-cutting concerns),而不是纵切关注点(vertical concerns)。横切关注点通常包括日志记录、事务管理、权限控制等,它们会分散在应用程序的不同模块中。AOP的目标是通过将这些关注点从主要的业务逻辑中分离出来,提高代码的可维护性和可重用性。
在AOP中,有两个主要概念:切面
(Aspect)和连接点
(Join Point)。切面定义了在哪里和何时应该执行横切关注点,而连接点是实际发生横切关注点的地方。
以下是AOP的一些核心概念和使用方法:
- 切面(Aspect): 切面是横切关注点的模块化单元。它定义了横切关注点的行为以及何时执行。切面使用通知(Advice)来实现横切逻辑。
- 连接点(Join Point): 连接点是在程序执行过程中能够插入切面的点。这可以是方法调用、异常处理等。
- 通知(Advice): 通知定义了在连接点上执行的代码。有几种类型的通知:
- 前置通知(Before Advice): 在连接点之前执行。
- 后置通知(After Advice): 在连接点之后执行,无论连接点是否正常完成。
- 返回通知(After Returning Advice): 在连接点正常完成并返回值后执行。
- 异常通知(After Throwing Advice): 在连接点抛出异常后执行。
- 环绕通知(Around Advice): 在连接点前后都执行,控制连接点的执行流程。
- 切点(Pointcut): 切点是一组连接点的集合,用于定义在何处应该应用通知。切点使用表达式来匹配连接点。
2. 使用场景
- 日志记录: 记录应用程序的运行时信息,如方法调用、参数、返回值等,以便于调试和监控。
- 事务管理: 控制事务的启动、提交和回滚,确保数据一致性和完整性。
- 权限控制: 对用户进行身份验证和授权,确保只有授权用户能够执行特定的操作。
- 异常处理: 集中处理应用程序中的异常,提高代码的健壮性和可维护性。
- 性能监控: 记录方法执行时间、资源消耗等信息,进行性能分析和优化。
- 缓存管理: 在方法调用前检查缓存,如果有缓存则直接返回结果,提高系统性能。
- 国际化: 在方法调用前或后处理国际化相关的逻辑,例如设置语言环境、处理国际化资源等。
- 审计跟踪: 跟踪应用程序的操作历史,记录用户行为,用于审计和安全监控。
- 重试机制: 在方法执行失败时自动进行重试,提高系统的稳定性。
- 动态代理: 利用AOP的动态代理功能,可以在运行时动态生成代理对象,实现面向接口的编程和横向扩展。
3. 使用
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.1 日志打印
直接在需要打印日志的方法上添加注解
参数说明:
JoinPoint
和 ProceedingJoinPoint
是在 AOP 中使用的两个重要的参数类型,它们提供了在横切逻辑中获取和控制执行流程的能力。
- JoinPoint:
- 作用:
JoinPoint
是在 AOP 中表示连接点的对象,它提供了对当前执行的方法的许多信息的访问。 - 信息包括: 方法名称、目标对象、参数列表等。
- 使用场景: 在通知中,你可以通过
JoinPoint
获取执行连接点的上下文信息,例如,在前置通知中获取方法参数,或者在后置通知中获取方法返回值。
- 作用:
- ProceedingJoinPoint:
- 作用:
ProceedingJoinPoint
是JoinPoint
的子接口,它提供了控制执行流程的能力。特别是,在环绕通知中,你可以使用ProceedingJoinPoint
来决定是否继续执行连接点的方法。 - 使用场景: 在环绕通知中,你可以通过调用
proceed()
方法决定是否执行原始的连接点,或者在连接点前后添加额外的逻辑。
- 作用:
代码
注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLog {
}
切面
@Aspect
@Component
public class LogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);
private final String UN_KNOWN = "unknown";
/**
* 记录业务请求的时间
*/
private long req;
private String reqTime;
/**
* 定义空方法用于切点表达式
*
* @apiNote 作用在controller包中各个类的每个方法上
* 使用注解
*
* <p>
* 使用注解
* ("@annotation(cn.bughub.annotation.ApiLog)")
* </p>
*/
@Pointcut("execution(* cn.bughub.controller..*(..))")
public void pointcut() {
}
/**
* 在进入controller之前拦截并打印请求报文日志
*
* @param joinPoint 连接点
*/
@Before("pointcut()")
public void printRequestDatagram(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Enumeration headerNames = request.getHeaderNames();
Map map = new HashMap(10);
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
String ip = getIpAddress(request);
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
req = System.currentTimeMillis();
reqTime = dateFormat.format(new Date(req));
Object[] args = joinPoint.getArgs();
LOGGER.info("==> 拦截到请求\n"
+ "==> 操作者:" + request.getHeader("auth") + "\n"
+ "==> 请求者IP:" + ip + "\n"
+ "==> 请求时间:" + reqTime + "\n"
+ "==> 请求接口:" + request.getMethod() + " " + request.getRequestURL() + "\n"
+ "==> 请求方法:" + method.getName() + "\n"
+ "==> 请求头:" + map + "\n"
+ "==> 参数内容:" + Arrays.toString(args));
}
/**
* 返回信息后,打印应答报文的日志
*
* @param joinPoint 连接点
* @return {@link Object}
* @throws Throwable throwable
*/
@Around("pointcut()")
public Object printResponseDatagram(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
result = joinPoint.proceed();
long respTime = System.currentTimeMillis() - req;
String d = String.valueOf(respTime);
LOGGER.info("<== 响应请求\n"
+ "<== 请求时间:" + reqTime + "\n"
+ "<== 请求耗时:" + Double.parseDouble(d) / 1000 + "s\n"
+ "<== 应答内容:" + result);
return result;
}
/**
* 获得ip地址
*
* @param request 请求
* @return {@link String}
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
3.2 数据加解密
在方法和参数上添加相应的注解
代码
解密方法注解
/**
* 解密方法注解
*
* @author zwj
* @date 2024/02/28
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptMethod {
}
解密字段注解
/**
* 解密属性注解
*
* @author zwj
* @date 2024/02/28
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
}
切面实现
/**
* AES切面类
*
* @author zwj
* @date 2024/02/28
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AesAspect {
private final KeyServiceImpl keyService;
/**
* 解密切点
*/
@Pointcut("@annotation(cn.bughub.annotation.DecryptMethod)")
public void decryptPointcut() {
}
@Before("decryptPointcut()")
public void decrypt(JoinPoint joinPoint) {
try {
handleDecrypt(joinPoint.getArgs());
} catch (Exception e) {
log.error("切面解密出现异常:{}", e.getMessage(), e);
throw new ServiceException("查询失败");
}
}
/**
* 解密处理
*
* @param args 参数
* @throws Exception 异常信息
*/
private void handleDecrypt(Object[] args) throws Exception {
for (Object arg : args) {
Field[] fields;
//处理PageParam<T>类型的参数
if (arg instanceof PageParam) {
PageParam<?> pageParam = (PageParam<?>) arg;
Object entity = pageParam.getEntity();
if (entity != null) {
fields = entity.getClass().getDeclaredFields();
} else {
continue;
}
} else if (arg != null) {
fields = arg.getClass().getDeclaredFields();
} else {
continue;
}
for (Field field : fields) {
if (field.isAnnotationPresent(DecryptField.class)) {
field.setAccessible(true);
Object fieldValue;
if (arg instanceof PageParam) {
PageParam<?> pageParam = (PageParam<?>) arg;
Object entity = pageParam.getEntity();
if (entity != null) {
fieldValue = field.get(entity);
} else {
// 如果实体对象为null,则跳过该字段的处理
continue;
}
} else {
fieldValue = field.get(arg);
}
// 检查字段的值是否是字符串类型
if (fieldValue instanceof String) {
// 将字段的值转换为字符串
String cipherText = (String) fieldValue;
if (!ObjectUtils.isEmpty(cipherText)) {
//解密具体实现
String token = UserUtils.getAuthUser().getToken();
//原文
String plainText = keyService.decrypt(token, cipherText);
//处理PageParam<T>泛型赋值问题
if (arg instanceof PageParam) {
// 如果arg是PageParam对象,将解密后的值设置到其内部实体对象的相应字段中
PageParam<?> pageParam = (PageParam<?>) arg;
Object entity = pageParam.getEntity();
field.set(entity, plainText);
} else {
// 如果arg不是PageParam对象,则将解密后的值直接设置回arg对象中的字段
field.set(arg, plainText);
}
}
} else {
// 处理不是字符串类型的情况
}
}
}
}
}
}
说明
接口中如果直接传一个对象没有别的类包装如PageParam<实体>,则不需要考虑类型转换,直接操作arg