在我们创建了一个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控制器主要包括的功能
- 处理对 "/demo/hello" 的 GET 请求。
- 返回一个包含 "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 = "zhangsan@example.com")@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: stringformat: emaildescription: 用户的电子邮件地址
在控制器中使用 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 的字符串表示替换。
- 这对于调试和监控非常有用。
工作流程:
- 当服务器收到对应 URL 的 POST 请求时,Spring 会调用这个方法。
- 请求体中的 JSON 数据会被反序列化为 DemoDTO 对象。
- Spring 执行 @Validated 注解触发的验证。
- 如果验证通过,方法被执行,日志被记录。
- 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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, phoneNumber=13800138000, birthDate=2000-01-01, planDate=2025-10-05, score=90.5, hobbies=[篮球, 足球, 游泳, 阅读, 编程], agreeTerms=true)
我们再来分析一下三种环绕的区别:
环绕通知,前置通知和返回通知的区别
主要区别:
-
执行时机:
- 环绕通知:方法执行前后。
- 前置通知:仅在方法执行前。
- 返回通知:仅在方法成功返回后。
-
控制能力:
- 环绕通知:可以完全控制方法的执行。
- 前置通知和返回通知:不能控制方法的执行流程。
-
异常处理:
- 环绕通知:可以处理方法执行期间的异常。
- 前置通知:不涉及异常处理。
- 返回通知:只在方法成功执行时触发,不处理异常。
-
返回值处理:
- 环绕通知:可以修改返回值。
- 前置通知:无法访问返回值。
- 返回通知:可以访问但不能修改返回值。
-
复杂性:
- 环绕通知:最复杂但最灵活。
- 前置通知和返回通知:相对简单,职责单一。
使用注解进行开发
添加一个注解类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
对象强制转换为 MethodSignature
。MethodSignature
提供了更多与方法相关的具体信息。
其他获取类名和方法名的原理和上面一样
给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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, 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=zhangsan@example.com, 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 响应返回给客户端。
-
具体工作流程:
-
如果抛出
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消费者更容易解析和处理错误。
客户端可以准确知道哪些字段没有通过验证,以及具体的原因。
这有助于用户界面的快速反馈和表单验证的实现。
我们来看响应结果