diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java index 0182256591..399f032de4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.data.r2dbc.dialect.DialectResolver; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; import org.springframework.data.relational.RelationalManagedTypes; +import org.springframework.data.relational.core.mapping.DefaultNamingStrategy; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.Table; import org.springframework.r2dbc.core.DatabaseClient; @@ -79,11 +80,10 @@ public class R2dbcDataAutoConfiguration { @Bean @ConditionalOnMissingBean - @SuppressWarnings("deprecation") public R2dbcMappingContext r2dbcMappingContext(ObjectProvider namingStrategy, R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) { R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext( - namingStrategy.getIfAvailable(() -> NamingStrategy.INSTANCE)); + namingStrategy.getIfAvailable(() -> DefaultNamingStrategy.INSTANCE)); relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder()); relationalMappingContext.setManagedTypes(r2dbcManagedTypes); return relationalMappingContext; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ProblemDetailsExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ProblemDetailsExceptionHandler.java new file mode 100644 index 0000000000..3b65e511fc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ProblemDetailsExceptionHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler; + +/** + * {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is + * auto-configured for problem details support. + * + * @author Brian Clozel + */ +@ControllerAdvice +final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 37ed6a5efb..7296f6c3b4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -74,6 +74,7 @@ import org.springframework.web.reactive.result.method.HandlerMethodArgumentResol import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -334,6 +335,18 @@ public class WebFluxAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.webflux.problemdetails", name = "enabled", havingValue = "true") + static class ProblemDetailsErrorHandlingConfiguration { + + @Bean + @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { + return new ProblemDetailsExceptionHandler(); + } + + } + static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore { private final Duration timeout; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java index e7b2b1ba2f..0ab2bec77e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java @@ -36,6 +36,8 @@ public class WebFluxProperties { private final Format format = new Format(); + private final Problemdetails problemdetails = new Problemdetails(); + /** * Path pattern used for static resources. */ @@ -74,6 +76,10 @@ public class WebFluxProperties { return this.format; } + public Problemdetails getProblemdetails() { + return this.problemdetails; + } + public String getStaticPathPattern() { return this.staticPathPattern; } @@ -133,4 +139,21 @@ public class WebFluxProperties { } + public static class Problemdetails { + + /** + * Whether RFC 7807 Problem Details support should be enabled. + */ + private boolean enabled = false; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ProblemDetailsExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ProblemDetailsExceptionHandler.java new file mode 100644 index 0000000000..6ca7cf25e7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ProblemDetailsExceptionHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.servlet; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +/** + * {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is + * auto-configured for problem details support. + * + * @author Brian Clozel + */ +@ControllerAdvice +final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 93b2b3d5a4..7ef353439a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -114,6 +114,7 @@ import org.springframework.web.servlet.i18n.FixedLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.servlet.resource.EncodedResourceResolver; import org.springframework.web.servlet.resource.ResourceResolver; import org.springframework.web.servlet.resource.ResourceUrlProvider; @@ -643,6 +644,18 @@ public class WebMvcAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true") + static class ProblemDetailsErrorHandlingConfiguration { + + @Bean + @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { + return new ProblemDetailsExceptionHandler(); + } + + } + /** * Decorator to make * {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java index b779bf5299..137d194345 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java @@ -106,6 +106,8 @@ public class WebMvcProperties { private final Pathmatch pathmatch = new Pathmatch(); + private final Problemdetails problemdetails = new Problemdetails(); + public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() { return this.messageCodesResolverFormat; } @@ -213,6 +215,10 @@ public class WebMvcProperties { return this.pathmatch; } + public Problemdetails getProblemdetails() { + return this.problemdetails; + } + public static class Async { /** @@ -447,4 +453,21 @@ public class WebMvcProperties { } + public static class Problemdetails { + + /** + * Whether RFC 7807 Problem Details support should be enabled. + */ + private boolean enabled = false; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index bb9cd8889b..4d5b717b03 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -70,6 +70,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -85,6 +86,7 @@ import org.springframework.web.reactive.resource.ResourceWebHandler; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; @@ -622,6 +624,25 @@ class WebFluxAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(propertiesClass)); } + @Test + void problemDetailsDisabledByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)); + } + + @Test + void problemDetailsEnabledAddsExceptionHandler() { + this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true") + .run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class)); + } + + @Test + void problemDetailsBacksOffWhenExceptionHandler() { + this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true") + .withUserConfiguration(CustomExceptionResolverConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class) + .hasSingleBean(CustomExceptionResolver.class)); + } + private ContextConsumer assertExchangeWithSession( Consumer exchange) { return (context) -> { @@ -911,4 +932,19 @@ class WebFluxAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomExceptionResolverConfiguration { + + @Bean + CustomExceptionResolver customExceptionResolver() { + return new CustomExceptionResolver(); + } + + } + + @ControllerAdvice + static class CustomExceptionResolver extends ResponseEntityExceptionHandler { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 9421e84a5a..b610ca83fb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -77,6 +77,7 @@ import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ParameterContentNegotiationStrategy; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -106,6 +107,7 @@ import org.springframework.web.servlet.i18n.FixedLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.servlet.resource.CachingResourceResolver; import org.springframework.web.servlet.resource.CachingResourceTransformer; @@ -959,6 +961,25 @@ class WebMvcAutoConfigurationTests { } } + @Test + void problemDetailsDisabledByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)); + } + + @Test + void problemDetailsEnabledAddsExceptionHandler() { + this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true") + .run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class)); + } + + @Test + void problemDetailsBacksOffWhenExceptionHandler() { + this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true") + .withUserConfiguration(CustomExceptionResolverConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class) + .hasSingleBean(CustomExceptionResolver.class)); + } + private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context, Consumer handlerConsumer) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); @@ -1485,4 +1506,19 @@ class WebMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomExceptionResolverConfiguration { + + @Bean + CustomExceptionResolver customExceptionResolver() { + return new CustomExceptionResolver(); + } + + } + + @ControllerAdvice + static class CustomExceptionResolver extends ResponseEntityExceptionHandler { + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index f47aaf1515..43b028e73d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -123,6 +123,23 @@ For machine clients, it produces a JSON response with details of the error, the For browser clients, there is a "`whitelabel`" error handler that renders the same data in HTML format. You can also provide your own HTML templates to display errors (see the <>). +Before customizing error handling in Spring Boot directly, you can leverage the {spring-framework-docs}/web-reactive.html#webflux-ann-rest-exceptions[RFC 7807 Problem Details] support in Spring WebFlux. +Spring WebFlux can produce custom error messages with the `application/problem+json` media type, like: + +[source,json,indent=0,subs="verbatim"] +---- +{ + "type": "https://example.org/problems/unknown-project", + "title": "Unknown project", + "status": 404, + "detail": "No project found for id 'spring-unknown'", + "instance": "/projects/spring-unknown" +} +---- + +This support can be enabled by setting configprop:spring.webflux.problemdetails.enabled[] to `true`. + + The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents. For that, you can add a bean of type `ErrorAttributes`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index a125fc5110..ef637a9ba4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -298,6 +298,22 @@ TIP: The `BasicErrorController` can be used as a base class for a custom `ErrorC This is particularly useful if you want to add a handler for a new content type (the default is to handle `text/html` specifically and provide a fallback for everything else). To do so, extend `BasicErrorController`, add a public method with a `@RequestMapping` that has a `produces` attribute, and create a bean of your new type. +As of Spring Framework 6.0, {spring-framework-docs}/web.html#mvc-ann-rest-exceptions[RFC 7807 Problem Details] is supported. +Spring MVC can produce custom error messages with the `application/problem+json` media type, like: + +[source,json,indent=0,subs="verbatim"] +---- +{ + "type": "https://example.org/problems/unknown-project", + "title": "Unknown project", + "status": 404, + "detail": "No project found for id 'spring-unknown'", + "instance": "/projects/spring-unknown" +} +---- + +This support can be enabled by setting configprop:spring.mvc.problemdetails.enabled[] to `true`. + You can also define a class annotated with `@ControllerAdvice` to customize the JSON document to return for a particular controller and/or exception type, as shown in the following example: include::code:MyControllerAdvice[]