在开发过程中,为了调试及后期维护过程快速排错都会记录请求的入参以及返回值,比较常用的方式是借助日志生成器通过硬编码的方式记录日志,代码不够简洁、优雅。因此,可以借助AOP来实现日志记录,无需在代码中打印日志,并且能够满足不同的日志场景下的定制需求。
日志注解
package com.cube.share.log.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* @author cube.li
* @date 2021/4/3 21:42
* @description 日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ApiLog {
/**
* 标题
*/
String title() default "";
@AliasFor("title")
String name() default "";
/**
* 日志打印时排除的类型(例如 File),对入参,出参都有效
*/
Class<?>[] excludes() default {};
LogLevel level() default LogLevel.DEBUG;
LogType type() default LogType.BOTH;
/**
* 是否开启
*/
boolean enable() default true;
/**
* 是否打印方法信息
*/
boolean printMethodInfo() default true;
/**
* 是否打印请求信息
*/
boolean printRequestInfo() default true;
/**
* 是否打印耗时
*/
boolean timeConsumption() default true;
/**
* 日志级别
*/
enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR
}
/**
* 记日志类型
*/
enum LogType {
/**
* 入参
*/
PARAM,
/**
* 返回值
*/
RETURN,
/**
* 入参+返回值
*/
BOTH
}
}
在需要打印日志的方法上增加该注解,该注解内定义了若干参数,可以根据实际场景给这些参数赋值。
切面和切点
package com.cube.share.log.annotation.aspect;
import com.cube.share.log.annotation.ApiLog;
import com.cube.share.log.util.ApiLogHelper;
import lombok.extern.slf4j.Slf4j;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* @author cube.li
* @date 2021/4/3 21:58
* @description 日志切面
*/
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
public class ApiLogAop {
@Resource
private ApiLogHelper logHelper;
@Pointcut("@annotation(com.cube.share.log.annotation.ApiLog)")
public void pointCut() {
}
@Around("pointCut()")
public Object log(@NonNull ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//获取此方法上的注解
ApiLog apiLog = method.getAnnotation(ApiLog.class);
if (!apiLog.enable()) {
return point.proceed();
}
switch (apiLog.level()) {
case DEBUG:
if (!log.isDebugEnabled()) {
return point.proceed();
}
break;
case INFO:
if (!log.isInfoEnabled()) {
return point.proceed();
}
break;
case WARN:
if (!log.isWarnEnabled()) {
return point.proceed();
}
break;
case ERROR:
if (!log.isErrorEnabled()) {
return point.proceed();
}
break;
default:
break;
}
//记录时间
long start = System.currentTimeMillis(), end;
logHelper.logBeforeProceed(apiLog, method, point.getArgs());
Object result = point.proceed();
end = System.currentTimeMillis();
logHelper.logAfterProceed(apiLog, result, end - start);
return result;
}
}
定义日志切面,并且将日志注解作为切入点,只要请求方法上具有该日志注解,即可自动进行日志记录。
打印日志的工具类
package com.cube.share.log.util;
import com.cube.share.base.utils.IpUtil;
import com.cube.share.log.annotation.ApiLog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.MessageFormat;
/**
* @author cube.li
* @date 2021/4/3 22:05
* @description 日志打印
*/
@Component
@Slf4j
public class ApiLogHelper {
@Resource
private HttpServletRequest request;
private static final String SEPARATOR = " | ";
private static final String REQUEST_INFO = "###请求信息###: URI:{0},Content-Type:{1},请求IP:{2}";
private static final String METHOD_INFO = "###方法信息###: 方法名称:{0}";
/**
* 记录在执行proceed()方法之前的日志,包括:
* 方法信息
* 请求信息
* 参数信息
*
* @param apiLog 注解
* @param method 方法
* @param args 方法参数
*/
public void logBeforeProceed(ApiLog apiLog, Method method, Object[] args) {
StringBuilder content = new StringBuilder("######日志######\n");
content.append("Title:").append(StringUtils.isEmpty(apiLog.title()) ? apiLog.name() : apiLog.title()).append("\n");
if (apiLog.printRequestInfo()) {
content.append(MessageFormat.format(REQUEST_INFO, request.getRequestURI(),
request.getContentType(),
IpUtil.getIpAddress(request)))
.append("\n");
}
if (apiLog.printMethodInfo()) {
content.append(MessageFormat.format(METHOD_INFO,
method.getDeclaringClass().getSimpleName() + SEPARATOR + method.getName()))
.append("\n");
}
if (apiLog.type() == ApiLog.LogType.RETURN) {
content.append("参数打印未启用!\n");
} else {
//排除类型
Class<?>[] excludes = apiLog.excludes();
content.append(getParamContent(args, excludes));
}
print(content.toString(), apiLog.level());
}
private StringBuilder getParamContent(Object[] args, Class<?>[] excludes) {
StringBuilder paramContent = new StringBuilder("###参数信息###: ");
for (Object arg : args) {
if (arg == null) {
continue;
}
if (exclude(arg.getClass(), excludes)) {
paramContent.append("#排除的参数类型:").append(arg.getClass()).append(SEPARATOR);
} else {
paramContent.append("#参数类型:").append(arg.getClass())
.append(" ").append("参数值:")
.append(arg.toString())
.append(SEPARATOR);
}
}
return paramContent;
}
/**
* 判断指定类型是否需要排除
*
* @param target 指定类型
* @param excludes 需要排除的类型集合
* @return 排除:true
*/
private boolean exclude(@Nullable Class<?> target, @NonNull Class<?>[] excludes) {
if (ArrayUtils.isEmpty(excludes) || target == null) {
return false;
}
for (Class<?> clazz : excludes) {
if (clazz.equals(target)) {
return true;
}
}
return false;
}
/**
* 记录在执行proceed()方法之后的日志,包括:
* 返回值信息
* 执行耗时
*
* @param apiLog 注解
* @param result 返回结果
* @param timeConsumption 耗时
*/
public void logAfterProceed(ApiLog apiLog, Object result, long timeConsumption) {
StringBuilder content = new StringBuilder("###返回值信息###: ");
if (apiLog.type() == ApiLog.LogType.PARAM) {
content.append("未启用返回值打印");
} else {
content.append(getReturnContent(result, apiLog.excludes()));
}
if (apiLog.timeConsumption()) {
content.append("执行耗时:").append(timeConsumption).append("MS");
} else {
content.append("未启用方法耗时打印");
}
print(content.toString(), apiLog.level());
}
private StringBuilder getReturnContent(@Nullable Object result, @NonNull Class<?>[] excludes) {
StringBuilder content = new StringBuilder();
try {
if (result == null) {
content.append("null");
return content;
}
Class<?> clazz = result.getClass();
if (exclude(clazz, excludes)) {
content.append("被排除的类型:").append(clazz.getSimpleName());
} else {
content.append("返回值类型:").append(clazz.getSimpleName())
.append(SEPARATOR).append("返回值:").append(new ObjectMapper().writeValueAsString(result));
}
content.append("\n");
} catch (JsonProcessingException e) {
log.error("Java对象转Json字符串失败!");
}
return content;
}
/**
* 打印日志
*
* @param content 日志内容
* @param level 日志级别
*/
public void print(String content, ApiLog.LogLevel level) {
switch (level) {
case DEBUG:
log.debug(content);
break;
case INFO:
log.info(content);
break;
case WARN:
log.warn(content);
break;
case ERROR:
log.error(content);
break;
default:
break;
}
}
}
测试
package com.cube.share.log.controller;
import com.cube.share.base.templates.ApiResult;
import com.cube.share.log.annotation.ApiLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* @author cube.li
* @date 2021/4/4 10:49
* @description
*/
@RestController
@RequestMapping("/log")
@Slf4j
public class LogController {
@GetMapping("/info")
@ApiLog(level = ApiLog.LogLevel.INFO)
public ApiResult info(@RequestParam("name") String name, @RequestParam("id") Integer id) {
return ApiResult.success();
}
@PostMapping("/upload")
@ApiLog(excludes = Integer.class)
public ApiResult upload(MultipartFile file, @RequestParam("fileName") String fileName, @RequestParam("id") Integer id) {
return ApiResult.success();
}
@ApiLog(level = ApiLog.LogLevel.ERROR, title = "标题")
@RequestMapping("/error")
public void error() {
}
}
调用info方法,日志打印如下:
2021-04-04 12:54:08.390 INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper : ######日志######
Title:
###请求信息###: URI:/log/info,Content-Type:null,请求IP:127.0.0.1
###方法信息###: 方法名称:LogController | info
###参数信息###: #参数类型:class java.lang.String 参数值:li | #参数类型:class java.lang.Integer 参数值:4 |
2021-04-04 12:54:08.395 INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper : ###返回值信息###: 返回值类型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
执行耗时:1MS
调用upload方法,日志打印如下:
2021-04-04 12:54:42.860 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper : ######日志######
Title:
###请求信息###: URI:/log/upload,Content-Type:multipart/form-data; boundary=--------------------------112997737393373574958179,请求IP:127.0.0.1
###方法信息###: 方法名称:LogController | upload
###参数信息###: #参数类型:class java.lang.String 参数值:文件名 | #排除的参数类型:class java.lang.Integer |
2021-04-04 12:54:42.861 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper : ###返回值信息###: 返回值类型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
执行耗时:1MS
由于注解中设置了排除Integer类型,因此参数id并未被打印
调用error方法,日志打印如下:
021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper : ######日志######
Title:标题
###请求信息###: URI:/log/error,Content-Type:null,请求IP:127.0.0.1
###方法信息###: 方法名称:LogController | error
###参数信息###:
2021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper : ###返回值信息###: null执行耗时:0MS
@ConditionalOnProperty注解
这里顺带提一下@ConditionalOnProperty注解,这个注解定义如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
// 数组,获取对应property名称的值,与name不可同时使用
String[] value() default {};
// 配置属性名称的前缀
String prefix() default "";
// 数组,配置属性完整名称或部分名称
// 可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用
String[] name() default {};
// 可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置
String havingValue() default "";
// 缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效
boolean matchIfMissing() default false;
}
可以使用@ConditionalOnProperty来配置一个Bean或者切面是否生效,例如,在本文中,如果想要使ApiLogAop不生效,则可以在ApiLogAop切面上加上@ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
表示,如果在配置了api-log.enable属性且其值为true,则启用ApiLogAop切面,如果没有配置则默认为true即启用。
在配置文件中增加如下配置:
api-log:
enable: false
调用如上几个方法发现并没有打印日志,即ApiLogAop未生效。
完整代码:https://gitee.com/li-cube/share/tree/master/log
网友评论