在開發(fā)過程中,為了調試及后期維護過程快速排錯都會記錄請求的入?yún)⒁约胺祷刂党谇铮容^常用的方式是借助日志生成器通過硬編碼的方式記錄日志线召,代碼不夠簡潔铺韧、優(yōu)雅。因此缓淹,可以借助AOP來實現(xiàn)日志記錄哈打,無需在代碼中打印日志,并且能夠滿足不同的日志場景下的定制需求讯壶。
日志注解
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),對入?yún)?出參都有效
*/
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 {
/**
* 入?yún)? */
PARAM,
/**
* 返回值
*/
RETURN,
/**
* 入?yún)?返回值
*/
BOTH
}
}
在需要打印日志的方法上增加該注解料仗,該注解內定義了若干參數(shù),可以根據(jù)實際場景給這些參數(shù)賦值伏蚊。
切面和切點
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}";
/**
* 記錄在執(zhí)行proceed()方法之前的日志,包括:
* 方法信息
* 請求信息
* 參數(shù)信息
*
* @param apiLog 注解
* @param method 方法
* @param args 方法參數(shù)
*/
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("參數(shù)打印未啟用!\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("###參數(shù)信息###: ");
for (Object arg : args) {
if (arg == null) {
continue;
}
if (exclude(arg.getClass(), excludes)) {
paramContent.append("#排除的參數(shù)類型:").append(arg.getClass()).append(SEPARATOR);
} else {
paramContent.append("#參數(shù)類型:").append(arg.getClass())
.append(" ").append("參數(shù)值:")
.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;
}
/**
* 記錄在執(zhí)行proceed()方法之后的日志,包括:
* 返回值信息
* 執(zhí)行耗時
*
* @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("執(zhí)行耗時:").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
###參數(shù)信息###: #參數(shù)類型:class java.lang.String 參數(shù)值:li | #參數(shù)類型:class java.lang.Integer 參數(shù)值: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}
執(zhí)行耗時: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
###參數(shù)信息###: #參數(shù)類型:class java.lang.String 參數(shù)值:文件名 | #排除的參數(shù)類型: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}
執(zhí)行耗時:1MS
由于注解中設置了排除Integer類型平窘,因此參數(shù)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
###參數(shù)信息###:
2021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper : ###返回值信息###: null執(zhí)行耗時:0MS
@ConditionalOnProperty注解
這里順帶提一下@ConditionalOnProperty注解凳怨,這個注解定義如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
// 數(shù)組,獲取對應property名稱的值是鬼,與name不可同時使用
String[] value() default {};
// 配置屬性名稱的前綴
String prefix() default "";
// 數(shù)組肤舞,配置屬性完整名稱或部分名稱
// 可與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
調用如上幾個方法發(fā)現(xiàn)并沒有打印日志段化,即ApiLogAop未生效。
完整代碼:https://gitee.com/li-cube/share/tree/master/log