寫個日志請求切面吭敢,前后端甩鍋更方便

最近項目進入聯(lián)調(diào)階段,服務(wù)層的接口需要和協(xié)議層進行交互暮芭,協(xié)議層需要將入?yún)json字符串]組裝成服務(wù)層所需的json字符串鹿驼,組裝的過程中很容易出錯。入?yún)⒊鲥e導(dǎo)致接口調(diào)試失敗問題在聯(lián)調(diào)中出現(xiàn)很多次辕宏,因此就想寫一個請求日志切面把入?yún)⑿畔⒋蛴∫幌滦笪瑫r協(xié)議層調(diào)用服務(wù)層接口名稱對不上也出現(xiàn)了幾次,通過請求日志切面就可以知道上層是否有沒有發(fā)起調(diào)用瑞筐,方便前后端甩鍋還能拿出證據(jù)

寫在前面

本篇文章是實戰(zhàn)性的凄鼻,對于切面的原理不會講解,只會簡單介紹一下切面的知識點

切面介紹

面向切面編程是一種編程范式,它作為OOP面向?qū)ο缶幊痰囊环N補充块蚌,用于處理系統(tǒng)中分布于各個模塊的橫切關(guān)注點闰非,比如事務(wù)管理權(quán)限控制峭范、緩存控制财松、日志打印等等。
AOP把軟件的功能模塊分為兩個部分:核心關(guān)注點和橫切關(guān)注點纱控。業(yè)務(wù)處理的主要功能為核心關(guān)注點游岳,而非核心、需要拓展的功能為橫切關(guān)注點其徙。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點,將核心關(guān)注點和橫切關(guān)注點進行分離喷户,使用切面有以下好處:

  • 集中處理某一關(guān)注點/橫切邏輯
  • 可以很方便的添加/刪除關(guān)注點
  • 侵入性少唾那,增強代碼可讀性及可維護性
    因此當(dāng)想打印請求日志時很容易想到切面,對控制層代碼0侵入

切面的使用【基于注解】

  • @Aspect => 聲明該類為一個注解類

切點注解:

  • @Pointcut => 定義一個切點褪尝,可以簡化代碼

通知注解:

  • @Before => 在切點之前執(zhí)行代碼
  • @After => 在切點之后執(zhí)行代碼
  • @AfterReturning => 切點返回內(nèi)容后執(zhí)行代碼闹获,可以對切點的返回值進行封裝
  • @AfterThrowing => 切點拋出異常后執(zhí)行
  • @Around => 環(huán)繞,在切點前后執(zhí)行代碼

動手寫一個請求日志切面

  • 使用@Pointcut定義切點
    @Pointcut("execution(* your_package.controller..*(..))")
    public void requestServer() {
    }
    
    @Pointcut定義了一個切點河哑,因為是請求日志切邊避诽,因此切點定義的是Controller包下的所有類下的方法。定義切點以后在通知注解中直接使用requestServer方法名就可以了
  • 使用@Before再切點前執(zhí)行
    @Before("requestServer()")
    public void doBefore(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) 
    RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
    
        LOGGER.info("===============================Start========================");
        LOGGER.info("IP                 : {}", request.getRemoteAddr());
        LOGGER.info("URL                : {}", request.getRequestURL().toString());
        LOGGER.info("HTTP Method        : {}", request.getMethod());
        LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
    }
    
    在進入Controller方法前璃谨,打印出調(diào)用方IP沙庐、請求URL、HTTP請求類型佳吞、調(diào)用的方法名
  • 使用@Around打印進入控制層的入?yún)?
    @Around("requestServer()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        LOGGER.info("Request Params       : {}", getRequestParams(proceedingJoinPoint));
        LOGGER.info("Result               : {}", result);
        LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);
    
        return result;
    }
    
    打印了入?yún)⒐俺⒔Y(jié)果以及耗時
    • getRquestParams方法
      private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {
           Map<String, Object> requestParams = new HashMap<>();
      
            //參數(shù)名
           String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();
           //參數(shù)值
           Object[] paramValues = proceedingJoinPoint.getArgs();
      
           for (int i = 0; i < paramNames.length; i++) {
               Object value = paramValues[i];
      
               //如果是文件對象
               if (value instanceof MultipartFile) {
                   MultipartFile file = (MultipartFile) value;
                   value = file.getOriginalFilename();  //獲取文件名
               }
      
               requestParams.put(paramNames[i], value);
           }
      
           return requestParams;
       }
      
      通過 @PathVariable以及@RequestParam注解傳遞的參數(shù)無法打印出參數(shù)名,因此需要手動拼接一下參數(shù)名底扳,同時對文件對象進行了特殊處理铸抑,只需獲取文件名即可
  • @After方法調(diào)用后執(zhí)行
    @After("requestServer()")
    public void doAfter(JoinPoint joinPoint) {
        LOGGER.info("===============================End========================");
    }
    

沒有業(yè)務(wù)邏輯只是打印了End

  • 完整切面代碼
    @Component
    @Aspect
    public class RequestLogAspect {
        private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);
    
        @Pointcut("execution(* your_package.controller..*(..))")
        public void requestServer() {
        }
    
        @Before("requestServer()")
        public void doBefore(JoinPoint joinPoint) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) 
    RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
    
            LOGGER.info("===============================Start========================");
            LOGGER.info("IP                 : {}", request.getRemoteAddr());
            LOGGER.info("URL                : {}", request.getRequestURL().toString());
            LOGGER.info("HTTP Method        : {}", request.getMethod());
            LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), 
     joinPoint.getSignature().getName());
        }
    
    
        @Around("requestServer()")
        public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            long start = System.currentTimeMillis();
            Object result = proceedingJoinPoint.proceed();
            LOGGER.info("Request Params     : {}", getRequestParams(proceedingJoinPoint));
            LOGGER.info("Result               : {}", result);
            LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);
    
            return result;
        }
    
        @After("requestServer()")
        public void doAfter(JoinPoint joinPoint) {
            LOGGER.info("===============================End========================");
        }
    
        /**
         * 獲取入?yún)?     * @param proceedingJoinPoint
         *
         * @return
         * */
        private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {
            Map<String, Object> requestParams = new HashMap<>();
    
            //參數(shù)名
            String[] paramNames = 
    ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();
            //參數(shù)值
            Object[] paramValues = proceedingJoinPoint.getArgs();
    
            for (int i = 0; i < paramNames.length; i++) {
                Object value = paramValues[i];
    
                //如果是文件對象
                if (value instanceof MultipartFile) {
                    MultipartFile file = (MultipartFile) value;
                    value = file.getOriginalFilename();  //獲取文件名
                }
    
                requestParams.put(paramNames[i], value);
            }
    
            return requestParams;
        }
    }
    

高并發(fā)下請求日志切面

寫完以后對自己的代碼很滿意,但是想著可能還有完善的地方就和朋友交流了一下衷模。emmmm


image

果然還有繼續(xù)優(yōu)化的地方
每個信息都打印一行鹊汛,在高并發(fā)請求下確實會出現(xiàn)請求之間打印日志串行的問題,因為測試階段請求數(shù)量較少沒有出現(xiàn)串行的情況阱冶,果然生產(chǎn)環(huán)境才是第一發(fā)展力刁憋,能夠遇到更多bug,寫更健壯的代碼
解決日志串行的問題只要將多行打印信息合并為一行就可以了木蹬,因此構(gòu)造一個對象

  • RequestInfo.java

    @Data
    public class RequestInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private Object result;
        private Long timeCost;
    }
    
  • 環(huán)繞通知方法體

    @Around("requestServer()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Object result = proceedingJoinPoint.proceed();
        RequestInfo requestInfo = new RequestInfo();
                requestInfo.setIp(request.getRemoteAddr());
        requestInfo.setUrl(request.getRequestURL().toString());
        requestInfo.setHttpMethod(request.getMethod());
        requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),
                proceedingJoinPoint.getSignature().getName()));
        requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
        requestInfo.setResult(result);
        requestInfo.setTimeCost(System.currentTimeMillis() - start);
        LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));
    
        return result;
    }
    

    將url职祷、http request這些信息組裝成RequestInfo對象,再序列化打印對象
    打印序列化對象結(jié)果而不是直接打印對象是因為序列化有更直觀、更清晰有梆,同時可以借助在線解析工具對結(jié)果進行解析

image

是不是還不錯
在解決高并發(fā)下請求串行問題的同時添加了對異常請求信息的打印是尖,通過使用 @AfterThrowing注解對拋出異常的方法進行處理

  • RequestErrorInfo.java

    @Data
    public class RequestErrorInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private RuntimeException exception;
    }
    
  • 異常通知環(huán)繞體

    @AfterThrowing(pointcut = "requestServer()", throwing = "e")
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        RequestErrorInfo requestErrorInfo = new RequestErrorInfo();
        requestErrorInfo.setIp(request.getRemoteAddr());
        requestErrorInfo.setUrl(request.getRequestURL().toString());
        requestErrorInfo.setHttpMethod(request.getMethod());
        requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName()));
        requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
        requestErrorInfo.setException(e);
        LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));
    }
    

    對于異常,耗時是沒有意義的泥耀,因此不統(tǒng)計耗時饺汹,而是添加了異常的打印

最后放一下完整日志請求切面代碼:

@Component
@Aspect
public class RequestLogAspect {
    private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);

    @Pointcut("execution(* your_package.controller..*(..))")
    public void requestServer() {
    }

    @Around("requestServer()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Object result = proceedingJoinPoint.proceed();
        RequestInfo requestInfo = new RequestInfo();
                requestInfo.setIp(request.getRemoteAddr());
        requestInfo.setUrl(request.getRequestURL().toString());
        requestInfo.setHttpMethod(request.getMethod());
        requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),
                proceedingJoinPoint.getSignature().getName()));
        requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
        requestInfo.setResult(result);
        requestInfo.setTimeCost(System.currentTimeMillis() - start);
        LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));

        return result;
    }


    @AfterThrowing(pointcut = "requestServer()", throwing = "e")
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        RequestErrorInfo requestErrorInfo = new RequestErrorInfo();
        requestErrorInfo.setIp(request.getRemoteAddr());
        requestErrorInfo.setUrl(request.getRequestURL().toString());
        requestErrorInfo.setHttpMethod(request.getMethod());
        requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName()));
        requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
        requestErrorInfo.setException(e);
        LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));
    }

    /**
     * 獲取入?yún)?     * @param proceedingJoinPoint
     *
     * @return
     * */
    private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {
        //參數(shù)名
        String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();
        //參數(shù)值
        Object[] paramValues = proceedingJoinPoint.getArgs();

        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {
        //參數(shù)名
        String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
        //參數(shù)值
        Object[] paramValues = joinPoint.getArgs();

        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {
        Map<String, Object> requestParams = new HashMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            Object value = paramValues[i];

            //如果是文件對象
            if (value instanceof MultipartFile) {
                MultipartFile file = (MultipartFile) value;
                value = file.getOriginalFilename();  //獲取文件名
            }

            requestParams.put(paramNames[i], value);
        }

        return requestParams;
    }

    @Data
    public class RequestInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private Object result;
        private Long timeCost;
    }

    @Data
    public class RequestErrorInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private RuntimeException exception;
    }
}

趕緊給你們的應(yīng)用加上吧【如果沒加的話】,沒有日志的話痰催,總懷疑上層出錯兜辞,但是卻拿不出證據(jù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市夸溶,隨后出現(xiàn)的幾起案子逸吵,更是在濱河造成了極大的恐慌,老刑警劉巖缝裁,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扫皱,死亡現(xiàn)場離奇詭異,居然都是意外死亡捷绑,警方通過查閱死者的電腦和手機韩脑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粹污,“玉大人段多,你說我怎么就攤上這事∽撤裕” “怎么了进苍?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鸭叙。 經(jīng)常有香客問我琅捏,道長,這世上最難降的妖魔是什么递雀? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任柄延,我火速辦了婚禮,結(jié)果婚禮上缀程,老公的妹妹穿的比我還像新娘搜吧。我一直安慰自己,他們只是感情好杨凑,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布滤奈。 她就那樣靜靜地躺著,像睡著了一般撩满。 火紅的嫁衣襯著肌膚如雪蜒程。 梳的紋絲不亂的頭發(fā)上绅你,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音昭躺,去河邊找鬼忌锯。 笑死,一個胖子當(dāng)著我的面吹牛领炫,可吹牛的內(nèi)容都是我干的偶垮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼帝洪,長吁一口氣:“原來是場噩夢啊……” “哼似舵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起葱峡,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砚哗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后砰奕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛛芥,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年脆淹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沽一。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡盖溺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铣缠,到底是詐尸還是另有隱情烘嘱,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布蝗蛙,位于F島的核電站蝇庭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏捡硅。R本人自食惡果不足惜哮内,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壮韭。 院中可真熱鬧北发,春花似錦、人聲如沸喷屋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屯曹。三九已至狱庇,卻和暖如春惊畏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背密任。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工颜启, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人批什。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓农曲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親驻债。 傳聞我的和親對象是個殘疾皇子乳规,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容