在SpringBoot中用SpringAOP实现日志记录功能
背景:
我需要在一个SpringBoot的项目中的每个controller加入一个日志记录,记录关于请求的一些信息。
代码类似于:
logger.info(request.getRequestUrl());
之类的。
代码不难,但由于Controller的数量不少,干起来也是体力活。所以想到了用Spring AOP来解决这个问题。
首先,在pom中加入SpringAOP的相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
上一篇我们说到,如果要直接用@Aspect注解的话,要在spring的配置文件中加入
<aop:aspectj-autoproxy />
那么我们这里要不要在程序的主类中增加@EnableAspectJAutoProxy来启用呢? 实际并不需要,可以看下面关于AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy。
好的也就是说,只要引入SpringAOP相关的jar包依赖,我们就可以开始相关的Aspet的编程了。
这里直接上代码,然后再做解释:
首先是包结构的图:
这里涉及到接收请求的Controller的包有两个,com.stuPayment.controller还有com.stuPayment.uiController
然后看我们的切面类WebLogAspect类的代码:
package com.stuPayment.util; import java.util.Arrays; import javax.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
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.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; @Aspect
@Component
public class WebLogAspect { private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.stuPayment.controller..*.*(..))")//切入点描述 这个是controller包的切入点
public void controllerLog(){}//签名,可以理解成这个切入点的一个名称 @Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入点描述,这个是uiController包的切入点
public void uiControllerLog(){} @Before("controllerLog() || uiControllerLog()") //在切入点的方法run之前要干的
public void logBeforeController(JoinPoint joinPoint) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//这个RequestContextHolder是Springmvc提供来获得请求的东西
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest(); // 记录下请求内容
logger.info("################URL : " + request.getRequestURL().toString());
logger.info("################HTTP_METHOD : " + request.getMethod());
logger.info("################IP : " + request.getRemoteAddr());
logger.info("################THE ARGS OF THE CONTROLLER : " + Arrays.toString(joinPoint.getArgs())); //下面这个getSignature().getDeclaringTypeName()是获取包+类名的 然后后面的joinPoint.getSignature.getName()获取了方法名
logger.info("################CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
//logger.info("################TARGET: " + joinPoint.getTarget());//返回的是需要加强的目标类的对象
//logger.info("################THIS: " + joinPoint.getThis());//返回的是经过加强后的代理类的对象 } }
针对这个切面类,来展开说明@Aspect切面类的编程。
@Aspect和@Component
首先,这个@Aspect注释告诉Spring这是个切面类,然后@Compoment将转换成Spring容器中的bean或者是代理bean。 总之要写切面这两个注解一起用就是了。
既然是切面类,那么肯定是包含PointCut还有Advice两个要素的,下面对几个注解展开讲来看看在@Aspect中是怎么确定切入点(PointCut)和增强通知(Advice)的。
@PointCut
这个注解包含两部分,PointCut表达式和PointCut签名。表达式是拿来确定切入点的位置的,说白了就是通过一些规则来确定,哪些方法是要增强的,也就是要拦截哪些方法。
@PointCut(...........)括号里面那些就是表达式。这里的execution是其中的一种匹配方式,还有:
execution: 匹配连接点 within: 某个类里面 this: 指定AOP代理类的类型 target:指定目标对象的类型 args: 指定参数的类型 bean:指定特定的bean名称,可以使用通配符(Spring自带的) @target: 带有指定注解的类型 @args: 指定运行时传的参数带有指定的注解 @within: 匹配使用指定注解的类 @annotation:指定方法所应用的注解
注意,由于是动态代理的实现方法,所以不是所有的方法都能拦截得下来,对于JDK代理只有public的方法才能拦截得下来,对于CGLIB只有public和protected的方法才能拦截。
这里我们主要介绍execution的匹配方法,因为大多数时候都会用这个来定义pointcut:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?) execution(方法修饰符(可选) 返回类型 类路径 方法名 参数 异常模式(可选))
除了返回类型,方法名还有参数之外,其他都是可选的
ret-type-pattern:可以为*表示任何返回值,全路径的类名等.
name-pattern:指定方法名,*代表所以,set*,代表以set开头的所有方法.
parameters pattern:指定方法参数(声明的类型), ()匹配没有参数; (..)代表任意多个参数; (*)代表一个参数,但可以是任意型; (*,String)代表第一个参数为任何值,第二个为String类型。
下面给几个例子:
1)execution(public * *(..))——表示匹配所有public方法
2)execution(* set*(..))——表示所有以“set”开头的方法
3)execution(* com.xyz.service.AccountService.*(..))——表示匹配所有AccountService接口的方法
4)execution(* com.xyz.service.*.*(..))——表示匹配service包下所有的方法
5)execution(* com.xyz.service..*.*(..))——表示匹配service包和它的子包下的方法
然后其他的匹配法要用的时候再百度吧~
然后是@PointCut的第二个部分,签名signature,也就是代码中的
@Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入点描述,这个是uiController包的切入点
public void uiControllerLog(){}
像方法定义的这个Public void uiControllerLog(){}这个看起来像是方法定义的东西,就是签名,签名没有实际用处,只是用来标记一个Pointcut,可以理解成这个切入点的一个记号。
@Before
这个是决定advice在切入点方法的什么地方执行的标签,这个注解的意思是在切入点方法执行之前执行我们定义的advice。
@Before("controllerLog() || uiControllerLog()") //在切入点的方法run之前要干的
public void logBeforeController(JoinPoint joinPoint) {
@Before注解括号里面写的是一个切入点,这里看见切入点表达式可以用逻辑符号&&,||,!来描述。 括号里面也可以内置切点表达式,也就是直接写成:
@Before("execution(public * com.stuPayment.uiController..*.*(..))")
跟写成@Before("uiControllerLog()")的效果是一样的。
然后看到注解下面的方法,就是描述advice的,我们看到有个参数JoinPoint,这个东西代表着织入增强处理的连接点。JoinPoint包含了几个很有用的参数:
- Object[] getArgs:返回目标方法的参数
- Signature getSignature:返回目标方法的签名
- Object getTarget:返回被织入增强处理的目标对象
- Object getThis:返回AOP框架为目标对象生成的代理对象
除了注解@Around的方法外,其他都可以加这个JoinPoint作参数。@Around注解的方法的参数一定要是ProceedingJoinPoint,下面会介绍。
@After
这个注解就是在切入的方法运行完之后把我们的advice增强加进去。一样方法中可以添加JoinPoint。
@Around
这个注解可以简单地看作@Before和@After的结合。这个注解和其他的比比较特别,它的方法的参数一定要是ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身,这个proceedingJoinPoint有个proceed()方法,相当于就是那切入点的那个方法执行,简单地说就是让目标方法执行,然后这个方法会返回一个对象,这个对象就是那个切入点所在位置的方法所返回的对象。
除了这个Proceed方法(很重要的方法),其他和那几个注解一样。
@AfterReturning
顾名思义,这个注解是在目标方法正常完成后把增强处理织入。这个注解可以指定两个属性(之前的三个注解后面的括号只写一个@PointCut表达式,也就是只有一个属性),一个是和其他注解一样的PointCut表达式,也就是描述该advice在哪个接入点被织入;然后还可以有个returning属性,表明可以在Advice的方法中有目标方法返回值的形参。
@AfterReturning(returning = "returnOb", pointcut = "controllerLog() || uiControllerLog()")
public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
System.out.println("##################### the return of the method is : " + returnOb);
}
浏览器发出一个请求后,效果截图:
(这里是一个请求登录界面的请求,所以uicontroller返回一个String作为视图。)
@AfterThrowing
异常抛出增强,在异常抛出后织入的增强。有点像上面的@AfterReturning,这个注解也是有两个属性,pointcut和throwing。
用法也和刚刚的那个returning差不多:
@AfterThrowing(pointcut = "controllerLog() || uiControllerLog()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex); }
好了现在注解都介绍完了,这里还要提到上面用到的一个类:RequestContextHolder
比如说,有个需求需要在service中获得request和response,我们一般会(我就是)直接在controller那把request或response作为参数传到service,这就很不美观。后来知道,原来SpringMVC提供了个很强大的类ReqeustContextHolder,通过他你就可以获得request和response什么的。
//下面两个方法在没有使用JSF的项目中是没有区别的
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
// RequestContextHolder.getRequestAttributes();
//从session里面获取对应的值
String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION); HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();
好了完成了这个切面的编程后,你就成功把日志功能切入到各个controller中了。看个效果图。
最后,再记录一下各个不同的advice的拦截顺序的问题。
情况一,只有一个Aspect类:
无异常:@Around(proceed()之前的部分) → @Before → 方法执行 → @Around(proceed()之后的部分) → @After → @AfterReturning
有异常:@Around(proceed(之前的部分)) → @Before → 扔异常ing → @After → @AfterThrowing (大概是因为方法没有跑完抛了异常,没有正确返回所有@Around的proceed()之后的部分和@AfterReturning两个注解的加强没有能够织入)
情况二,同一个方法有多个@Aspect类拦截:
单个Aspect肯定是和只有一个Aspect的时候的情况是一样的,但不同的Aspect里面的advice的顺序呢??答案是不一定,像是线程一样,没有谁先谁后,除非你给他们分配优先级,同样地,在这里你也可以为@Aspect分配优先级,这样就可以决定谁先谁后了。
优先级有两种方式:
- 实现org.springframework.core.Ordered接口,实现它的getOrder()方法
- 给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order
不管是哪种,都是order的值越小越先执行:
@Order(5)
@Component
@Aspect
public class Aspect1 {
// ...
} @Order(6)
@Component
@Aspect
public class Aspect2 {
// ...
}
这样Aspect1就永远比Aspect2先执行了。
注意点:
- 如果在同一个 aspect 类中,针对同一个 pointcut,定义了两个相同的 advice(比如,定义了两个 @Before),那么这两个 advice 的执行顺序是无法确定的,哪怕你给这两个 advice 添加了 @Order 这个注解,也不行。这点切记。
- 对于@Around这个advice,不管它有没有返回值,但是必须要方法内部,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发。
参考过的博客:
https://blog.csdn.net/bombsklk/article/details/79143145 SpringBoot用AOP实现日志
https://blog.csdn.net/loongshawn/article/details/72303040?utm_source=tuicool&utm_medium=referral execution表达式
https://blog.csdn.net/caomiao2006/article/details/51287200 advice中获取参数的方法
https://blog.csdn.net/rainbow702/article/details/52185827 advice的顺序
https://blog.csdn.net/zzy7075/article/details/53559902 RequestContextHolder类的相关介绍
在SpringBoot中用SpringAOP实现日志记录功能的更多相关文章
- HAproxy增加日志记录功能和自定义日志输出内容、格式
http://blog.51cto.com/eric1/1854574 一.增加haproxy日志记录功能 1.1 由于数据分析的需要,我们必须打开haproxy日志,记录相关信息. 在配置前,我 ...
- 如何自行给指定的SAP OData服务添加自定义日志记录功能
有的时候,SAP标准的OData实现或者相关的工具没有提供我们想记录的日志功能,此时可以利用SAP系统强大的扩展特性,进行自定义日志功能的二次开发. 以SAP CRM Fiori应用"My ...
- 个人理解---在开发中何时加入日志记录功能[java]
是这样的:俩个月前做的一个小功能,今天经理突然问我这个'清除复投记录'功能是不是我做的,我说是,很久以前了.他说昨天一个客户找过来了,后台把人家的复投记录清除掉了,不知道何时清除的,我记得当时做的时候 ...
- tp5下通过composer实现日志记录功能
tp5实现日志记录 1.安装 psr/log composer require psr/log 它的作用就是提供一套接口,实现正常的日志功能! 我们可以来细细的分析一下,LoggerInterface ...
- iptables log日志记录功能扩展应用:iptables自动配置临时访问策略,任意公网登录服务器
一.修改日志记录: 1. 修改配置文件: vi /etc/rsyslog.conf 添加以下内容 #iptables log kern.=notice /var/log/iptables.log 2. ...
- springcloud zuulfilter 实现get,post请求日志记录功能
import com.alibaba.fastjson.JSONObject; import com.idoipo.infras.gateway.open.model.InvokeLogModel; ...
- 【SpringBoot】13. logback日志记录
logback日志记录 Spring Boot 1.5.19.RELEASE 1.导入相关jar包 在spring-boot-starter-web 中已经包含 2.添加logback.xml配置文件 ...
- php之框架增加日志记录功能类
<?php /* 思路:给定文件,写入读取(fopen ,fwrite……) 如果大于1M 则重写备份 传给一个内容, 判断大小,如果大于1M,备份 小于则写入 */ class Log{ // ...
- 使用日志记录功能查看PHP扩展的执行过程
了解过PHP内核的同学都知道,PHP的一次请求的生命周期 1.启动Apache后,PHP解释程序也随之启动.PHP调用各个扩展的MINIT方法,从而使这些扩展切换到可用状态 2.当一个页面请求发生时, ...
随机推荐
- sql 逻辑运算符 优先级
SELECT * FROM tl_documentation WHERE storehouse_id =2 OR customer_id =2 AND product_id =20 ORDER BY ...
- Eclipse配置python环境
主要分为四步 1.安装java环境 2.在Eclipse下安装Pydev 1)启动 Eclipse,利用 Eclipse Update Manager 安装 PyDev.在 Eclipse 菜单栏中找 ...
- 存储过程系列五:完整的存储过程备份使用函数REPLACE()substr()
CREATE OR REPLACE PROCEDURE "YLQXSCXKESL_GGXKZ_TO_QB" ( ...
- 对服务器上所有Word文件做全文检索的解决方案-Java
一.背景介绍 Word文档与日常办公密不可分,在实际应用中,当某一文档服务器中有很多Word文档,假如有成千上万个文档时,用户查找打开包含某些指定关键字的文档就变得很困难,目前这一问题没有好的解 ...
- swoole异步redis安装前置条件和流程
---恢复内容开始--- 1.redis服务 确认redis在服务器中已经安装了 2.hiredis库 第二步安装hiredis 下载位置 2.1获取 安装包https://github.com/re ...
- 兼容html5新标签及媒体查询引入插件
<!-- 以下2个插件是用于在IE8支持HTML5元素和媒体查询的,如果不用可移除 --> <!--[if lt IE 9]> <script src="htt ...
- MTK HDMI 流程
一.HDMI初始化 1. kernel-3.18/drivers/misc/mediatek/ext_disp/mtk_extd_mgr.c static int __init mtk_extd_mg ...
- bzoj4289
最短路 很容易想到边和边之间连边,但是这样菊花图就完蛋了 我们想办法优化一下,能不能不要每条边都连. 考虑查分,把一个点的出边串起来,这样就行了,每条无向边拆成两条就能保证了 #include< ...
- Java int与String互相转化大全
int -> String //int 转化才 string int num = 123456; //方法一 会产生两个String对象 String s1 = num+"" ...
- java命令行从编译到打jar包到执行
目录: 一. javac编译 1. 没有额外的jar包 2. 包含额外的jar包 二. jar打jar包 三. java运行 1. java命令执行 2. jar包执 ...