zoukankan      html  css  js  c++  java
  • 来一手 AOP 注解方式进行日志记录

       系统日志对于定位/排查问题的重要性不言而喻,相信许多开发和运维都深有体会。

       通过日志追踪代码运行状况,模拟系统执行情况,并迅速定位代码/部署环境问题。

       系统日志同样也是数据统计/建模的重要依据,通过分析系统日志能窥探出许多隐晦的内容。

       如系统的健壮性(服务并发访问/数据库交互/整体响应时间...)

       某位用户的喜好(分析用户操作习惯,推送对口内容...)

       当然系统开发者还不满足于日志组件打印出来的日志,毕竟冗余且篇幅巨长。

       so,对于关键的系统操作设计日志表,并在代码中进行操作的记录,配合 SQL 统计和搜索数据是件很愉快的事情。

       本篇旨在总结在 Spring 下使用 AOP 注解方式进行日志记录的过程,如果能对你有所启发阁下不甚感激。

    1. 依赖类库

           <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"/>

    2. 定义切入点日志注解

       

       目标操作日志表,其中设计了一些必要的字段,具体字段请拿捏具体项目场景,根据表结构设计注解如下。

    @Inherited
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface OperationLog {
    
        String operationModular() default "";
    
        String operationContent() default "";
    }

       上述我只做了两个必要的参数,一个为操作的模块,一个为具体的操作内容。

       其实根据项目场景这里参数的设计可以非常丰富,不被其他程序员吐槽在此一举。

    3. 编写处理日志切点类

       @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();
        }
    }
    View Code

       上述三步骤之后,你就可以在想记录日志的方法上面添加注解来进行记录操作日志,像下面这样。

        源码托管地址:https://git.oschina.net/LanboEx/spmvc-mybatis.git  有这方面需求和兴趣的可以检出到本地跑一跑。

      

  • 相关阅读:
    Leetcode 191.位1的个数 By Python
    反向传播的推导
    Leetcode 268.缺失数字 By Python
    Leetcode 326.3的幂 By Python
    Leetcode 28.实现strStr() By Python
    Leetcode 7.反转整数 By Python
    Leetcode 125.验证回文串 By Python
    Leetcode 1.两数之和 By Python
    Hdoj 1008.Elevator 题解
    TZOJ 车辆拥挤相互往里走
  • 原文地址:https://www.cnblogs.com/java-class/p/7261937.html
Copyright © 2011-2022 走看看