Bootstrap

springboot入门学习笔记

在我们创建了一个Springboot项目之后,我们会看到有很多文件夹和文件

Springboot程序中各类文件的意义

一.pom.xml

在 Spring Boot 项目中,pom.xml(Project Object Model)文件是 Maven 构建工具的核心配置文件。起到项目信息定义,依赖管理,构建配置,项目继承,属性定义等作用。

Maven 是什么

Maven提供了一个标准化的方式来构建、管理和部署 Java 项目。它自动下载和管理项目所需的 Java 库和其他依赖。在Springboot中,

  • 通过 spring-boot-starter 依赖,轻松集成各种 Spring Boot 功能。
  • 自动化编译、测试、打包 Spring Boot 应用。
  • 统一管理 Spring Boot 及其相关依赖的版本。
  • 通过 parent POM 继承 Spring Boot 推荐的依赖版本。
  • 提供标准的目录结构,如 src/main/java, src/main/resources 等。
  • 自动运行单元测试和集成测试。
  • 管理本地和远程 Maven 仓库,确保依赖的可用性。

我们可以在idea的setting方法中配置maven

依赖是什么

依赖是项目运行或编译所需的外部代码库(JAR 文件)。不仅可以提供了必要的功能,还通过自动配置和版本管理大大简化了开发过程

  • Spring Boot 的"starter"依赖预配置了常用的库组合。
  • 例如,spring-boot-starter-web 包含了构建 web 应用所需的所有依赖。
  • 例如,添加 spring-boot-starter-data-jpa 来支持 JPA。

我们通过一个例子来看一下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

这个例子展示了如何在 pom.xml 文件中添加 Web 和 JPA 支持

注意:每一次引入新的依赖之后我们都需要点击右侧Maven的刷新功能刷新依赖,这样才能成功部署。

二.application.yml文件

application.yml 文件是 Spring Boot 应用程序中非常重要的配置文件。它用于配置应用程序的各种设置和属性。

我们来看个例子

spring:
  application:
    name: springboot3-learn
server:
  port: 8000

spring顶级配置节点,用于组织所有与 Spring 框架相关的配置。

application是 spring 下的一个子节点,专门用于配置应用程序级别的设置。

name: springboot3-learn设置了应用程序的名称。

  • 应用程序被命名为 "springboot3-learn"。
  • 这个名称可以在分布式系统、日志、监控等场景中用来识别该应用。

server是另一个顶级配置节点,用于配置嵌入式服务器的相关设置。

port: 8000设置了应用程序运行的端口号。

  • 应用程序将在 8000 端口上运行。
  • 这意味着你可以通过 http://localhost:8000 访问该应用。

三.Controller类

在 Spring Boot 中创建 Controller 类可以实现多种重要功能,它是构建 Web 应用和 RESTful API 的核心组件。

我们先来了解RESTful API

RESTful API(Representational State Transfer API)是一种软件架构风格,用于设计网络应用程序,特别是 Web 服务。它定义了一套规则和约束,用于创建可扩展、灵活和易于理解的 Web API。

HTTP 方法使用:

  • GET:获取资源
  • POST:创建新资源
  • PUT:更新现有资源
  • DELETE:删除资源
  • PATCH:部分更新资源

示例:

GET /users           # 获取所有用户
GET /users/123       # 获取特定用户
POST /users          # 创建新用户
PUT /users/123       # 更新特定用户
DELETE /users/123    # 删除特定用户

再来看controller类可以实现的功能

我们先看代码

package net.chatmindai.springboot3learn.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * Controller demo
 * 这是一个演示用的控制器类
 *
 * @author zk
 * @date 2024/10/04
 */
@RestController // 表示这是一个RESTful Web服务的控制器,组合了@Controller和@ResponseBody
@RequestMapping("/demo") // 定义该控制器的基础URL路径
public class DemoController {

    /**
     * 处理GET请求的方法
     * @GetMapping是@RequestMapping(method = RequestMethod.GET)的简写
     * 其他HTTP方法还有:
     * @PostMapping - 处理POST请求
     * @PutMapping - 处理PUT请求
     * @DeleteMapping - 处理DELETE请求
     * @PatchMapping - 处理PATCH请求
     */
    @GetMapping("/hello")
    public Object hello(){
        // 创建一个Map对象并初始化,使用Java 9引入的Map.of()方法
        Map<String,Object> map = new java.util.HashMap<>(Map.of("name", "chatmindai", "age", 18));
        
        // 向map中添加新的键值对
        map.put("introduction","we are chatmindai");
        
        // 直接返回map对象,Spring会自动将其转换为JSON格式
        // 如果想要更细粒度的控制,可以考虑使用ResponseEntity<>
        return map;
    }

    // 可以添加更多的方法来处理不同的请求
    // 例如:
    // @PostMapping("/create")
    // public ResponseEntity<?> createSomething(@RequestBody SomeDTO dto) { ... }
    
    // @PutMapping("/update/{id}")
    // public ResponseEntity<?> updateSomething(@PathVariable Long id, @RequestBody SomeDTO dto) { ... }
    
    // @DeleteMapping("/delete/{id}")
    // public ResponseEntity<?> deleteSomething(@PathVariable Long id) { ... }
}

我们来分析这段代码

@RestController
@RequestMapping("/demo")
  • @RestController: 表示这是一个 RESTful Web 服务的控制器,结合了 @Controller 和 @ResponseBody。
  • @RequestMapping("/demo"): 定义了该控制器的基础 URL 路径。所有方法的 URL 都会以 "/demo" 开头。
@GetMapping("/hello")
public Object hello() {
  • @GetMapping("/hello"): 表示这个方法处理 GET 请求,完整路径为 "/demo/hello"。
  • 方法返回 Object 类型,允许返回任何类型的对象。
  • public Object hello() 方法是控制器的核心部分,它定义了如何处理特定的 HTTP 请求。

方法实现

Map<String,Object> map = new java.util.HashMap<>(Map.of("name", "chatmindai", "age", 18));
map.put("introduction","we are chatmindai");
return map;

在这个方法中创建了一个Map对象并初值化,向map中添加键值对,直接返回object对象,Spring会自动将其转换为JSON格式。

这个controlledr控制器主要包括的功能

  1. 处理对 "/demo/hello" 的 GET 请求。
  2. 返回一个包含 "name"、"age" 和 "introduction" 信息的 JSON 对象。当客户端发送 GET 请求到 "/demo/hello" 时,会收到类似这样的 JSON 响应:
{
  "name": "chatmindai",
  "age": 18,
  "introduction": "we are chatmindai"
}

四.DTO类

DTO 主要用于在不同层或组件之间传输数据,特别是在客户端和服务器之间,它封装了需要传输的数据,使数据传输更加高效和安全。

我们来通过例子去理解

我们先加入相关依赖validation和lombok包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>

 spring-boot-starter-validation:这是Spring Boot的验证启动器,主要用于数据验证。

 使用场景:验证用户输入,确保ApI接受的数据符合预期

Lombok是一个java库,用于减少样板代码,通过注解自动生成常用的Java代码,如getter、setter、构造函数等。

@Data
public class DemoDTO implements Serializable {

    @Schema(description = "名称", example = "张三")
    @NotBlank(message = "名称不能为空")
    @Length(min = 2, max = 50, message = "名称长度必须在2到50个字符之间")
    private String name;

    @Schema(description = "年龄", example = "20")
    @Min(value = 0, message = "年龄必须大于或等于0")
    @Max(value = 150, message = "年龄必须小于或等于150")
    private int age;

    @Schema(description = "邮箱", example = "[email protected]")
    @NotNull(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @Schema(description = "手机号", example = "13800138000")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phoneNumber;

    @Schema(description = "生日", example = "2000-01-01")
    @Past(message = "生日必须是过去的日期")
    private LocalDate birthDate;

    @Schema(description = "计划日期", example = "2025-10-05")
    @Future(message = "计划日期必须是将来的日期")
    private LocalDate planDate;

    @Schema(description = "分数", example = "90.5")
    @Positive(message = "分数必须为正数")
    private double score;

    @Schema(description = "兴趣爱好列表", example = "[\"篮球\", \"足球\", \"游泳\", \"阅读\", \"编程\"]")
    @Size(min = 1, max = 5, message = "兴趣爱好列表必须包含1到5项")
    private List<String> hobbies;

    @Schema(description = "是否同意服务条款", example = "true")
    @AssertTrue(message = "必须同意服务条款")
    private boolean agreeTerms;
}
  • @Data: Lombok 注解,自动生成 getter、setter、equals、hashCode 和 toString 方法。
  • implements Serializable: 使类可序列化,便于网络传输或持久化。 
  • @Schema 注解是 OpenAPI 3.0 规范一部分提供字段的描述、示例值等信息,使 API 更易于理解和使用。description 属性提供字段的详细说明, example 属性提供字段的示例值。

我们来通过一个例子去理解

@Schema(type = "string", format = "email", description = "用户的电子邮件地址")
@Email(message = "请提供有效的电子邮件地址")
private String email;

 生成的 OpenAPI 文档中,这个字段会被描述为:

email:
  type: string
  format: email
  description: 用户的电子邮件地址

 在控制器中使用 DTO:

@Slf4j
@Tag(name = "演示用的控制器", description = "演示用的控制器")
@RestController
@RequestMapping("/demo")
public class DemoController {

    @Operation(summary = "返回一个简单的json")
    @GetMapping("/hello")
    public Object hello(){
        // ......
        return null;
    }
    @Operation(summary = "使用DemoDTO对象")
    @PostMapping("/demo2")
    public DemoDTO useDemoDTO(@Validated @RequestBody DemoDTO demoDTO) {
        log.info("入参为: {}", demoDTO);
        return demoDTO;
    }
}

 @Validated

  • 它会触发 DemoDTO 类中定义的所有验证注解(如 @NotNull, @Size 等)。

@RequestBody

  • 指示 Spring 将 HTTP 请求体反序列化到 DemoDTO 对象中。
  • 通常用于处理 JSON 或 XML 格式的请求数据。

DemoDTO demoDTO

  • 这是方法的参数,表示从请求体中解析出的 DemoDTO 对象。
  • 它包含了客户端在请求中发送的所有数据。

log.info("入参为: {}", demoDTO);

  • 这行代码使用 SLF4J 日志框架记录日志。
  • 它会打印接收到的 DemoDTO 对象的内容。
  • {} 是 SLF4J 的占位符,会被 demoDTO 的字符串表示替换。
  • 这对于调试和监控非常有用。

工作流程:

  1. 当服务器收到对应 URL 的 POST 请求时,Spring 会调用这个方法。
  2. 请求体中的 JSON 数据会被反序列化为 DemoDTO 对象。
  3. Spring 执行 @Validated 注解触发的验证。
  4. 如果验证通过,方法被执行,日志被记录。
  5. DemoDTO 对象被返回,并自动序列化为 JSON 响应。

五.引导类,springboot项目的入口

package com.example.springboot3learn;

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

@SpringBootApplication
public class Springboot3LearnApplication {

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

}

AOP的使用

一,我们先引入对应的依赖
 

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

我们通过代码和运行实例来详细理解AOP

二,创建切片类

@Pointcut("execution(* net.chatmindai.springboot3learn.controller..*.*(..))")
public void controllerPointcut() {}
  • 定义了一个切入点,匹配 net.chatmindai.springboot3learn.controller 包及其子包中所有类的所有方法。

切入点的含义:

  • 它匹配 net.chatmindai.springboot3learn.controller 包及其所有子包中的所有类的所有方法,不论方法的返回类型、名称或参数如何。

切入点的使用

  @Around("controllerPointcut()")
    public Object logAroundController(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();

        // 记录方法调用信息和入参
        log.info("调用控制器方法: {}.{}", className, methodName);
        log.info("入参: {}", Arrays.toString(args));

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

        // 记录出参
        log.info("出参: {}", result);

        return result;
    }

    /**
     * 前置通知,在方法执行前进行处理
     */
    @Before("controllerPointcut()")
    public void logBeforeController(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();

        // 记录方法调用信息和入参
        log.info("Before调用控制器方法: {}.{}", className, methodName);
        log.info("Before入参: {}", Arrays.toString(args));
    }

    /**
     * 返回通知,在方法正常返回后进行处理
     */
    @AfterReturning(pointcut = "controllerPointcut()", returning = "result")
    public void logAfterController(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        // 记录方法执行完毕信息和出参
        log.info("AfterReturning控制器方法执行完毕: {}.{}", className, methodName);
        log.info("AfterReturning出参: {}", result);
    }
}

PS:环绕通知(Around Advice),它是 AOP 中最强大和灵活的通知类型。

环绕通知提供了一个强大的机制来增强控制器方法的功能。它主要用于全面的日志记录,捕获方法的执行上下文、入参和出参。这种方式不仅提高了代码的可维护性和可调试性,还为进一步的功能扩展(如性能监控、安全检查等)提供了基础。

@Around 注解指定这是一个环绕通知。

joinPoint.getSignature() 返回一个 Signature 对象,它代表了连接点(在这个场景中是方法)的签名。所以返回的是方法名UserDemoDTO

JoinPoint.getTarget().getClass().getSimpleName();返回的是切入的类名,返回的是DemoController

"controllerPointcut()" 引用了之前定义的切入点,指定了这个通知应用的范围。

返回类型是 Object,允许修改或替换原方法的返回值。

ProceedingJoinPoint 参数提供了访问和控制目标方法执行的能力

继承了Throwable在 @Around 通知中,通常使用 Throwable 来捕获和处理可能发生的任何异常。

前置通知(Before Advice),它在目标方法执行之前运行。

前置通知提供了一种简洁有效的方式来记录控制器方法的调用信息。它主要用于在方法执行前进行日志记录,提供了valuable的调试和监控信息。这种方式增强了代码的可追踪性和可维护性,同时保持了较低的复杂度和性能开销

@Before 注解指定这是一个前置通知。

"controllerPointcut()" 引用了之前定义的切入点,指定了这个通知应用的范围。

JoinPoint 参数提供了访问被拦截方法信息的能力。

返回通知(AfterReturning Advice),它在目标方法成功执行并返回结果后运行。

返回通知提供了一种有效的方式来记录控制器方法的执行结果。它增强了应用程序的可观察性,提供了valuable的调试和监控信息。

@AfterReturning 注解指定这是一个返回通知。

pointcut = "controllerPointcut()" 指定了这个通知应用的切入点。

returning = "result" 指定了用于接收方法返回值的参数名。

JoinPoint 参数提供了访问被拦截方法信息的能力。

Object result 参数用于接收原方法的返回值。

在Apifox中发送 我们来看在日志中是怎么体现的

2024-10-26T15:21:05.447+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 调用控制器方法: DemoController.useDemoDTO
2024-10-26T15:21:05.451+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 入参: [DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)]
2024-10-26T15:21:05.451+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : Before调用控制器方法: DemoController.useDemoDTO
2024-10-26T15:21:05.452+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : Before入参: [DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)]
2024-10-26T15:21:05.452+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.s.controller.DemoController          : 入参为: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)
2024-10-26T15:21:05.452+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : AfterReturning控制器方法执行完毕: DemoController.useDemoDTO
2024-10-26T15:21:05.452+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : AfterReturning出参: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)
2024-10-26T15:21:05.452+08:00  INFO 49440 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 出参: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)

我们再来分析一下三种环绕的区别:

环绕通知,前置通知和返回通知的区别

主要区别:

  1. 执行时机:

    • 环绕通知:方法执行前后。
    • 前置通知:仅在方法执行前。
    • 返回通知:仅在方法成功返回后。
  2. 控制能力:

    • 环绕通知:可以完全控制方法的执行。
    • 前置通知和返回通知:不能控制方法的执行流程。
  3. 异常处理:

    • 环绕通知:可以处理方法执行期间的异常。
    • 前置通知:不涉及异常处理。
    • 返回通知:只在方法成功执行时触发,不处理异常。
  4. 返回值处理:

    • 环绕通知:可以修改返回值。
    • 前置通知:无法访问返回值。
    • 返回通知:可以访问但不能修改返回值。
  5. 复杂性:

    • 环绕通知:最复杂但最灵活。
    • 前置通知和返回通知:相对简单,职责单一。

使用注解进行开发

添加一个注解类LogInfo

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * LogInfo 注解
 * 用于标记需要记录日志的方法,并提供方法的描述信息
 *
 * @author zk
 * @date 2024/10/05
 */
@Target(ElementType.METHOD) // 指定该注解只能应用于方法
@Retention(RetentionPolicy.RUNTIME) // 指定该注解在运行时可以通过反射获取
public @interface LogInfo {
    /**
     * 方法描述
     * 
     * @return 返回描述该方法功能的字符串
     */
    String value() default "";//它定义了一个名为 value 的属性,这个属性可以在使用注解时被赋值。
}

 

修改切面类的代码

@Around("controllerPointcut()")
public Object logAroundController(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    LogInfo logInfo = signature.getMethod().getAnnotation(LogInfo.class);

    // 如果方法没有 @LogInfo 注解,直接执行方法并返回结果
    if (logInfo == null) {
        return joinPoint.proceed();
    }

    String methodDescription = logInfo.value();
    String methodName = signature.getName();
    String className = joinPoint.getTarget().getClass().getSimpleName();
    Object[] args = joinPoint.getArgs();

    log.info("执行方法: {}.{} - {}", className, methodName, methodDescription);
    log.info("入参: {}", Arrays.toString(args));

    Object result = joinPoint.proceed();

    log.info("方法返回: {}.{} - {}", className, methodName, methodDescription);
    log.info("出参: {}", result);

    return result;
}

这个方法将返回的 Signature 对象强制转换为 MethodSignatureMethodSignature 提供了更多与方法相关的具体信息。

 其他获取类名和方法名的原理和上面一样

给Controller添加注解,用来获取用户信息

@LogInfo("获取用户信息")

我们来看日志,也同样获取了整个流程

2024-10-26T20:40:04.682+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 执行方法: DemoController.useDemoDTO - 获取用户信息
2024-10-26T20:40:04.686+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 入参: [DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)]
2024-10-26T20:40:04.686+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : Before调用控制器方法: DemoController.useDemoDTO
2024-10-26T20:40:04.686+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : Before入参: [DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)]
2024-10-26T20:40:04.687+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.s.controller.DemoController          : 入参为: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)
2024-10-26T20:40:04.687+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : AfterReturning控制器方法执行完毕: DemoController.useDemoDTO
2024-10-26T20:40:04.687+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : AfterReturning出参: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)
2024-10-26T20:40:04.687+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 方法返回: DemoController.useDemoDTO - 获取用户信息
2024-10-26T20:40:04.687+08:00  INFO 49288 --- [springboot3-learn] [nio-8000-exec-1] c.e.springboot3learn.aspect.LogAspect    : 出参: DemoDTO(name=张三, age=20, [email protected], phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)

依赖注入与控制反转

 

使用全局异常处理器进行异常处理

我们再来思考一个问题,我们在Apifox中发送请求,所示的结果如下

将入参中的name设置为空字符串,会返回这个

 

 所以我们可以试着使用异常处理器对此进行处理

创建CommonResult类

这个类用于包装所有的接口出参,将出参信息结构化,使前端方便处理

import java.io.Serializable;


@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CommonResult<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private T data;
    public static <T> CommonResult<T> success(T data) {
        return success(data, "操作成功");
    }

   
    public static <T> CommonResult<T> success(T data, String message) {
        return CommonResult.<T>builder()
                .code(200)
                .message(message)
                .data(data)
                .build();
    }

   
    public static <T> CommonResult<T> error(int code, String message) {
        return CommonResult.<T>builder()
                .code(code)
                .message(message)
                .build();
    }

  
    public static <T> CommonResult<T> error(String message) {
        return error(500, message);
    }

   
    public static <T> CommonResult<T> any(int code, String message, T data) {
        return CommonResult.<T>builder()
                .code(code)
                .message(message)
                .data(data)
                .build();
    }
}

导入 Serializable 接口,使类可序列化

@Data: Lombok 注解,自动生成 getter、setter、toString 等方法。

@AllArgsConstructor: 生成包含所有字段的构造函数。

@NoArgsConstructor: 生成无参构造函数。

@Builder: 启用 Builder 模式。

<T>: 泛型参数,允许结果包含任意类型的数据。

implements Serializable: 使类可序列化。

 这段代码中的方法实现

success(T data): 创建成功响应,默认消息。

success(T data, String message): 创建成功响应,自定义消息。

error(int code, String message): 创建错误响应,自定义状态码和消息。

error(String message): 创建错误响应,默认状态码 500。

any(int code, String message, T data): 创建完全自定义的响应

这段代码定义了一个通用的结果类 CommonResult<T>,用于统一封装 API 响应的格式。

添加异常处理类

import com.example.springboot3learn.entity1.CommonResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import lombok.extern.slf4j.Slf4j;

/**
 * 全局异常处理器
 * 用于统一处理应用中抛出的异常,并返回标准化的错误响应
 */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理所有未被特定处理器捕获的异常
     *
     * @param ex 捕获到的异常
     * @param request 当前的web请求
     * @return 包含错误信息的ResponseEntity
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<CommonResult<String>> handleAllExceptions(Exception ex, WebRequest request) {
        // 记录异常日志
        log.error("发生未处理的异常", ex);

        // 创建错误响应
        CommonResult<String> result = CommonResult.error(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "发生未处理的异常: " + ex.getMessage()
        );

        // 返回HTTP 500 内部服务器错误状态码
        return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * 处理所有RuntimeException及其子类的异常
     *
     * @param ex 捕获到的RuntimeException
     * @param request 当前的web请求
     * @return 包含错误信息的ResponseEntity
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<CommonResult<String>> handleRuntimeException(RuntimeException ex, WebRequest request) {
        // 记录运行时异常日志
        log.error("发生运行时异常", ex);

        // 创建错误响应
        CommonResult<String> result = CommonResult.error(
                HttpStatus.BAD_REQUEST.value(),
                "发生运行时异常: " + ex.getMessage()
        );

        // 返回HTTP 400 错误请求状态码
        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
    }
}

@ControllerAdvice这个注解标记 GlobalExceptionHandler 类为全局异常处理器,Spring 会自动扫描并注册这个类,使其能够处理来自所有 Controller 的异常。

@ExceptionHandler(Exception.class): 指定此方法处理所有 Exception 类型的异常。

异常处理过程: 当应用中抛出异常时:

a. Spring 拦截这个异常。

b. 寻找能处理这个异常的 @ExceptionHandler 方法。

c. 调用匹配的处理方法。

d. 处理方法生成并返回一个 ResponseEntity

e. Spring 将这个 ResponseEntity 转换为 HTTP 响应返回给客户端。

  1. 具体工作流程:

    • 如果抛出 RuntimeException

      • handleRuntimeException 方法被调用。
      • 记录错误日志。
      • 创建一个 CommonResult 对象,包含错误信息。
      • 返回一个带有 400 状态码的 ResponseEntity
    • 如果抛出其他类型的 Exception

      • handleAllExceptions 方法被调用。
      • 记录错误日志。
      • 创建一个 CommonResult 对象,包含错误信息。
      • 返回一个带有 500 状态码的 ResponseEntity

我们来看此时APifox接受的响应结果,冒号后面即为ex.getmessage的内容

对入参校验抛出的异常进行解析

 在日志中可以看到处理的日志,它输出了异常的类型 MethodArgumentNotValidException

2024-10-26T21:34:56.669+08:00 ERROR 51280 --- [springboot3-learn] [nio-8000-exec-1] c.e.s.exception.GlobalExceptionHandler   : 发生未处理的异常

为了返回更清晰的响应,我们进一步改善,我们给异常处理类添加新的代码

/**
     * 处理参数校验失败的异常
     *
     * @param ex 捕获到的MethodArgumentNotValidException
     * @param request 当前的web请求
     * @return 包含错误信息的ResponseEntity
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<CommonResult<Map<String, String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex, WebRequest request) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage()));

        log.warn("参数校验失败", ex);

        CommonResult<Map<String, String>> result = CommonResult.error(
            HttpStatus.BAD_REQUEST.value(),
            "参数校验失败"
        );
        result.setData(errors);

        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
    }

 加入了这段代码后的异常处理类,有了哪些改进?

原来的类主要处理一般的 Exception 和 RuntimeException。
现在增加了对 MethodArgumentNotValidException 的专门处理,这是一种更具体的异常类型。

对于参数校验失败,不再只是返回一个通用的错误消息。
现在能够提供每个失败字段的具体错误信息,大大提高了错误反馈的精确度。

使用 Map<String, String> 来存储和返回错误信息,每个字段的错误都能被清晰地表示。
这种结构化的方式使得前端或API消费者更容易解析和处理错误。

客户端可以准确知道哪些字段没有通过验证,以及具体的原因。
这有助于用户界面的快速反馈和表单验证的实现。

我们来看响应结果

;