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  有这方面需求和兴趣的可以检出到本地跑一跑。

      

  • 相关阅读:
    Djano restframework
    python测试一
    SQL分类,DDL,DML,DCL
    sql查询时,根据特定的条件给表的某一个字段赋值
    数据类型之Nullable
    web.config节点
    拼凑的宿主-host
    css的优先级
    jquery——write less,do more
    double类型计算
  • 原文地址:https://www.cnblogs.com/java-class/p/7261937.html
Copyright © 2011-2022 走看看