在開發項目中,咱們常常會須要打印日誌,這樣方便開發人員瞭解接口調用狀況及定位錯誤問題,不少時候對於Controller或者是Service的入參
和出參
須要打印日誌,可是咱們又不想重複的在每一個方法裏去使用logger
打印,這個時候但願有一個管理者統一來打印,這時Spring AOP就派上用場了,利用切面的思想,咱們在進入、出入Controller或Service時給它切一刀
實現統一日誌打印。
SpringAOP不只能夠實如今不產生新類的狀況下打印日誌,還能夠管理事務、緩存等。具體能夠了解官方文檔。https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-apihtml
在使用SpringAOP,這裏仍是先簡單講解一些基本的知識吧,若是說的不對請及時指正,這裏主要是根據官方文檔來總結的。本章內容主要涉及的知識點。java
Pointcut: 切入點,這裏用於定義規則,進行方法的切入(形象的比喻就是一把刀)。web
JoinPoint: 鏈接點,用於鏈接定義的切面。spring
Before: 在以前,在切入點方法執行以前。mongodb
AfterReturning: 在切入點方法結束並返回時執行。數據庫
這裏除了SpringAOP相關的知識,還涉及到了線程相關的知識點,由於咱們須要考慮多線程中它們各自須要保存本身的變量,因此就用到了ThreadLocal
。api
這裏主要是用到aop
和mongodb
,在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>
/** * 請求日誌實體,用於保存請求日誌 */ @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) } } /** * 業務對象,上一章講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) } }
/** * 定義一個切入,只要是爲io.intodream..web下public修飾的方法都要切入 */ @Pointcut(value = "execution(public * io.intodream..web.*.*(..))") fun webLog() {}
定義切入點的表達式還可使用within
、如:安全
/** * 表示在io.intodream.web包下的方法都會被切入 */ @Pointcut(value = "within(io.intodream.web..*")
/** * 切面的鏈接點,並聲明在該鏈接點進入以前須要作的一些事情 */ @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() }
這裏的主要思路是,在方法執行前,先記錄詳情的請求參數,請求方法,請求ip, 請求方式及進入時間,而後將對象放入到ThreadLocal
中,在方法結束後並取到對應的返回對象且計算出請求耗時,而後將請求日誌保存到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 /** * 定義一個切入,只要是爲io.intodream..web下public修飾的方法都要切入 */ @Pointcut(value = "execution(public * io.intodream..web.*.*(..))") fun webLog() {} /** * 切面的鏈接點,並聲明在該鏈接點進入以前須要作的一些事情 */ @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層的切面,對於Service層我也能夠定義一個切面,可是對於Service層的進入和返回的日誌咱們能夠把級別稍等調低一點,這裏改debug
,具體實現以下:
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修飾的方法調用返回日誌 * * @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)) } }
這裏就不在貼出Service層和web的代碼實現了,由於我是拷貝以前將JPA那一章的代碼,惟一不一樣的就是加入了切面,切面的加入並不影響原來的業務流程。
執行以下請求:
咱們會在控制檯看到以下日誌
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"}
查看數據庫會看到咱們的請求日誌已經寫入了:
這裏有一個地方須要注意,在Service層的實現,具體以下:
return studentRepository.findById(id).get()
這裏的findById
會返回一個Optional<T>
對象,若是沒有查到數據,咱們使用get
獲取數據會出現異常java.util.NoSuchElementException: No value present
,能夠改成返回對象能夠爲空只要在返回類型後面加一個?
便可,同時調用Optional
的ifPresent
進行安全操做。