• spring boot使用AOP统一处理web请求


    为了保证服务的高可用,及时发现问题,迅速解决问题,为应用添加log是必不可少的。

    但是随着项目的增大,方法增多,每个方法加单独加日志处理会有很多冗余

    那在SpringBoot项目中如何统一的处理Web请求日志?

    基本思想:

      采用AOP的方式,拦截请求,写入日志

    AOP 是面向切面的编程,就是在运行期通过动态代理的方式对代码进行增强处理

    基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

    1.添加依赖

    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    引入spring-boot-starter-web 依赖之后无需在引入相关的日志依赖,spring-boot-starter-web中已经集成了slf4j 的依赖
    引入spring-boot-starter-aop 依赖之后,AOP 的功能即是启动状态

    2.配置

    application.properties添加

    # AOP
    spring.aop.auto=true
    spring.aop.proxy-target-class=true

    logback-spring.xml,主要是ControllerRequest那部分

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">
    
        <property name="log.path" value="logs" />
    
        <!--0. 日志格式和颜色渲染 -->
        <!-- 彩色日志依赖的渲染类 -->
        <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
        <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
        <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
        <!-- 彩色日志格式 -->
        <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    
        <!--1. 输出到控制台-->
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>info</level>
            </filter>
            <encoder>
                <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
                <!-- 设置字符集 -->
                <charset>UTF-8</charset>
            </encoder>
        </appender>
    
        <!--2. 输出到文档-->
        <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
        <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/debug/debug.log</file>
            <!--日志文档输出格式-->
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                <charset>UTF-8</charset> <!-- 设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 日志归档 -->
                <fileNamePattern>${log.path}/debug/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!--日志文档保留天数-->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录debug级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>debug</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
        <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/info/info.log</file>
            <!--日志文档输出格式-->
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 每天日志归档路径以及格式 -->
                <fileNamePattern>${log.path}/info/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!--日志文档保留天数-->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录info级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>info</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
        <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/warn/warn.log</file>
            <!--日志文档输出格式-->
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                <charset>UTF-8</charset> <!-- 此处设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${log.path}/warn/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!--日志文档保留天数-->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录warn级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>warn</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
        <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/error/error.log</file>
            <!--日志文档输出格式-->
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                <charset>UTF-8</charset> <!-- 此处设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${log.path}/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!--日志文档保留天数-->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录ERROR级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <springProfile name="dev">
            <root level="info">
                <appender-ref ref="CONSOLE" />
                <appender-ref ref="DEBUG_FILE" />
                <appender-ref ref="INFO_FILE" />
                <appender-ref ref="WARN_FILE" />
                <appender-ref ref="ERROR_FILE" />
            </root>
        </springProfile>
    
        <springProfile name="prod">
            <root level="info">
                <appender-ref ref="DEBUG_FILE" />
                <appender-ref ref="INFO_FILE" />
                <appender-ref ref="WARN_FILE" />
                <appender-ref ref="ERROR_FILE" />
            </root>
        </springProfile>
    
        <appender name="ControllerRequest" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${log.path}/request/info.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${log.path}/request/info.log.%d{yyyy-MM-dd}</FileNamePattern>
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <logger name="ControllerRequest" level="DEBUG" additivity="false">
            <appender-ref ref="ControllerRequest"/>
        </logger>
    
    </configuration>
    View Code

    3..实现

    实现切面的注解

      (1)类注解

        A. @Aspect 将一个java类定义为切面类

        B. @order(i) 标记切面类的处理优先级,i值越小,优先级别越高。可以注解类,也能注解到方法上

      (2)方法注解

        A. @Pointcut 定义一个切入点,可以是一个表达式

    execution表达式,eg: 

    任意公共方法的执行
    execution(public * *(..)) 
    
    任何一个以“set”开始的方法的执行
    execution(* set*(..)) 
    
    定义在controller包里的任意方法的执行
    execution(public * com.example.demo.controller.*(..))  
    
    定义在controller包里的任意方法的执行
    execution(public * com.example.demo.controller.*.*(..))  
    
    定义在controller包和所有子包里的任意类的任意方法的执行
    execution(public * com.example.demo.controller..*.*(..))  

        B. 实现在不同的位置切入

    @Before   在切点前执行方法,内容为指定的切点

    @After    在切点后,return前执行

    @AfterReturning  切入点在 return内容之后(可用作处理返回值)

    @Around       切入点在前后切入内容,并自己控制何时执行切入的内容

    @AfterThrowing  处理当切入部分抛出异常后的逻辑

        C.@order(i) 标记切点的优先级,i越小,优先级越高

    @order(i)注解说明

    注解类,i值是,值越小,优先级越高

    注解方法,分两种情况 

    注解的是 @Before 是i值越小,优先级越高

    注解的是 @After或@AfterReturning 中,i值越大,优先级越高

    具体实现

    package com.example.demo.configure;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.DefaultParameterNameDiscoverer;
    import org.springframework.core.ParameterNameDiscoverer;
    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;
    import java.net.InetAddress;
    import java.util.HashMap;
    import java.util.Map;
    
    @Aspect
    @Component
    public class WebRequestLogAspect {
    
        private final Logger loggerController = LoggerFactory.getLogger("ControllerRequest");
        private final Logger logger = LoggerFactory.getLogger(WebRequestLogAspect.class);
        ThreadLocal<Long> startTime = new ThreadLocal<>();
        ThreadLocal<String> beanName = new ThreadLocal<>();
        ThreadLocal<String> user = new ThreadLocal<>();
        ThreadLocal<String> methodName = new ThreadLocal<>();
        ThreadLocal<String> params = new ThreadLocal<>();
        ThreadLocal<String> remoteAddr = new ThreadLocal<>();
        ThreadLocal<String> uri = new ThreadLocal<>();
    
        private static Map<String, Object> getFieldsName(ProceedingJoinPoint joinPoint) {
            // 参数值
            Object[] args = joinPoint.getArgs();
            ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            String[] parameterNames = pnd.getParameterNames(method);
            Map<String, Object> paramMap = new HashMap<>(32);
            for (int i = 0; i < parameterNames.length; i++) {
                paramMap.put(parameterNames[i], args[i] + "(" + args[i].getClass().getSimpleName() + ")");
            }
            return paramMap;
        }
    
        @Pointcut("execution(public * com.example.demo.controller..*.*(..))")
        public void webRequestLog() {
        }
    
        /**
         * 前置通知,方法调用前被调用
         * @param joinPoint
         */
        @Before("webRequestLog()")
        public void doBefore(JoinPoint joinPoint) {
            try {
                startTime.set(System.currentTimeMillis());
                // 接收到请求,记录请求内容
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                beanName.set(joinPoint.getSignature().getDeclaringTypeName());
                methodName.set(joinPoint.getSignature().getName());
                uri.set(request.getRequestURI());
                remoteAddr.set(getIpAddr(request));
                user.set((String) request.getSession().getAttribute("user"));
            } catch (Exception e) {
                logger.error("***操作请求日志记录失败doBefore()***", e);
            }
        }
    
        /**
         * 环绕通知,环绕增强,相当于MethodInterceptor
         * @param thisJoinPoint
         */
        @Around("webRequestLog()")
        public Object proceed(ProceedingJoinPoint thisJoinPoint) throws Throwable {
            Object object = thisJoinPoint.proceed();
            Map<String, Object> fieldsName = getFieldsName(thisJoinPoint);
            params.set(fieldsName.toString());
            return object;
        }
    
        /**
         * 处理完请求返回内容
         * @param result
         */
        @AfterReturning(returning = "result", pointcut = "webRequestLog()")
        public void doAfterReturning(Object result) {
            try {
                long requestTime = (System.currentTimeMillis() - startTime.get()) / 1000;
                loggerController.info("请求耗时:" + requestTime + ", uri=" + uri.get() + "; beanName=" + beanName.get() + "; remoteAddr=" + remoteAddr.get() + "; user=" + user.get()
                        + "; methodName=" + methodName.get() + "; params=" + params.get() + "; RESPONSE : " + result);
    
            } catch (Exception e) {
                logger.error("***操作请求日志记录失败doAfterReturning()***", e);
            }
        }
    
        /**
         * 获取登录用户远程主机ip地址
         *
         * @param request
         * @return
         */
        private String getIpAddr(HttpServletRequest request) {
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
                if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")) {
                    //根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    ip = inet.getHostAddress();
                }
            }
            // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ip != null && ip.length() > 15) {
                if (ip.indexOf(",") > 0) {
                    ip = ip.substring(0, ip.indexOf(","));
                }
            }
            return ip;
        }
    
    }

     4.测试类

    package com.example.demo.controller;
    
    import com.alibaba.fastjson.JSONObject;
    import com.example.demo.dao.UserRepository;
    import com.example.demo.domain.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    import java.util.Map;
    
    @RestController
    public class Demo {
    
        @RequestMapping (value = "test1")
        public String test1(@RequestParam(defaultValue = "0") Integer id,@RequestParam(defaultValue = "0")String name){
    
            return id+name;
        }
    
        @RequestMapping("hello")
        public String hello() {
    
            return "Hello World!";
        }
    
        @PostMapping("/updateStatus")
        public Object updateStatus(@RequestBody JSONObject jsonParam) {
    
            return jsonParam;
        }
    
    }

    输出到logs/request/info.log内容

    2019-09-11 13:31:45.729 [http-nio-8080-exec-4] INFO  ControllerRequest - 请求耗时:0, uri=/test1; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=test1; params={name=abcdef(String), id=123(Integer)}; RESPONSE : 123abcdef
    2019-09-11 13:32:16.692 [http-nio-8080-exec-5] INFO  ControllerRequest - 请求耗时:0, uri=/updateStatus; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=updateStatus; params={jsonParam={"id":"17","type":3,"status":2}(JSONObject)}; RESPONSE : {"id":"17","type":3,"status":2}
    2019-09-11 13:33:32.584 [http-nio-8080-exec-7] INFO  ControllerRequest - 请求耗时:0, uri=/hello; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=hello; params={}; RESPONSE : Hello World!
  • 相关阅读:
    玩个JAVA爬虫,没想玩大
    利用 Ruoyi 开发自己的业务管理系统__测试结构完成
    Vmware 和 VisualSVN-Server端口冲突
    Ruoyi的确不错,不知后续能否坚持 允许商用
    张勇:海底捞店长最高年薪600万!
    自己安装windows版本的Flink
    windows平台上运行Flink_转载于CSDN
    洛谷P3980 [NOI2008]志愿者招募
    线段树优化连边
    [HNOI2013]题解
  • 原文地址:https://www.cnblogs.com/baby123/p/11505784.html
Copyright © 2020-2023  润新知