内容简介:在开发项目中,我们经常会需要打印日志,这样方便开发人员了解接口调用情况及定位错误问题,很多时候对于Controller或者是Service的SpringAOP不仅可以实现在不产生新类的情况下打印日志,还可以管理事务、缓存等。具体可以了解官方文档。
在开发项目中,我们经常会需要打印日志,这样方便开发人员了解接口调用情况及定位错误问题,很多时候对于Controller或者是Service的 入参
和 出参
需要打印日志,但是我们又不想重复的在每个方法里去使用 logger
打印,这个时候希望有一个管理者统一来打印,这时Spring AOP就派上用场了,利用切面的思想,我们在进入、出入Controller或Service时给它 切一刀
实现统一日志打印。
SpringAOP不仅可以实现在不产生新类的情况下打印日志,还可以管理事务、缓存等。具体可以了解官方文档。 https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-api
基础概念
在使用SpringAOP,这里还是先简单讲解一些基本的知识吧,如果说的不对请及时指正,这里主要是根据官方文档来总结的。本章内容主要涉及的知识点。
Pointcut: 切入点,这里用于定义规则,进行方法的切入(形象的比喻就是一把刀)。
JoinPoint: 连接点,用于连接定义的切面。
Before: 在之前,在切入点方法执行之前。
AfterReturning: 在切入点方法结束并返回时执行。
这里除了SpringAOP相关的知识,还涉及到了线程相关的知识点,因为我们需要考虑多线程中它们各自需要保存自己的变量,所以就用到了 ThreadLocal
。
依赖引入
这里主要是用到 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
进行安全操作。
以上所述就是小编给大家介绍的《SpringBoot 2.X Kotlin系列之AOP统一打印日志》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Arthas 3.5.0 版本发布,支持反编译打印行号和统一鉴权
- Spring Boot 2.x 教程系列 | AOP 切面统一打印请求日志最佳实践
- 阿里巴巴 Arthas 3.5.0 版本发布,支持反编译打印行号和统一鉴权
- Python3之打印操作打印流重定向(下)
- java – Zebra打印机GC420t无法打印图像EPL 2 GW
- React模版打印
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
进化式运营:从互联网菜鸟到绝顶高手
李少加 / 电子工业出版社 / 2016-11 / 59
互联网运营作为一个新兴的岗位,一方面它是企业的核心岗职,身负重任,另一方面,又由于其短暂的历史,缺乏成熟体系的工作方法论,而目前业界主流的运营方法却是从企业视角出发,存在极大的改进空间。 《进化式运营:从互联网菜鸟到绝顶高手》作者基于自身十年的互联网洞察、实践经验,并融合了信息论、心理学、经济学、管理学、甚至包括生态学、进化论等跨学科跨学业的知识,从无到有地构建了一套全新的互联网运营体系:基......一起来看看 《进化式运营:从互联网菜鸟到绝顶高手》 这本书的介绍吧!