在开发项目中,我们经常会需要打印日志,这样方便开发人员了解接口调用情况及定位错误问题,很多时候对于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文件中加入以下依赖即可:
相关实体类org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-data-mongodb
/** * 请求日志实体,用于保存请求日志 */ @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方法结束后执行(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= 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 (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接口测试(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
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/74105.html
摘要:一本节目标前两章主要讲了的基本操作,这一章我们将学习使用访问,并通过完成简单操作。这里有一个问题什么不选用数据库呢答案是目前支持。突出点是,即非阻塞的。二构建项目及配置本章不在讲解如何构建项目了,大家可以参考第一章。 showImg(https://segmentfault.com/img/remote/1460000018819338?w=1024&h=500); 一、本节目标 前两...
摘要:一本节目标前两章主要讲了的基本操作,这一章我们将学习使用访问,并通过完成简单操作。这里有一个问题什么不选用数据库呢答案是目前支持。突出点是,即非阻塞的。二构建项目及配置本章不在讲解如何构建项目了,大家可以参考第一章。 showImg(https://segmentfault.com/img/remote/1460000018819338?w=1024&h=500); 一、本节目标 前两...
摘要:是一门最近比较流行的静态类型编程语言,而且和一样同属系。这个生成的构造函数是合成的,因此不能从或中直接调用,但可以使用反射调用。 showImg(https://segmentfault.com/img/remote/1460000012958496); Kotlin是一门最近比较流行的静态类型编程语言,而且和Groovy、Scala一样同属Java系。Kotlin具有的很多静态语言...
摘要:二教程环境三创建项目创建项目有两种方式一种是在官网上创建二是在上创建如图所示勾选然后点,然后一直默认最后点击完成即可。我们这里看到和普通的接口没有异同,除了返回类型是用包装之外。与之对应的还有,这个后面我们会讲到。 showImg(https://segmentfault.com/img/remote/1460000018819338?w=1024&h=500); 从去年开始就开始学习...
摘要:下一代服务端开发下一代服务端开发第部门快速开始第章快速开始环境准备,,快速上手实现一个第章企业级服务开发从到语言的缺点发展历程的缺点为什么是产生的背景解决了哪些问题为什么是的发展历程容器的配置地狱是什么从到下一代企业级服务开发在移动开发领域 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》 Kotlin + Spring Boot : 下一代 Java...
阅读 2043·2021-09-07 10:14
阅读 1477·2019-08-30 15:53
阅读 2269·2019-08-30 12:43
阅读 2860·2019-08-29 16:37
阅读 753·2019-08-26 13:29
阅读 1999·2019-08-26 13:28
阅读 437·2019-08-23 18:33
阅读 3500·2019-08-23 16:09