@SysLog – AOP最佳实践:日志规范注解

2023-01-19 09:48:56 浏览数 (1)

@SysLog前置需要

1、拦截器:日志中有操作人的信息,通过拦截器放信息到ThreadLocal中。

2、自定义注解:定义一个注解。

3、AOP:@Before方法打印日志,@AfterReturning方法处理异常信息

@SysLog实现效果:方法加入@SysLog注解可实现

1、打印入参信息(默认全参,可控制不打印参数)

2、打印指定excludes实现排除部分入参打印

3、打印异常日志

4、打印场景(如不指定场景是干嘛的,会打印全限定类名)、操作人、入参

上手编码

1、 编写拦截器

代码语言:javascript复制
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;

/**
 * @Author :zanglk
 * @DateTime :2021/06/27 14:16
 * @Description :暂无说明
 * @Notes :To change this template:Click IDEA-Preferences to search 'File Templates'
 */
@Log4j2
@Configuration
public class UserInfoInterceptor implements HandlerInterceptor {

    private static final ThreadLocal<HashMap> USER_INFO = new InheritableThreadLocal<>();

    public static void setUser(HashMap user) {
        USER_INFO.set(user);
    }

    public static HashMap getUser() {
        return USER_INFO.get();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HashMap hashMap = new HashMap();
        String name = request.getHeader("name");
        if (StringUtils.isBlank(name)) {
            hashMap.put("name", "请求头没有放入name,请PostMan添加!");
        } else {
            hashMap.put("name", name);
        }
        USER_INFO.set(hashMap);
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

2、开启拦截器

代码语言:javascript复制
@Configuration
public class MVCConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor()).order(0);
    }
}

3、自定义注解 SysLog

代码语言:javascript复制
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;

/**
 * @Author :zanglk
 * @DateTime :2021/10/25 13:04
 * @Description :定义一个SysLog注解,未来被标记的注解会额外打印日志
 * @Notes :To change this template:Click IDEA-Preferences to search 'File Templates'
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {

    // 场景名
    @AliasFor("log")
    String value() default "";

    // 日志内容,默认方法场景名
    @AliasFor("value")
    String log() default "";

    // 只打印的接口参数名
    String[] params() default {};

    // 排除打印的接口参数名 数组形式
    String[] excludes() default {};

    // 是否打印全部接口参数
    boolean allParams() default true;

    // 异常日志内容:如果出现日志,就会以errorLog作为场景名。如果不传入,优先使用log作为场景名,如果还没有,就以全限定类名为准
    String errorLog() default "";

    // 是否打印当前用户
    boolean printUser() default true;

}

4、SysLogAop

代码语言:javascript复制
import com.alibaba.excel.util.DateUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

/**
 * @Author :zanglk
 * @DateTime :2022/10/25 13:04
 * @Description :日志SysLog注解拦截AOP
 * @Notes :To change this template:Click IDEA-Preferences to search 'File Templates'
 */
@Log4j2
@Aspect
@Component
public class SysLogAop {

    private static final Class<?>[] EXCLUDE_TYPE = new Class<?>[]{
            HttpServletRequest.class,
            HttpServletResponse.class
    };

    // 指定一个注解 作为切点
    @Pointcut("@annotation(com.zanglikun.springdataredisdemo.aop.sysLogAop.SysLog)")
    public void pointcut() {
    }

    /**
     * 对切点进行代码增强,Before作为切点之前
     */
    @Before("pointcut()")
    public void beforeLog(JoinPoint joinPoint) {
        try {
            // 获取切点签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取注解对象,主要调用annotation.log() 获取注解的参数信息
            SysLog annotation = AnnotationUtils.getAnnotation(signature.getMethod(), SysLog.class);
            if (null == annotation) {
                return;
            }
            // 通过日志工厂获取日志对象
            Logger logger = LoggerFactory.getLogger(signature.getMethod().getDeclaringClass().getSimpleName());
            StringBuilder logPrefix = new StringBuilder(annotation.log());
            // 如果注解没指定value或log,就会打印全限定类名!如果指定了,使用注解value的内容
            if (StringUtils.isBlank(logPrefix.toString())) {
                logPrefix.append("【").append(signature.getMethod().getDeclaringClass().getName()   "."   signature.getMethod().getName()).append("】");
            }
            logger.info(getLog(joinPoint, annotation, logPrefix.toString()));
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
        }
    }

    /**
     * 在方法执行完成执行的内容,无业务需要,尚未实现!
     */
    @AfterReturning(value = "pointcut()", returning = "result")
    public void afterRunningLog(JoinPoint joinPoint, Object result) {

    }

    /**
     * 如果目标方法异常,执行下面逻辑
     */
    @AfterThrowing(value = "pointcut()", throwing = "error")
    public void afterThrowingLog(JoinPoint joinPoint, Throwable error) {
        try {
            // 获取签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取日志注解
            SysLog annotation = AnnotationUtils.getAnnotation(signature.getMethod(), SysLog.class);
            if (null == annotation) {
                return;
            }
            // 获取日志工厂对象
            Logger logger = LoggerFactory.getLogger(signature.getMethod().getDeclaringClass().getSimpleName());
            // 如果指定了错误日志场景,就以错误的来,如果没指定优先以log为准,如果log也为空,就以全限定类名为准。
            String errorPrefix = StringUtils.isNotBlank(annotation.errorLog()) ? annotation.errorLog() : annotation.log();
            errorPrefix = StringUtils.isNotBlank(errorPrefix) ? errorPrefix : signature.getMethod().getDeclaringClass().getName()   "."   signature.getMethod().getName();
            // 制作日志文本
            String errorLog = getLog(joinPoint, annotation, errorPrefix   "异常")   ",exception:"   ExceptionUtils.getStackTrace(error);
            // 打印日志
            logger.error(errorLog);
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
        }
    }

    /**
     * 获取日志文本信息
     */
    private String getLog(JoinPoint joinPoint, SysLog annotation, String log) {
        // 获取用户登录标识用于打印,如果注解指定不打印,就不打印
        if (annotation.printUser()) {
            log  = getUserInfo();
        }
        // 判断是否需要打印接口的入餐
        if (!annotation.allParams() && annotation.params().length == 0) {
            return log;
        }
        // 获取接口参数
        // 获取签名。(目的是通过签名获取方法全限定类名,以及方法名)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取封装方法入参信息,方便调用。
        Map<String, ParamVo> canLogParams = getFunctionParams(signature.getParameterNames(), signature.getParameterTypes(), joinPoint.getArgs(), annotation.excludes());
        // 日志拼接对象 上文已经封装了场景名   操作人的String信息了,后面还需要打印参数信息
        StringBuilder builder = new StringBuilder(StringUtils.isNotBlank(log) ? log : signature.getMethod().getDeclaringClass().getName()   "."   signature.getMethod().getName());
        builder.append(",方法入参依此是:");
        // 接口参数拼接 1、判断注解指定打印的参数长度是否大于0。如果指定了某些参数,就会只打印哪些参数
        boolean hasPointJustPrintParams = annotation.params().length > 0;
        // 遍历要打印的日志信息。已经过滤排除的内容了!
        for (Map.Entry<String, ParamVo> entry : canLogParams.entrySet()) {
            if (hasPointJustPrintParams && !ArrayUtils.contains(annotation.params(), entry.getKey())) {
                continue;
            }
            // 拼接Key
            builder.append(entry.getKey());
            builder.append(":");
            // 拼接Value 1 如果Value是空,则直接拼接NULL
            if (null == entry.getValue()) {
                builder.append("NULL,");
                continue;
            }
            // 拼接Value 2:处理日期(格式是:Thu Oct 27 15:34:23 CST 2022)、文件 如果都不是上述格式,直接打印日志Value
            if (entry.getValue().getParamValue() instanceof Date) {
                builder.append(DateUtils.format((Date) entry.getValue().getParamValue()));
            } else if (entry.getValue().getParamValue() instanceof MultipartFile) {
                MultipartFile file = (MultipartFile) entry.getValue().getParamValue();
                String fileParam = "{MultipartFile: fileName: %s, byteSize: %s, fileSize: %s}";
                builder.append(String.format(fileParam, file.getOriginalFilename(), file.getSize(), FileUtils.byteCountToDisplaySize(file.getSize())));
            } else {
                builder.append(JSON.toJSONString(entry.getValue().getParamValue(), SerializerFeature.WriteMapNullValue));
            }
            builder.append(",");
        }
        return builder.substring(0, builder.length() - 1); // 去除最后一个逗号
    }

    /**
     * 获取用户登录标识用于打印
     *
     * @return 登录标识
     */
    private String getUserInfo() {
        // 自己编写拦截器,请求进来的时候,直接将线程与用户塞入ThreadLoacal中,然后取的时候 获取当前线程从ThreadLocal获取。
        if (UserInfoInterceptor.getUser() != null && UserInfoInterceptor.getUser().isEmpty()) {
            return ",【操作人:Not logged in】";
        }
        final String temp = ",【操作人:%s】";
        return String.format(temp, UserInfoInterceptor.getUser().get("name"));
    }

    /**
     * 封装方法入参信息,方便调用。(依此是 入参变量名,入参类全限定类名,参数值)
     */
    private Map<String, ParamVo> getFunctionParams(String[] paramNames, Class[] paramTypes, Object[] paramObjs, String[] excludes) {
        Map<String, ParamVo> params = new HashMap<>();
        for (int i = 0; i < paramNames.length; i  ) {
            // 如果某入参被排除,将不会被打印日志
            if (ArrayUtils.contains(EXCLUDE_TYPE, paramTypes[i]) || ArrayUtils.contains(excludes, paramNames[i])) {
                continue;
            }
            params.put(paramNames[i], new ParamVo(paramNames[i], paramTypes[i], paramObjs[i]));
        }
        return params;
    }


    // 方便打印的日志实体
    @Data
    @AllArgsConstructor
    private static class ParamVo<T> {
        private String paramName; // 入参变量名
        private Class<T> paramType; // 入参类字节码
        private T paramValue; // 入参值
    }

}

Go测试!

代码语言:javascript复制
    @RequestMapping("/upload")
    //@SysLog
    //@SysLog(value = "【我是测试场景标头】上传头像")
    //@SysLog(value = "【我是测试场景标头】上传头像", excludes = {"name"})
    //@SysLog(params = {"name","date"},printUser = false)
    @SysLog(value = "【我是测试场景标头】上传头像", errorLog = "我是错误场景表头")
    public void upLoad(MultipartFile file, String name, Date date,String[] hobbits) {
        System.out.println("请求进入喽");
        // int i = 1 / 0;
    }

Postman测试

代码语言:javascript复制
curl --location --request POST '127.0.0.1:8081/testFileLimit/upload' 
--header 'name: zhangsan' 
--form 'file=@"/Users/zanglikun/Downloads/个人简历.doc"' 
--form 'name=" "' 
--form 'date="Thu Oct 27 15:34:23 CST 2022"' 
--form 'hobbits=""'

打印日志

代码语言:javascript复制
【我是测试场景标头】上传头像,【操作人:zhangsan】,方法入参依此是:date:2022-10-28 05:34:23,file:{MultipartFile: fileName: 个人简历.doc, byteSize: 39424, fileSize: 38 KB},hobbits:[]
请求进入喽

完结!

0 人点赞