MessageSource support for Spring MVC and WebFlux exceptions

See gh-28814
This commit is contained in:
rstoyanchev
2022-10-04 20:34:08 +01:00
parent ff81d64fb5
commit a4210854fb
31 changed files with 676 additions and 129 deletions

View File

@@ -16,10 +16,14 @@
package org.springframework.web.reactive.result.method.annotation;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
@@ -55,13 +59,22 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
* @author Rossen Stoyanchev
* @since 6.0
*/
public abstract class ResponseEntityExceptionHandler {
public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {
/**
* Common logger for use in subclasses.
*/
protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private MessageSource messageSource;
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Handle all exceptions raised within Spring MVC handling of the request .
@@ -306,12 +319,25 @@ public abstract class ResponseEntityExceptionHandler {
}
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.getBody();
body = resolveDetailViaMessageSource(errorResponse, exchange.getLocaleContext().getLocale());
}
return createResponseEntity(body, headers, status, exchange);
}
private ProblemDetail resolveDetailViaMessageSource(ErrorResponse response, @Nullable Locale locale) {
ProblemDetail body = response.getBody();
if (this.messageSource != null) {
locale = (locale != null ? locale : Locale.getDefault());
Object[] arguments = response.getDetailMessageArguments(this.messageSource, locale);
String detail = this.messageSource.getMessage(response.getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
body.setDetail(detail);
}
}
return body;
}
/**
* Create the {@link ResponseEntity} to use from the given body, headers,
* and statusCode. Subclasses can override this method to inspect and possibly

View File

@@ -18,13 +18,15 @@ package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -33,6 +35,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MethodNotAllowedException;
@@ -97,7 +100,7 @@ public class ResponseEntityExceptionHandlerTests {
@Test
void handleWebExchangeBindException() {
testException(new WebExchangeBindException(null, null));
testException(new WebExchangeBindException(null, new BeanPropertyBindingResult(new Object(), "foo")));
}
@Test
@@ -120,20 +123,40 @@ public class ResponseEntityExceptionHandlerTests {
testException(new ErrorResponseException(HttpStatus.CONFLICT));
}
@Test
void errorResponseProblemDetailViaMessageSource() {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
"problemDetail." + UnsupportedMediaTypeStatusException.class.getName(), locale,
"Content-Type {0} not supported. Supported: {1}");
this.exceptionHandler.setMessageSource(messageSource);
Exception ex = new UnsupportedMediaTypeStatusException(MediaType.APPLICATION_JSON,
List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML));
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")
.acceptLanguageAsLocales(locale).build());
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(ex, exchange).block();
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getDetail()).isEqualTo(
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
}
@SuppressWarnings("unchecked")
private ResponseEntity<ProblemDetail> testException(ErrorResponseException exception) {
ResponseEntity<?> responseEntity =
this.exceptionHandler.handleException(exception, this.exchange).block();
assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCode()).isEqualTo(exception.getStatusCode());
assertThat(responseEntity.getBody()).isNotNull().isInstanceOf(ProblemDetail.class);
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getType()).isEqualTo(URI.create(exception.getClass().getName()));
return (ResponseEntity<ProblemDetail>) responseEntity;
ResponseEntity<?> entity = this.exceptionHandler.handleException(exception, this.exchange).block();
assertThat(entity).isNotNull();
assertThat(entity.getStatusCode()).isEqualTo(exception.getStatusCode());
assertThat(entity.getBody()).isNotNull().isInstanceOf(ProblemDetail.class);
return (ResponseEntity<ProblemDetail>) entity;
}
@@ -142,9 +165,7 @@ public class ResponseEntityExceptionHandlerTests {
private Mono<ResponseEntity<Object>> handleAndSetTypeToExceptionName(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
ProblemDetail body = ex.getBody();
body.setType(URI.create(ex.getClass().getName()));
return handleExceptionInternal(ex, body, headers, status, exchange);
return handleExceptionInternal(ex, null, headers, status, exchange);
}
@Override