1. 程式人生 > >SpringBoot實戰 之 接口日誌篇

SpringBoot實戰 之 接口日誌篇

empty 工具 argument contains art utm ioe strong exception

在本篇文章中不會詳細介紹日誌如何配置、如果切換另外一種日誌工具之類的內容,只用於記錄作者本人在工作過程中對日誌的幾種處理方式。

1. Debug 日誌管理

在開發的過程中,總會遇到各種莫名其妙的問題,而這些問題的定位一般會使用到兩種方式,第一種是通過手工 Debug 代碼,第二種則是直接查看日誌輸出。Debug 代碼這種方式只能在 IDE 下使用,一旦程序移交部署,就只能通過日誌來跟蹤定位了。

在測試環境下,我們無法使用 Debug 代碼來定位問題,所以這時候需要記錄所有請求的參數及對應的響應報文。而在 數據交互篇 中,我們將請求及響應的格式都定義成了Json,而且傳輸的數據還是存放在請求體裏面。而請求體對應在 HttpServletRequest 裏面又只是一個輸入流,這樣的話,就無法在過濾器或者攔截器裏面去做日誌記錄了,而必須要等待輸入流轉換成請求模型後(響應對象轉換成輸出流前)做數據日誌輸出。

有目標那就好辦了,只需要找到轉換發生的地方就可以植入我們的日誌了。通過源碼的閱讀,終於在 AbstractMessageConverterMethodArgumentResolver 個類中發現了我們的期望的那個地方,對於請求模型的轉換,實現代碼如下:

@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
        Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

    MediaType contentType;
    boolean noContentType = false;
    try {
        contentType = inputMessage.getHeaders().getContentType();
    }
    catch (InvalidMediaTypeException ex) {
        throw new HttpMediaTypeNotSupportedException(ex.getMessage());
    }
    if (contentType == null) {
        noContentType = true;
        contentType = MediaType.APPLICATION_OCTET_STREAM;
    }

    Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
    Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
    if (targetClass == null) {
        ResolvableType resolvableType = (parameter != null ?
                ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType));
        targetClass = (Class<T>) resolvableType.resolve();
    }

    HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
    Object body = NO_VALUE;

    try {
        inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);

        for (HttpMessageConverter<?> converter : this.messageConverters) {
            Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
            if (converter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
                if (genericConverter.canRead(targetType, contextClass, contentType)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
                    }
                    if (inputMessage.getBody() != null) {
                        inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                        body = genericConverter.read(targetType, contextClass, inputMessage);
                        body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
                    }
                    break;
                }
            }
            else if (targetClass != null) {
                if (converter.canRead(targetClass, contentType)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
                    }
                    if (inputMessage.getBody() != null) {
                        inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                        body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
                        body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
                    }
                    break;
                }
            }
        }
    }
    catch (IOException ex) {
        throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
    }

    if (body == NO_VALUE) {
        if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                (noContentType && inputMessage.getBody() == null)) {
            return null;
        }
        throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
    }

    return body;
}

  

上面的代碼中有一處非常重要的地方,那就在在數據轉換前後都存在 Advice 相關的方法調用,顯然,只需要在 Advice 裏面完成日誌記錄就可以了,下面開始實現自定義 Advice。

首先,請求體日誌切面 LogRequestBodyAdvice 實現如下:

@ControllerAdvice
public class LogRequestBodyAdvice implements RequestBodyAdvice {

    private Logger logger = LoggerFactory.getLogger(LogRequestBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage,
                                  MethodParameter parameter, Type targetType,
                                  Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
                                           MethodParameter parameter, Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
                                MethodParameter parameter, Type targetType,
                                Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = parameter.getMethod();
        String classMappingUri = getClassMappingUri(method.getDeclaringClass());
        String methodMappingUri = getMethodMappingUri(method);
        if (!methodMappingUri.startsWith("/")) {
            methodMappingUri = "/" + methodMappingUri;
        }
        logger.debug("uri={} | requestBody={}", classMappingUri + methodMappingUri, JSON.toJSONString(body));
        return body;
    }

    private String getMethodMappingUri(Method method) {
        RequestMapping methodDeclaredAnnotation = method.getDeclaredAnnotation(RequestMapping.class);
        return methodDeclaredAnnotation == null ? "" : getMaxLength(methodDeclaredAnnotation.value());
    }

    private String getClassMappingUri(Class<?> declaringClass) {
        RequestMapping classDeclaredAnnotation = declaringClass.getDeclaredAnnotation(RequestMapping.class);
        return classDeclaredAnnotation == null ? "" : getMaxLength(classDeclaredAnnotation.value());
    }

    private String getMaxLength(String[] strings) {
        String methodMappingUri = "";
        for (String string : strings) {
            if (string.length() > methodMappingUri.length()) {
                methodMappingUri = string;
            }
        }
        return methodMappingUri;
    }
}

  

得到日誌記錄如下:

2017-05-02 22:48:15.435 DEBUG 888 --- [nio-8080-exec-1] c.q.funda.advice.LogRequestBodyAdvice    : uri=/sys/user/login | 
requestBody={"password":"123","username":"123"}

對應的,響應體日誌切面 LogResponseBodyAdvice 實現如下:

@ControllerAdvice
public class LogResponseBodyAdvice implements ResponseBodyAdvice {

    private Logger logger = LoggerFactory.getLogger(LogResponseBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        logger.debug("uri={} | responseBody={}", request.getURI().getPath(), JSON.toJSONString(body));
        return body;
    }
}

  

得到日誌記錄如下:

2017-05-02 22:48:15.520 DEBUG 888 --- [nio-8080-exec-1] c.q.funda.advice.LogResponseBodyAdvice   : uri=/sys/user/login | 
responseBody={"code":10101,"msg":"手機號格式不合法"}

  

2. 異常日誌管理

Debug 日誌只適用於開發及測試階段,一般應用部署生產,鑒於日誌裏面的敏感信息過多,往往只會在程序出現異常時輸出明細的日誌信息,在 ExceptionHandler 標註的方法裏面輸入異常日誌無疑是最好的,但擺在面前的一個問題是,如何將 @RequestBody 綁定的 Model 傳遞給異常處理方法?我想到的是通過 ThreadLocal 這個線程本地變量來存儲每一次請求的 Model,這樣就可以貫穿整個請求處理流程,下面使用 ThreadLocal 來協助完成異常日誌的記錄。

在綁定時,將綁定 Model 有存放到 ThreadLocal:

@RestController
@RequestMapping("/sys/user")
public class UserController {

    public static final ThreadLocal<Object> MODEL_HOLDER = new ThreadLocal<>();

    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {
        MODEL_HOLDER.set(webDataBinder.getTarget());
    }

}

  

異常處理時,從 ThreadLocal 中取出變量,並做相應的日誌輸出:

@ControllerAdvice
@ResponseBody
public class ExceptionHandlerAdvice {

    private Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e, HttpServletRequest request) {
        logger.error("uri={} | requestBody={}", request.getRequestURI(),
                JSON.toJSONString(UserController.MODEL_HOLDER.get()));
        return new Result(ResultCode.WEAK_NET_WORK);
    }

}

  

當異常產生時,輸出日誌如下:

2017-05-03 21:46:07.177 ERROR 633 --- [nio-8080-exec-1] c.q.funda.advice.ExceptionHandlerAdvice  : uri=/sys/user/login | 
requestBody={"password":"123","username":"13632672222"}

  

註意:當 Mapping 方法中帶有多個參數時,需要將 @RequestBody 綁定的變量當作方法的最後一個參數,否則 ThreadLocal 中的值將會被其它值所替換。如果需要輸出 Mapping 方法中所有參數,可以在 ThreadLocal 裏面存放一個 Map 集合。

項目的 github 地址:https://github.com/qchery/funda


原文地址:http://blog.csdn.net/chinrui/article/details/71056847

SpringBoot實戰 之 接口日誌篇