基於Spring框架的記錄操做日誌

HTTP 接口調用操做日誌

對於一些HTTP API接口,有時候須要記錄日誌,瞭解什麼用戶在什麼時間調用了什麼接口,調用的參數是什麼,返回的結果是什麼。可是,又不能侵入業務代碼邏輯。html

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.htmljava

在什麼地方記錄?

首先想到的時候使用Filter,可是發現要獲取response中的body內容,太麻煩,而ResponseBodyAdvice這個接口偏偏能夠知足咱們的需求。web

/**
 * 容許茲定於response內容,只能在@ResponseBody或者@responseEntity註解的controller方法
 * 有用。改方法會在把body內容經過HttpMessaheConverter寫入前調用
 * 
 * 實現該接口並經過 @ControllerAdvice來註冊
 *
 * @since 4.1
 */
public interface ResponseBodyAdvice<T> {

   /**
    * 當返回true的時候會調用beforeBodyWrite方法,不然不會
    */
   boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

   /**
    * {@code HttpMessageConverter}選擇後和寫入前調用
    * @param body the body to be written
    * @param returnType the return type of the controller method
    * @param selectedContentType the content type selected through content negotiation
    * @param selectedConverterType the converter type selected to write to the response
    * @param request the current request
    * @param response the current response
    * @return the body that was passed in or a modified, possibly new instance
    */
   T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
         Class<? extends HttpMessageConverter<?>> selectedConverterType,
         ServerHttpRequest request, ServerHttpResponse response);

}

怎麼進行有選擇性的記錄

@ResponseBody註解的方法都會調用這個Advice,但並非咱們都須要記錄,而且怎麼知道body內容是表示成功或者失敗了呢spring

咱們能夠經過自定義註解來解決api

@Documented
@Target({METHOD})
@Retention(RUNTIME)
public @interface OpLog {

    /**
     * 是否只記錄成功的操做,若是爲false,result和status和successFlag參數就沒用
     *
     * @return
     */
    boolean onlySuccess() default true;

    /**
     * 返回的數據是什麼樣的類
     *
     * @return
     */
    Class result() default Result.class;

    /**
     * 存儲成功的標誌
     *
     * @return
     */
    String status() default "state";

    /**
     * 成功標誌對應的值
     *
     * @return
     */
    String successFlag() default "success";
}

該註解有4個配置,其中爲了知道接口調用時成功仍是失敗,就必須知道3個事情mvc

  1. body是什麼類
  2. 該類用什麼來字段來存儲調用狀況
  3. 該字段的值爲何的時候,就是表示成功

這樣,在ResponseBodyAdvice的supports方法,經過判斷改方法是否有@Oplog來判斷是否須要進行日誌寫入,而後beforeBodyWrite方法中,生成日誌信息,並記錄日誌,下面是我本身定義類實現ResponseBodyAdvice接口。框架

@ControllerAdvice
public class OpLogSaveResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private LogWriterService logWriterService;

    @Autowired
    ExecutorService logWriterPool = Executors.newFixedThreadPool(2);

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        OpLog opLog = getOpLog(returnType);
        return opLog != null;
    }


    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        OpLog opLog = getOpLog(returnType);
        if (opLog != null && request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
            HttpServletRequest httpServletRequest = servletServerHttpRequest.getServletRequest();
            if (body != null && body.getClass() == opLog.result()) {
                logWriterPool.execute(new OpLogWriter(body, opLog, httpServletRequest));
            }
        }
        return body;
    }


    private OpLog getOpLog(MethodParameter returnType) {
        if (returnType != null && returnType.getMethod() != null) {
            return returnType.getMethod().getAnnotation(OpLog.class);
        }
        return null;
    }

    //根據 OpLog中定義的屬性,判斷是否成功。
    //從 httpServletRequest 中獲取相關的信息,生成日誌信息
    private OpLogMsg buildMsg(Object body, OpLog opLog, HttpServletRequest httpServletRequest) {
        //從httpServletRequest中獲取 username,parameter,url
        String username = BaseUserContext.getCurrentUser(httpServletRequest).getUsername();
        String parameter = JSONObject.toJSONString(httpServletRequest.getParameterMap());
        String url = httpServletRequest.getRequestURI();
        //設置
        OpLogMsg opLogMsg = new OpLogMsg();
        opLogMsg.setUsername(username);
        opLogMsg.setParamter(parameter);
        opLogMsg.setUrl(url);
        opLogMsg.setCreateDate(new Date());

        //獲取是否成功,根據OpLog中的相關信息判斷
        Class clazz = body.getClass();
        Field[] fields = clazz.getDeclaredFields();
        Field.setAccessible(fields, true);
        //用於判斷是否成功的屬性
        String flagFieldName = opLog.status();
        //屬性的值爲successFlag則表示成功
        String successFlag = opLog.successFlag();
        for (int i = 0; i < fields.length; i++) {
            String name = fields[i].getName();
            if (StringUtils.equalsIgnoreCase(name, flagFieldName)) {
                try {
                    if (StringUtils.equals(fields[i].get(body).toString(), successFlag)) {
                        opLogMsg.setSuccess(OpLogMsg.SUCCESS);
                    } else {
                        opLogMsg.setSuccess(OpLogMsg.FAIL);
                    }
                } catch (IllegalAccessException e) {
                    opLogMsg.setSuccess(OpLogMsg.UNKNOWN);
                }
                break;
            }
        }
        return opLogMsg;
    }

    class OpLogWriter implements Runnable {
        Object body;
        OpLog opLog;
        HttpServletRequest httpServletRequest;

        OpLogWriter(Object body, OpLog opLog, HttpServletRequest httpServletRequest) {
            this.body = body;
            this.opLog = opLog;
            this.httpServletRequest = httpServletRequest;
        }

        @Override
        public void run() {
            OpLogMsg opLogMsg = buildMsg(body, opLog, httpServletRequest);
            if(opLog.onlySuccess() && !OpLogMsg.SUCCESS.equals(opLogMsg.getSuccess())){
                return;
            }
            logWriterService.writeOpLog(opLogMsg);
        }
    }
}

注:異步

  1. 爲了避免阻塞原有的流程,所以使用一個線程池異步寫日誌
  2. buildMsg的方法能夠根據業務須要生成記錄信息,這裏是記錄了調用的用戶名(使用的Spring Security框架),調用時間,調用的url,url參數。OpLogMsg做爲封裝的Entity類

使用方法

通常狀況下,咱們HTTP API接口放回的數據格式都是一致的,所以能夠做爲@OpLog的默認配置,但也支持自定義配置,全部一般狀況下只用在須要記錄日誌的地方增長@OpLog註解就能夠了ide

相關文章
相關標籤/搜索