介绍
相信大家都了解过 AOP 的概念,“面向切面编程”,在本章节中我们会给大家介绍使用切面编程的作用,常见的使用切面的应用场景在哪里,以及当我们使用 Spring Boot 的时候怎样通过使用定义的切面来具体拦截我们的请求日志信息并且记录下 UserAgent 信息。在 AOP 中的一些注解的原理介绍以及在使用过程中会遇到的有用的工具类,希望大家在学习之后也会有所收获。
知识点
- AOP 面向切面编程的作用及应用场景
- Spring Boot 使用 AOP 拦截请求日志操作流程
- AOP 的相关注解的讲述
本课程内容中涉及 Spring Boot 项目源代码来自 xkcoding/spring-boot-demo,该项目使用 MIT License 开源协议。
AOP 面向切面编程的作用及应用场景
我们已经学习过了 Spring Boot 整合日志框架 Logback 的具体的方法,了解了日志在常规的日常开发过程中的重要性,这里会引进一个问题就是像 Logback 这种 Spring Boot 已经原生集成好了的日志框架是将日志记录到文件,那为什么我们还需要使用 AOP 来记录日志到文件呢?
对于这我们得从项目开发过程中代码整体的可读性和维护性层面来说明,因为 AOP 面向切面编程我们是将非业务逻辑层面的代码单独抽离出来,而不影响业务逻辑代码整体的完整性。如果在业务逻辑代码中添加上很多的输出调试信息的话,肯定是会影响整体代码的可读性的。如果是针对日志的非业务逻辑实现的话,日志的代码往往是水平地散步在我们所有的对象层面中的,我们将这一部分抽象成为一个切面之后后期的维护和修改就可以将这部分的输入输出的信息在切面进行单独的修改了。
并且具体使用 AOP 的业务场景挺多的,包括安全性权限认证、异常处理、日志信息、事务等都可以用到 AOP 的思想来获取我们 OOP(面向对象编程)封装好的对象内部那些影响类多个类的公共行为单独抽离出来定义为一个切面,这也是我们后面讲到的具体利用 Spring Boot 使用 AOP 来拦截请求进行日志信息记录的一个具体的实施过程。这样子我们也可以减少系统本身的代码,降低模块之间的耦合度。具体的使用的应用场景可以如以下图所示:
AOP 的作用和具体应用场景
这里我们会介绍一下 AOP 以静态织入的方式来定义一个切面的具体方法以及常见的语法。
我们在学习 AOP 的切面编程的方法的时候应该带着这三个方向,我们应该在哪里去切,在什么时候切入,切入过后应该要做哪些事情。下面是一张说明 AOP 整体体系的结构图。
从上面图中我们需要梳理一些基本的概念:
- Pointcut:切点,主要是决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为 execution 方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
- Advice:处理(通知),包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。这也回答了我们之前提到的什么时候切入,切入过后应该要做哪些事情。
- Aspect:切面,即 Pointcut 和 Advice,在代码中就是我们定义的这个切面的类,选择在什么地方织入。
- Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 AOP 中,一个连接点总是代表着一个方法执行。
- Weaving:织入,就是通过动态代理的方式在目标对象方法中执行处理内容的过程。
在项目中使用 AOP 我们需要在 pom.xml
文件中引入以下的依赖情况:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
下面我们通过具体的一个实例 demo 来看看 Spring Boot 是怎么使用 AOP 来拦截请求日志的吧。
AOP 的具体实现方式
首先我们还是需要创建一个 Maven 工程的,打开我们线上的 WebIDE 环境,在终端中可以输入以下的命令,可以很方便地创建我们初始的一个项目结构:
wget https://labfile.oss.aliyuncs.com/courses/3256/spring-boot-demo.zip
然后解压我们下载下来的这个项目压缩包:
unzip spring-boot-demo.zip
打开解压缩出来的这个文件夹,我们刚开始得到的一个项目的结构如下图所示,所有的文件夹基本都是空的,接下来跟着教程边学习边进行相应的配置,注意我们之后一定要把工作空间切换到当前项目路径下面: /home/project/spring-boot-demo
。
首先对我们的 pom.xml
文件进行如下的修改:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.1</version>
</dependency>
<!-- 解析 UserAgent 信息 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
</dependencies>
<build>
<finalName>spring-boot-demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
另外我们还引进了 Lombok 的依赖,在上一章节中我们为了和 Logback 日志框架更好的配置使用,简化我们的代码的操作,使用了 Lombok,这里也是一样的原因,我们可以直接使用 @Slf4J
注解就可以简化我们的代码结构:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
这里需要重点注意的是我们引进了另外两个重要的工具:一个是 Hutool,另外一个是 User-agent-utils。
Spring Boot 使用 AOP 拦截请求日志操作流程
Hutool 工具包
Hutool 可以理解为是一个功能较为齐全的 Java 辅助的工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,之前在开发项目中,我们可能会命名一个 util 包来专门编写和存放我们的各种 utils 工具类,Hutool 对这个就做到了很好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发者可以专注于业务,同时可以最大限度的避免封装不完善带来的 bug。它集成了很多功能,我们也可以理解为 Hutool 对许多的工具的包进行了二次的封装,它主要对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,组成各种 Util 工具类,同时提供了以下组件:
- hutool-aop :JDK 动态代理封装,提供非 IOC 下的切面支持。
- hutool-bloomFilter:布隆过滤,提供一些 Hash 算法的布隆过滤。
- hutool-cache:缓存。
- hutool-core:核心,包括 Bean 操作、日期、各种 Utils 等。
- hutool-cron:定时任务模块,提供类 Crontab 表达式的定时任务。
- hutool-crypto:加密解密模块。
- hutool-db:JDBC 封装后的数据操作,基于 ActiveRecord 思想。
- hutool-dfa:基于 DFA 模型的多关键字查找。
- hutool-extra:扩展模块,对第三方封装(模板引擎、邮件等)。
- hutool-http:基于 HttpUrlConnection 的 Http 客户端封装。
- hutool-log:自动识别日志实现的日志门面。
- hutool-script:脚本执行封装,例如 Javascript。
- hutool-setting:功能更强大的 Setting 配置文件和 Properties 封装。
- hutool-system:系统参数调用封装(JVM 信息等)。
- hutool-json:JSON 实现。
- hutool-captcha:图片验证码实现。
在 pom.xml
文件中的引入:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.1</version>
</dependency>
Hutool 的官方介绍中名字的由来也很有趣,“Hutool = Hu + tool”,是原公司项目底层代码剥离后的开源库,“Hu”是该公司名称的表示,tool 表示工具。Hutool 谐音“糊涂”,一方面简洁易懂,一方面寓意“难得糊涂”。它的目标也是希望能够通过一个工具方法来代替一段复杂代码,从而最大限度的避免“复制粘贴”代码的问题,彻底改变我们写代码的方式。
这里截取了部分的 Hutool 中所包含的工具方法,大家可以大致浏览一下:
User-agent-utils
有时候如果我们业务上有一些需求需要获取一些信息比如:浏览器名字,浏览器组,浏览器类型,浏览器版本,浏览器的渲染引擎,android 和 ios 设备的类型,操作系统相关信息等,这个时候我们就需要和 UserAgent 来打交道了,通过 UserAgent 我们可以方便地获取浏览器/操作系统/设备类型等信息。
在官网bitwalker.eu的介绍中我们看到:
User-agent-utils 可以被用于实时地解析 HTTP 请求,或者用于分析日志文件以手机有关用户的信息,所以我们可以以此来获取浏览器的名字,浏览器组或者类型等等信息。在项目中引进这样的工具组建需要在 pom.xml
中引入如下的依赖:
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
如果你需要频繁的更新的版本内容的话还可以使用它的 snapshot 版本的,我们需要添加这样子的依赖:
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.22-SNAPSHOT</version>
</dependency>
使用的常规方法,我们一般是将来自请求 request 中的 user-agent 信息作为参数传进 UserAgent 的解析器的,以此来获取一个 UserAgent 对象。
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
然后我们就可以通过诸如 userAgent.getBrowser().toString()
和userAgent.getOperatingSystem().toString()
的方式来获取我们的浏览器和操作系统的信息了。
然后我们继续修改我们的项目结构,完成后如下图所示:
可以看出我们现在在 src/main/java/com/example
路径下面新建了两个包:aspectj 和 controller,同时我们在 resources 包中新建了 applicaion.yml
配置文件和 logbook-spring.xml
配置文件。
修改 applicaion.yml
配置文件:
server:
port: 8080
servlet:
context-path: /demo
修改 logback-spring.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Info 级别的日志,只是过滤 info 还是会输出 Error 日志,因为 Error 的级别高, 所以我们使用下面的策略,可以避免输出 Error 的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--过滤 Error-->
<level>ERROR</level>
<!--匹配到就禁止-->
<onMatch>DENY</onMatch>
<!--没有匹配到就允许-->
<onMismatch>ACCEPT</onMismatch>
</filter>
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
<!--<File>logs/info.spring-boot-demo-log-aop.log</File>-->
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>logs/spring-boot-demo-log-aop/info.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
<!--只保留最近90天的日志-->
<maxHistory>90</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<!--<totalSizeCap>1GB</totalSizeCap>-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!--<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
<!--<maxFileSize>1KB</maxFileSize>-->
<!--</triggeringPolicy>-->
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
</appender>
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>Error</level>
</filter>
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
<!--<File>logs/error.spring-boot-demo-log-aop.log</File>-->
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>logs/spring-boot-demo-log-aop/error.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
<!--只保留最近90天的日志-->
<maxHistory>90</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_ERROR"/>
</root>
</configuration>
新建 TestController.java
然后在我们的 controller 包中新建我们的获取请求的 controller:TestController.java
package com.example.controller;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
/**
* 测试方法
*
* @param who 测试参数
* @return {@link Dict}
*/
@GetMapping("/test")
public Dict test(String who) {
return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
}
}
在这个 controller 中我们编写了一个 test 接口,首先我们观察这个参数和返回的对象,我们的获取的参数是一个字符串"String who",返回的是一个 Dict 对象,这个 Dict 对象是我们上面提到过的 hutool 工具类库的核心包中的一个数据结构对象,如果你了解 Python 的话应该会知道在 Python 中是有 Dict 这个数据结构的,我们查看 Dict 的源码可以看到 Dict 的继承结构是这样的:
Dict 是继承了 LinkedHashMap 并且是实现了 BasicTypeGetter 接口的,在 BasicTypeGetter 接口中定义了很多的 get 的接口方法,Dict 就类似于 Map,也是一种 KV 的数据结构,针对强类型,提供丰富的 getXxx 操作,将 HashMap 扩展为了无类型区别的数据结构。
在这个接口中我们的 return 语句中定义了业务逻辑,返回的是经过了一个三元运算的,首先判断传进来的参数 who 是否为空,如果为空的话就直接对 who 这个 key 设置其 value 为 me,如果不为空的话就赋值其 value 值为实际传入的参数的值。
AOP 的相关注解的讲述
新建 AopLog.java
接下来我们在 aspectj 这个包中新建 AOP 相关的类: AopLog.java
利用这个类我们来记录请求日志的信息。
package com.example.aspectj;
import cn.hutool.json.JSONUtil;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
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.Map;
import java.util.Objects;
@Aspect
@Component
@Slf4j
public class AopLog {
private static final String START_TIME = "request-start";
/**
* 切入点
*/
@Pointcut("execution(public * com.example.controller.*Controller.*(..))")
public void log() {
}
/**
* 前置操作
*
* @param point 切入点
*/
@Before("log()")
public void beforeLog(JoinPoint point) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
log.info("【请求 URL】:{}", request.getRequestURL());
log.info("【请求 IP】:{}", request.getRemoteAddr());
log.info("【请求类名】:{},【请求方法名】:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("【请求参数】:{},", JSONUtil.toJsonStr(parameterMap));
Long start = System.currentTimeMillis();
request.setAttribute(START_TIME, start);
}
/**
* 环绕操作
*
* @param point 切入点
* @return 原方法返回值
* @throws Throwable 异常信息
*/
@Around("log()")
public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
log.info("【返回值】:{}", JSONUtil.toJsonStr(result));
return result;
}
/**
* 后置操作
*/
@AfterReturning("log()")
public void afterReturning() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
Long start = (Long) request.getAttribute(START_TIME);
Long end = System.currentTimeMillis();
log.info("【请求耗时】:{}毫秒", end - start);
String header = request.getHeader("User-Agent");
UserAgent userAgent = UserAgent.parseUserAgentString(header);
log.info("【浏览器类型】:{},【操作系统】:{},【原始User-Agent】:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header);
}
}
在这个类中我们使用了 @Aspect
这个注解,定义一个切面,同时使用 @Component
注解将其注入 bean 中,另外我们也使用了 @Slf4j
注解来使用 Lombok 的功能简化我们在日志这一部分的代码的编写。然后我们使用了 @Pointcut
这个注解来定义我们的切入点即横切的位置。在括号中定义了我们要在哪些横切的 execution 表达式:
符号 | 涵义 |
---|---|
execution()表达式 | 表达式的主体部分 |
第一个“*”符号 | 表示返回值的类型任意 |
com.example.controller | AOP 所切的业务服务的包名 |
包名后面的"…" | 表示当前包及其子包 |
第二个"*" | 表示类名,*即所有类,此处可以自定义 |
.*(…) | 表示任何方法名,括号表示参数,两个点表示任何参数类型 |
这里我们定义的切点就是在 controller 包下面的所有类的所有方法。那么我们这个切面就会织入到这些切点中去。我们的这个切点的名字是 log()。
Advice 处理的注解介绍
下面就是我们的一些 Advice 处理的注解,以第一个 @Before
前置通知为例:
@Before("log()")
public void beforeLog(JoinPoint point) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
log.info("【请求 URL】:{}", request.getRequestURL());
log.info("【请求 IP】:{}", request.getRemoteAddr());
log.info("【请求类名】:{},【请求方法名】:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("【请求参数】:{},", JSONUtil.toJsonStr(parameterMap));
Long start = System.currentTimeMillis();
request.setAttribute(START_TIME, start);
}
这里我们想做的事情是获取请求的信息,我们就需要从请求上下文中获取 request。所以下面的两行就是现在方法中获取请求,记录请求的内容,然后根据请求上下文获取到 request。
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
然后我们在 log 中打印日志信息,该请求的 URL、IP、请求的类名,方法名、请求的参数信息,注意这里的{ }都表示的展位信息,填充的内容是 log.info()第二个参数的信息,比如请求 URL,最后日志出来的信息就会是 request.getRequestURL()的内容。这里还使用了 getParameterMap 方法来获得一个 Map 以获取所有的 request 的请求的信息。
并且在切面类的开始的时候我们定义了一个常量请求字符串 START_TIME = "request-start"
,最后通过记录系统当前的时间来设置 requst 的这个请求开始的时间的属性。
另外在环绕通知中我们需要比较一下 ProceedingJoinPoint 和 JoinPoint 的区别在哪里。Proceedingjoinpoint 是继承了 JoinPoint 的。是在 JoinPoint 的基础上暴露出 proceed 这个方法,我们看到 point 调用了 proceed 这个方法,这个是 AOP 代理链执行的方法。在 JoinPoint 接口中有很多的方法,其中包含有返回 AOP 代理对象和目标对象的方法。
另外我们在后置通知中也是先通过请求上下文获取到 request,然后记录当前系统的时间,我们之前不是通过设置 request 的 START_TIME 的属性时间的,所以这里我们利用 end-start 就可以获取到这个请求的时间持续了多长的时间,反过来就是我们的这个接口请求时长是多长的时间。另外我们通过:
String header = request.getHeader("User-Agent");
来获取请求头中的 User-Agent 的信息,然后此时就要使用到我们的 User-agent-utils 的具体方法了,我们通过:
UserAgent userAgent = UserAgent.parseUserAgentString(header);
根据请求头的这部分信息来获取 UserAgent 对象,然后我们这个后置通知在方法执行结束之后会在控制台打印以下的信息:
userAgent.getBrowser().toString():浏览器信息
userAgent.getOperatingSystem().toString():操作系统信息
header:原始的user-agent的信息
编写启动类并运行
接下来我们需要来编写我们的启动类 SpringBootDemoLogAopApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
然后我们在终端中输入以下代码运行我们的程序:得到的项目的运行的结果如下图所示。
mvn spring-boot:run
因为我们的运行的线上云环境是 WebIDE,打开右侧的工具栏,点击「 Web 服务」。点击后,会在浏览器中打开一个网页,网页地址类似于 https://**************.simplelab.cn
。因为我们在 application.yml
配置文件中设置了
servlet:
context-path: /demo
所以这个配置能够设置项目中的所有 API 的上下文路径(URL 的一部分),我们在 https://**************.simplelab.cn
请求后面加上/demo 以及我们的接口名,即变成: https://**************.simplelab.cn/demo/test
我们首先测试一下不加参数的时候进行这个请求,请求的响应结果如下图所示:
因为并没有加参数,所以判断给 who 设置属性值为 me,如果我们传入一些参数呢?比如此时我们的请求链接为: https://**************.simplelab.cn/demo/test?who=shiyanlou
此时观察这个请求的响应结果:
而且观察两次不同的请求我们的控制台的日志信息结果如下:
我们需要的请求参数等信息都是拦截获取到了的,满足了我们的项目需求。
总结
通过具体的一个实验知道了平时的开发过程中使用 AOP 面向切面编程的作用以及具体的应用场景,还有 Spring Boot 使用 AOP 拦截请求日志的操作流程是怎样的,以及 AOP 的相关注解的讲述。我们在项目过程中还引进了一些常用的工具类,比如 Hutool 和 user-agent-utils 以供大家学习,方便代码的编写和规范。
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jun 29,2022