SpringBoot 中使用 AOP 打印接口日志

前言

AOP 是 Aspect Oriented Program (面向切面)的編程的縮寫螃概。他是和面向?qū)ο缶幊滔鄬?duì)的一個(gè)概念砍聊。在面向?qū)ο蟮木幊讨姓擅ィ覀儍A向于采用封裝软免、繼承宫纬、多態(tài)等概念,將一個(gè)個(gè)的功能在對(duì)象中來實(shí)現(xiàn)膏萧。但是漓骚,我們?cè)趯?shí)際情況中也發(fā)現(xiàn),會(huì)有另外一種需求就是一類功能在很多對(duì)象的很多方法中都有需要榛泛。例如有一些對(duì)數(shù)據(jù)庫(kù)訪問的方法有事務(wù)管理的需求蝌蹂,有很多方法中要求打印日志。按照面向?qū)ο蟮姆绞讲芟牵敲催@些相同的功能要在很多地方來實(shí)現(xiàn)或者在很多地方來調(diào)用叉信。這就非常繁瑣并且和這些和業(yè)務(wù)不相關(guān)的需求耦合太緊密了。所以后來就出現(xiàn)了面向切面的編程來解決這一類問題艘希,并對(duì)面向?qū)ο蟮木幊套隽撕芎玫难a(bǔ)充

概念

要很好的理解面向切面的編程,先要理解 AOP 的一些概念硅急。在 Java 中 AspectJ 比較完整的實(shí)現(xiàn)了 AOP 的功能覆享,但是使用起來也比較復(fù),所以這里主要是討論 Spring 的 AOP 营袜。Spring AOP 采用簡(jiǎn)單夠用的原則撒顿,實(shí)現(xiàn)了 AOP 的核心功能。下面先說說 AOP 中的具體概念

  1. Aspect:方面荚板。一個(gè)可以切入多個(gè)類的關(guān)注點(diǎn)凤壁。這個(gè)關(guān)注點(diǎn)實(shí)現(xiàn)了我們前面說的具體的業(yè)務(wù)功能。例如打印日志跪另,進(jìn)行數(shù)據(jù)庫(kù)的事務(wù)管理等拧抖。
  2. Joint point:被切入點(diǎn)。是指具體要實(shí)現(xiàn)前面所說的例如打印日志免绿,數(shù)據(jù)庫(kù)事務(wù)管理的被切入的點(diǎn)唧席。也就是通過 AOP 將切面功能動(dòng)態(tài)加入進(jìn)去的程序位置。在 Spring AOP 里面這個(gè)指的都是某個(gè)方法
  3. Pointcut:切點(diǎn)嘲驾。用來指明如何通過規(guī)則匹配 Joint point淌哟。這個(gè)規(guī)則是一個(gè)表達(dá)式。在 Spring 中辽故,默認(rèn)使用的是 AspectJ 的 pointcut 表達(dá)式語(yǔ)言
  4. Advice:指明在一個(gè)切入點(diǎn)的不同位置上采取的動(dòng)作徒仓。例如對(duì)于一個(gè)數(shù)據(jù)庫(kù)訪問事務(wù)管理來說,在進(jìn)入方法后要開啟事務(wù)誊垢,在方法結(jié)束前要提交事務(wù)掉弛,在發(fā)生錯(cuò)誤的時(shí)候要回滾事務(wù)症见。這屬于三個(gè)不同的 Advice,要分別進(jìn)行實(shí)現(xiàn)狰晚。Advice 通常和具體的 Pointcut 關(guān)聯(lián)在一起筒饰。
  5. AOP proxy:AOP 代理。用來實(shí)現(xiàn)將 Advice 功能動(dòng)態(tài)加入到 Pointcut 的方法壁晒。在 Spring 的 AOP 中采用動(dòng)態(tài)代理和 CGLIB 代理的方式來實(shí)現(xiàn)瓷们。而 AspectJ 則采用了特定編譯器侵入字節(jié)碼的方式來實(shí)現(xiàn)。

SprinBoot AOP 實(shí)現(xiàn)

前面我們已經(jīng)用好幾章講述了 SpringBoot 的基本使用秒咐。那么這里我們就用 SpringBoot 和 AOP 結(jié)合來實(shí)現(xiàn)一個(gè)輸出所有 Rest 接口輸入?yún)?shù)和返回參數(shù)的日志的功能谬晕。

實(shí)現(xiàn) rest 服務(wù)功能。

根據(jù)前面的文章携取,我們先建立一個(gè) SpingBoot 的工程如下圖所示


demo 工程

SpringBoot 項(xiàng)目配置

我們對(duì) SpringBoot 項(xiàng)目配置如下

server:
  port: 3030
  servlet:
    context-path: /aop-demo
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
      indent-output: true
logging:
  level:
    com.yanggch: debug

其中 jackson 相關(guān)配置是為了將對(duì)象輸出成 json 字符串后能夠格式化輸出

實(shí)現(xiàn)一個(gè) rest 接口的 Controller 類

在這里攒钳,我們實(shí)現(xiàn)兩個(gè) rest 接口。一個(gè)是返回 hello 信息雷滋。一個(gè)是根據(jù)輸入返回登錄信息不撑。

package com.yanggch.demo.aop.web;

import com.yanggch.demo.aop.domain.LoginEntity;
import com.yanggch.demo.aop.domain.SecurityEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * 安全相關(guān) rest 服務(wù)
 *
 * @author : 楊高超
 * @since : 2018-05-27
 */
@RestController
@RequestMapping("/api/v1/security")
public class SecurityApi {
    @RequestMapping(value = "/login/{shopId}", method = RequestMethod.POST)
    public SecurityEntity login(@RequestBody LoginEntity loginEntity, @PathVariable Long shopId) {
        SecurityEntity securityEntity = new SecurityEntity();
        securityEntity.setShopId(shopId);
        securityEntity.setAccount(loginEntity.getAccount());
        securityEntity.setPwd(loginEntity.getPwd());
        securityEntity.setLoginTime(new Date());
        return securityEntity;
    }

    @RequestMapping(value = "/echo/{name}", method = RequestMethod.GET)
    public String login(@PathVariable String name) {
        return "hello," + name;
    }
}

先在我們要通過 AOP 功能將所有 Rest 接口的輸入?yún)?shù)和返回結(jié)果輸出到日志中。

實(shí)現(xiàn) Web Aop 功能晤斩。

package com.yanggch.demo.aop.comment;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * web 接口日志
 *
 * @author : 楊高超
 * @since : 2018-05-27
 */
@Aspect
@Component
public class WebLogAspect {
    private static Logger log = LoggerFactory.getLogger(WebLogAspect.class);

    private final ObjectMapper mapper;

    @Autowired
    public WebLogAspect(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        for (Object object : joinPoint.getArgs()) {
            if (
                object instanceof MultipartFile
                    || object instanceof HttpServletRequest
                    || object instanceof HttpServletResponse
                ) {
                continue;
            }
            try {
                if (log.isDebugEnabled()) {
                    log.debug(
                        joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName()
                            + " : request parameter : " + mapper.writeValueAsString(object)
                    );
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @AfterReturning(returning = "response", pointcut = "webLog()")
    public void doAfterReturning(Object response) throws Throwable {
        if (response != null) {
            log.debug("response parameter : " + mapper.writeValueAsString(response));
        }
    }
}

這里有幾個(gè)需要注意的地方焕檬,

  1. 需要在類上聲明 org.aspectj.lang.annotation.Aspect 注解。
  2. 需要通過方法上的 org.aspectj.lang.annotation.Pointcut 注解聲明一個(gè) Pointcut 澳泵,用來指明要在哪些方法切入实愚。我們的 rest 接口都有 org.springframework.web.bind.annotation.RequestMapping 注解,所以我們這里就用了 "@annotation(org.springframework.web.bind.annotation.RequestMapping)" 表達(dá)式來指明兔辅。
  3. 通過 Advice 相關(guān)注解來說明在切入方法的什么位置做什么事腊敲。這里用 org.aspectj.lang.annotation.Before

這個(gè)實(shí)現(xiàn)是指明在所有具備 org.springframework.web.bind.annotation.RequestMapping 注解的方法上,方法進(jìn)入后打印入口參數(shù)维苔。方法返回后碰辅,打印返回參數(shù)。

測(cè)試

在前臺(tái)通過 postman 發(fā)起請(qǐng)求蕉鸳,后臺(tái)日志輸入結(jié)果如下

2018-05-27 19:58:42.941 DEBUG 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.WebLogAspect  : com.yanggch.demo.aop.web.SecurityApi.login : request parameter : {
  "account" : "yanggch",
  "pwd" : "123456"
}
2018-05-27 19:58:42.941 DEBUG 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.WebLogAspect  : com.yanggch.demo.aop.web.SecurityApi.login : request parameter : 2001
2018-05-27 19:58:42.942 DEBUG 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.WebLogAspect  : response parameter : {
  "shopId" : 2001,
  "account" : "yanggch",
  "pwd" : "123456",
  "loginTime" : "2018-05-27 11:58:42"
}
2018-05-27 19:58:45.796 DEBUG 86072 --- [nio-3030-exec-5] c.yanggch.demo.aop.comment.WebLogAspect  : com.yanggch.demo.aop.web.SecurityApi.echo : request parameter : "yanggch"
2018-05-27 19:58:45.796 DEBUG 86072 --- [nio-3030-exec-5] c.yanggch.demo.aop.comment.WebLogAspect  : response parameter : "hello,yanggch"

由此可見乎赴,我們雖然沒有在 rest 接口方法中寫輸出日志的代碼,但是通過 AOP 的方式可以自動(dòng)的給各個(gè) rest 入口方法中添加上輸出入口參數(shù)和返回參數(shù)的代碼并正確執(zhí)行潮尝。

其他說明

前面提到了 Advice 的類型和 Pointcut 的 AOP 表達(dá)式語(yǔ)言榕吼。具體參考如下。

Advice 類型

  1. before advice 在方法執(zhí)行前執(zhí)行勉失。
  2. after returning advice 在方法執(zhí)行后返回一個(gè)結(jié)果后執(zhí)行羹蚣。
  3. after throwing advice 在方法執(zhí)行過程中拋出異常的時(shí)候執(zhí)行。
  4. Around advice 在方法執(zhí)行前后和拋出異常時(shí)執(zhí)行乱凿,相當(dāng)于綜合了以上三種通知顽素。

AOP 表達(dá)式語(yǔ)言

  1. 方法參數(shù)匹配
    @args()

  2. 方法描述匹配
    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
    其中 returning type pattern,name pattern, and parameters pattern是必須的.

    • ret-type-pattern:可以為*表示任何返回值,全路徑的類名等.
    • name-pattern:指定方法名, *代表所有
    • set*代表以set開頭的所有方法.
    • parameters pattern:指定方法參數(shù)(聲明的類型),(..)代表所有參數(shù),(*)代表一個(gè)參數(shù)
    • (*,String)代表第一個(gè)參數(shù)為任何值,第二個(gè)為String類型.
  3. 當(dāng)前AOP代理對(duì)象類型匹配

  4. 目標(biāo)類匹配
    @target()
    @within()

  5. 標(biāo)有此注解的方法匹配
    @annotation()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咽弦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胁出,更是在濱河造成了極大的恐慌型型,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件全蝶,死亡現(xiàn)場(chǎng)離奇詭異闹蒜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)抑淫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門绷落,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人始苇,你說我怎么就攤上這事砌烁。” “怎么了催式?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵函喉,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我荣月,道長(zhǎng)函似,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任喉童,我火速辦了婚禮,結(jié)果婚禮上顿天,老公的妹妹穿的比我還像新娘堂氯。我一直安慰自己,他們只是感情好牌废,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布咽白。 她就那樣靜靜地躺著,像睡著了一般鸟缕。 火紅的嫁衣襯著肌膚如雪晶框。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天懂从,我揣著相機(jī)與錄音授段,去河邊找鬼。 笑死番甩,一個(gè)胖子當(dāng)著我的面吹牛侵贵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缘薛,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窍育,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼卡睦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起漱抓,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤表锻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后乞娄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞬逊,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年补胚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了码耐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溶其,死狀恐怖骚腥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓶逃,我是刑警寧澤束铭,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站厢绝,受9級(jí)特大地震影響契沫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昔汉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一懈万、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧靶病,春花似錦会通、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至煤辨,卻和暖如春裳涛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背众辨。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工端三, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹃彻。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓技肩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虚婿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

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