关于SpringAOP日志管理的使用方法
在需要对业务日志进行特殊规则统一管理的时候,Slf4j提供的全局日志配置就不那么好使用了,这时候我们可以使用Spring的AOP特性来截断各个方法或接口,将其返回值或处理过程做统一管理,再落到日志中。
首先我们定义一个实体用来落日志,这个实体最后打印出来我希望字段是固定顺序的,字段为空则不打印,要巧妙的利用阿里的fastJson。
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonInclude;
// 这个DateUtil是自己封装的,用来获取当前时间
import xxx.utils.DateUtil;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LogData {
/**
* 日志主题
*/
@JSONField(ordinal = 1)
private String topic;
/**
* 类型
*/
@JSONField(ordinal = 2)
private String type = "default";
/**
* 启动时间戳
*/
@JSONField(ordinal = 3)
private String time;
/**
* 耗时
*/
@JSONField(ordinal = 4)
private Long cost;
/**
* 长度
*/
@JSONField(ordinal = 5)
private Integer size = 0;
@JSONField(ordinal = 6)
private Map<String, Object> data = new HashMap<>();
/**
* 异常
*/
@JSONField(ordinal = 7)
private String error;
/**
* 备注消息
*/
@JSONField(ordinal = 8)
private String msg;
public LogData(String topic) {
this.topic = topic;
this.time = DateUtil.get24DateTime();
}
public LogData(String topic, String type) {
this.topic = topic;
this.type = type;
this.time = DateUtil.get24DateTime();
}
}
由于我们更倾向于使用注解来用切面,所以我们要创建一个Annotation,本注解对应级别是方法级的
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 打印查询日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintLog {
}
接下来是关键的日志切面
/**
* 定义日志切面
*
* @Lazy 注解:容器一般都会在启动的时候实例化所有单实例 bean,如果我们想要 Spring 在启动的时候延迟加载 bean,需要用到这个注解
* value为true、false 默认为true,即延迟加载,@Lazy(false)表示对象会在初始化的时候创建
* @Modified By:
*/
@Aspect
@Component
@Lazy(false)
@Slf4j(topic = "traceLogger")
public class LoggerAspect {
/**
* 定义切入点:对要拦截的方法进行定义与限制,如包、类
* <p>
* 1、execution(public * *(..)) 任意的公共方法
* 2、execution(* set*(..)) 以set开头的所有的方法
* 3、execution(* com.poizon.annotation.LoggerApply.*(..))com.test.LoggerApply这个类里的所有的方法
* 4、execution(* com.poizon.annotation.*.*(..))com.test.annotation包下的所有的类的所有的方法
* 5、execution(* com.poizon.annotation..*.*(..))com.test.annotation包及子包下所有的类的所有的方法
* 6、execution(* com.poizon.annotation..*.*(String,?,Long)) com.test.annotation包及子包下所有的类的有三个参数,第一个参数为String类型,第二个参数为任意类型,第三个参数为Long类型的方法
* 7、execution(@annotation(com.test.annotation.poizon))
*/
@Pointcut("execution(* com.test.services..*.* (..)) && @annotation(com.test.annotation.PrintLog)")
private void cutMethod() {
}
/**
* 后置通知:在目标方法执行后调用,若目标方法出现异常,则不执行
@AfterReturning("cutMethod()")
public void afterReturning(ProceedingJoinPoint pjp) {
try {
Object proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}*/
@AfterReturning(returning = "rvt", pointcut = "execution(* com.test.services..*.*(..))")
public void AfterExec(JoinPoint joinPoint, Object rvt) {
// 这里是选择在方法执行完成后,对返回的结果进行拦截,如果返回的结果是LogData类型的,就对实体进行打印
if (rvt == null) {
return;
}
if (rvt instanceof LogData) {
log.info(JsonUtil.writeValueAsString(rvt));
}
}
}
这样只要我们业务返回的数据都按照需求存进LogData这样的实体中即可。
但这样是否还是过于复杂了?我们通常一个方法可能本身就要返回一个实体,而现在要求返回一个指定实体给切面用于日志,很大程度影响到了业务,而且改造成本可能会比较高,那么用另一种切面方式
@Around("@annotation(com.test.annotation.PrintLog)")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 1.方法执行前的处理,相当于前置通知
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
// 获取方法
Method method = methodSignature.getMethod();
// 获取方法上面的注解
PrintLog logAnno = method.getAnnotation(PrintLog.class);
LogData logData = new LogData(method.getName());
long startTime = System.currentTimeMillis();
Object result = null;
try{
//让代理方法执行
result = pjp.proceed();
if(result instanceof Collection){
logData.setSize(((Collection) result).size());
}else if (result instanceof Map){
logData.setData((HashMap<String,Object>)result);
}
}catch (Exception e){
logData.setError(e.getMessage());
}finally {
logData.setCost(System.currentTimeMillis()-startTime);
}
return result;
}
这样我们通过判断原有方法返回的内容,就能没有侵入性的切入各业务进行日志管理了。