企業實戰之切面程式設計《統一列印日誌》
阿新 • • 發佈:2019-02-16
前言
面向切面程式設計是spring裡一種很不錯的程式設計思想,簡單來講就是可以將一段功能程式碼在程式執行時,動態地將該段程式碼切入到目標方法前或後插入去執行,這種方式可以實現程式碼的可插拔性,之前我們在攔截器實戰篇中說過的攔截器其實就是切面程式設計的一種實現。
本篇文章我們將帶你使用spring的@Aspect註解來實現controller層方法的請求引數、響應體的日誌列印功能,這在企業開發中也是很有必要的哦,可以減少我們浪費在日誌列印上的一些時間,也可以統一日誌的列印格式,以後在使用ELK這種日誌搜尋服務時,你也將會得到很不錯的使用體驗。
思路
我們使用@Aspect標識一個切面類,使用@Around註解標識在你列印日誌的方法裡,主要捕捉@RestController帶這個註解的類方法,也就是我們的reset controller下的方法,這樣就可以在joinPoint.proceed() 程式的處理前後列印上日誌資訊,我們目前列印的主要資訊有,請求者、請求的類.方法名字、請求的引數(引數敏感資訊需要用 “**
好了,說了這麼多接下來我們看下原始碼吧。
package com.zhuma.demo.aspect;
import java.lang.reflect.Method;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.zhuma.demo.constant.HeaderConstants;
import com.zhuma.demo.handler.GlobalExceptionHandler;
import com.zhuma.demo.helper.LoginHelper;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.util.IpUtil;
/**
* @desc 請求引數、響應體統一日誌列印
*
* @author zhumaer
* @since 10/10/2017 9:54 AM
*/
@Aspect
@Component
public class RestControllerAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 環繞通知
* @param joinPoint 連線點
* @return 切入點返回值
* @throws Throwable 異常資訊
*/
@Around("@within(org.springframework.web.bind.annotation.RestController) || @annotation(org.springframework.web.bind.annotation.RestController)")
public Object apiLog(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
boolean logFlag = this.needToLog(method);
if (!logFlag) {
return joinPoint.proceed();
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
User loginUser = LoginHelper.getLoginUserFromRequest(request);
String ip = IpUtil.getRealIp(request);
String methodName = this.getMethodName(joinPoint);
String params = this.getParamsJson(joinPoint);
String requester = loginUser == null ? "unknown" : String.valueOf(loginUser.getId());
String callSource = request.getHeader(HeaderConstants.CALL_SOURCE);
String appVersion = request.getHeader(HeaderConstants.APP_VERSION);
String apiVersion = request.getHeader(HeaderConstants.API_VERSION);
String userAgent = request.getHeader("user-agent");
logger.info("Started request requester [{}] method [{}] params [{}] IP [{}] callSource [{}] appVersion [{}] apiVersion [{}] userAgent [{}]", requester, methodName, params, ip, callSource, appVersion, apiVersion, userAgent);
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
logger.info("Ended request requester [{}] method [{}] params[{}] response is [{}] cost [{}] millis ",
requester, methodName, params, this.deleteSensitiveContent(result), System.currentTimeMillis() - start);
return result;
}
private String getMethodName(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().toShortString();
String SHORT_METHOD_NAME_SUFFIX = "(..)";
if (methodName.endsWith(SHORT_METHOD_NAME_SUFFIX)) {
methodName = methodName.substring(0, methodName.length() - SHORT_METHOD_NAME_SUFFIX.length());
}
return methodName;
}
private String getParamsJson(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
for (Object arg : args) {
//移除敏感內容
String paramStr;
if (arg instanceof HttpServletResponse) {
paramStr = HttpServletResponse.class.getSimpleName();
} else if (arg instanceof HttpServletRequest) {
paramStr = HttpServletRequest.class.getSimpleName();
} else if (arg instanceof MultipartFile) {
long size = ((MultipartFile) arg).getSize();
paramStr = MultipartFile.class.getSimpleName() + " size:" + size;
} else {
paramStr = this.deleteSensitiveContent(arg);
}
sb.append(paramStr).append(",");
}
return sb.deleteCharAt(sb.length() - 1).toString();
}
/**
* 判斷是否需要記錄日誌
*/
private boolean needToLog(Method method) {
return method.getAnnotation(GetMapping.class) == null
&& !method.getDeclaringClass().equals(GlobalExceptionHandler.class);
}
/**
* 刪除引數中的敏感內容
* @param obj 引數物件
* @return 去除敏感內容後的引數物件
*/
private String deleteSensitiveContent(Object obj) {
JSONObject jsonObject = new JSONObject();
if (obj == null || obj instanceof Exception) {
return jsonObject.toJSONString();
}
try {
String param = JSON.toJSONString(obj);
jsonObject = JSONObject.parseObject(param);
List<String> sensitiveFieldList = this.getSensitiveFieldList();
for (String sensitiveField : sensitiveFieldList) {
if (jsonObject.containsKey(sensitiveField)) {
jsonObject.put(sensitiveField, "******");
}
}
} catch (ClassCastException e) {
return String.valueOf(obj);
}
return jsonObject.toJSONString();
}
/**
* 敏感欄位列表(當然這裡你可以更改為可配置的)
*/
private List<String> getSensitiveFieldList() {
List<String> sensitiveFieldList = Lists.newArrayList();
sensitiveFieldList.add("pwd");
sensitiveFieldList.add("password");
return sensitiveFieldList;
}
}
說明
下面我們解釋幾處你可能不太清楚的程式碼邏輯。
- 下面的邏輯是對@GetMapping註解註釋的方法和GlobalExceptionHandler這個類下的所有方法都不需要列印日誌,也就是說我們不需要列印GET請求因為這通常不會對資料做出更改操作(當然這需要你的系統以Restful的風格去命名方法),還有就是全域性異常處理類也會不需要列印日誌,這個類我們後續會說。
private boolean needToLog(Method method) {
return method.getAnnotation(GetMapping.class) == null
&& !method.getDeclaringClass().equals(GlobalExceptionHandler.class);
}
- 如果你很認真的讀懂了這段程式碼,你會發現上面的joinPoint.proceed()程式碼如果出現了異常,響應結果的日誌就打不出來了,其實這是有原因的,在出現異常的情況下,日誌交由GlobalExceptionHandler全域性異常處理類去列印就好了。
- 如果你很疑惑下面獲取使用者的這句程式碼是如何寫的,你可以讀下這兩篇文章《登入校驗》、《統一引數校驗》
User loginUser = LoginHelper.getLoginUserFromRequest(request);
String callSource = request.getHeader(HeaderConstants.CALL_SOURCE);
成果展示
趕快讓我們看下我們的成果吧,我們以建立使用者為例:
package com.zhuma.demo.web.user;
import java.util.Date;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.zhuma.demo.model.po.User;
/**
* @desc 使用者管理控制器
*
* @author zhumaer
* @since 6/20/2017 16:37 PM
*/
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User addUser(@Valid @RequestBody User user) {
user.setId(10000L);
user.setCreateTime(new Date());
return user;
}
}
PostMan呼叫截圖:
我們看下下面就是我們列印的日誌資訊啦:
2017-10-23 22:17:17,560 [http-nio-8723-exec-1] INFO (RestControllerAspect.java:71)- Started request requester [unknown] method [UserController.addUser] params [{"img":"http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg","nickname":"小竹馬","pwd":"******"}] IP [192.168.1.5] callSource [WEB] appVersion [1.0] apiVersion [1.0] userAgent [Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36]
2017-10-23 22:17:18,576 [http-nio-8723-exec-1] INFO (RestControllerAspect.java:74)- Ended request requester [unknown] method [UserController.addUser] params[{"img":"http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg","nickname":"小竹馬","pwd":"******"}] response is [{"img":"http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg","createTime":1508768237565,"nickname":"小竹馬","id":10000,"pwd":"******"}] cost [1015] millis
最後
好啦,列印日誌的文章我們就介紹到這裡,如果你對文章有疑問或者有更好的建議那請掃一下下面的二維碼吧,在公眾號裡我們會同步的更新關於企業開發實戰性的一些文章,後面我們主要講解下在企業開發中如何更好的《校驗引數》和《自定義異常》。
歡迎關注我們的公眾號或加群,等你哦!