一、自定义Log注解
-
- 代码块
/**
* @Author: AaronJonah
* @Description: 自自定义 log aspect切面
* @Date Created in 2022−12-28 17:45
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface Log {
/**
*
*/
String value() default "";
/**
* 是否开启日志
*/
boolean enabled() default true;
/**
* 是否打印入参日志,默认是
*/
boolean reqLog() default true;
/**
* 是否打印出参日志,默认是
*/
boolean resLog() default true;
/**
* 是否打印耗时日志,默认是
*/
boolean cost() default true;
}
2.注解说明
-
@Documented
如果一个注解@B,被@Documented标注,那么被@B修饰的类,生成javadoc文档时,会显示@B。如果@B没有被@Documented标准,最终生成的文档中就不会显示@B。只是用来做标识,没什么实际作用。
-
@Retention(RetentionPolicy.RUNTIME)
按生命周期 可分为3类:
-
RetentionPolicy.SOURCE:注解只保留源文件,当Java文件编译成class文件的时候,注解被遗弃;
-
RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
-
RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
详情参考:@Retention(RetentionPolicy.RUNTIME)-CSDN博客
-
@Target(value = ElementType.METHOD)
修饰注解目标作用范围
ElementType.TYPE:接口、类、枚举、注解
ElementType.FIELD:字段、枚举常量 声明
ElementType.METHOD:方法声明
ElementType.PARAMETER:参数声明
ElementType.CONSTRUCTOR:构造函数声明
ElementType.LOCAL_VARIABLE:局部变量声明
ElementType.ANNOTATION_TYPE:注解声明
ElementType.PACKAGE:包声明
ElementType.TYPE_PARAMETER:类型参数声明(jdk 1.8 @since 1.8)
ElementType.TYPE_USE:使用的类型声明(jdk 1.8 @since 1.8)
二、定义Log切面类
1.代码块
/**
* @author AaronJonah
* @Description Log切面配置
* @Date 2022/12/28 18:21
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* 请求Id
*/
private static final String REQ_ID = "requestId";
/**
* @Author AaronJonah
* @Description 标注切点位置
* @Date 2022/12/28 19:37
*/
@Pointcut("@annotation(com.jdaz.ins.galaxy.settlement.service.annotation.Log)")
public void pointcutInterface() {
}
/**
* @return Object
* @Author AaronJonah
* @Description 切点执行
* @Param joinPoint
* @Date 2022/12/29 9:23
*/
@Order(2) //定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序
@Around("pointcutInterface()") // 增强方法处理
public Object doAroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Map<String, String> mdc = MDC.getCopyOfContextMap();
//获取调用类 和方法
String key = joinPoint.getTarget().getClass().getSimpleName() + "#" + getMethodName(joinPoint) + "()";
// 请求参数
Object[] args = joinPoint.getArgs();
// 日志说明
String logDesc = StringUtils.EMPTY;
// 是否输出log日志 true-打印 false-不打印
boolean reqLog = true, resLog = false, cost = true;
Log insLog = getLog(joinPoint);
if (null != insLog) {
if (!insLog.enabled()) {
return joinPoint.proceed();
}
logDesc = insLog.value();
reqLog = insLog.reqLog();
resLog = insLog.resLog();
cost = insLog.cost();
}
//获取请求id
String reqId = MDC.get(REQ_ID);
if (StringUtils.isBlank(reqId)) {
reqId = System.currentTimeMillis() + "-" + Arrays.hashCode(args);
MDC.put(REQ_ID, reqId);
}
// log请求json字符串
String logReqJson = getLogReqJson(args);
Object res;
if (reqLog) {
log.info("【Log {}请求】{},参数={}", logDesc, key, logReqJson);
}
long beginMs = System.currentTimeMillis();
try {
// 调用目标方法
res = joinPoint.proceed();
// 由于多线程,会导致MDC清空,此处补全
if (StringUtils.isBlank(MDC.get(REQ_ID))) {
MDC.setContextMap(mdc);
}
long costTime = System.currentTimeMillis() - beginMs;
if (resLog) {
log.info("【Log {}返回】{}{},结果={}", logDesc, key, cost ? " 耗时=" + costTime + "ms" : "", JSONObject.toJSONString(res, SerializerFeature.DisableCircularReferenceDetect));
} else if (cost) {
log.info("【Log {}返回】{} 耗时={}ms", logDesc, key, costTime);
}
return res;
} catch (Exception e) {
String errorMsg = e.getMessage();
log.error("【Log {}异常】{} {}{}", logDesc, key, errorMsg, reqLog ? ",参数=" + logReqJson : "", e);
throw e;
}
}
/**
* @return String
* @Author AaronJonah
* @Description 获取Log请求参数JSON字符串
* @Param args
* @Date 2022/12/29 10:10
*/
private String getLogReqJson(Object[] args) {
if (null == args || args.length == 0) {
return StringUtils.EMPTY;
}
// 无需序列化输出的字段
PropertyFilter filter = (arg, name, val) -> {
if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse) {
return false;
}
if (arg instanceof InputStream || arg instanceof MultipartFile) {
return false;
}
return !(arg instanceof Exception);
};
try {
return JSONObject.toJSONString(args, filter, SerializerFeature.DisableCircularReferenceDetect);
} catch (Exception e) {
log.error("getLogReqJson Exception");
return StringUtils.EMPTY;
}
}
/**
* @return Log
* @Author AaronJonah
* @Description 获取Log日志
* @Param joinPoint
* @Date 2022/12/29 9:59
*/
private Log getLog(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature.getMethod().getAnnotation(Log.class);
}
return null;
}
/**
* @return String
* @Author AaronJonah
* @Description 获取方法名
* @Param joinPoint
* @Date 2022/12/29 9:35
*/
private String getMethodName(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature.getMethod().getName();
}
return "未获取到方法";
}
}
2.注解说明
-
@Order(2)
注解定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序。
-
注解可以作用在类(接口、枚举)、方法、字段声明(包括枚举常量)。
-
注解有一个int类型的参数,可以不传,默认是最低优先级。
-
通过常量类的值我们可以推测参数值越小优先级越高。
-
@Around("pointcutInterface()") Spring AOP 增强处理
-
既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作。
-
可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行。
-
可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法。
-
虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。
详情参考:@Around简单使用示例——SpringAOP增强处理_around方法是在什么时候发生-CSDN博客
三、使用方式
-
- 代码块
/**
* @return ResponseEntity
* @Author AaronJonah
* @Description 数据推送失败再次推送
* @Param smallPolicyDTO
* @Date 2022/11/2 15:06
*/
@PostMapping(value = "/pushFailedAgainPush")
@Log(value = "数据推送失败再次推送", resLog = false)
public ResponseEntity pushFailedAgainPush(@RequestBody SettlementSmallPolicyDTO smallPolicyDTO) {
if (PUSH_CODE.equals(smallPolicyDTO.getCode())) {
settlementSmallPolicyService.pushFailedAgainPush(smallPolicyDTO);
return Replys.ok("数据推送失败再次推送成功");
}
return Replys.fail("失败", "数据推送失败再次推送失败");
}
-
- 日志
com.jdaz.ins.galaxy.settlement.service.aspect.LogAspect.doAroundAdvice(LogAspect.java:89)||【Log 数据推送失败再次推送请求】ReSgBillCheckDetailController#pushFailedAgainPush(),参数=[{"checkTaskNo":"DZ20211228016","code":"jdaz","endUpdateTime":1696089600000,"insType":"JXAZ0002","isSummary":false,"planCode":"BZX-XFZLY-JDSC-POPJBX-30D","pushSgStatus":1,"pushedFlag":true,"sgRemoveAccountStatus":0,"updateTime":1633017600000}]
com.jdaz.ins.galaxy.settlement.service.aspect.LogAspect.doAroundAdvice(LogAspect.java:103)||【Log 数据推送失败再次推送返回】ReSgBillCheckDetailController#pushFailedAgainPush() 耗时=2713ms