系统日志对于定位/排查问题的重要性不言而喻,相信许多开发和运维都深有体会。git
经过日志追踪代码运行情况,模拟系统执行状况,并迅速定位代码/部署环境问题。程序员
系统日志一样也是数据统计/建模的重要依据,经过分析系统日志能窥探出许多隐晦的内容。数据库
如系统的健壮性(服务并发访问/数据库交互/总体响应时间...)mybatis
某位用户的喜爱(分析用户操做习惯,推送对口内容...)并发
固然系统开发者还不知足于日志组件打印出来的日志,毕竟冗余且篇幅巨长。mvc
so,对于关键的系统操做设计日志表,并在代码中进行操做的记录,配合 SQL 统计和搜索数据是件很愉快的事情。运维
本篇旨在总结在 Spring 下使用 AOP 注解方式进行日志记录的过程,若是能对你有所启发阁下不甚感激。ide
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectjweaver.version}</version> </dependency>
AspectJ 中的不少语法结构基本上已成为 AOP 领域的标准。函数
Spring 也有本身的 Spring-AOP,采用运行时生成代理类,底层能够选用 JDK 或者 CGLIB 动态代理。性能
通俗点,AspectJ 在编译时加强要切入的类,而 Spring-AOP 是在运行时经过代理类加强切入的类,效率和性能可想而知。
Spring 在 2.0 的时候就已经开始支持 AspectJ ,如今到 4.X 的时代已经很完美的和 AspectJ 拥抱到了一块儿。
开启扫描 AspectJ 注解的支持:
<!-- proxy-target-class等于true是强制使用cglib代理,proxy-target-class默认false,若是你的类实现了接口 就走JDK代理,若是没有,走cglib代理 --> <!-- 注:对于单利模式建议使用cglib代理,虽然JDK动态代理比cglib代理速度快,但性能不如cglib --> <aop:aspectj-autoproxy proxy-target-class="true"/>
目标操做日志表,其中设计了一些必要的字段,具体字段请拿捏具体项目场景,根据表结构设计注解以下。
@Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String operationModular() default ""; String operationContent() default ""; }
上述我只作了两个必要的参数,一个为操做的模块,一个为具体的操做内容。
其实根据项目场景这里参数的设计能够很是丰富,不被其余程序员吐槽在此一举。
@Pointcut("@annotation(com.rambo.spm.common.aop.OperationLog)") public void operationLogAspect() { }
类的构造函数上描述了该类要拦截的为 OperationLog 的注解方法, 一样你也能够配置 XML 进行拦截。
切入点的姿式有不少,不只是正则一样也支持组合表达式,强大的表达式能让你精准的切入到任何你想要的地方。
更多详情:http://blog.csdn.net/zhengchao1991/article/details/53391244
看到这里若是你对 Spring AOP 数据库事务控制熟悉,其实 Spring AOP 记录日志是类似的机制。
@Before("operationLogAspect()") public void doBefore(JoinPoint joinPoint) { logger.info("before aop:{}", joinPoint); //do something } @Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); //do somthing } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 异常信息:{}", throwable.getMessage()); } return proceed; } @AfterThrowing("operationLogAspect()") public void doAfterThrowing(JoinPoint pjp) { logger.info("@After:{}", pjp); //do somthing } @After("operationLogAspect()") public void doAfter(JoinPoint pjp) { logger.info("@After:{}", pjp); } @AfterReturning("operationLogAspect()") public void doAfterReturning(JoinPoint point) { logger.info("@AfterReturning:{}", point); }
AspectJ 提供了几种通知方法,经过在方法上注解这几种通知,解析对应的方法入参,你就能洞悉切点的一切运行状况。
前置通知(@Before):在某链接点(join point)以前执行的通知,但这个通知不能阻止链接点前的执行(除非它抛出一个异常);
返回后通知(@AfterReturning):在某链接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回;
抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知;
后通知(@After):当某链接点退出的时候执行的通知(不管是正常返回仍是异常退出);
环绕通知(@Around):包围一个链接点(joinpoint)的通知,如方法调用;
通知方法中的值与构造函数一致,指定该通知对哪一个切点有效,
上述 @Around 为最强大的一种通知类型,能够在方法调用先后完成自定义的行为,它可选择是否继续执行切点、直接返回、抛出异常来结束执行。
@Around 之因此如此强大是和它的入参有关,别的注解注解入参只允许 JoinPoint ,而 @Around 注解允许入参 ProceedingJoinPoint。
package org.aspectj.lang; import org.aspectj.runtime.internal.AroundClosure; public interface ProceedingJoinPoint extends JoinPoint { void set$AroundClosure(AroundClosure var1); Object proceed() throws Throwable; Object proceed(Object[] var1) throws Throwable; }
反编译 ProceedingJoinPoint 你会恍然大悟,Proceedingjoinpoint 继承了 JoinPoint 。
在 JoinPoint 的基础上暴露出 proceed 这个方法。proceed 方法很重要,这是 aop 代理链执行的方法。
暴露出这个方法,就能支持 aop:around 这种切面(而其余的几种切面只须要用到 JoinPoint,这跟切面类型有关), 能决定是否走代理链仍是走本身拦截的其余逻辑。
若是项目没有特定的需求,妥善使用 @Around 注解就能帮你解决一切问题。
@Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); Object pointTarget = point.getTarget(); Signature pointSignature = point.getSignature(); String targetName = pointTarget.getClass().getName(); String methodName = pointSignature.getName(); Method method = pointTarget.getClass().getMethod(pointSignature.getName(), ((MethodSignature) pointSignature).getParameterTypes()); OperationLog methodAnnotation = method.getAnnotation(OperationLog.class); String operationModular = methodAnnotation.operationModular(); String operationContent = methodAnnotation.operationContent(); OperationLogPO log = new OperationLogPO(); log.setOperUserid(SecureUtil.simpleUUID()); log.setOperUserip(HttpUtil.getClientIP(getHttpReq())); log.setOperModular(operationModular); log.setOperContent(operationContent); log.setOperClass(targetName); log.setOperMethod(methodName); log.setOperTime(new Date()); log.setOperResult("Y"); operationLogService.insert(log); } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 异常信息:{}", throwable.getMessage()); } return proceed; }
别忘记将上面切点处理类/和要切入的类托管给 Spring,Aop 日志是否是很简单,复杂的应该是 aspectj 内部实现机制,有机会要看看源码哦。
处理切点类完整代码:
@Aspect @Component public class OperationLogAspect { private static final Logger logger = LoggerFactory.getLogger(OperationLogAspect.class); //ProceedingJoinPoint 与 JoinPoint //注入Service用于把日志保存数据库 //这里我用resource注解,通常用的是@Autowired,他们的区别若有时间我会在后面的博客中来写 @Resource private OperationLogService operationLogService; //@Pointcut("execution (* com.rambo.spm.*.controller..*.*(..))") @Pointcut("@annotation(com.rambo.spm.common.aop.OperationLog)") public void operationLogAspect() { } @Before("operationLogAspect()") public void doBefore(JoinPoint joinPoint) { logger.info("before aop:{}", joinPoint); gePointMsg(joinPoint); } @Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); Object pointTarget = point.getTarget(); Signature pointSignature = point.getSignature(); String targetName = pointTarget.getClass().getName(); String methodName = pointSignature.getName(); Method method = pointTarget.getClass().getMethod(pointSignature.getName(), ((MethodSignature) pointSignature).getParameterTypes()); OperationLog methodAnnotation = method.getAnnotation(OperationLog.class); String operationModular = methodAnnotation.operationModular(); String operationContent = methodAnnotation.operationContent(); OperationLogPO log = new OperationLogPO(); log.setOperUserid(SecureUtil.simpleUUID()); log.setOperUserip(HttpUtil.getClientIP(getHttpReq())); log.setOperModular(operationModular); log.setOperContent(operationContent); log.setOperClass(targetName); log.setOperMethod(methodName); log.setOperTime(new Date()); log.setOperResult("Y"); operationLogService.insert(log); } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 异常信息:{}", throwable.getMessage()); } return proceed; } @AfterThrowing("operationLogAspect()") public void doAfterThrowing(JoinPoint pjp) { logger.info("@AfterThrowing:{}", pjp); } @After("operationLogAspect()") public void doAfter(JoinPoint pjp) { logger.info("@After:{}", pjp); } @AfterReturning("operationLogAspect()") public void doAfterReturning(JoinPoint point) { logger.info("@AfterReturning:{}", point); } private void gePointMsg(JoinPoint joinPoint) { logger.info("切点所在位置:{}", joinPoint.toString()); logger.info("切点所在位置的简短信息:{}", joinPoint.toShortString()); logger.info("切点所在位置的所有信息:{}", joinPoint.toLongString()); logger.info("切点AOP代理对象:{}", joinPoint.getThis()); logger.info("切点目标对象:{}", joinPoint.getTarget()); logger.info("切点被通知方法参数列表:{}", joinPoint.getArgs()); logger.info("切点签名:{}", joinPoint.getSignature()); logger.info("切点方法所在类文件中位置:{}", joinPoint.getSourceLocation()); logger.info("切点类型:{}", joinPoint.getKind()); logger.info("切点静态部分:{}", joinPoint.getStaticPart()); } private HttpServletRequest getHttpReq() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; return servletRequestAttributes.getRequest(); } }
上述三步骤以后,你就能够在想记录日志的方法上面添加注解来进行记录操做日志,像下面这样。
源码托管地址:https://git.oschina.net/LanboEx/spmvc-mybatis.git 有这方面需求和兴趣的能够检出到本地跑一跑。