Spring WebClient - 如何在出现 HTTP 错误(4xx、5xx)时访问响应主体?
Spring WebClient - how to access response body in case of HTTP errors (4xx, 5xx)?
我想将我的异常从我的 "Database" REST API 重新抛出到我的 "Backend" REST API 但我丢失了原始异常的消息。
这是我通过 Postman 从我的 "Database" REST API 得到的:
{
"timestamp": "2020-03-18T15:19:14.273+0000",
"status": 400,
"error": "Bad Request",
"message": "I'm DatabaseException (0)",
"path": "/database/api/vehicle/test/0"
}
这部分还可以。
这是我通过 Postman 从我的 "Backend" REST API 得到的:
{
"timestamp": "2020-03-18T15:22:12.801+0000",
"status": 400,
"error": "Bad Request",
"message": "400 BAD_REQUEST \"\"; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8090/database/api/vehicle/test/0",
"path": "/backend/api/vehicle/test/0"
}
如您所见,原始 "message" 字段已丢失。
我使用:
- Spring 引导 2.2.5.RELEASE
- spring-boot-starter-web
- spring-boot-starter-webflux
后端和数据库以 Tomcat () 开头。
这是后端:
@GetMapping(path = "/test/{id}")
public Mono<Integer> test(@PathVariable String id) {
return vehicleService.test(id);
}
与vehicleService.test:
public Mono<Integer> test(String id) {
return WebClient
.create("http://localhost:8090/database/api")
.get()
.uri("/vehicle/test/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Integer.class);
}
这是数据库:
@GetMapping(path = "/test/{id}")
public Mono<Integer> test(@PathVariable String id) throws Exception {
if (id.equals("0")) {
throw new DatabaseException("I'm DatabaseException (0)");
}
return Mono.just(Integer.valueOf(2));
}
我也试过 return Mono.error(new DatabaseException("I'm DatabaseException (0)"));
和数据库异常:
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
看来我的后端转换了响应,在互联网上找不到任何答案。
您可以使用 exchange
而不是 retrieve
的 WebClient
,它可以让您处理错误并使用从服务响应中检索到的消息传播自定义异常。
private void execute()
{
WebClient webClient = WebClient.create();
webClient.get()
.uri("http://localhost:8089")
.exchangeToMono(this::handleResponse)
.doOnNext(System.out::println)
.block(); // not required, just for testing purposes
}
private Mono<Response> handleResponse(ClientResponse clientResponse)
{
if (clientResponse.statusCode().isError())
{
return clientResponse.bodyToMono(Response.class)
.flatMap(response -> Mono.error(new RuntimeException(response.message)));
}
return clientResponse.bodyToMono(Response.class);
}
private static class Response
{
private String message;
public Response()
{
}
public String getMessage()
{
return message;
}
public void setMessage(String message)
{
this.message = message;
}
@Override
public String toString()
{
return "Response{" +
"message='" + message + '\'' +
'}';
}
}
下面的代码现在可以工作了,它是我原来问题之外的另一个代码,但它的想法几乎相同(后端 REST api 和数据库 REST api)。
我的数据库 REST api:
@RestController
@RequestMapping("/user")
public class UserControl {
@Autowired
UserRepo userRepo;
@Autowired
UserMapper userMapper;
@GetMapping("/{login}")
public Mono<UserDTO> getUser(@PathVariable String login) throws DatabaseException {
User user = userRepo.findByLogin(login);
if(user == null) {
throw new DatabaseException(HttpStatus.BAD_REQUEST, "error.user.not.found");
}
return Mono.just(userMapper.toDTO(user));
}
}
UserRepo 只是一个@RestReporitory。
UserMapper 使用 MapStruct 将我的实体映射到 DTO 对象。
有:
@Data
@EqualsAndHashCode(callSuper=false)
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
@Data & EqualsAndHashCode 来自 Lombok 库。
扩展 ResponseStatusException 在这里非常重要,如果你不这样做,那么响应将被错误处理。
我的后端 REST api 从数据库 REST API 接收数据:
@RestController
@RequestMapping("/user")
public class UserControl {
@Value("${database.api.url}")
public String databaseApiUrl;
private String prefixedURI = "/user";
@GetMapping("/{login}")
public Mono<UserDTO> getUser(@PathVariable String login) {
return WebClient
.create(databaseApiUrl)
.get()
.uri(prefixedURI + "/{login}", login).retrieve()
.onStatus(HttpStatus::isError, GlobalErrorHandler::manageError)
.bodyToMono(UserDTO.class);
}
}
使用 GlobalErrorHandler::
public class GlobalErrorHandler {
/**
* Translation key for i18n
*/
public final static String I18N_KEY_ERROR_TECHNICAL_EXCEPTION = "error.technical.exception";
public static Mono<ResponseStatusException> manageError(ClientResponse clientResponse) {
if (clientResponse.statusCode().is4xxClientError()) {
// re-throw original status and message or they will be lost
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(response.getStatus(), response.getMessage()));
});
} else { // Case when it's 5xx ClientError
// User doesn't have to know which technical exception has happened
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
I18N_KEY_ERROR_TECHNICAL_EXCEPTION));
});
}
}
}
以及从 clientResponse 中检索某些数据所必需的 ExceptionResponseDTO:
/**
* Used to map <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html">ClientResponse</a> from WebFlux
*/
@Data
@EqualsAndHashCode(callSuper=false)
public class ExceptionResponseDTO extends Exception {
private static final long serialVersionUID = 1L;
private HttpStatus status;
public ExceptionResponseDTO(String message) {
super(message);
}
/**
* Status has to be converted into {@link HttpStatus}
*/
public void setStatus(String status) {
this.status = HttpStatus.valueOf(Integer.valueOf(status));
}
}
另一个可能有用的相关 class:ExchangeFilterFunctions.java
我在这个问题上找到了很多资料:
https://github.com/spring-projects/spring-framework/issues/20280
即使这些信息是旧的,它们仍然相关!
我想将我的异常从我的 "Database" REST API 重新抛出到我的 "Backend" REST API 但我丢失了原始异常的消息。
这是我通过 Postman 从我的 "Database" REST API 得到的:
{
"timestamp": "2020-03-18T15:19:14.273+0000",
"status": 400,
"error": "Bad Request",
"message": "I'm DatabaseException (0)",
"path": "/database/api/vehicle/test/0"
}
这部分还可以。
这是我通过 Postman 从我的 "Backend" REST API 得到的:
{
"timestamp": "2020-03-18T15:22:12.801+0000",
"status": 400,
"error": "Bad Request",
"message": "400 BAD_REQUEST \"\"; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8090/database/api/vehicle/test/0",
"path": "/backend/api/vehicle/test/0"
}
如您所见,原始 "message" 字段已丢失。
我使用:
- Spring 引导 2.2.5.RELEASE
- spring-boot-starter-web
- spring-boot-starter-webflux
后端和数据库以 Tomcat (
这是后端:
@GetMapping(path = "/test/{id}")
public Mono<Integer> test(@PathVariable String id) {
return vehicleService.test(id);
}
与vehicleService.test:
public Mono<Integer> test(String id) {
return WebClient
.create("http://localhost:8090/database/api")
.get()
.uri("/vehicle/test/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Integer.class);
}
这是数据库:
@GetMapping(path = "/test/{id}")
public Mono<Integer> test(@PathVariable String id) throws Exception {
if (id.equals("0")) {
throw new DatabaseException("I'm DatabaseException (0)");
}
return Mono.just(Integer.valueOf(2));
}
我也试过 return Mono.error(new DatabaseException("I'm DatabaseException (0)"));
和数据库异常:
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
看来我的后端转换了响应,在互联网上找不到任何答案。
您可以使用 exchange
而不是 retrieve
的 WebClient
,它可以让您处理错误并使用从服务响应中检索到的消息传播自定义异常。
private void execute()
{
WebClient webClient = WebClient.create();
webClient.get()
.uri("http://localhost:8089")
.exchangeToMono(this::handleResponse)
.doOnNext(System.out::println)
.block(); // not required, just for testing purposes
}
private Mono<Response> handleResponse(ClientResponse clientResponse)
{
if (clientResponse.statusCode().isError())
{
return clientResponse.bodyToMono(Response.class)
.flatMap(response -> Mono.error(new RuntimeException(response.message)));
}
return clientResponse.bodyToMono(Response.class);
}
private static class Response
{
private String message;
public Response()
{
}
public String getMessage()
{
return message;
}
public void setMessage(String message)
{
this.message = message;
}
@Override
public String toString()
{
return "Response{" +
"message='" + message + '\'' +
'}';
}
}
下面的代码现在可以工作了,它是我原来问题之外的另一个代码,但它的想法几乎相同(后端 REST api 和数据库 REST api)。
我的数据库 REST api:
@RestController
@RequestMapping("/user")
public class UserControl {
@Autowired
UserRepo userRepo;
@Autowired
UserMapper userMapper;
@GetMapping("/{login}")
public Mono<UserDTO> getUser(@PathVariable String login) throws DatabaseException {
User user = userRepo.findByLogin(login);
if(user == null) {
throw new DatabaseException(HttpStatus.BAD_REQUEST, "error.user.not.found");
}
return Mono.just(userMapper.toDTO(user));
}
}
UserRepo 只是一个@RestReporitory。
UserMapper 使用 MapStruct 将我的实体映射到 DTO 对象。
有:
@Data
@EqualsAndHashCode(callSuper=false)
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
@Data & EqualsAndHashCode 来自 Lombok 库。
扩展 ResponseStatusException 在这里非常重要,如果你不这样做,那么响应将被错误处理。
我的后端 REST api 从数据库 REST API 接收数据:
@RestController
@RequestMapping("/user")
public class UserControl {
@Value("${database.api.url}")
public String databaseApiUrl;
private String prefixedURI = "/user";
@GetMapping("/{login}")
public Mono<UserDTO> getUser(@PathVariable String login) {
return WebClient
.create(databaseApiUrl)
.get()
.uri(prefixedURI + "/{login}", login).retrieve()
.onStatus(HttpStatus::isError, GlobalErrorHandler::manageError)
.bodyToMono(UserDTO.class);
}
}
使用 GlobalErrorHandler::
public class GlobalErrorHandler {
/**
* Translation key for i18n
*/
public final static String I18N_KEY_ERROR_TECHNICAL_EXCEPTION = "error.technical.exception";
public static Mono<ResponseStatusException> manageError(ClientResponse clientResponse) {
if (clientResponse.statusCode().is4xxClientError()) {
// re-throw original status and message or they will be lost
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(response.getStatus(), response.getMessage()));
});
} else { // Case when it's 5xx ClientError
// User doesn't have to know which technical exception has happened
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
I18N_KEY_ERROR_TECHNICAL_EXCEPTION));
});
}
}
}
以及从 clientResponse 中检索某些数据所必需的 ExceptionResponseDTO:
/**
* Used to map <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html">ClientResponse</a> from WebFlux
*/
@Data
@EqualsAndHashCode(callSuper=false)
public class ExceptionResponseDTO extends Exception {
private static final long serialVersionUID = 1L;
private HttpStatus status;
public ExceptionResponseDTO(String message) {
super(message);
}
/**
* Status has to be converted into {@link HttpStatus}
*/
public void setStatus(String status) {
this.status = HttpStatus.valueOf(Integer.valueOf(status));
}
}
另一个可能有用的相关 class:ExchangeFilterFunctions.java
我在这个问题上找到了很多资料:
https://github.com/spring-projects/spring-framework/issues/20280
即使这些信息是旧的,它们仍然相关!