@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:[]
请求进入喽
完结!