使用aop和注解实现日志记录
问题描述:
生产上遇到一个问题,就是第三方厂商调用我们服务创建数据库实例,后面创建成功后,因为某些条件不适合,又调用卸载接口进行卸载了。卸载后再次进行创建,创建成功。但是过了一周后,第三方厂商人员反馈创建的数据库实例集群映射的DNS域名的ip还是之前旧的删除的哪套的,不是最新的。那么我去通过前几天的日志定位问题。
但是发现日志记录不全。所以我萌生了使用aop切面来记录请求和参数的想法,从而避免漏掉记录重要日志。
于是就干起来了,实现主要有3点
1:打印日志的注解实现
@Target({ElementType.TYPE, ElementType.METHOD}) // 注解类型, 级别
@Retention(RetentionPolicy.RUNTIME) // 运行时注解
public @interface PrintLog {
String module() default ""; //方法标识,比如赋值为 新增数据库实例
boolean printParam() default true; //默认打印请求参数在日志中
}
2:切面定义
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author mengfl
* @Description:
* @date 2021/9/184:54 下午
*/
@Aspect // 切面标识
@Component // 交给spring容器管理
@Slf4j
public class PrintLogAspect {
@Autowired
/**
* 选取切入点为自定义注解
*/
@Pointcut("@annotation(cn.com.huacloud.cdd.annotation.PrintLog)")
public void PrintLog(){}
@Around(value="PrintLog()")
public Object printlog(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
PrintLog printLog = methodSignature.getMethod().getDeclaredAnnotation(PrintLog.class);
String module = printLog.module();
if(StringUtils.isEmpty(module)){
//获取当前方法类名
String classname = joinPoint.getTarget().getClass().getName();
//获取方法名称
String methodName= methodSignature.getMethod().getName();
// 如果被代理的方法没有给module赋值,那么按照下面方式组装,以方便在日志打印出来后,知道被打印出来的日志
// 属于哪个类的哪个方法
module = classname+":"+methodName;
}
String paramsString = "";
// 是否要打印方法的参数数据,默认打印
if (printLog.printParam()) {
// 参数名
String[] paramNames = methodSignature.getParameterNames();
if (paramNames != null && paramNames.length > 0) {
// 参数值
Object[] args = joinPoint.getArgs();
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < paramNames.length; i++) {
Object value = args[i];
params.put(paramNames[i], value);
}
// 以json的形式记录参数
paramsString = JSONObject.toJSONString(params);
}
}
String username = "";
try{
LoginUser loginUser = SecurityUtil.getLoginUser();
//当前登录用户名称
username = loginUser.getNickName();
}catch(Exception e){
log.info("切面获取用户异常!");
e.printStackTrace();
}
try {
String startTime = DateUtil.now();
//统一打印执行前日志
log.info(StrUtil.format("当前时间{},用户{}请求进入方法模块{}开始,请求参数为{}!", startTime,username, module,paramsString));
// 执行原方法
Object object = joinPoint.proceed();
//统一打印执行后日志
String endTime = DateUtil.now();
log.info(StrUtil.format("当前时间{},用户{}请求进入方法模块{}结束,请求参数为{}", endTime,username, module,paramsString));
return object;
} catch (Exception e) {
// 备注记录失败原因
String exceptionTime = DateUtil.now();
log.info(StrUtil.format("异常时间{},用户{}请求进入方法模块{}异常,请求参数为{},异常原因是{}",exceptionTime,username, module,paramsString, e));
throw e;
}
}
}
3:注解使用方式
@PrintLog(module = "新增yaml文件",printParam = true)
public R<Void> addYaml(@RequestBody @Validated({YamlDto.Add.class,YamlDto.Update.class}) YamlDto yamlDto){
yamlService.addYaml(yamlDto);
return R.ok();
}
这样你在调用queryIMysqlInstance方法前后就会输出日志了。
代码输出日志如下:
2021-09-18 23:59:46.426 nacos [TID: N/A] [http-nio-8091-exec-1] INFO c.c.h.cdd.aspect.PrintLogAspect line:87 |
当前时间2021-09-18 23:59:44,用户超级管理员请求进入方法模块cn.com.huacloud.cdd.k8s.controller.YamlController:addYaml开始,请求参数为{"yamlDto":{"content":"test2","description":"test2","id":0,"kind":"test2","lastVersionId":"100","templateName":"test2","versionId":"100"}}!
备注:
在AOP编程中,我们经常会遇到下面的概念:
连接点 - Joinpoint
连接点是指程序执行过程中的一些点,比如方法调用,异常处理等。在 Spring AOP 中,仅支持方法级别的连接点。以上是官方说明,通俗地讲就是能够被拦截的地方,每个成员方法都可以称之为连接点。
接下来我们看看连接点的定义:
public interface JoinPoint {
String METHOD_EXECUTION = "method-execution";
String METHOD_CALL = "method-call";
String CONSTRUCTOR_EXECUTION = "constructor-execution";
String CONSTRUCTOR_CALL = "constructor-call";
String FIELD_GET = "field-get";
String FIELD_SET = "field-set";
String STATICINITIALIZATION = "staticinitialization";
String PREINITIALIZATION = "preinitialization";
String INITIALIZATION = "initialization";
String EXCEPTION_HANDLER = "exception-handler";
String SYNCHRONIZATION_LOCK = "lock";
String SYNCHRONIZATION_UNLOCK = "unlock";
String ADVICE_EXECUTION = "adviceexecution";
String toString();
String toShortString();
String toLongString();
//获取代理对象
Object getThis();
/**
返回目标对象。该对象将始终与target切入点指示符匹配的对象相同。除非您特别需要此反射访问,否则应使用 target切入点指示符到达此对象,以获得更好的静态类型和性能。
如果没有目标对象,则返回null。
**/
Object getTarget();
//获取传入目标方法的参数对象
Object[] getArgs();
//获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Signature getSignature();
/**
返回与连接点对应的源位置。
如果没有可用的源位置,则返回null。
返回默认构造函数的定义类的SourceLocation。
**/
SourceLocation getSourceLocation();
String getKind();
JoinPoint.StaticPart getStaticPart();
public interface EnclosingStaticPart extends JoinPoint.StaticPart {
}
//该帮助对象仅包含有关连接点的静态信息。它可以从JoinPoint.getStaticPart()方法中获得,也可以使用特殊形式在建议中单独访问 thisJoinPointStaticPart。
public interface StaticPart {
Signature getSignature();
SourceLocation getSourceLocation();
String getKind();
int getId();
String toString();
String toShortString();
String toLongString();
}
}
JoinPoint 接口中常用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。但是我们平时使用并不直接使用 JoinPoint 的实现类,中间还有一个接口实现,叫做 ProceedingJoinPoint,其定义如下:
public interface ProceedingJoinPoint extends JoinPoint {
void set$AroundClosure(AroundClosure var1);
//执行目标方法
Object proceed() throws Throwable;
//传入的新的参数去执行目标方法
Object proceed(Object[] var1) throws Throwable;
}
- Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
- Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
- Pointcut:切入点,即一组连接点的集合;
- Advice:增强,指特定连接点上执行的动作;
- Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
- Weaving:织入,指将切面整合到程序的执行流程中;
- Interceptor:拦截器,是一种实现增强的方式;
- Target Object:目标对象,即真正执行业务的核心逻辑对象;
- AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
引申思考:
这里提到了代理模式,后续文章我会对这里如何使用代理模式进行重点阐述。
参考文献:
《Spring AOP核心概念》https://zhuanlan.zhihu.com/p/109741656

浙公网安备 33010602011771号