SpringBoot 2.X Kotlin系列之AOP統(tǒng)一打印日志

image

在開發(fā)項(xiàng)目中,我們經(jīng)常會(huì)需要打印日志,這樣方便開發(fā)人員了解接口調(diào)用情況及定位錯(cuò)誤問(wèn)題,很多時(shí)候?qū)τ贑ontroller或者是Service的入?yún)?/code>和出參需要打印日志,但是我們又不想重復(fù)的在每個(gè)方法里去使用logger打印,這個(gè)時(shí)候希望有一個(gè)管理者統(tǒng)一來(lái)打印,這時(shí)Spring AOP就派上用場(chǎng)了,利用切面的思想,我們?cè)谶M(jìn)入、出入Controller或Service時(shí)給它切一刀實(shí)現(xiàn)統(tǒng)一日志打印。

SpringAOP不僅可以實(shí)現(xiàn)在不產(chǎn)生新類的情況下打印日志,還可以管理事務(wù)、緩存等。具體可以了解官方文檔。https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-api

基礎(chǔ)概念

在使用SpringAOP,這里還是先簡(jiǎn)單講解一些基本的知識(shí)吧,如果說(shuō)的不對(duì)請(qǐng)及時(shí)指正,這里主要是根據(jù)官方文檔來(lái)總結(jié)的。本章內(nèi)容主要涉及的知識(shí)點(diǎn)。

Pointcut: 切入點(diǎn),這里用于定義規(guī)則,進(jìn)行方法的切入(形象的比喻就是一把刀)。

JoinPoint: 連接點(diǎn),用于連接定義的切面。

Before: 在之前,在切入點(diǎn)方法執(zhí)行之前。

AfterReturning: 在切入點(diǎn)方法結(jié)束并返回時(shí)執(zhí)行。

這里除了SpringAOP相關(guān)的知識(shí),還涉及到了線程相關(guān)的知識(shí)點(diǎn),因?yàn)槲覀冃枰紤]多線程中它們各自需要保存自己的變量,所以就用到了ThreadLocal

依賴引入

這里主要是用到aopmongodb,在pom.xml文件中加入以下依賴即可:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

相關(guān)實(shí)體類

/**
 * 請(qǐng)求日志實(shí)體,用于保存請(qǐng)求日志
 */
@Document
class WebLog {
    var id: String = ""
    var request: String? = null
    var response: String? = null
    var time: Long? = null
    var requestUrl: String? = null
    var requestIp: String? = null
    var startTime: Long? = null
    var endTime: Long? = null
    var method: String? = null

    override fun toString(): String {
        return ObjectMapper().writeValueAsString(this)
    }
}

/**
 * 業(yè)務(wù)對(duì)象,上一章講JPA中有定義
 */
@Document
class Student {
    @Id
    var id :String? = null
    var name :String? = null
    var age :Int? = 0
    var gender :String? = null
    var sclass :String ?= null

    override fun toString(): String {
        return ObjectMapper().writeValueAsString(this)
    }
}

定義切面

定義切入點(diǎn)

/**
 * 定義一個(gè)切入,只要是為io.intodream..web下public修飾的方法都要切入
 */
@Pointcut(value = "execution(public * io.intodream..web.*.*(..))")
fun webLog() {}

定義切入點(diǎn)的表達(dá)式還可以使用within、如:

/**
 * 表示在io.intodream.web包下的方法都會(huì)被切入
 */
@Pointcut(value = "within(io.intodream.web..*")

定義一個(gè)連接點(diǎn)

/**
 * 切面的連接點(diǎn),并聲明在該連接點(diǎn)進(jìn)入之前需要做的一些事情
 */
@Before(value = "webLog()")
@Throws(Throwable::class)
fun doBefore(joinPoint: JoinPoint) {
    val webLog = WebLog()
    webLog.startTime = System.currentTimeMillis()
    val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes?
    val request = attributes!!.request
    val args = joinPoint.args
    val paramNames = (joinPoint.signature as CodeSignature).parameterNames
    val params = HashMap<String, Any>(args.size)
    for (i in args.indices) {
        if (args[i] !is BindingResult) {
            params[paramNames[i]] = args[i]
        }
    }
    webLog.id = UUID.randomUUID().toString()
    webLog.request = params.toString()
    webLog.requestUrl = request.requestURI.toString()
    webLog.requestIp = request.remoteAddr
    webLog.method = request.method
    webRequestLog.set(webLog)
    logger.info("REQUEST={} {}; SOURCE IP={}; ARGS={}", request.method,
            request.requestURL.toString(), request.remoteAddr, params)
}

方法結(jié)束后執(zhí)行

@AfterReturning(returning = "ret", pointcut = "webLog()")
@Throws(Throwable::class)
fun doAfterReturning(ret: Any) {
    val webLog = webRequestLog.get()
    webLog.response = ret.toString()
    webLog.endTime = System.currentTimeMillis()
    webLog.time = webLog.endTime!! - webLog.startTime!!
    logger.info("RESPONSE={}; SPEND TIME={}MS", ObjectMapper().writeValueAsString(ret), webLog.time)
    logger.info("webLog:{}", webLog)
    webLogRepository.save(webLog)
    webRequestLog.remove()
}

這里的主要思路是,在方法執(zhí)行前,先記錄詳情的請(qǐng)求參數(shù),請(qǐng)求方法,請(qǐng)求ip, 請(qǐng)求方式及進(jìn)入時(shí)間,然后將對(duì)象放入到ThreadLocal中,在方法結(jié)束后并取到對(duì)應(yīng)的返回對(duì)象且計(jì)算出請(qǐng)求耗時(shí),然后將請(qǐng)求日志保存到mongodb中。

完成的代碼

package io.intodream.kotlin07.aspect

import com.fasterxml.jackson.databind.ObjectMapper
import io.intodream.kotlin07.dao.WebLogRepository
import io.intodream.kotlin07.entity.WebLog
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.aspectj.lang.reflect.CodeSignature
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import org.springframework.validation.BindingResult
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.util.*

/**
 * {描述}
 *
 * @author yangxianxi@gogpay.cn
 * @date 2019/4/10 19:06
 *
 */
@Aspect
@Order(5)
@Component
class WebLogAspect {

    private val logger:Logger = LoggerFactory.getLogger(WebLogAspect::class.java)

    private val webRequestLog: ThreadLocal<WebLog> = ThreadLocal()

    @Autowired lateinit var webLogRepository: WebLogRepository

    /**
     * 定義一個(gè)切入,只要是為io.intodream..web下public修飾的方法都要切入
     */
    @Pointcut(value = "execution(public * io.intodream..web.*.*(..))")
    fun webLog() {}

    /**
     * 切面的連接點(diǎn),并聲明在該連接點(diǎn)進(jìn)入之前需要做的一些事情
     */
    @Before(value = "webLog()")
    @Throws(Throwable::class)
    fun doBefore(joinPoint: JoinPoint) {
        val webLog = WebLog()
        webLog.startTime = System.currentTimeMillis()
        val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes?
        val request = attributes!!.request
        val args = joinPoint.args
        val paramNames = (joinPoint.signature as CodeSignature).parameterNames
        val params = HashMap<String, Any>(args.size)
        for (i in args.indices) {
            if (args[i] !is BindingResult) {
                params[paramNames[i]] = args[i]
            }
        }
        webLog.id = UUID.randomUUID().toString()
        webLog.request = params.toString()
        webLog.requestUrl = request.requestURI.toString()
        webLog.requestIp = request.remoteAddr
        webLog.method = request.method
        webRequestLog.set(webLog)
        logger.info("REQUEST={} {}; SOURCE IP={}; ARGS={}", request.method,
                request.requestURL.toString(), request.remoteAddr, params)
    }

    @AfterReturning(returning = "ret", pointcut = "webLog()")
    @Throws(Throwable::class)
    fun doAfterReturning(ret: Any) {
        val webLog = webRequestLog.get()
        webLog.response = ret.toString()
        webLog.endTime = System.currentTimeMillis()
        webLog.time = webLog.endTime!! - webLog.startTime!!
        logger.info("RESPONSE={}; SPEND TIME={}MS", ObjectMapper().writeValueAsString(ret), webLog.time)
        logger.info("webLog:{}", webLog)
        webLogRepository.save(webLog)
        webRequestLog.remove()
    }
}

這里定義的是Web層的切面,對(duì)于Service層我也可以定義一個(gè)切面,但是對(duì)于Service層的進(jìn)入和返回的日志我們可以把級(jí)別稍等調(diào)低一點(diǎn),這里改debug,具體實(shí)現(xiàn)如下:

package io.intodream.kotlin07.aspect

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.aspectj.lang.reflect.CodeSignature
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import org.springframework.validation.BindingResult

/**
 * service層所有public修飾的方法調(diào)用返回日志
 *
 * @author yangxianxi@gogpay.cn
 * @date 2019/4/10 17:33
 *
 */
@Aspect
@Order(2)
@Component
class ServiceLogAspect {

    private val logger: Logger = LoggerFactory.getLogger(ServiceLogAspect::class.java)

    /**
     *
     */
    @Pointcut(value = "execution(public * io.intodream..service.*.*(..))")
    private fun serviceLog(){}

    @Before(value = "serviceLog()")
    fun deBefore(joinPoint: JoinPoint) {
        val args = joinPoint.args
        val codeSignature = joinPoint.signature as CodeSignature
        val paramNames = codeSignature.parameterNames
        val params = HashMap<String, Any>(args.size).toMutableMap()
        for (i in args.indices) {
            if (args[i] !is BindingResult) {
                params[paramNames[i]] = args[i]
            }
        }
        logger.debug("CALL={}; ARGS={}", joinPoint.signature.name, params)
    }

    @AfterReturning(returning = "ret", pointcut = "serviceLog()")
    @Throws(Throwable::class)
    fun doAfterReturning(ret: Any) {
        logger.debug("RESPONSE={}", ObjectMapper().writeValueAsString(ret))
    }
}

接口測(cè)試

這里就不在貼出Service層和web的代碼實(shí)現(xiàn)了,因?yàn)槲沂强截愔皩PA那一章的代碼,唯一不同的就是加入了切面,切面的加入并不影響原來(lái)的業(yè)務(wù)流程。

執(zhí)行如下請(qǐng)求:


image

我們會(huì)在控制臺(tái)看到如下日志

2019-04-14 19:32:27.208  INFO 4914 --- [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect         : REQUEST=POST http://localhost:9000/api/student/; SOURCE IP=0:0:0:0:0:0:0:1; ARGS={student={"id":"5","name":"Rose","age":17,"gender":"Girl","sclass":"Second class"}}
2019-04-14 19:32:27.415  INFO 4914 --- [nio-9000-exec-1] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:4}] to localhost:27017
2019-04-14 19:32:27.431  INFO 4914 --- [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect         : RESPONSE={"id":"5","name":"Rose","age":17,"gender":"Girl","sclass":"Second class"}; SPEND TIME=239MS
2019-04-14 19:32:27.431  INFO 4914 --- [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect         : webLog:{"id":"e7b0ca1b-0a71-4fa0-9f5f-95a29d4d54a1","request":"{student={\"id\":\"5\",\"name\":\"Rose\",\"age\":17,\"gender\":\"Girl\",\"sclass\":\"Second class\"}}","response":"{\"id\":\"5\",\"name\":\"Rose\",\"age\":17,\"gender\":\"Girl\",\"sclass\":\"Second class\"}","time":239,"requestUrl":"/api/student/","requestIp":"0:0:0:0:0:0:0:1","startTime":1555241547191,"endTime":1555241547430,"method":"POST"}

查看數(shù)據(jù)庫(kù)會(huì)看到我們的請(qǐng)求日志已經(jīng)寫入了:


image

這里有一個(gè)地方需要注意,在Service層的實(shí)現(xiàn),具體如下:

return studentRepository.findById(id).get()

這里的findById會(huì)返回一個(gè)Optional<T>對(duì)象,如果沒(méi)有查到數(shù)據(jù),我們使用get獲取數(shù)據(jù)會(huì)出現(xiàn)異常java.util.NoSuchElementException: No value present,可以改為返回對(duì)象可以為空只要在返回類型后面加一個(gè)?即可,同時(shí)調(diào)用OptionalifPresent進(jìn)行安全操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

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