【Spring】spring AOP 注解方式实现日志拦截并通过MDC设置日志跟踪标识

package com.test.aspect;

import java.util.Collection;
import java.util.UUID;

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.AfterThrowing;
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.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSON;

/**
 * 
 * @Description
 *## 本示例主要包含两个功能:
 * 1. 记录日志,打印业务方法的参数、返回值、耗时
 * 2. 通过slf4j的MDC,对每条日志增加唯一标识,便于集群部署后,易于日志追踪
 * 
 * ##AOP概念
 *  - 切面(Aspect): 
 *  							 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。本例使用@Aspect实现
 *  - 连接点(Joinpoint):
 *  							在程序执行过程中某个特定的点(就是要执行的方法),比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
 *  - 通知(Advice): 
 *  							在切面的某个特定的连接点上执行的动作。通知类型详见后续说明
 *  - 切入点(Pointcut): 
 *  							匹配连接点的断言(就是定义一下你要拦截哪些方法)。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。
 *  
 * 
 *## 下面是AOP示例中用到的相关注解知识
 * - 类注解:
 *@Aspect 将一个类定义为一个切面类
 *@order(i) 标记切面类的处理优先级,i值越小,优先级别越高.PS:可以注解类,也能注解到方法上
 *@Component 把切面类加入到IOC容器中
 *
 * - 方法注解: 其中@Before、@After、@AfterReturning、@Around、@AfterThrowing为通知advice的几种类型。
 *                   这里需要说明的是,@Before是业务逻辑执行前执行,与其对应的是@AfterReturning,
 *                   而不是@After,@After是所有的切面逻辑执行完之后才会执行,无论是否抛出异常。
 *@Pointcut 切入点: 定义一个方法为切入点
 *@Before 前置通知: 该注解标注的方法在业务模块代码执行(即连接点)之前执行,其不能阻止业务模块的执行,除非抛出异常;
 *@AfterReturning 后置通知: 该注解标注的方法在业务模块代码执行(即连接点)之后执行, 想获取返回值可以通过该注解;
 *@Around 环绕通知: 该注解功能最为强大,其所标注的方法用于编写业务模块执行的代码,其可以传入一个ProceedingJoinPoint用于调用业务模块的代码,
 *                  无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用;
 *@AfterThrowing 后置异常通知: 该注解标注的方法在业务模块抛出指定异常后执行;
 *@After 后置最终通知: 目标方法只要执行完了就会执行后置通知方法, 在@AfterReturning/@doAfterThrowing之前执行
 *@order(i) 标记切点的优先级,i越小,优先级越高
 *
 *## 通知Advice方法参数说明
 *任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。
 *JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、
 *getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)
 *
 *
 *## 切入点Pointcut表达式
 *表达式分为三部分:指示器、通配符和操作符。
 * - 指示器: 
 *              1. 匹配方法:execution()
 *              				格式:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
 *              				这里问号?表示当前项可以有也可以没有,其中各项的语义如下:
 *              				 - modifiers-pattern:方法的可见性,如public,protected;
 *               				 - ret-type-pattern:方法的返回值类型,如int,void等;
 *                				 - declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
 *                				 - name-pattern:方法名类型,如buisinessService();
 *                 				 - param-pattern:方法的参数类型,如java.lang.String;
 *                  			 - throws-pattern:方法抛出的异常类型,如java.lang.Exception;
 *              2. 匹配注解:@target(),@args(),@within(),@annotation()
 *								@target(org.springframework.web.bind.annotation.RestController) //匹配标注指定注解的类中的所有方法,且要求注解的保留策略为RUNTIME。此例为匹配标注RestController注解的类中的所有方法
 *								@args(org.springframework.web.bind.annotation.RequestBody) //匹配方法参数中标注指定注解的方法。此例为匹配方法参数标注RequestBody的方法
 *              				@within(org.springframework.web.bind.annotation.RestController) //匹配标注指定注解的类中的所有方法。此例为匹配标注RestController注解的类中的所有方法
 *								@annotation(org.springframework.web.bind.annotation.PostMapping) //匹配方法上标注指定注解的方法。此例匹配方法上标注PostMapping的方法
 *              3. 匹配包/类型:within()
 *              				within(com.zjs.route.basic.service.impl.HelloService*)  //匹配类HelloService下的所有方法
 *							    within(com.zjs.route.basic.service.impl.*) //匹配com.zjs.route.basic.service.impl这个包下所有类的方法
 *								within(com.zjs.route.basic.service...*) //匹配com.zjs.route.basic.service这个包及子包下所有类的方法
 *              4. 匹配对象:this(),bean(),target()
 *              				#this(com.test.TestDao)  
 *									- SpringAOP是基于代理的,this就代表代理对象,语法是this(type),当生成的代理对象可以转化为type指定的类型时表示匹配。
 *								  	this(com.demo.service.IUserService)匹配生成的代理对象是IUserService类型的所有方法的外部调用
 *                             #bean()
 *                             		- 通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
 *                              	bean(person)
 *                             #target()
 *                             		- SpringAOP是基于代理的,target表示被代理的目标对象,当被代理的目标对象可以转换为指定的类型时则表示匹配。
 *                               	target(com.demo.service.IUserService) 匹配所有被代理的目标对象能够转化成IuserService类型的所有方法的外部调用。
 *              5. 匹配参数:args()
 *              				- args用来匹配方法参数
 *                               args() 匹配不带参数的方法
 *                               args(java.lang.String) 匹配方法参数是String类型的
 *                               args(…) 带任意参数的方法
 *                               args(java.lang.String,…) 匹配第一个参数是String类型的,其他参数任意。最后一个参数是String的同理。
 * - 通配符: 
 * 				1. *:匹配任意数量的字符
 * 				2. +:匹配指定类及其子类
 *  			3. ..:匹配任意数量的子包或者参数
 * - 运算符
 * 				1. && : 与
 * 				2. || : 或
 * 				3. ! : 非
 * 
 *## 关于@order(i)注解的一些注意事项:
 *1. 注解类,i值是,值越小,优先级越高
 *2. 注解方法,分两种情况:
 *  注解的是 @Before 是i值越小,优先级越高
 *  注解的是 @After或者@AfterReturning 中,i值越大,优先级越高
 */
@Aspect
@Order(10)
@Component
public class LogControllerAspect {
	private static final Logger logger = LoggerFactory.getLogger(LogControllerAspect.class);

	private static final String MDC_KEY_LOGTRACEID = "logTraceId";
	private ThreadLocal<Long> startTime = new ThreadLocal<>();//用于存储执行方法的开始时间

	/**
	 * 匹配方法
	 * 格式:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
	 * 这里问号表示当前项可以有也可以没有,其中各项的语义如下:
	 * - modifiers-pattern:方法的可见性,如public,protected;
	 * - ret-type-pattern:方法的返回值类型,如int,void等;
	 * - declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
	 * - name-pattern:方法名类型,如buisinessService();
	 * - param-pattern:方法的参数类型,如java.lang.String;
	 * - throws-pattern:方法抛出的异常类型,如java.lang.Exception;
	 * 
	 * 此例表示匹配web包及其下所有类的所有方法
	 */
	@Pointcut("execution(public * com.zjs.web..*.*(..))")
	public void logPointcut1() {
	}

	@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
	public void logPointcut() {
	}

	/**
	 * 匹配com.zjs.route.basic.service.impl这个包下所有类的方法
	 */
	@Pointcut("within(com.zjs.route.basic.service.impl.*)")
	public void logPointcut2() {
	}
	
	/**
	 * 前置通知: 该注解标注的方法在业务模块代码执行(即连接点)之前执行,其不能阻止业务模块的执行,除非抛出异常;
	 * @param joinPoint
	 */
	@Before("logPointcut()")
	public void doBefore(JoinPoint joinPoint) {

		//为了便于日志追踪,通过MDC增加uuid作为唯一表示, 在日志输出格式配置中使用%X{}获取自定义的值,本例:%X{logTraceId}
		String logTraceId = UUID.randomUUID().toString().replace("-", "");
		MDC.put(MDC_KEY_LOGTRACEID, logTraceId);

		startTime.set(System.currentTimeMillis());

		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();

		logger.info("当前请求的URL: {}, HTTP_METHOD: {} ", request.getRequestURL(), request.getMethod());
		logger.info(">>>>doBefore开始调用方法 : {}, 参数 : {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
				JSON.toJSONString(joinPoint.getArgs()));

	}

	/**
	 * * 后置通知 
	 * 这里需要注意的是: 
	 *      如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息 
	 *      如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 
	 *       returning:限定了只有目标方法返回值类型与通知方法的参数类型相同时才能执行后置返回通知,否则不执行,
	 *       对于returning对应的通知方法参数为Object类型将匹配任何目标返回值  
	 * @param returnValue
	 * @throws Throwable
	 */
	@AfterReturning(returning = "returnValue", pointcut = "logPointcut()")
	public void doAfterReturning(Object returnValue) {
		//打印返回值,对于集合类型的数据只打印数据总数,不打印具体数据明细
		if (returnValue instanceof Collection) {
			logger.info("返回值的类型为集合,集合大小 : {}", ((Collection) returnValue).size());
		} else {
			logger.info("返回值: {} ", JSON.toJSONString(returnValue));
		}

		logger.info(">>>>doAfterReturning结束调用方法, 耗时[{}]毫秒", System.currentTimeMillis() - startTime.get());
		startTime.remove();
		//用完后清除, 此例中可以不清除,应为mdc的key可以只有一个,每个请求过来时会覆盖该key的值,并不会产生更多的mdc的值
		//		MDC.clear();
	}

	/** 
	 * 后置异常通知 
	 *  定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法; 
	 *  throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行, 
	 *           对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。 
	 * @param joinPoint 
	 * @param exception 
	 */
	@AfterThrowing(pointcut = "logPointcut()", throwing = "throwable")
	public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
		logger.info(">>>>doAfterThrowing结束调用方法, {}, 耗时[{}]毫秒", throwable.getMessage(), System.currentTimeMillis() - startTime.get());
		startTime.remove();
		//用完后清除, 此例中可以不清除,应为mdc的key可以只有一个,每个请求过来时会覆盖该key的值,并不会产生更多的mdc的值
		//		MDC.clear();
	}

	/**
	 * 后置最终通知(目标方法只要执行完了就会执行后置通知方法), 在@AfterReturning/@doAfterThrowing之前执行
	 * 
	 * @param joinPoint
	 */
	@After("logPointcut()")
	public void doAfter(JoinPoint joinPoint) {
		logger.info(">>>>doAfter...........................");
	}

	/** 
	 * 
	 * 环绕通知: 
	 *   环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。 
	 *   环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型 
	 *   
	 *   注意用了环绕通知,就可以不要再用@Before、@After、@AfterReturning、@AfterThrowing通知了,因为环绕通知可以处理所有情况,再用的话有点多余了。。。。
	 */
	@Around("logPointcut()")
	public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
		logger.info(">>>>doAroundAdvice...........................");
		long startTime1 = System.currentTimeMillis();
		Object returnValue = null;
		String declaringTypeName = null;
		String methodName = null;
		String methodArgs = null;

		try {
			declaringTypeName = proceedingJoinPoint.getSignature().getDeclaringTypeName();
			methodName = proceedingJoinPoint.getSignature().getName();
			methodArgs = JSON.toJSONString(proceedingJoinPoint.getArgs());
			logger.info(">>>>开始调用方法 : [{}], 参数 : {}", declaringTypeName + "." + methodName, methodArgs);

			returnValue = proceedingJoinPoint.proceed();
			//打印返回值,对于集合类型的数据只打印数据总数,不打印具体数据明细
			if (returnValue instanceof Collection) {
				logger.info("返回值的类型为集合,集合大小 : {}", ((Collection) returnValue).size());
			} else {
				logger.info("返回值: {} ", JSON.toJSONString(returnValue));
			}

			
		} catch (Throwable e) {
			logger.error(">>>>出现异常, 异常内容: [{}]", e.getMessage());
		}
		
		logger.info(">>>>结束调用方法[{}], 耗时[{}]毫秒", declaringTypeName + "." + methodName, System.currentTimeMillis() - startTime1);

		return returnValue;
	}

}

 

参考:https://www.cnblogs.com/zhangxufeng/p/9160869.html

https://www.cnblogs.com/lic309/p/4079194.html

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值