本文不涉及過多的Spring aop
基本概念以及基本用法介紹,以實際場景使用爲主。java
咱們一般有這樣一個需求:打印後臺接口請求的具體參數,打印接口請求的最終響應結果,以及記錄哪一個用戶在什麼時間點,訪問了哪些接口,接口響應耗時多長時間等等。這樣作的目的是爲了記錄用戶的訪問行爲,同時便於跟蹤接口調用狀況,以便於出現問題時可以快速定位問題所在。web
最簡單的作法是這樣的:spring
@GetMapping(value = "/info") public BaseResult userInfo() { //1.打印接口入參日誌信息,標記接口訪問時間戳 BaseResult result = mUserService.userInfo(); //2.打印/入庫 接口響應信息,響應時間等 return result; }
這種作法沒毛病,可是稍微比較敏感的同窗就會發覺有如下缺點:數據庫
Spring aop
面向切面執行這段公共代碼。elk
日誌系統等待,同時這些操做要作到可控,有沒有什麼操做能夠直接聲明便可?答案是使用自定義註解
,聲明式的處理訪問日誌。新增日誌註解類,註解做用於方法級別,運行時起做用。app
@Target({ElementType.METHOD}) //註解做用於方法級別 @Retention(RetentionPolicy.RUNTIME) //運行時起做用 public @interface Loggable { /** * 是否輸出日誌 */ boolean loggable() default true; /** * 日誌信息描述,能夠記錄該方法的做用等信息。 */ String descp() default ""; /** * 日誌類型,可能存在多種接口類型都須要記錄日誌,好比dubbo接口,web接口 */ LogTypeEnum type() default LogTypeEnum.WEB; /** * 日誌等級 */ String level() default "INFO"; /** * 日誌輸出範圍,用於標記須要記錄的日誌信息範圍,包含入參、返回值等。 * ALL-入參和出參, BEFORE-入參, AFTER-出參 */ LogScopeEnum scope() default LogScopeEnum.ALL; /** * 入參輸出範圍,值爲入參變量名,多個則逗號分割。不爲空時,入參日誌僅打印include中的變量 */ String include() default ""; /** * 是否存入數據庫 */ boolean db() default true; /** * 是否輸出到控制檯 * * @return */ boolean console() default true; }
日誌類型枚舉類:ide
public enum LogTypeEnum { WEB("-1"), DUBBO("1"), MQ("2"); private final String value; LogTypeEnum(String value) { this.value = value; } public String value() { return this.value; } }
日誌做用範圍枚舉類:this
public enum LogScopeEnum { ALL, BEFORE, AFTER; public boolean contains(LogScopeEnum scope) { if (this == ALL) { return true; } else { return this == scope; } } @Override public String toString() { String str = ""; switch (this) { case ALL: break; case BEFORE: str = "REQUEST"; break; case AFTER: str = "RESPONSE"; break; default: break; } return str; } }
相關說明已在代碼中註釋,這裏再也不說明。編碼
引入依賴:spa
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.8</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.22.0-GA</version> </dependency>
配置文件啓動aop
註解,基於類的代理,而且在 spring
中注入 aop
實現類。代理
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" .....省略部分代碼"> <!-- 掃描controller --> <context:component-scan base-package="**.*controller"/> <context:annotation-config/> <!-- 啓動aop註解基於類的代理(這時須要cglib庫),若是proxy-target-class屬值被設置爲false或者這個屬性被省略,那麼標準的JDK 基於接口的代理將起做用 --> <aop:config proxy-target-class="true"/> <!-- web層日誌記錄AOP實現 --> <bean class="com.easywits.common.aspect.WebLogAspect"/> </beans>
新增 WebLogAspect
類實現
/** * 日誌記錄AOP實現 * create by zhangshaolin on 2018/5/1 */ @Aspect @Component public class WebLogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class); // 開始時間 private long startTime = 0L; // 結束時間 private long endTime = 0L; /** * Controller層切點 */ @Pointcut("execution(* *..controller..*.*(..))") public void controllerAspect() { } /** * 前置通知 用於攔截Controller層記錄用戶的操做 * * @param joinPoint 切點 */ @Before("controllerAspect()") public void doBeforeInServiceLayer(JoinPoint joinPoint) { } /** * 配置controller環繞通知,使用在方法aspect()上註冊的切入點 * * @param point 切點 * @return * @throws Throwable */ @Around("controllerAspect()") public Object doAround(ProceedingJoinPoint point) throws Throwable { // 獲取request RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); //目標方法實體 Method method = ((MethodSignature) point.getSignature()).getMethod(); boolean hasMethodLogAnno = method .isAnnotationPresent(Loggable.class); //沒加註解 直接執行返回結果 if (!hasMethodLogAnno) { return point.proceed(); } //日誌打印外部開關默認關閉 String logSwitch = StringUtils.equals(RedisUtil.get(BaseConstants.CACHE_WEB_LOG_SWITCH), BaseConstants.YES) ? BaseConstants.YES : BaseConstants.NO; //記錄日誌信息 LogMessage logMessage = new LogMessage(); //方法註解實體 Loggable methodLogAnnon = method.getAnnotation(Loggable.class); //處理入參日誌 handleRequstLog(point, methodLogAnnon, request, logMessage, logSwitch); //執行目標方法內容,獲取執行結果 Object result = point.proceed(); //處理接口響應日誌 handleResponseLog(logSwitch, logMessage, methodLogAnnon, result); return result; } /** * 處理入參日誌 * * @param point 切點 * @param methodLogAnnon 日誌註解 * @param logMessage 日誌信息記錄實體 */ private void handleRequstLog(ProceedingJoinPoint point, Loggable methodLogAnnon, HttpServletRequest request, LogMessage logMessage, String logSwitch) throws Exception { String paramsText = ""; //參數列表 String includeParam = methodLogAnnon.include(); Map<String, Object> methodParamNames = getMethodParamNames( point.getTarget().getClass(), point.getSignature().getName(), includeParam); Map<String, Object> params = getArgsMap( point, methodParamNames); if (params != null) { //序列化參數列表 paramsText = JSON.toJSONString(params); } logMessage.setParameter(paramsText); //判斷是否輸出日誌 if (methodLogAnnon.loggable() && methodLogAnnon.scope().contains(LogScopeEnum.BEFORE) && methodLogAnnon.console() && StringUtils.equals(logSwitch, BaseConstants.YES)) { //打印入參日誌 LOGGER.info("【{}】 接口入參成功!, 方法名稱:【{}】, 請求參數:【{}】", methodLogAnnon.descp().toString(), point.getSignature().getName(), paramsText); } startTime = System.currentTimeMillis(); //接口描述 logMessage.setDescription(methodLogAnnon.descp().toString()); //...省略部分構造logMessage信息代碼 } /** * 處理響應日誌 * * @param logSwitch 外部日誌開關,用於外部動態開啓日誌打印 * @param logMessage 日誌記錄信息實體 * @param methodLogAnnon 日誌註解實體 * @param result 接口執行結果 */ private void handleResponseLog(String logSwitch, LogMessage logMessage, Loggable methodLogAnnon, Object result) { endTime = System.currentTimeMillis(); //結束時間 logMessage.setEndTime(DateUtils.getNowDate()); //消耗時間 logMessage.setSpendTime(endTime - startTime); //是否輸出日誌 if (methodLogAnnon.loggable() && methodLogAnnon.scope().contains(LogScopeEnum.AFTER)) { //判斷是否入庫 if (methodLogAnnon.db()) { //...省略入庫代碼 } //判斷是否輸出到控制檯 if (methodLogAnnon.console() && StringUtils.equals(logSwitch, BaseConstants.YES)) { //...省略打印日誌代碼 } } } /** * 獲取方法入參變量名 * * @param cls 觸發的類 * @param methodName 觸發的方法名 * @param include 須要打印的變量名 * @return * @throws Exception */ private Map<String, Object> getMethodParamNames(Class cls, String methodName, String include) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(cls)); CtMethod cm = pool.get(cls.getName()).getDeclaredMethod(methodName); LocalVariableAttribute attr = (LocalVariableAttribute) cm .getMethodInfo().getCodeAttribute() .getAttribute(LocalVariableAttribute.tag); if (attr == null) { throw new Exception("attr is null"); } else { Map<String, Object> paramNames = new HashMap<>(); int paramNamesLen = cm.getParameterTypes().length; int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1; if (StringUtils.isEmpty(include)) { for (int i = 0; i < paramNamesLen; i++) { paramNames.put(attr.variableName(i + pos), i); } } else { // 若include不爲空 for (int i = 0; i < paramNamesLen; i++) { String paramName = attr.variableName(i + pos); if (include.indexOf(paramName) > -1) { paramNames.put(paramName, i); } } } return paramNames; } } /** * 組裝入參Map * * @param point 切點 * @param methodParamNames 參數名稱集合 * @return */ private Map getArgsMap(ProceedingJoinPoint point, Map<String, Object> methodParamNames) { Object[] args = point.getArgs(); if (null == methodParamNames) { return Collections.EMPTY_MAP; } for (Map.Entry<String, Object> entry : methodParamNames.entrySet()) { int index = Integer.valueOf(String.valueOf(entry.getValue())); if (args != null && args.length > 0) { Object arg = (null == args[index] ? "" : args[index]); methodParamNames.put(entry.getKey(), arg); } } return methodParamNames; } }
接口改造以下:
@Loggable(descp = "用戶我的資料", include = "") @GetMapping(value = "/info") public BaseResult userInfo() { return mUserService.userInfo(); }
能夠看到,只添加了註解@Loggable
,全部的web
層接口只須要添加@Loggable
註解就能實現日誌處理了,方便簡潔!最終效果以下:
訪問入參,響應日誌信息:
用戶行爲日誌入庫部分信息:
Spring aop
能夠在方法執行前,執行時,執行後切入執行一段公共代碼,很是適合用於公共邏輯處理。更多原創文章會第一時間推送公衆號【張少林同窗】,歡迎關注!