異常奇談 - 揭示全局異常處理的神秘面紗
在今天的學(xué)習(xí)中,我們深入研究了全局異常處理,以提升系統(tǒng)的穩(wěn)定性和用戶體驗(yàn)。通過建立全局異常處理器,我們能夠捕獲和處理應(yīng)用程序中產(chǎn)生的各種異常,并向前端返回更加友好的錯(cuò)誤信息。接下來,我們將詳細(xì)介紹一些常用的全局異常處理注解,同時(shí)提供相應(yīng)的示例代碼,幫助您更好地理解和應(yīng)用這些異常處理機(jī)制。
在 Spring Boot 中,通過使用 @ControllerAdvice 注解結(jié)合不同的注解,我們可以實(shí)現(xiàn)全局異常的處理。以下是一些常用的全局異常注解的詳細(xì)介紹及示例代碼:
@ControllerAdvice
- 作用: 聲明一個(gè)類為全局控制器增強(qiáng)類,用于集中處理異常。
- 位置: 通常放在類的頂部,作為一個(gè)全局異常處理器的聲明。
- 示例代碼:
@ControllerAdvice
public class GlobalExceptionHandler {
// ...
}
@ExceptionHandler
- 作用: 用于標(biāo)識方法為異常處理方法,處理特定類型的異常。
- 位置: 可以放在類級別的 @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
- 作用: 指定異常發(fā)生時(shí)返回的 HTTP 狀態(tài)碼。
- 位置: 可以標(biāo)注在異常處理方法上,也可以標(biāo)注在自定義異常類上。
- 示例代碼:
@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 接口的異常處理。
- 位置: 通常放在類的頂部,作為一個(gè)全局異常處理器的聲明。
- 示例代碼:
@RestControllerAdvice
public class RestExceptionHandler {
// ...
}
@Validated 和 @Valid
- 作用: 用于參數(shù)校驗(yàn),通常用于在 Controller 層進(jìn)行入?yún)⑿r?yàn)。
- 位置: 可以標(biāo)注在方法參數(shù)上,也可以標(biāo)注在類的字段上。
- 示例代碼:
@PostMapping("/create")
public ResponseEntity<String> createUser(@Validated @RequestBody User user) {
// 處理用戶創(chuàng)建邏輯
return ResponseEntity.ok("用戶創(chuàng)建成功");
}
以上是一些常用的全局異常處理注解及參數(shù)校驗(yàn)注解的詳細(xì)介紹及示例代碼。通過合理使用這些注解,可以使全局異常處理更加靈活和規(guī)范。
讓我們開始全局異常處理的探索之旅吧,共同深入研究如何提升系統(tǒng)的穩(wěn)定性和用戶體驗(yàn)。通過建立全局異常處理器,我們將能夠捕獲并處理應(yīng)用程序中產(chǎn)生的各種異常,并向前端返回更加友好的錯(cuò)誤信息。
創(chuàng)建 Day 8 子模塊
首先,進(jìn)入項(xiàng)目根目錄:
cd springboot60days
創(chuàng)建 Day 8 子模塊:
mkdir day8-global-exception
cd day8-global-exception
在 Day 7 子模塊的基礎(chǔ)上,創(chuàng)建 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>
創(chuàng)建配置文件:
在 src/main/resources 目錄下創(chuàng)建 application.properties :
# springboot60days/day8-global-exception/src/main/resources/application.properties
# Spring Boot 應(yīng)用程序名稱
spring.application.name=day8-global-exception
# 數(shù)據(jù)庫配置
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);
}
}
編寫實(shí)體類
創(chuàng)建一個(gè)具有年齡和郵箱屬性的用戶實(shí)體類 User,并在該屬性上添加 Validation 注解,進(jìn)行最大值、最小值和郵箱格式的驗(yàn)證。
// 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 接口及實(shí)現(xiàn)類
創(chuàng)建一個(gè) MyBatis-Plus Service 接口 UserService 和其實(shí)現(xiàn)類 UserServiceImpl,用于處理與數(shù)據(jù)庫的交互。
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> {
// 可以添加自定義業(yè)務(wù)方法...
}
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 {
// 可以添加自定義業(yè)務(wù)方法...
}
編寫控制器 UserController
更新 UserController 類,調(diào)用 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 "用戶創(chuàng)建成功!";
}
@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 {
// 初始化數(shù)據(jù)庫表
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 {
/**
* 分頁插件,自動(dòng)識別數(shù)據(jù)庫類型
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
創(chuàng)建啟動(dòng)類 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>
啟動(dòng)應(yīng)用程序:
在 day8-global-exception 子模塊的根目錄下,執(zhí)行以下命令啟動(dòng) Spring Boot 應(yīng)用程序:
mvn spring-boot:run
測試功能實(shí)現(xiàn)
在 Apifox 或 Postman 中,你可以使用以下命令測試 RESTful API 的增、刪、改、查操作:
獲取所有用戶信息(GET請求)
- 請求URL:http://localhost:8080/users
- 請求方式:GET
創(chuàng)建用戶(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
測試異常處理 - 數(shù)據(jù)校驗(yàn)失?。≒OST請求)
- 請求URL:http://localhost:8080/users
- 請求方式:POST
- 請求體:
{
"username": "icoderoad",
"age": 15, // 年齡小于最小值
"email": "icoderoad@example.com"
}
測試異常處理 - 數(shù)據(jù)校驗(yàn)失?。≒UT請求)
- 請求URL:http://localhost:8080/users/{userId}
- 請求方式:PUT
- 請求體:
{
"username": "newUsername",
"age": 8, // 年齡小于最小值
"email": "newEmail@example.com"
}
測試異常處理 - 數(shù)據(jù)校驗(yàn)失?。℅ET請求,ID為負(fù)數(shù))
- 請求URL:http://localhost:8080/users/-1
- 請求方式:GET
以上測試用例覆蓋了正常的 CRUD 操作以及異常處理情況,確保全局異常處理器能夠返回友好的錯(cuò)誤信息。
通過今天的學(xué)習(xí),我們成功踏上了全局異常處理的探索之旅,為提升系統(tǒng)的穩(wěn)定性和用戶體驗(yàn)打下了堅(jiān)實(shí)的基礎(chǔ)。通過建立全局異常處理器,我們能夠更加靈活地捕獲和處理應(yīng)用程序中各種異常,從而提供更友好、更有針對性的錯(cuò)誤信息。在這一過程中,我們深入了解了一系列常用的全局異常處理注解,并通過詳細(xì)的示例代碼展示了它們的應(yīng)用方式,為今后更加自信地應(yīng)對異常情況奠定了基礎(chǔ)。