# Day1-Springboot项目初始化与配置

在本项目中使用的开发环境：

* *JDK 1.8 版本*（此版本是目前企业中使用最广泛的）;
* *Springboot 2.6.3 版本*
* *MySQL 5.7 版本* (或者 8.x 版本都可以，本项目使用的是 5.7 版本)；
* *Maven 3.8 版本* （项目构建工具）;

## 多模块项目的起始

博客使用多模块结构进行编写，对于多模块项目，父目录和每个子目录都存在`pom.xml`文件，父目录下用于定义一些公用的插件和依赖管理，而子目录可以细分各个功能模块，并且按需加载插件与依赖

### 项目结构说明

该项目是一个多模块的 Maven 项目，主要包括以下模块以及文件：

* **weblog-springboot:** 父项目，定义了所有公共的依赖和插件管理，与多模块的结构。
* **weblog-web:** 前端展示模块，负责博客前台展示相关功能。
* **weblog-module-admin:** 管理后台模块。
* **weblog-module-common:** 公共模块，提供通用功能。
* **.gitignore:** 用于指定 Git 需要忽略的文件和目录。

<figure><img src="/files/5t5AC5FkrxUFtIDQmq1h" alt=""><figcaption><p>目录结构</p></figcaption></figure>

### 项目配置

#### 项目初始化

所有模块通过 `Spring Initializr` 进行初始化，在IDEA中新建项目中有内置的 `Spring Initializr` 支持初始化，可以直接使用

步骤如下：

* ①：选择 Spring Boot 项目初始化器；
* ②：填写父项目名称;
* ③：选择新建项目所在的位置；
* ④：选择通过 Maven 来构建项目；
* ⑤：填写 Group 组织名称，通常为域名倒写，例如我的项目中为 `cn.dogalist`；
* ⑥：选择前面小节中已经安装好的 JDK 1.8 版本；
* ⑦：选择 Java 版本，和 JDK 版本保持一致，选择 8；

> <mark style="color:blue;">注意：</mark>IDEA 通过 Spring Initializr 来创建 Spring Boot 项目，新版本不支持勾选 Java 8 了 ， 可以将初始化链接换成阿里云的 `http://start.aliyun.com`, 就可以正常选择 Java 8 了：

按照以上步骤初始化`weblog-springboot`文件夹后，再建立子模块`weblog-web`、`weblog-module-common`、`weblog-module-admin`，删除其中的无用文件夹，得到如下文件结构：

```
weblog-springboot/
├── .gitignore
├── README.md
├── pom.xml
├── weblog-web/
│   ├── .gitignore
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/cn/dogalist/weblog/web/
│       │   │   └── WeblogWebApplication.java
│       │   └── resources/
│       │       └── application.yml
│       └── test/
│           └── java/cn/dogalist/weblog/web/
│               └── WeblogWebApplicationTests.java
├── weblog-module-admin/
│   ├── .gitignore
│   └── pom.xml
│   └── src/
│       └── test/
│           └── java/cn/dogalist/weblog/admin/
│               └── WeblogModuleAdminApplicationTests.java
└── weblog-module-common/
    ├── .gitignore
    └── pom.xml
    └── src/
        ├── main/
        │   └── java/cn/dogalist/weblog/common/
        └── test/
            └── java/cn/dogalist/weblog/common/
                └── WeblogModuleCommonApplicationTests.java
```

#### 模块配置

weblog-springboot（根目录）

* pom.xml

```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.dogalist</groupId>
    <artifactId>weblog-springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>weblog-springboot</name>
    <description>weblog-springboot</description>
    <!-- 多模块项目父工程打包模式必须指定为 pom -->
    <packaging>pom</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!-- 将 Spring Boot 的版本号切换成 2.6 版本 -->
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <!-- 子模块管理 -->
    <modules>
        <module>weblog-web</module>
        <module>weblog-module-admin</module>
        <module>weblog-module-common</module>
    </modules>


    <!-- 版本号统一管理 -->
    <properties>
        <!-- 项目版本号 -->
        <revision>0.0.1-SNAPSHOT</revision>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- Maven 相关 -->
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>

        <!-- 依赖包版本 -->
        <jackson.version>2.15.2</jackson.version>
        <lombok.version>1.18.28</lombok.version>
        <guava.version>31.1-jre</guava.version>
        <commons-lang3.version>3.12.0</commons-lang3.version>
    </properties>

    <!-- 统一依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>cn.dogalist</groupId>
                <artifactId>weblog-module-admin</artifactId>
                <version>${revision}</version>
            </dependency>

            <dependency>
                <groupId>cn.dogalist</groupId>
                <artifactId>weblog-module-common</artifactId>
                <version>${revision}</version>
            </dependency>

            <!-- 常用工具库 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${guava.version}</version>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>

            <!-- Jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <!-- 统一插件管理 -->
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <!-- 使用阿里云的 Maven 仓库源，提升包下载速度 -->
    <repositories>
        <repository>
            <id>aliyunmaven</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

</project>

```

weblog-web

* 模块依赖：

  ​ 勾选上 `Lombok` 和 `Spring Web` 依赖
* pom.xml

```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.dogalist</groupId>
    <artifactId>weblog-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>weblog-web</name>
    <description>weblog-web (入口项目，负责博客前台展示相关功能，打包也放在这个模块负责)</description>
    <!-- 指定父项目为 weblog-springboot -->
    <parent>
        <groupId>cn.dogalist</groupId>
        <artifactId>weblog-springboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <dependencies>
<!--    web依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 免写冗余的 Java 样板式代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>cn.dogalist</groupId>
            <artifactId>weblog-module-common</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.dogalist</groupId>
            <artifactId>weblog-module-admin</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

weblog-module-admin

* pom.xml

```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.dogalist</groupId>
    <artifactId>weblog-module-admin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>weblog-module-admin</name>
    <description>weblog-module-admin (负责管理后台相关功能)</description>
    <parent>
        <groupId>cn.dogalist</groupId>
        <artifactId>weblog-springboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.dogalist</groupId>
            <artifactId>weblog-module-common</artifactId>
        </dependency>

    </dependencies>

</project>

```

weblog-module-common

* pom.xml

```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.dogalist</groupId>
    <artifactId>weblog-module-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>weblog-module-common</name>
    <description>weblog-module-common</description>
    <parent>
        <groupId>cn.dogalist</groupId>
        <artifactId>weblog-springboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <dependencies>
        <!-- 免写冗余的 Java 样板式代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 常用工具库 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <!-- 单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- AOP 切面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

    </dependencies>

</project>

```

## AOP和日志模块整合

### 实现自定义注解

Java 注解是从 Java 5 开始引入的，它为我们提供了一种元编程的方法，允许我们在不改变代码逻辑的情况下为代码添加元数据。这些元数据可以在编译时或运行时通过反射被访问。而自定义注解是用户定义的，用于为代码提供元数据的注解。

在 `weblog-module-common` 通用模块下，新建一个名为 `aspect` 的包，用于放置切面相关的功能类，接着，在其中创建一个名为 `ApiOperationLog` 的自定义注解：

```java
package cn.dogalist.weblog.common.aspect;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperationLog {
    /**
     * API 功能描述
     *
     * @return
     */
    String description() default "";
}

```

* `@Retention(RetentionPolicy.RUNTIME)`： 这个元注解用于指定注解的保留策略，即注解在何时生效。`RetentionPolicy.RUNTIME` 表示该注解将<mark style="color:blue;">在运行时保留</mark>。
* `@Target({ElementType.METHOD})`： 这个元注解用于指定注解的目标元素，即可以在哪些地方使用这个注解。`ElementType.METHOD` 表示<mark style="color:blue;">该注解只能用于方法上</mark>。
* `@Documented`：使用 `@Documented`，表示<mark style="color:blue;">在生成文档时，被注解的元素及其注解信息会被包含在文档中</mark>。

### 什么是AOP

AOP（Aspect-Oriented Programming，面向切面编程）是一个编程范式，它提供了一种能力，让开发者能够模块化跨多个对象的横切关注点（例如日志、事务管理、安全等）。

主要概念包括：

* **切点 (Pointcuts)**: 定义在哪里应用切面（即在哪里插入横切关注点的代码）。
* **通知 (Advices)**: 定义在特定切点上要执行的代码。常见的通知类型有：前置通知、后置通知、环绕通知等。
* **切面 (Aspects)**: 切面将切点和通知结合起来，定义了在何处和何时应用特定的逻辑。

在面向切面编程（AOP）中，Pointcut是一个核心概念，它定义了程序执行过程中的哪些点（如方法调用、异常抛出等）是我们关注的。简单来说，Pointcut就是指定了我们希望在哪些地方应用切面（Aspect）。切面包含了前置通知（Before Advice）、后置通知（After Advice）、环绕通知（Around Advice）等。

`aspectj` 相关注解的作用：

* **@Aspect**：声明<mark style="color:blue;">该类为一个切面类</mark>；
* **@Pointcut**：定义一个切点，后面跟随一个表达式，表达式<mark style="color:blue;">可以定义为切某个注解，也可以切某个 package 下的方法</mark>；

切点定义好后，就是围绕这个切点做文章了：

* **@Before**: 在<mark style="color:blue;">切点之前，织入相关代码</mark>；
* **@After**: 在<mark style="color:blue;">切点之后，织入相关代码</mark>;
* **@AfterReturning**: 在<mark style="color:blue;">切点返回内容后，织入相关代码</mark>，一般用于对返回值做些加工处理的场景；
* **@AfterThrowing**: 用来处理<mark style="color:blue;">当织入的代码抛出异常后的逻辑处理</mark>;
* **@Around**: 环绕，可以在<mark style="color:blue;">切入点前后织入代码</mark>，并且可以自由的控制何时执行切点；

### Pointcut的基本概念和用法

* **静态Pointcut**：基于类的结构信息来决定是否应用切面，例如所有公共方法、特定包下的所有类等。
* **动态Pointcut**：不仅依赖于静态结构信息，还可能依赖于运行时的信息，比如方法的参数值等。

#### 在Spring框架中使用Pointcut

Spring AOP使用了AspectJ的切点表达式语言，但只支持AspectJ的一部分功能。以下是一些常用的Pointcut表达式示例：

1. **匹配特定的方法签名**

   ```java
   @Before("execution(public * com.example.service.MyService.*(..))")
   public void beforeMethod(JoinPoint joinPoint) {
       // 方法执行前的操作
   }
   ```

   这个例子表示，在`com.example.service.MyService`类中的任何公共方法执行之前，都会先执行`beforeMethod`方法。
2. **匹配特定的参数**

   ```java
   @Before("execution(* com.example.service.MyService.find*(..)) && args(name)")
   public void beforeFindByName(JoinPoint joinPoint, String name) {
       // 方法执行前的操作
   }
   ```

   这里，只有当`find*`方法的参数为`name`时，才会触发`beforeFindByName`方法。
3. **匹配特定的异常**

   ```java
   @AfterThrowing(pointcut="execution(* com.example.service.MyService.*(..))", throwing="ex")
   public void afterThrowing(JoinPoint joinPoint, Exception ex) {
       // 异常抛出后的操作
   }
   ```

   当`MyService`中的任意方法抛出异常时，会执行`afterThrowing`方法。
4. **匹配多个条件**

   可以使用逻辑运算符`&&`（与）、`||`（或）、`!`（非）组合多个Pointcut条件。

   ```java
   @Before("execution(* com.example.service.MyService.*(..)) && !within(com.example.service.MyService+)")
   public void beforeMethodExceptSubclasses(JoinPoint joinPoint) {
       // 方法执行前的操作
   }
   ```

   这个例子表示除了`MyService`的子类之外的所有`MyService`方法执行前都会调用`beforeMethodExceptSubclasses`。

#### 创建自定义Pointcut

如果标准的Pointcut表达式不能满足需求，还可以创建自定义的Pointcut。这通常涉及到实现Spring的`org.aspectj.lang.annotation.Pointcut`注解，并定义自己的逻辑来确定何时应该应用切面。

```java
@Pointcut("execution(* com.example.service.MyService.*(..))")
public void myServiceMethods() {}

@Before("myServiceMethods()")
public void beforeMyServiceMethods(JoinPoint joinPoint) {
    // 方法执行前的操作
}
```

这里，`myServiceMethods`是一个自定义的Pointcut，它定义了所有`MyService`类的方法。然后，`beforeMyServiceMethods`会在这些方法执行前被调用。

### 使用AOP添加日志逻辑

在我们的项目中，使用AOP，我们可以为所有使用 `@ApiOperationLog` 注解的方法自动添加日志逻辑，而不需要在每个方法中手动添加。

#### 配置日志模块Logback

Logback 是日志框架 SLF4J 的一个实现，它被设计用来替代 `log4j`。Logback 提供了更高的性能，更丰富的日志功能和更好的配置选项。

在 `src/main/resources` 目录下，创建一个名为 `logback-weblog.xml` 的文件，这是Logback的配置文件。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration >
    <jmxConfigurator/>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />

    <!-- 应用名称 -->
    <property scope="context" name="appName" value="weblog" />
    <!-- 自定义日志输出路径，以及日志名称前缀 -->
<!--   日志文件存储地址linux格式: /app/weblog/logs/${appName}.%d{yyyy-MM-dd}
       测试时使用的windows格式（按自己的地址填）： E:\\work\\java\\weblog\\logs\\${appName}.%d{yyyy-MM-dd}
-->
    <property name="LOG_FILE" value="/app/weblog/logs/${appName}.%d{yyyy-MM-dd}"/>
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    <!--<property name="CONSOLE_LOG_PATTERN" value="${FILE_LOG_PATTERN}"/>-->

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <FileNamePattern>${LOG_FILE}-%i.log</FileNamePattern>
            <!-- 日志文件保留天数 -->
            <MaxHistory>30</MaxHistory>
            <!-- 日志文件最大的大小 -->
            <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </TimeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 格式化输出：%d 表示日期，%thread 表示线程名，%-5level：级别从左显示 5 个字符宽度 %errorMessage：日志消息，%n 是换行符-->
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- dev 环境（仅输出到控制台） -->
    <springProfile name="dev">
        <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
        <root level="info">
            <appender-ref ref="CONSOLE" />
        </root>
    </springProfile>

    <!-- prod 环境（仅输出到文件中） -->
    <springProfile name="prod">
        <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
        <root level="INFO">
            <appender-ref ref="FILE" />
        </root>
    </springProfile>
</configuration>
```

#### 创建 JSON 工具类

在 `weblog-module-common` 通用模块下，创建一个 `utils` 包，用于统一放置工具类相关，然后，新建一个名为 `JsonUtil` 的工具类， 代码如下：

```java
package cn.dogalist.weblog.common.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

/*
 * @author: monthwolf
 * @url: dogalist.cn
 * @date: 2024-10-21
 * @description: JSON 工具类， toJsonString 方法，用于将传入的对象打印成 JSON 字符串
 */

@Slf4j
public class JsonUtil {
    private static final ObjectMapper INSTANCE = new ObjectMapper();

    public static String toJsonString(Object obj) {
        try {
            return INSTANCE.writeValueAsString(obj);
        } catch (JsonProcessingException e){
            return obj.toString();
        }
    }
}
```

#### 定义日志切面类

工具类搞定后，在 `aspect` 包下，新建切面类 `ApiOperationLogAspect`

```java
package cn.dogalist.weblog.common.aspect;

import cn.dogalist.weblog.common.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;


@Aspect // 声明这是一个切面
@Component // 声明这是一个Spring Bean
@Slf4j // 日志
public class ApiOperationLogAspect {
    /** 以自定义 @ApiOperationLog 注解为切点，凡是添加 @ApiOperationLog 的方法，都会执行环绕中的代码 */
    @Pointcut("@annotation(cn.dogalist.weblog.common.aspect.ApiOperationLog)")
    public void apiOperationLog() {}

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("apiOperationLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            //请求开始实践
            long startTime = System.currentTimeMillis();

            //MDC
            MDC.put("traceId", UUID.randomUUID().toString());

            // 获取被请求的类和方法名
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();

            // 请求入参
            Object[] args = joinPoint.getArgs();
            // 入参转JSON
            String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));

            // 功能描述
            String description = getApiOperationLogDescription(joinPoint);

            // 打印请求参数
            log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ",
                    description, argsJsonStr, className, methodName);

            // 执行切点方法
            Object result = joinPoint.proceed();

            // 请求耗时
            long executionTime = System.currentTimeMillis() - startTime;
            // 打印请求结果
            log.info("====== 请求结束: [{}], 出参: {}, 请求耗时: {} =================================== ",description, JsonUtil.toJsonString(result), executionTime);
            return result;
        } finally {
            // 清除日志上下文
            MDC.clear();
        }
    }

    /**
     * 获取注解的描述信息
     * @param joinPoint
     * @return
     */
    private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {
        // ①从 ProceedingJoinPoint 获取 MethodSignature（方法签名）
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // ②从 MethodSignature 获取 Method（方法对象）
        Method method = methodSignature.getMethod();
        // ③从 Method 获取注解
        ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);
        // ④获取注解的描述信息
        return apiOperationLog.description();
    }


    /**
     * 转 JSON 字符串
     * @return
     */
    private Function<Object, String> toJsonStr() {
        return arg -> JsonUtil.toJsonString(arg);
    }

}
```

#### 添加包扫描

在启动类 `WeblogWebApplication` 中，手动添加包扫描 `@ComponentScan` , 指定扫描 `cn.dogalist.weblog` 包下面的所有类:

```java
package cn.dogalist.weblog.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; 
import org.springframework.context.annotation.ComponentScan; //添加

@SpringBootApplication
@ComponentScan({"cn.dogalist.weblog.*"}) //添加该内容，多模块必须手动指定扫描
public class WeblogWebApplication {

    public static void main(String[] args) {

        SpringApplication.run(WeblogWebApplication.class, args);
    }

}
```

### 环境区分和日志测试

![文件结构](/files/fya935GYAojJmEtXv1kV)

#### 环境划分

如上面文件结构，在`weblog-web` 的`resources`文件下创建`application.yml`配置文件，`application.yml`中定义选择的环境

```yml
spring:
  profiles:
    # 默认激活 dev 环境
    active: dev
```

对应的激活环境配置则与`application-{active}.yml`配置的内容相同，可以通过定义不同的`application-*.yml`文件创建不同的配置，在这个项目中只定义了测试环境和生产环境

#### 日志测试

如上面文件结构，在`weblog-web` 下创建`controller`包和`model`包，分别在两个包中新建类`TestController`和`User`类

* `TestController`类

```java
package cn.dogalist.weblog.web.controller;

import cn.dogalist.weblog.common.aspect.ApiOperationLog;
import cn.dogalist.weblog.web.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class TestController {
    @PostMapping("/test")
    @ApiOperationLog(description = "测试接口")
    public User test(@RequestBody User user) {
        return user;
    }
}
```

* `User`类

```java
package cn.dogalist.weblog.web.model;

import lombok.Data;

@Data
public class User {
    private String username;
    private Integer sex;
}
```

添加完成后，运行`WeblogWebApplication`文件，同时使用api测试工具发送POST请求到`localhost:8080/test`，携带请求参数如下：

```json
{
    "username": "admin",
    "sex": 1
}
```

api请求正常：

![api请求](/files/BRnYi5SuraPSzpUQ52cP)

成功得到日志信息：

![测试结果](/files/hUiztHbK9zMYVU9oyrRw)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.dogalist.top/day1springboot-xiang-mu-chu-shi-hua-yu-pei-zhi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
