【Java】使用AOP進行異常處理與日誌記錄

目錄

背景

RPC接口

原因

實現

自定義註解

切面

Web接口

原因

實現

自定義註解

切面

建議

參考


背景

最近在讀《代碼精進之路 從碼農到工匠》,書中有講到異常規範和日誌規範,大致是下面這幾點:

  • 自定義BusinessException和SystemException,用於區分業務異常和系統異常,業務異常應該是warn級別,系統異常纔可以記error
  • 異常日誌監控:error級別即時報警,warn級別單位時間內數量超過預設閥值也要報警。由於監控報警所以日誌等級要按照規範使用
  • 異常要使用AOP統一處理,而不應該使try-catch代碼散落在項目中的各處。使得代碼的可讀性和簡潔性降低

感覺說得很有道理,就規範進行了實踐,對目前項目中的異常和日誌進行了V1版本的優化,路漫漫其修遠兮,不斷的迭代吧。主要進行了兩點,一是包裝外部RPC接口優化,二是controller層web接口優化。優化點是將方法內進行try-catch處理及參數返回值打印的地方進行了統一處理。

RPC接口

原因

對於被調用的外部接口,最好不要直接調用,要在自己的項目內封裝一層,主要做三件事:異常處理、參數及返回記錄、特定邏輯處理。剛開始開發的時候不懂,都是直接調用,多個地方都調用後就會發現代碼重複度很高,後面有改動的話,就要全局搜索改很多個地方,還是封裝一層更爲合理,磨刀不誤砍柴工。

實現

自定義註解

用於標記rpc接口信息

import java.lang.annotation.*;

/**
 * 調用外部接口檢查
 *
 * @yx8102 2020/5/15
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RpcCheck {

    // 服務描述
    String serviceNameCN() default "外部服務";
    
    // 服務名稱
    String serviceNameEN() default "EXTERNAL_SERVICE";

    // 方法描述
    String methodNameCN() default "外部方法";
    
    // 方法名稱
    String methodNameEN() default "EXTERNAL_METHOD";

    // 是否打印入參
    boolean printParam() default true;
}

切面

進行日誌、異常、耗時統一處理

import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import lombok.SneakyThrows;
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.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 外部接口調用check
 *
 * @yx8102 2020/5/15
 */
@Slf4j
@Aspect
@Component
public class RpcCheckAspect {

    @SneakyThrows
    @Around("@annotation(rpcCheck)")
    public Object around(ProceedingJoinPoint point, RpcCheck rpcCheck) {
        Object result;
        try {

            // 拼裝接口入參, 入參名稱-入參值map
            Map<String, Object> paramMap = new HashMap<>();
            Object[] paramValueArr = point.getArgs();
            String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();
            for (int i = 0; i < paramValueArr.length; i++) {
                Object paramValue = paramValueArr[i];

                if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {
                    continue;
                }

                if (paramValue instanceof MultipartFile) {
                    paramValue = ((MultipartFile) paramValue).getSize();
                }
                paramMap.put(paramNameArr[i], paramValue);
            }

            log.info("調用[服務]:{} {} {} {},[參數]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), rpcCheck.printParam() ? JSON.toJSONString(paramMap) : point.getArgs().length);
            Stopwatch stopwatch = Stopwatch.createStarted();
            result = point.proceed();
            log.info("調用[服務]:{} {} {} {},[返回]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), JSON.toJSONString(result));
            log.info("調用[服務]:{} {} {} {},[耗時]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), stopwatch.elapsed(TimeUnit.MILLISECONDS));

        } catch (NullPointerException e) {
            log.error("調用[服務]:{} {} {} {} 異常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);
            throw new SystemException(String.format("[服務]: %s,調用發生異常(無可用服務): %s", rpcCheck.methodNameEN(), e.getMessage()));
        } catch (Exception e) {
            log.error("調用[服務]:{} {} {} {} 異常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);
            throw new SystemException(String.format("[服務]: %s,調用發生異常: %s", rpcCheck.methodNameEN(), e.getMessage()));
        }

        if (Objects.isNull(result)) {
            throw new SystemException(String.format("[服務]: %s, 返回爲null", rpcCheck.methodNameEN()));
        }

        return result;
    }

}

Web接口

原因

因爲請求的出口是controller層,所以在這一層增加切面進行統一處理。

實現

自定義註解

用於標記需要個性化處理的接口,比如文件上傳等不需要打印入參的接口

import java.lang.annotation.*;

/**
 * controller接口check
 *
 * @yx8102 2020/5/19
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebCheck {

    /**
     * 無異常時,是否打印入參, 默認否
     * @return
     */
    boolean printParam() default false;

    /**
     * 是否打印返回值, 默認否
     * @return
     */
    boolean printReturn() default false;

    /**
     * 是否打印耗時, 默認否
     * @return
     */
    boolean printTime() default false;
}

切面

進行異常處理、日誌打印

import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
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.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * controller 接口日誌、異常統一處理
 *
 * @xyang010 2020/5/18
 */
@Aspect
@Slf4j
@Component
public class WebCheckAspect {

    @Pointcut("execution(public * com.yx.controller..*.*(..)))")
    public void controller() {
    }

    @SneakyThrows
    @Around("controller()")
    public Object around(ProceedingJoinPoint point) {
        // 接口調用計時
        Stopwatch stopwatch = Stopwatch.createStarted();
        // 接口正常返回
        Object result = null;
        // 接口異常返回
        MyResult<Void> errorResult = new MyResult<>(ResultCodes.UNKNOW_ERROR, ResultCodes.UNKNOW_ERROR.getText());

        // controller請求路徑
        String targetClassPath = Objects.nonNull(point.getTarget().getClass().getAnnotation(RequestMapping.class)) ? point.getTarget().getClass().getAnnotation(RequestMapping.class).value()[0] : "";
        // controller功能描述
        String targetClassDesc = Objects.nonNull(point.getTarget().getClass().getAnnotation(Api.class)) ? point.getTarget().getClass().getAnnotation(Api.class).value() : "";
        // 接口
        Method targetMethod  = ((MethodSignature) point.getSignature()).getMethod();
        // 接口功能描述
        String targetMethodDesc = Objects.nonNull(targetMethod.getAnnotation(ApiOperation.class)) ? targetMethod.getAnnotation(ApiOperation.class).value() : "";
        // 接口請求路徑
        String methodPost = Objects.nonNull(targetMethod.getAnnotation(PostMapping.class)) ? targetMethod.getAnnotation(PostMapping.class).value()[0] : "";
        String methodGet = Objects.nonNull(targetMethod.getAnnotation(GetMapping.class)) ? targetMethod.getAnnotation(GetMapping.class).value()[0] : "";
        String methodRequest = Objects.nonNull(targetMethod.getAnnotation(RequestMapping.class)) ? targetMethod.getAnnotation(RequestMapping.class).value()[0] : "";
        String methodPath = methodPost + methodGet + methodRequest;
        // 接口打印信息配置
        WebCheck webCheck = targetMethod.getAnnotation(WebCheck.class);
        // 無異常時,是否打印入參
        boolean printParam = Objects.nonNull(webCheck) && webCheck.printParam();
        // 是否打印返回值
        boolean printReturn = Objects.nonNull(webCheck) && webCheck.printReturn();
        // 是否打印接口耗時
        boolean printTime = Objects.nonNull(webCheck) && webCheck.printTime();

        // 拼裝接口入參, 入參名稱-入參值map
        Map<String, Object> paramMap = new HashMap<>();
        Object[] paramValueArr = point.getArgs();
        String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();
        for (int i = 0; i < paramValueArr.length; i++) {
            Object paramValue = paramValueArr[i];

            if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {
                continue;
            }

            if (paramValue instanceof MultipartFile) {
                paramValue = ((MultipartFile) paramValue).getSize();
            }
            paramMap.put(paramNameArr[i], paramValue);
        }

        try {
            log.info("[接口] {} {} {} {}, " + (printParam ? "[參數] {}" : ">>>>>>>> start"), targetClassDesc, targetMethodDesc, targetClassPath, methodPath, (printParam ? JSON.toJSONString(paramMap) : ""));
            result = point.proceed();

        } catch (BusinessException e) {
            log.warn("[接口][業務異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
            errorResult.initializeFail(ResultCodes.BUSINESS_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
        } catch (SystemException e) {
            log.error("[接口][系統異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
            errorResult.initializeFail(ResultCodes.INNER_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
        } catch (Exception e) {
            log.error("[接口][未知異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
            errorResult.initializeFail(ResultCodes.UNKNOW_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
        }

        if (printReturn) {
            log.info("[接口] {} {} {} {}, [返回] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(Objects.nonNull(result) ? result : errorResult));
        }

        if (printTime) {
            log.info("[接口] {} {} {} {}, [耗時] {} {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, stopwatch.elapsed(TimeUnit.MILLISECONDS), "ms");
        }

        if (!printReturn && !printReturn) {
            log.info("[接口] {} {} {} {}, >>>>>>>> end", targetClassDesc, targetMethodDesc, targetClassPath, methodPath);
        }

        return Objects.nonNull(result) ? result : errorResult;
    }

}

建議

最好再使用@RestControllerAdvice + @ExceptionHandler 進行controller異常兜底,因爲框架層的異常返回,上面aop是無法攔截的。

示例代碼

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;

/**
 * 全局異常處理類
 *
 * @yx8102 2019/10/28
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public MyResult<Void> exceptionHandler(Exception e) {
        MyResult<Void> result = new MyResult<>();
        result.initializeFail(ResultCodes.INNER_ERROR.getCode(), e.getMessage());
        log.error("系統異常. result {}", JSON.toJSONString(result), e);
        return result;
    }
}

參考

Spring aop獲取目標對象,方法,接口上的註解

spring aop獲取目標對象的方法對象(包括方法上的註解)

Spring AOP 之二:Pointcut註解表達式

AOP Aspect 統一日誌、異常處理、數據格式

Spring AOP獲取攔截方法的參數名稱跟參數值

使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler註解實現全局處理Controller層的異常

異常 try – finally 注意的地方

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章