开启掘金成长之旅!这是我参与「掘金日新计划 · 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 项目后,将以下两个类添加到项目中:ApplicationException和ApiErrorResponse。
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"
}
而在应用日志中,我们也可以看到相应的错误信息:
结论
使用 Spring Boot 可以轻松实现自定义异常处理程序。使用RestControllerAdvice和ExceptionHandler注释,我们在一个组件中实现了全局错误处理。
本文中提供的代码仅用于演示目的,您在实现自定义异常处理逻辑时应考虑应用程序的各个方面