spring boot实现AOP打印日志功能
简单谈一谈我对AOP的理解。
已简单的web请求为例,从Controller收到一个请求,到实现业务功能,再到与数据库交互,是一个链条一样的结构,几乎每一个链条都分可为上述的三层。
例如:Controller层会有记录http请求到日志的需求,且各个链条这部分的逻辑通常都是一致的。从面向对象的角度考虑,如果每一根链条都单独实现这个需求,会造成代码的冗余以及难以维护,因此就出现了用一份代码实现,在所有Controller获得请求时记录一条日志的设计,这就好像是一个穿插在所有链条上的一个很切面,与各个链条相交的点可以称之为切点,这就是我所理解的AOP的设计思想。
这里用AOP实现了打印HTTP记录HTTP请求到日志中的小例子,定义一个User类,实现一个查询用户的方法,访问这个方法时,将HTTP请求的内容记录到日志中。直接上代码:
1.pom.xml文件
主要引入了三个依赖【spring-boot-starter】【spring-boot-starter-web】【spring-boot-starter-aop】
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.monkey.springboot</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.User类
【@Component】改注解将User对象添加到spring容器中
package org.monkey.springboot.demo.entity;
import org.springframework.stereotype.Component;
@Component
public class User {
private Integer id;
private String name;
private String password;
private String gender;
private int status;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", status=" + status +
'}';
}
}
3.接收请求的UserController
【@RestController】注解表示该类是一个Controller;
【@RequestMapping("/user")】为该类设置一个URL前缀 /user;
在getUsers方法中,创建三个user对象并返回到页面模拟获取用户的操作;
参数【id】的作用,是便于在利用AOP打印参数时,使效果更加直观。
package org.monkey.springboot.demo.controller;
import org.monkey.springboot.demo.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.server.PathParam;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/list/{id}")
public List<User> getUsers(@PathVariable("id") Integer id) {
logger.info("getUsers");
logger.info("id = {}", id);
List<User> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
User user = new User();
user.setId(i);
user.setName("name-" + i);
user.setPassword("password-" + i);
user.setGender(i % 2 == 0 ? "男" : "女");
user.setStatus(0);
users.add(user);
}
return users;
}
}
4.AOP的关键类HttpAspect
【@Aspect】注解说明这是AOP的类(不知道官方说法是啥,姑且这么认为吧)
【@Pointcut】整个类可以认为是一个切面,该注解标注的是一个切点,(前面提到的面和链条的交点)
package org.monkey.springboot.demo.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class HttpAspect {
private static final Logger logger = LoggerFactory.getLogger(HttpAspect.class);
// 切点,用于匹配所要执行功能的方法
@Pointcut("execution(public * org.monkey.springboot.demo.controller.UserController.*(..))")
public void log(){
}
// @Before该注解下的方法,要在执行与切点匹配的方法之前执行,匹配模式有log() 决定
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// url
logger.info("url={}.", request.getRequestURL());
// method
logger.info("method={}.", request.getMethod());
// ip
logger.info("ip={}.", request.getRemoteAddr());
// 类方法
logger.info("class_method={}.", joinPoint.getSignature().getDeclaringTypeName() + "." +
joinPoint.getSignature().getName());
// 参数
logger.info("args={}.", joinPoint.getArgs());
}
// @After该注解下的方法,要在执行与切点匹配的方法之后执行,匹配模式有log() 决定
@After("log()")
public void doAfter() {
logger.info("doAfter");
}
// @After该注解下的方法,要在执行与切点匹配的方法之后执行,该注解能够获取方法执行完成之后返回值,匹配模式有log() 决定
@AfterReturning(returning = "object", pointcut = "log()")
public void doAfterReturn(Object object) {
logger.info("returning={}.", object);
}
}
最后,再贴一下我的运行结果