美文网首页SpringJava程序员
自定义注解&Spring AOP实现日志组件(可重用)

自定义注解&Spring AOP实现日志组件(可重用)

作者: markfork | 来源:发表于2016-11-26 21:34 被阅读1208次

    项目需求


    **需求1: **web项目一般而言都需要日志记录,比较常用的是实用log4j来记录项目的异常日志,将日志单独存储于文件当中,这样有利于我们快速进行bug 排解。
    **需求2: **异常的记录一般就是将异常的堆栈保存在文件中,这样文件大小会急剧上升,有效异常信息也不能被立即定位,有没有一种方式可以可以让我们重写异常记录,并保存在异常日志文件当中呢。
    **需求3: **在异常日志之上,我们一般还需要对系统中各角色的各个重要操作进行一些日志记录,以方便我们找到操作人,以及责任人。

    针对1中的需求,我们大家熟悉的是实用log4j进行日志记录。配置简单,实现快速。
    针对2,3中的需求我们一般采用的是基于拦截器实现(Aop思想的一种实现方式)在方法操作之前进行一定的处理,获取操作人、操作方法名、操作参数,异常捕获与记录,这样实现也是完全可以的。
    今天记录的是基于自定义注解面向切面(AOP)进行统一操作日志以及异常日志记录的实现。

    项目代码


    项目中代码如下所示:
    1.首先定义两个注解:分别为SystemControllerLog(用于拦截Controller层操作注解,起切点表达式作用,明确切面应该从哪里注入),SystemServiceLog(用于拦截Service层操作注解,起切点表达式作用,明确切面应该从哪里注入)
    这两个注解在切面中定义切点表达式的时候会用到。
    SystemControllerLog.java

    package com.fxmms.common.log.logannotation;
    import java.lang.annotation.*;
    
    /**
     * Created by mark on 16/11/25.
     * @usage 自定义注解,拦截Controller
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SystemControllerLog {
        String description() default "";
    }
    

    SystemServiceLog

    package com.fxmms.common.log.logannotation;
    import java.lang.annotation.*;
    
    /**
     * Created by mark on 16/11/25.
     * @usage 自定义注解 拦截service
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SystemServiceLog {
        String description() default "";
    }
    

    2.接下来定义切面类,这里面主要定义了几个通知,在调用被代理对象目标方法前后,或目标方法抛出异常之后使用。

    package com.fxmms.common.log.logaspect;
    import com.fxmms.common.log.logannotation.SystemControllerLog;
    import com.fxmms.common.log.logannotation.SystemServiceLog;
    import com.fxmms.common.security.ScottSecurityUtil;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    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.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    
    /**
     * 切点类
     * @author tiangai
     * @since 2014-08-05 Pm 20:35
     * @version 1.0
     */
    @Aspect
    @Component
    public class SystemLogAspect {
        //注入Service用于把日志保存数据库 nodo service 层实现
    
        //本地异常日志记录对象
        private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
    
        /**
         * Service层切点 使用到了我们定义的 SystemServiceLog 作为切点表达式。
         * 而且我们可以看出此表达式基于 annotation。
         *
         */
        @Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemServiceLog)")
        public void serviceAspect() {
        }
    
        /**
         * Controller层切点 使用到了我们定义的 SystemControllerLog 作为切点表达式。
         * 而且我们可以看出此表达式是基于 annotation 的。
         */
        @Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemControllerLog)")
        public void controllerAspect() {
        }
    
        /**
         * 前置通知 用于拦截Controller层记录用户的操作
         *
         * @param joinPoint 连接点
         */
        @Before("controllerAspect()")
        public void doBefore(JoinPoint joinPoint) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //请求的IP
            String ip = request.getRemoteAddr();
            System.out.println(ip+"sdsdsdsdsd");
            try {
                //控制台输出
                System.out.println("=====前置通知开始=====");
                Object object = joinPoint.getTarget();
                System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
                System.out.println("方法描述:" + getControllerMethodDescription(joinPoint));
                System.out.println("请求人:" + ScottSecurityUtil.getLoginName());
                System.out.println("请求IP:" + ip);
                //构造数据库日志对象
    
                //保存数据库
    
                System.out.println("=====前置通知结束=====");
            } catch (Exception e) {
                //记录本地异常日志
                logger.error("==前置通知异常==");
                logger.error("异常信息:{}", e.getMessage());
            }
        }
    
        /**
         * 异常通知 用于拦截service层记录异常日志
         *
         * @param joinPoint
         * @param e
         */
        @AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
        public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //获取请求ip
            String ip = request.getRemoteAddr();
            //获取用户请求方法的参数并组织成字符串
            String params = "";
            if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
                for (int i = 0; i < joinPoint.getArgs().length; i++) {
                    params += joinPoint.getArgs()[i]+ ",";
                }
            }
            try {
                //控制台输出
                System.out.println("=====异常通知开始=====");
                System.out.println("异常代码:" + e.getClass().getName());
                System.out.println("异常信息:" + e.getMessage());
                System.out.println("异常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
                System.out.println("方法描述:" + getServiceMthodDescription(joinPoint));
                System.out.println("请求人:" + ScottSecurityUtil.getLoginName());
                System.out.println("请求IP:" + ip);
                System.out.println("请求参数:" + params);
                //构造数据库日志对象
    
                //保存数据库
    
                System.out.println("=====异常通知结束=====");
            } catch (Exception ex) {
                //记录本地异常日志
                logger.error("==异常通知异常==");
                logger.error("异常信息:{}", ex);
            }
             //录本地异常日志
            logger.error("异常方法:{}异常代码:{}异常信息:{}参数:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);
        }
    
        /**
         * 获取注解中对方法的描述信息 用于service层注解
         *
         * @param joinPoint 切点
         * @return 方法描述
         * @throws Exception
         */
        public static String getServiceMthodDescription(JoinPoint joinPoint)
                throws Exception {
            String targetName = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            Object[] arguments = joinPoint.getArgs();
            Class targetClass = Class.forName(targetName);
            Method[] methods = targetClass.getMethods();
            String description = "";
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    Class[] clazzs = method.getParameterTypes();
                    if (clazzs.length == arguments.length) {
                        description = method.getAnnotation(SystemServiceLog.class).description();
                        break;
                    }
                }
            }
            return description;
        }
    
        /**
         * 获取注解中对方法的描述信息 用于Controller层注解
         *
         * @param joinPoint 切点
         * @return 方法描述
         * @throws Exception
         */
        public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
            String targetName = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            Object[] arguments = joinPoint.getArgs();
            Class targetClass = Class.forName(targetName);
            Method[] methods = targetClass.getMethods();
            String description = "";
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    Class[] clazzs = method.getParameterTypes();
                    if (clazzs.length == arguments.length) {
                        description = method.getAnnotation(SystemControllerLog.class).description();
                        break;
                    }
                }
            }
            return description;
        }
    }
    

    上面的切面类中定义了公共切点 serviceAspectserviceAspect,并实现了Controller层的前置通知,Service业务逻辑层的异常通知。
    其中预留了保存日志到数据库的代码段,我们可以根据业务自行进行填充。
    3.创建好切点、切面类之后,如何让他起作用呢,我们需要在配置文件中进行配置了。我将web项目中关于不同层的配置文件进行的切割,数据访问层配置文件是data-access.xml、业务逻辑层是service-application.xml、控制层是defalut-servlet.xml
    首先看defalut-servlet.xml中的配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">
        <!--开启aop-->
        <aop:aspectj-autoproxy proxy-target-class="true"/>
        <mvc:annotation-driven>
            <!--json解析-->
            <mvc:message-converters>
                <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
            </mvc:message-converters>
        </mvc:annotation-driven>
    
        <context:component-scan base-package="com.fxmms.www.controller">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        </context:component-scan>
        <!--扫描日志记录切面-->
        <context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
        </context:component-scan>
        <!--配置异常处理器-->
        <context:component-scan base-package="com.fxmms.common.exception_handler" use-default-filters="false">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
        </context:component-scan>
        <!--因为web.xml中defaultDispatcherServlet对所有请求进行了拦截,所以对一些.css .jpg .html .jsp也进行了拦截,所以此配置项
        保证对对静态资源不拦截-->
        <mvc:default-servlet-handler/>
        <!--视图解析器-->
        <bean id="viewResolver"
              class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/"/>
            <property name="suffix" value=".jsp"/>
        </bean>
        <!--配置文件上上传-->
        <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
            <property name="defaultEncoding" value="utf-8"/>
            <property name="maxUploadSize" value="10485760000"/>
            <property name="maxInMemorySize" value="40960"/>
        </bean>
    </beans>
    

    注意:以上配置有两个重要的点:

    1.项,
    2. <aop:aspectj-autoproxy proxy-target-class="true"/>
    proxy-target-class="true"默认是false,更改为true时使用的是cglib动态代理。这样只能实现对Controller层的日志记录。

    service-application.xml配置AOP,实现对Service层的日志记录.

    <?xml version="1.0" encoding="UTF-8"?>
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:task="http://www.springframework.org/schema/task"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/tx
      http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/task
            http://www.springframework.org/schema/task/spring-task.xsd">
        <!--开启AOP-->
        <aop:aspectj-autoproxy/>
    
        <!--设置定时任务-->
        <task:annotation-driven/>
        <context:component-scan base-package="com.fxmms.www" use-default-filters="false">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        </context:component-scan>
        <!--ioc管理切面-->
        <context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
        </context:component-scan>
    
        <!-- enable the configuration of transactional behavior based on annotations -->
        <tx:annotation-driven transaction-manager="txManager"/>
    
        <bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
            <property name="sessionFactory" ref="sessionFactory"/>
        </bean>
    
    </beans>
    

    这样Service也是可以实现操作、异常日志记录了。
    4.在代码中使用自定义注解,相当于在目标方法上设置了一个切点,通过切点注入切面。
    Controller层上运用SystemControllerLog注解:
    TestNullPointExceptionController.java(验证Controller层中异常,Controller中调用Service层代码)

    package com.fxmms.www.controller.admin;
    
    import com.fxmms.common.log.logannotation.SystemControllerLog;
    import com.fxmms.www.service.TestExceptionLog;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    /**
     * Created by mark on 16/11/25.
     */
    @Controller
    public class TestNullPointExceptionController {
        private static Log logger = LogFactory.getLog(TestNullPointExceptionController.class);
        //自动注入一个Service层类对象
        @Autowired
        TestExceptionLog testExceptionLog;
    
        @ResponseBody
        @RequestMapping("/admin/testexcption")
        @SystemControllerLog(description = "testException")//使用   SystemControllerLog注解,此为切点
        public String testException(String str){
            return testExceptionLog.equalStr(str);
        }
    }
    

    ** TestExceptionLog.java**

    package com.fxmms.www.service;
    
    import com.fxmms.common.log.logannotation.SystemServiceLog;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.stereotype.Service;
    
    /**
     * Created by mark on 16/11/25.
     */
    @Service
    public class TestExceptionLog {
        private static Log logger = LogFactory.getLog(TestExceptionLog.class);
    
        @SystemServiceLog(description = "equalstr")
        public String equalStr(String str) {
            str = null;
            if (str.equals("sd")) {
                return "sd";
            }else {
                return "sd";
            }
        }
    }
    

    我在其中手动设置str = null,用于模拟前台输入。
    程序在运行时会报运行时异常。
    最终启动项目项目console日志输出如下图所示:


    日志控制台输出 error.log输出

    这样就完成了自定义注解&Aop&自定义异常&操作日志的记录,而且自定义的注解与切面可以进行重用,操作日志与异常日志可以进行数据库记录,后期甚至可以做一个关于异常分析的系统,我们可以直接从日志后台系统中查看异常出现的频率,以及定位异常的发声位置,明确操作人等。
    完。

    博客搬家:大坤的个人博客
    欢迎评论哦~

    相关文章

      网友评论

      • ce5a182db8aa:十分感谢您!!!!
      • ce5a182db8aa:可以把这篇文章的源码给我发一份吗?我的邮箱是15538323378@163.com
        ce5a182db8aa:@fxliutao 我的邮箱是15538323378@163.com
        ce5a182db8aa:@fxliuta您有空的时候,帮忙发给我一下!谢谢您!对我有很大帮助!
        markfork:@辰夕小狼睡不醒 :
        稍后,晚上发你~

      本文标题:自定义注解&Spring AOP实现日志组件(可重用)

      本文链接:https://www.haomeiwen.com/subject/qdqgpttx.html