SpringBoot中该如何处理异常?

461 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

介绍

创建 API 的一项重要任务是返回可理解的错误消息。Spring Boot 已经有预定义的错误消息格式,但这种格式并不总是可以接受的,我们的应用程序可能需要自定义格式。

在本教程中,我们将配置 Spring Boot 的异常处理,以便我们的后端应用程序以以下格式响应错误消息:

{  
"guid": "DCF70619-01D8-42a9-97DC-6005F205361A",  
"errorCode": "application-specific-error-code",  
"message": "Error message",  
"statusCode": 400,  
"statusName": "BAD_REQUEST",  
"path": "/some/path",  
"method": "POST",  
"timestamp": "2022-12-06"  
}

格式说明:

  • guid — 错误的唯一全局标识符,此字段对于在大型日志中搜索错误很有用。
  • errorCode — 源自业务逻辑规则的特定于应用程序的错误代码。
  • message — 错误描述。
  • statusCode — HTTP 状态代码。
  • statusName — HTTP 状态代码的全名。
  • path — 发生错误的资源的 URI。
  • 方法——使用的 HTTP 方法。
  • timestamp — 错误创建的时间戳。

执行

例如,让我们创建一个用于处理城市列表的 REST API。

手动或使用 Spring Initializer 创建 Spring Boot 项目后,将以下两个类添加到项目中:ApplicationExceptionApiErrorResponse

import lombok.AllArgsConstructor;  
import lombok.Getter;  
import org.springframework.http.HttpStatus;  
  
@Getter  
@AllArgsConstructor  
public class ApplicationException extends RuntimeException {  
private final String errorCode;  
private final String message;  
private final HttpStatus httpStatus;  
}

当应用程序发生异常时,将抛出 ApplicationException。

import lombok.Data;  
import java.time.LocalDateTime;  
  
@Data  
public class ApiErrorResponse {  
private final String guid;  
private final String errorCode;  
private final String message;  
private final Integer statusCode;  
private final String statusName;  
private final String path;  
private final String method;  
private final LocalDateTime timestamp;  
}

ApiErrorResponse是要序列化为 JSON 响应的 DTO。

然后添加一个ApplicationExceptionHandler类来处理所有应用程序异常。

import io.github.sergiusac.exceptionhandling.response.ApiErrorResponse;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.core.Ordered;  
import org.springframework.core.annotation.Order;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.ExceptionHandler;  
import org.springframework.web.bind.annotation.RestControllerAdvice;  
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;  
  
import javax.servlet.http.HttpServletRequest;  
import java.time.LocalDateTime;  
import java.util.UUID;  
  
@Slf4j  
@RestControllerAdvice  
@Order(Ordered.HIGHEST_PRECEDENCE)  
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {  
  
@ExceptionHandler(ApplicationException.class)  
public ResponseEntity<?> handleApplicationException(  
final ApplicationException exception, final HttpServletRequest request  
) {  
var guid = UUID.randomUUID().toString();  
log.error(  
String.format("Error GUID=%s; error message: %s", guid, exception.getMessage()),  
exception  
);  
var response = new ApiErrorResponse(  
guid,  
exception.getErrorCode(),  
exception.getMessage(),  
exception.getHttpStatus().value(),  
exception.getHttpStatus().name(),  
request.getRequestURI(),  
request.getMethod(),  
LocalDateTime.now()  
);  
return new ResponseEntity<>(response, exception.getHttpStatus());  
}  
  
@ExceptionHandler(Exception.class)  
public ResponseEntity<?> handleUnknownException(  
final Exception exception, final HttpServletRequest request  
) {  
var guid = UUID.randomUUID().toString();  
log.error(  
String.format("Error GUID=%s; error message: %s", guid, exception.getMessage()),  
exception  
);  
var response = new ApiErrorResponse(  
guid,  
ErrorCodes.INTERNAL_ERROR,  
"Internal server error",  
HttpStatus.INTERNAL_SERVER_ERROR.value(),  
HttpStatus.INTERNAL_SERVER_ERROR.name(),  
request.getRequestURI(),  
request.getMethod(),  
LocalDateTime.now()  
);  
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);  
}  
  
}

我们使用RestControllerAdvice注释创建一个全局处理应用程序中异常的 bean,并使用ExceptionHandler注释来指定异常。

handleApplicationException方法处理ApplicationException类的所有异常。此方法为异常生成一个 GUID,将异常写入日志,并将错误响应发送回客户端。

handleUnknownException方法执行相同的操作,但用于所有其他异常

接下来,我们创建CityService来处理城市列表。

import io.github.sergiusac.exceptionhandling.exception.ApplicationException;  
import io.github.sergiusac.exceptionhandling.model.City;  
import org.springframework.http.HttpStatus;  
import org.springframework.stereotype.Service;  
  
import java.time.LocalDateTime;  
import java.util.List;  
import java.util.Map;  
import java.util.Set;  
import java.util.concurrent.ConcurrentHashMap;  
import java.util.concurrent.ConcurrentSkipListSet;  
import java.util.stream.Collectors;  
  
@Service  
public class CityService {  
  
private final Map<Long, City> cities = new ConcurrentHashMap<>() {  
{  
put(1L, new City(1L, "Paris", LocalDateTime.now(), LocalDateTime.now()));  
put(2L, new City(2L, "New-York", LocalDateTime.now(), LocalDateTime.now()));  
put(3L, new City(3L, "Barcelona", LocalDateTime.now(), LocalDateTime.now()));  
}  
};  
  
public List<City> getAllCities() {  
return cities.values().stream().collect(Collectors.toUnmodifiableList());  
}  
  
public City getCityById(final Long id) {  
var city = cities.get(id);  
if (city == null) {  
throw new ApplicationException(  
"city-not-found",  
String.format("City with id=%d not found", id),  
HttpStatus.NOT_FOUND  
);  
}  
return city;  
}  
  
}

该服务有一个内置的城市列表和两种查看城市列表的方法。此外,getCityById方法会抛出自定义ApplicationException以及所提供的信息,例如错误代码、消息和 HTTP 状态。

接下来,我们创建一个 REST 控制器。

import io.github.sergiusac.exceptionhandling.service.CityService;  
import lombok.RequiredArgsConstructor;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
@RestController  
@RequiredArgsConstructor  
@RequestMapping("/cities")  
public class CityController {  
  
private final CityService cityService;  
  
@GetMapping  
public ResponseEntity<?> getAllCities() {  
return ResponseEntity.ok(cityService.getAllCities());  
}  
  
@GetMapping("/{id}")  
public ResponseEntity<?> getCityById(@PathVariable final Long id) {  
return ResponseEntity.ok(cityService.getCityById(id));  
}  
  
}

这个控制器只是提供了两个类似于CityService的方法。

编译并运行应用程序后,我们应该看到以下结果:

GET http://localhost:8080/cities  
  
[  
{  
"id": 1,  
"cityName": "Paris",  
"createdAt": "2022-12-06T22:06:18.6921738",  
"updatedAt": "2022-12-06T22:06:18.6921738"  
},  
{  
"id": 2,  
"cityName": "New-York",  
"createdAt": "2022-12-06T22:06:18.6921738",  
"updatedAt": "2022-12-06T22:06:18.6921738"  
},  
{  
"id": 3,  
"cityName": "Barcelona",  
"createdAt": "2022-12-06T22:06:18.6921738",  
"updatedAt": "2022-12-06T22:06:18.6921738"  
}  
]

如我们所见,第一个方法返回包含三个城市列表的正确响应。但是当我们尝试获取 ID 未知的城市时,我们会收到以下错误响应:

GET http://localhost:8080/cities/4  
  
{  
"guid": "01913964-5777-4ec1-bd5e-392c5a5fecc9",  
"errorCode": "city-not-found",  
"message": "City with id=4 not found",  
"statusCode": 404,  
"statusName": "NOT_FOUND",  
"path": "/cities/4",  
"method": "GET",  
"timestamp": "2022-12-06T22:10:37.8993481"  
}

而在应用日志中,我们也可以看到相应的错误信息:

image.png

结论

使用 Spring Boot 可以轻松实现自定义异常处理程序。使用RestControllerAdviceExceptionHandler注释,我们在一个组件中实现了全局错误处理。

本文中提供的代码仅用于演示目的,您在实现自定义异常处理逻辑时应考虑应用程序的各个方面