在今天的学习中,我们深入研究了全局异常处理,以提升系统的稳定性和用户体验。通过建立全局异常处理器,我们能够捕获和处理应用程序中产生的各种异常,并向前端返回更加友好的错误信息。接下来,我们将详细介绍一些常用的全局异常处理注解,同时提供相应的示例代码,帮助您更好地理解和应用这些异常处理机制。
在 Spring Boot 中,通过使用 @ControllerAdvice 注解结合不同的注解,我们可以实现全局异常的处理。以下是一些常用的全局异常注解的详细介绍及示例代码:
@ControllerAdvice
- 作用: 声明一个类为全局控制器增强类,用于集中处理异常。
- 位置: 通常放在类的顶部,作为一个全局异常处理器的声明。
- 示例代码:
@ControllerAdvice
public class GlobalExceptionHandler {
// ...
}
@ExceptionHandler
- 作用: 用于标识方法为异常处理方法,处理特定类型的异常。
- 位置: 可以放在类级别的 @ControllerAdvice 下,也可以放在普通的 Controller 类中。
- 示例代码:
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred: " + e.getMessage());
}
@ResponseStatus
- 作用: 指定异常发生时返回的 HTTP 状态码。
- 位置: 可以标注在异常处理方法上,也可以标注在自定义异常类上。
- 示例代码:
@ExceptionHandler(CustomException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> handleCustomException(CustomException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Custom exception: " + e.getMessage());
}
@RestControllerAdvice
- 作用: 类似于 @ControllerAdvice,但用于 RESTful 接口的异常处理。
- 位置: 通常放在类的顶部,作为一个全局异常处理器的声明。
- 示例代码:
@RestControllerAdvice
public class RestExceptionHandler {
// ...
}
@Validated 和 @Valid
- 作用: 用于参数校验,通常用于在 Controller 层进行入参校验。
- 位置: 可以标注在方法参数上,也可以标注在类的字段上。
- 示例代码:
@PostMapping("/create")
public ResponseEntity<String> createUser(@Validated @RequestBody User user) {
// 处理用户创建逻辑
return ResponseEntity.ok("用户创建成功");
}
以上是一些常用的全局异常处理注解及参数校验注解的详细介绍及示例代码。通过合理使用这些注解,可以使全局异常处理更加灵活和规范。
让我们开始全局异常处理的探索之旅吧,共同深入研究如何提升系统的稳定性和用户体验。通过建立全局异常处理器,我们将能够捕获并处理应用程序中产生的各种异常,并向前端返回更加友好的错误信息。
创建 Day 8 子模块
首先,进入项目根目录:
cd springboot60days
创建 Day 8 子模块:
mkdir day8-global-exception
cd day8-global-exception
在 Day 7 子模块的基础上,创建 Day 8 子模块。在父模块的 pom.xml 中添加 Day 8 子模块的配置:
<!-- springboot60days/pom.xml -->
<modules>
<module>day4-database-magic</module>
<module>day5-mybatis-mystery</module>
<module>day6-mybatis-plus-journey</module>
<module>day7-data-validation</module>
<module>day8-global-exception</module>
</modules>
子模块 pom.xml 配置
<!-- day8-global-exception/pom.xml -->
<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>
<parent>
<groupId>com.icoderoad.springboot60days</groupId>
<artifactId>springboot60days</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>day8-global-exception</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- Lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter Web (Assuming you need web support) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis-Plus Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4.1</version>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- H2 Database (for testing) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建配置文件:
在 src/main/resources 目录下创建 application.properties :
# springboot60days/day8-global-exception/src/main/resources/application.properties
# Spring Boot 应用程序名称
spring.application.name=day8-global-exception
# 数据库配置
spring.datasource.url=jdbc:h2:mem:userdb;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
# MyBatis-Plus 配置
mybatis-plus.mapper-locations=classpath:mapper/*.xml
# 分页插件配置
mybatis-plus.configuration.plugins=com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
mybatis-plus.configuration.plugins.pagination.type=com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor
全局异常处理器 GlobalExceptionHandler.java
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/exception/GlobalExceptionHandler.java
package com.icoderoad.springboot60days.day8.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.List;
import java.util.stream.Collectors;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred: " + e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String errorMessage = fieldErrors.stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> handleBindingException(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String errorMessage = fieldErrors.stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}
}
编写实体类
创建一个具有年龄和邮箱属性的用户实体类 User,并在该属性上添加 Validation 注解,进行最大值、最小值和邮箱格式的验证。
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/entity/User.java
package com.icoderoad.springboot60days.day8.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@Data
@TableName(value = "my_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
@Max(value = 60, message = "年龄不能大于60岁")
@Min(value = 18, message = "年龄不能小于18岁")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
Mapper 接口 UserMapper.java
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/mapper/UserMapper.java
package com.icoderoad.springboot60days.day8.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.icoderoad.springboot60days.day8.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
编写 MyBatis-Plus Service 接口及实现类
创建一个 MyBatis-Plus Service 接口 UserService 和其实现类 UserServiceImpl,用于处理与数据库的交互。
UserService.java
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/service/UserService.java
package com.icoderoad.springboot60days.day8.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.icoderoad.springboot60days.day8.entity.User;
public interface UserService extends IService<User> {
// 可以添加自定义业务方法...
}
UserServiceImpl.java
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/service/impl/UserServiceImpl.java
package com.icoderoad.springboot60days.day8.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.icoderoad.springboot60days.day8.entity.User;
import com.icoderoad.springboot60days.day8.mapper.UserMapper;
import com.icoderoad.springboot60days.day8.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 可以添加自定义业务方法...
}
编写控制器 UserController
更新 UserController 类,调用 MyBatis-Plus Service 接口处理用户的 CRUD 操作。
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/controller/UserController.java
package com.icoderoad.springboot60days.day8.controller;
import com.icoderoad.springboot60days.day8.entity.User;
import com.icoderoad.springboot60days.day8.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.list();
return new ResponseEntity<>(users, HttpStatus.OK);
}
@PostMapping
public String createUser(@Valid @RequestBody User user) {
userService.save(user);
return "用户创建成功!";
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getById(id);
}
@PutMapping("/{id}")
public String updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
user.setId(id);
userService.updateById(user);
return "用户更新成功!";
}
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
userService.removeById(id);
return "用户删除成功!";
}
}
初始化类 DatabaseInitializer
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/init/DatabaseInitializer.java
package com.icoderoad.springboot60days.day8.init;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.core.JdbcTemplate;
@Component
public class DatabaseInitializer implements CommandLineRunner {
private final JdbcTemplate jdbcTemplate;
public DatabaseInitializer(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void run(String... args) throws Exception {
// 初始化数据库表
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS my_user (" +
"id INT AUTO_INCREMENT PRIMARY KEY," +
"username VARCHAR(255)," +
"email VARCHAR(255)," +
"age INT" +
")");
}
}
配置类 MyBatisPlusConfig.java
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/config/MyBatisPlusConfig.java
package com.icoderoad.springboot60days.day8.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.icoderoad.springboot60days.day8.mapper"})
public class MyBatisPlusConfig {
/**
* 分页插件,自动识别数据库类型
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
创建启动类 Day8GlobalExceptionApplication.java:
// springboot60days/day8-global-exception/src/main/java/com/icoderoad/springboot60days/day8/Day8GlobalExceptionApplication.java
package com.icoderoad.springboot60days.day8;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Day8GlobalExceptionApplication {
public static void main(String[] args) {
SpringApplication.run(Day8GlobalExceptionApplication.class, args);
}
}
Mapper 文件 UserMapper.xml
<!--springboot60days/day8-global-exception/src/main/java/resource/mapper/UserMapper.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.icoderoad.springboot60days.day8.mapper.UserMapper">
<resultMap id="userResultMap" type="com.icoderoad.springboot60days.day8.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<!-- 其他字段 -->
</resultMap>
</mapper>
启动应用程序:
在 day8-global-exception 子模块的根目录下,执行以下命令启动 Spring Boot 应用程序:
mvn spring-boot:run
测试功能实现
在 Apifox 或 Postman 中,你可以使用以下命令测试 RESTful API 的增、删、改、查操作:
获取所有用户信息(GET请求)
- 请求URL:http://localhost:8080/users
- 请求方式:GET
创建用户(POST请求)
- 请求URL:http://localhost:8080/users
- 请求方式:POST
- 请求体:
{
"username": "icoderoad",
"age": 25,
"email": "icoderoad@example.com"
}
更新用户信息(PUT请求)
- 请求URL:http://localhost:8080/users/{userId}
- 请求方式:PUT
- 请求体:
{
"username": "newUsername",
"age": 26,
"email": "newEmail@example.com"
}
删除用户(DELETE请求)
- 请求URL:http://localhost:8080/users/{userId}
- 请求方式:DELETE
测试异常处理 - 数据校验失败(POST请求)
- 请求URL:http://localhost:8080/users
- 请求方式:POST
- 请求体:
{
"username": "icoderoad",
"age": 15, // 年龄小于最小值
"email": "icoderoad@example.com"
}
测试异常处理 - 数据校验失败(PUT请求)
- 请求URL:http://localhost:8080/users/{userId}
- 请求方式:PUT
- 请求体:
{
"username": "newUsername",
"age": 8, // 年龄小于最小值
"email": "newEmail@example.com"
}
测试异常处理 - 数据校验失败(GET请求,ID为负数)
- 请求URL:http://localhost:8080/users/-1
- 请求方式:GET
以上测试用例覆盖了正常的 CRUD 操作以及异常处理情况,确保全局异常处理器能够返回友好的错误信息。
通过今天的学习,我们成功踏上了全局异常处理的探索之旅,为提升系统的稳定性和用户体验打下了坚实的基础。通过建立全局异常处理器,我们能够更加灵活地捕获和处理应用程序中各种异常,从而提供更友好、更有针对性的错误信息。在这一过程中,我们深入了解了一系列常用的全局异常处理注解,并通过详细的示例代码展示了它们的应用方式,为今后更加自信地应对异常情况奠定了基础。