AOP之獲取Controller請求(Request)、返回(Response)引數、報錯資訊實現日誌記錄
需求:為系統中所有的提交,修改,刪除等等操作(除查詢以外的所有操作)做日誌記錄,記錄的內容包括:請求引數,返回引數,如果報錯就儲存報錯資訊。日誌要新增一個日誌型別。
方案:最好的選擇就是用註解標記切點,用AOP實現此需求。
一、準備
版本:
- JDK1.8
- Spring-5.0.5.RELEASE
- SpringBoot-2.0.1.RELEASE
- mybatis-3.4.5
- mybatis-spring-boot-starter-1.3.2
- mysql8
日誌表(log)表結構:
二、解析
日誌按功能分類,定義一個列舉類,用於在切點註解上標記日誌型別。儲存日誌資訊的時候儲存到type欄位上。
日誌型別列舉類:LogType.java
標記此操作是什麼型別,比如,增加使用者,刪除使用者、、、、
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午4:20
*/
public enum LogType {
/**
* 應用-增加
*/
APP_ADD(30001),
/**
* 部署-部署應用
*/
DEP_ADD(40001),
/**
* 部署-回滾應用
*/
DEP_ROLLBACK(40002),
private int value;
LogType(int value) {
this.value = value;
}
LogType(String value) {
for (LogType item : values()) {
if (item.name().equals(value)) {
this.value = item.value;
}
}
throw new IllegalArgumentException("Invalid type value");
}
public int value() {
return value;
}
public static LogType valueOf(int value) {
for (LogType item : values()) {
if (item.value() == value) {
return item;
}
}
throw new IllegalArgumentException("Invalid type value");
}
}
日誌切點註解:LogInfo.java
用來標記某個方法需要新增日誌。
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午2:19
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogInfo {
/**
* 描述
* @return
*/
public String description() default "";
/**
* 日誌型別
* @return
*/
public LogType logType();
}
需要被記錄日誌的某個Controller方法:
@ApiOperation("新增應用")
@Auth(allowRoles = {RoleType.ADMIN, RoleType.MASTER})
@PostMapping("/apps")
@LogInfo(logType = LogType.APP_ADD)
public GenericResult<AppDTO> add(@RequestBody @Valid AddAppRequest request) {
return appService.add(request);
}
為其添加了@LogInfo(logType = LogType.APP_ADD)註解,標記為一個切點,表明了方法型別:LogType.APP_ADD 新增應用。
Log實體類:Log.java
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午3:27
*/
@Data
public class Log extends BaseEntity{
private int id;
private int type;
private String request = "{}";
private String response = "{}";
private String error = "{}";
}
LogService(及Impl),LogMapper(及xml)這些的程式碼就不貼了,直接看最關鍵的切面類:LogAop.java
import cn.hutool.core.util.ReflectUtil;
import com.mistra.api.aspect.annotaion.LogInfo;
import com.mistra.core.service.LogService;
import com.mistra.domain.entity.Log;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Description: 採集日誌
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午2:22
*/
@Aspect
@Component
@Slf4j
@Order(Integer.MIN_VALUE)
public class LogAop {
@Autowired
private LogService logService;
private ThreadLocal<Log> threadLocal = new ThreadLocal<>();
@Pointcut("@annotation(com.t4f.eunomia.api.aspect.annotaion.LogInfo)")
public void controllerMethodPointcut() {
}
/**
* 前置advice
* @param point
*/
@Before("controllerMethodPointcut()")
public void before(JoinPoint point) {
Log logEntity = new Log();
//將當前實體儲存到threadLocal
threadLocal.set(logEntity);
//獲取連線點的方法簽名物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊
MethodSignature signature = (MethodSignature) point.getSignature();
//獲取到該方法@LogInfo註解中的日誌型別:列舉類LogType的值,儲存到log實體中
logEntity.setType(signature.getMethod().getAnnotation(LogInfo.class).logType().value());
//RequestContextHolder:持有上下文的Request容器,獲取到當前請求的request
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest httpServletRequest = sra.getRequest();
JSONObject jsonObject = new JSONObject();
//儲存uri到json中
jsonObject.accumulate("uri", httpServletRequest.getRequestURI().toString());
//這一步獲取到的方法有可能是代理方法也有可能是真實方法
Method m = ((MethodSignature) point.getSignature()).getMethod();
//判斷代理物件本身是否是連線點所在的目標物件,不是的話就要通過反射重新獲取真實方法
if (point.getThis().getClass() != point.getTarget().getClass()) {
m = ReflectUtil.getMethod(point.getTarget().getClass(), m.getName(), m.getParameterTypes());
}
//通過真實方法獲取該方法的引數名稱
LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = paramNames.getParameterNames(m);
//獲取連線點方法執行時的入參列表
Object[] args = point.getArgs();
//將引數名稱與入參值一一對應起來
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
params.put(parameterNames[i], args[i]);
}
jsonObject.accumulate("params", params);
//為log實體類的request欄位賦值
logEntity.setRequest(jsonObject.toString());
System.out.println("============================ 》Before : " + logEntity.toString());
}
/**
* 方法成功return之後的advice
* @param point
* @param rtv
*/
@AfterReturning(value = "controllerMethodPointcut()", returning = "rtv")
public void after(JoinPoint point, Object rtv) {
//得到當前執行緒的log物件
Log log = threadLocal.get();
//rtv為controller方法返回資料
JSONObject jsonObject = JSONObject.fromObject(rtv);
//為log實體的response欄位賦值
log.setResponse(jsonObject.toString());
//插入一條log資訊
logService.add(threadLocal.get());
//移除當前log實體
threadLocal.remove();
System.out.println("============================ 》AfterReturning : " + log.toString());
}
/**
* 報錯之後的advice
* @param throwing
*/
@AfterThrowing(value = "controllerMethodPointcut()", throwing = "throwing")
public void error(Throwable throwing) {
Log log = threadLocal.get();
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
//將報錯資訊寫入error欄位
throwing.printStackTrace(new PrintStream(byteArrayOutputStream));
log.setError(byteArrayOutputStream.toString());
} catch (IOException e) {
e.printStackTrace();
}
logService.add(threadLocal.get());
threadLocal.remove();
System.out.println("============================ 》AfterThrowing : " + log.toString());
}
}
三、相關API
JoinPoint
AspectJ中的切入點匹配的執行點稱作連線的(Join Point),在通知方法中可以宣告一個JoinPoint型別的引數。通過JoinPoint可以訪問連線點的細節。下面簡要介紹JponPoint的方法:
1.java.lang.Object[] getArgs():獲取連線點方法執行時的入參列表;
2.Signature getSignature() :獲取連線點的方法簽名物件在該物件中可以獲取到目標方法名,所屬類的Class等資訊;
3.java.lang.Object getTarget() :獲取連線點所在的目標物件;
4.java.lang.Object getThis() :獲取代理物件本身;
RequestContextHolder
持有上下文的Request容器,RequestContextHolder裡面有兩個ThreadLocal儲存當前執行緒下的request。getRequestAttributes()方法,相當於直接獲取ThreadLocal裡面的值,這樣就保證了每一次獲取到的Request是該請求的request。
JSONObject
public Object put (Object key, Object value) 將value對映到key下。如果此JSONObject物件之前存在一個value在這個key下,當前的value會替換掉之前的value。
public JSONObject accumulate (String key, Object value) 追加value到這個key下。這個方法同element()方法類似,特殊的是,如果當前已經存在一個value在這個key下,那麼一個JSONArray將會儲存在這個key下,來儲存所有追加的value。如果已經存在一個JSONArray,那麼當前的value就會新增到這個JSONArray中。
public JSONObject element (String key, Object value) 將鍵值對放到這個JSONObject物件裡面。如果當前value為空(null),那麼如果這個key存在的話,這個key就會移除掉。如果這個key之前有value值,那麼此方法會呼叫accumulate()方法。