Spring Boot中使用AOP统一处理Web请求日志

一 点睛

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

二 实战

1 新建pom


    
        org.springframework.boot
        spring-boot-starter
        
            
                org.springframework.boot
                spring-boot-starter-logging
            
        
    
    
        org.springframework.boot
        spring-boot-starter-log4j
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-aop
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    

2 控制器

package com.didispace.web;

import org.springframework.web.bind.annotation.*;

@RestController
public class HelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    @ResponseBody
    public String hello(@RequestParam String name) {
        return "Hello " + name;
    }

}

3 实现web层的日志切面

实现AOP的切面主要有以下几个要素:

  • 使用@Aspect注解将一个java类定义为切面类

  • 使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。

  • 根据需要在切入点不同位置的切入内容

  1. 使用@Before在切入点开始处切入内容

  2. 使用@After在切入点结尾处切入内容

  3. 使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)

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

  5. 使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑

package com.didispace.aspect;

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
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.util.Arrays;


@Aspect
@Order(5)
@Component
public class WebLogAspect {

    private Logger logger = Logger.getLogger(getClass());

    ThreadLocal startTime = new ThreadLocal<>();

    // 通过@Pointcut定义的切入点为com.didispace.web包下的所有函数(对web层所有请求处理做切入点)
    @Pointcut("execution(public * com.didispace.web..*.*(..))")
    public void webLog(){}
    
    // 通过@Before实现,对请求内容的日志记录
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());

        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));

    }

    // 最后通过@AfterReturning记录请求返回的对象。
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        logger.info("RESPONSE : " + ret);
        logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
    }
}

4 启动类

package com.didispace;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

5 log4j.properties配置

# LOG4J配置
log4j.rootCategory=INFO, stdout, file, errorfile
log4j.category.com.didispace=DEBUG, didifile
log4j.logger.error=errorfile

# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n

# root日志输出
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.file=logs/all.log
log4j.appender.file.DatePattern='.'yyyy-MM-dd
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n

# error日志输出
log4j.appender.errorfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.errorfile.file=logs/error.log
log4j.appender.errorfile.DatePattern='.'yyyy-MM-dd
log4j.appender.errorfile.Threshold = ERROR
log4j.appender.errorfile.layout=org.apache.log4j.PatternLayout
log4j.appender.errorfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n

# com.didispace下的日志输出
log4j.appender.didifile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.didifile.file=logs/my.log
log4j.appender.didifile.DatePattern='.'yyyy-MM-dd
log4j.appender.didifile.layout=org.apache.log4j.PatternLayout
log4j.appender.didifile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L ---- %m%n

三 测试结果

1 浏览器输入:

http://localhost:8080/hello?name=didi

2 控制台输出:

2018-11-03 14:08:59,581  INFO WebLogAspect:46 - URL : http://localhost:8080/hello

2018-11-03 14:08:59,582  INFO WebLogAspect:47 - HTTP_METHOD : GET

2018-11-03 14:08:59,582  INFO WebLogAspect:48 - IP : 0:0:0:0:0:0:0:1

2018-11-03 14:08:59,585  INFO WebLogAspect:49 - CLASS_METHOD : com.didispace.web.HelloController.hello

2018-11-03 14:08:59,585  INFO WebLogAspect:50 - ARGS : [didi]

2018-11-03 14:08:59,605  INFO WebLogAspect:57 - RESPONSE : Hello didi

2018-11-03 14:08:59,607  INFO WebLogAspect:58 - SPEND TIME : 26

四 补充

1 AOP切面的优先级

由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。

所以,我们需要定义每个切面的优先级,我们需要@Order(i)注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect用来校验name必须为didi,我们为其设置@Order(10),而上文中WebLogAspect设置为@Order(5),所以WebLogAspect有更高的优先级,这个时候执行顺序是这样的:

  • 在@Before中优先执行@Order(5)的内容,再执行@Order(10)的内容

  • 在@After和@AfterReturning中优先执行@Order(10)的内容,再执行@Order(5)的内容

所以我们可以这样子总结:

  • 在切入点前的操作,按order的值由小到大执行

  • 在切入点后的操作,按order的值由大到小执行

五 参考

http://blog.didispace.com/springbootaoplog/

 

 

你可能感兴趣的:(Spring,Boot)