Add nullability annotations to module/spring-boot-webmvc

See gh-46587
This commit is contained in:
Moritz Halbritter
2025-08-07 10:48:44 +02:00
parent e57be21b20
commit 279258f699
33 changed files with 276 additions and 155 deletions
@@ -30,6 +30,7 @@ import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import org.springframework.aot.hint.RuntimeHints;
@@ -58,6 +59,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.ResponseEntity.BodyBuilder;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
@@ -96,12 +98,11 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
private final EndpointMediaTypes endpointMediaTypes;
private final CorsConfiguration corsConfiguration;
private final @Nullable CorsConfiguration corsConfiguration;
private final boolean shouldRegisterLinksMapping;
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle",
HttpServletRequest.class, Map.class);
private final Method handleMethod = getHandleMethod();
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
@@ -130,7 +131,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
*/
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) {
@Nullable CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) {
this.endpointMapping = endpointMapping;
this.endpoints = endpoints;
this.endpointMediaTypes = endpointMediaTypes;
@@ -139,6 +140,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
setOrder(-100);
}
private static Method getHandleMethod() {
Method method = ReflectionUtils.findMethod(OperationHandler.class, "handle", HttpServletRequest.class,
Map.class);
Assert.state(method != null, "'method' must not be null");
return method;
}
@Override
public void afterPropertiesSet() {
this.builderConfig = new RequestMappingInfo.BuilderConfiguration();
@@ -219,8 +227,10 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
.options(this.builderConfig)
.build();
LinksHandler linksHandler = getLinksHandler();
registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links",
HttpServletRequest.class, HttpServletResponse.class));
Method links = ReflectionUtils.findMethod(linksHandler.getClass(), "links", HttpServletRequest.class,
HttpServletResponse.class);
Assert.state(links != null, "'links' must not be null");
registerMapping(mapping, linksHandler, links);
}
@Override
@@ -229,12 +239,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) {
protected @Nullable CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
@Override
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
protected @Nullable CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
CorsConfiguration corsConfiguration = super.getCorsConfiguration(handler, request);
return (corsConfiguration != null) ? corsConfiguration : this.corsConfiguration;
}
@@ -245,7 +256,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
return null;
}
@@ -269,7 +280,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
@FunctionalInterface
protected interface LinksHandler {
Object links(HttpServletRequest request, HttpServletResponse response);
@Nullable Object links(HttpServletRequest request, HttpServletResponse response);
}
@@ -279,7 +290,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
@FunctionalInterface
protected interface ServletWebOperation {
Object handle(HttpServletRequest request, Map<String, String> body);
@Nullable Object handle(HttpServletRequest request, @Nullable Map<String, String> body);
}
@@ -309,7 +320,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
}
@Override
public Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
public @Nullable Object handle(HttpServletRequest request,
@RequestBody(required = false) @Nullable Map<String, String> body) {
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
Map<String, Object> arguments = getArguments(request, body);
try {
@@ -341,7 +353,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
return "Actuator web endpoint '" + this.operation.getId() + "'";
}
private Map<String, Object> getArguments(HttpServletRequest request, Map<String, String> body) {
private Map<String, Object> getArguments(HttpServletRequest request, @Nullable Map<String, String> body) {
Map<String, Object> arguments = new LinkedHashMap<>(getTemplateVariables(request));
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
@@ -386,7 +398,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private Object handleResult(Object result, HttpMethod httpMethod) {
private @Nullable Object handleResult(@Nullable Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(
(httpMethod != HttpMethod.GET) ? HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND);
@@ -396,12 +408,17 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
}
MediaType contentType = (response.getContentType() != null) ? new MediaType(response.getContentType())
: null;
return ResponseEntity.status(response.getStatus())
.contentType(contentType)
.body(convertIfNecessary(response.getBody()));
BodyBuilder builder = ResponseEntity.status(response.getStatus());
if (contentType != null) {
builder = builder.contentType(contentType);
}
return builder.body(convertIfNecessary(response.getBody()));
}
private Object convertIfNecessary(Object body) {
private @Nullable Object convertIfNecessary(@Nullable Object body) {
if (body == null) {
return null;
}
for (Function<Object, Object> converter : BODY_CONVERTERS) {
body = converter.apply(body);
}
@@ -435,7 +452,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
@ResponseBody
@Reflective
Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
@Nullable Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
return this.operation.handle(request, body);
}
@@ -504,7 +521,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, OperationHandler.class);
}
@@ -20,7 +20,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
@@ -37,18 +40,18 @@ import org.springframework.web.servlet.HandlerMapping;
*/
public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final ExposableWebEndpoint healthEndpoint;
private final @Nullable ExposableWebEndpoint healthEndpoint;
private final Set<HealthEndpointGroup> groups;
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint healthEndpoint,
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(@Nullable ExposableWebEndpoint healthEndpoint,
Set<HealthEndpointGroup> groups) {
super(new EndpointMapping(""), asList(healthEndpoint), null, false);
super(new EndpointMapping(""), asList(healthEndpoint), new EndpointMediaTypes(), false);
this.healthEndpoint = healthEndpoint;
this.groups = groups;
}
private static Collection<ExposableWebEndpoint> asList(ExposableWebEndpoint healthEndpoint) {
private static Collection<ExposableWebEndpoint> asList(@Nullable ExposableWebEndpoint healthEndpoint) {
return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList();
}
@@ -73,7 +76,7 @@ public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractW
@Override
protected LinksHandler getLinksHandler() {
return null;
return (request, response) -> null;
}
}
@@ -25,6 +25,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.endpoint.Access;
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
@@ -34,9 +36,11 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* {@link HandlerMapping} that exposes
@@ -58,7 +62,7 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
private final EndpointMapping endpointMapping;
private final CorsConfiguration corsConfiguration;
private final @Nullable CorsConfiguration corsConfiguration;
private final Map<Object, ExposableControllerEndpoint> handlers;
@@ -72,7 +76,7 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration) {
Collection<ExposableControllerEndpoint> endpoints, @Nullable CorsConfiguration corsConfiguration) {
this(endpointMapping, endpoints, corsConfiguration, (endpointId, defaultAccess) -> Access.NONE);
}
@@ -85,7 +89,7 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
* @param endpointAccessResolver resolver for endpoint access
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration,
Collection<ExposableControllerEndpoint> endpoints, @Nullable CorsConfiguration corsConfiguration,
EndpointAccessResolver endpointAccessResolver) {
Assert.notNull(endpointMapping, "'endpointMapping' must not be null");
Assert.notNull(endpoints, "'endpoints' must not be null");
@@ -110,6 +114,7 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
ExposableControllerEndpoint endpoint = this.handlers.get(handler);
Assert.state(endpoint != null, "'endpoint' must not be null");
Access access = this.accessResolver.accessFor(endpoint.getEndpointId(), endpoint.getDefaultAccess());
if (access == Access.NONE) {
return;
@@ -138,9 +143,13 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoint endpoint,
RequestMappingInfo mapping) {
Set<PathPattern> patterns = mapping.getPathPatternsCondition().getPatterns();
PathPatternsRequestCondition condition = mapping.getPathPatternsCondition();
Assert.state(condition != null, "'condition' must not be null");
Set<PathPattern> patterns = condition.getPatterns();
if (patterns.isEmpty()) {
patterns = Collections.singleton(getPatternParser().parse(""));
PathPatternParser parser = getPatternParser();
Assert.state(parser != null, "'parser' must not be null");
patterns = Collections.singleton(parser.parse(""));
}
String[] endpointMappedPatterns = patterns.stream()
.map((pattern) -> getEndpointMappedPattern(endpoint, pattern))
@@ -158,7 +167,8 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) {
protected @Nullable CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
@@ -22,6 +22,7 @@ import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
@@ -64,7 +65,7 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
*/
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
EndpointMediaTypes endpointMediaTypes, @Nullable CorsConfiguration corsConfiguration,
EndpointLinksResolver linksResolver, boolean shouldRegisterLinksMapping) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping);
this.linksResolver = linksResolver;
@@ -104,7 +105,7 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, WebMvcLinksHandler.class);
this.bindingRegistrar.registerReflectionHints(hints.reflection(), Link.class);
}
@@ -17,4 +17,7 @@
/**
* Spring MVC support for actuator endpoints.
*/
@NullMarked
package org.springframework.boot.webmvc.actuate.endpoint.web;
import org.jspecify.annotations.NullMarked;
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import io.undertow.servlet.api.DeploymentManager;
import jakarta.servlet.ServletException;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
@@ -133,7 +134,10 @@ final class DispatcherServletHandlerMappings {
void initializeServlet(String name) {
try {
this.webServer.getDeploymentManager().getDeployment().getServlets().getManagedServlet(name).forceInit();
DeploymentManager deploymentManager = this.webServer.getDeploymentManager();
if (deploymentManager != null) {
deploymentManager.getDeployment().getServlets().getManagedServlet(name).forceInit();
}
}
catch (ServletException ex) {
// Continue
@@ -16,6 +16,8 @@
package org.springframework.boot.webmvc.actuate.mappings;
import org.jspecify.annotations.Nullable;
import org.springframework.web.servlet.DispatcherServlet;
/**
@@ -30,9 +32,10 @@ public class DispatcherServletMappingDescription {
private final String predicate;
private final DispatcherServletMappingDetails details;
private final @Nullable DispatcherServletMappingDetails details;
DispatcherServletMappingDescription(String predicate, String handler, DispatcherServletMappingDetails details) {
DispatcherServletMappingDescription(String predicate, String handler,
@Nullable DispatcherServletMappingDetails details) {
this.handler = handler;
this.predicate = predicate;
this.details = details;
@@ -46,7 +49,7 @@ public class DispatcherServletMappingDescription {
return this.predicate;
}
public DispatcherServletMappingDetails getDetails() {
public @Nullable DispatcherServletMappingDetails getDetails() {
return this.details;
}
@@ -16,6 +16,8 @@
package org.springframework.boot.webmvc.actuate.mappings;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.web.mappings.HandlerMethodDescription;
import org.springframework.web.servlet.DispatcherServlet;
@@ -28,33 +30,33 @@ import org.springframework.web.servlet.DispatcherServlet;
*/
public class DispatcherServletMappingDetails {
private HandlerMethodDescription handlerMethod;
private @Nullable HandlerMethodDescription handlerMethod;
private HandlerFunctionDescription handlerFunction;
private @Nullable HandlerFunctionDescription handlerFunction;
private RequestMappingConditionsDescription requestMappingConditions;
private @Nullable RequestMappingConditionsDescription requestMappingConditions;
public HandlerMethodDescription getHandlerMethod() {
public @Nullable HandlerMethodDescription getHandlerMethod() {
return this.handlerMethod;
}
void setHandlerMethod(HandlerMethodDescription handlerMethod) {
void setHandlerMethod(@Nullable HandlerMethodDescription handlerMethod) {
this.handlerMethod = handlerMethod;
}
public HandlerFunctionDescription getHandlerFunction() {
public @Nullable HandlerFunctionDescription getHandlerFunction() {
return this.handlerFunction;
}
void setHandlerFunction(HandlerFunctionDescription handlerFunction) {
void setHandlerFunction(@Nullable HandlerFunctionDescription handlerFunction) {
this.handlerFunction = handlerFunction;
}
public RequestMappingConditionsDescription getRequestMappingConditions() {
public @Nullable RequestMappingConditionsDescription getRequestMappingConditions() {
return this.requestMappingConditions;
}
void setRequestMappingConditions(RequestMappingConditionsDescription requestMappingConditions) {
void setRequestMappingConditions(@Nullable RequestMappingConditionsDescription requestMappingConditions) {
this.requestMappingConditions = requestMappingConditions;
}
@@ -28,6 +28,7 @@ import java.util.function.Function;
import java.util.stream.Stream;
import jakarta.servlet.Servlet;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
@@ -272,7 +273,7 @@ public class DispatcherServletsMappingDescriptionProvider implements MappingDesc
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
this.bindingRegistrar.registerReflectionHints(hints.reflection(),
DispatcherServletMappingDescription.class);
}
@@ -19,9 +19,13 @@ package org.springframework.boot.webmvc.actuate.mappings;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
import org.springframework.web.servlet.mvc.condition.NameValueExpression;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
/**
@@ -73,8 +77,12 @@ public class RequestMappingConditionsDescription {
private Set<String> extractPathPatterns(RequestMappingInfo requestMapping) {
org.springframework.web.servlet.mvc.condition.PatternsRequestCondition patternsCondition = requestMapping
.getPatternsCondition();
return (patternsCondition != null) ? patternsCondition.getPatterns()
: requestMapping.getPathPatternsCondition().getPatternValues();
if (patternsCondition != null) {
return patternsCondition.getPatterns();
}
PathPatternsRequestCondition condition = requestMapping.getPathPatternsCondition();
Assert.state(condition != null, "'condition' must not be null");
return condition.getPatternValues();
}
public List<MediaTypeExpressionDescription> getConsumes() {
@@ -132,7 +140,7 @@ public class RequestMappingConditionsDescription {
private final String name;
private final Object value;
private final @Nullable Object value;
private final boolean negated;
@@ -146,7 +154,7 @@ public class RequestMappingConditionsDescription {
return this.name;
}
public Object getValue() {
public @Nullable Object getValue() {
return this.value;
}
@@ -17,4 +17,7 @@
/**
* Actuator request mappings support for Spring MVC.
*/
@NullMarked
package org.springframework.boot.webmvc.actuate.mappings;
import org.jspecify.annotations.NullMarked;
@@ -47,6 +47,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
@@ -132,6 +133,7 @@ public final class DispatcherServletAutoConfiguration {
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("Default DispatcherServlet");
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Assert.state(beanFactory != null, "'beanFactory' must not be null");
List<String> dispatchServletBeans = Arrays
.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false));
if (dispatchServletBeans.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
@@ -158,6 +160,7 @@ public final class DispatcherServletAutoConfiguration {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Assert.state(beanFactory != null, "'beanFactory' must not be null");
ConditionOutcome outcome = checkDefaultDispatcherName(beanFactory);
if (!outcome.isMatch()) {
return outcome;
@@ -18,6 +18,7 @@ package org.springframework.boot.webmvc.autoconfigure;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
@@ -25,6 +26,7 @@ import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
@@ -74,9 +76,12 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Contract;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.DefaultMessageCodesResolver;
@@ -201,9 +206,9 @@ public final class WebMvcAutoConfiguration {
private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations;
private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private final @Nullable ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private ServletContext servletContext;
private @Nullable ServletContext servletContext;
private final ObjectProvider<ApiVersionResolver> apiVersionResolvers;
@@ -328,7 +333,7 @@ public final class WebMvcAutoConfiguration {
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
public @Nullable MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
resolver.setMessageCodeFormatter(this.mvcProperties.getMessageCodesResolverFormat());
@@ -370,13 +375,20 @@ public final class WebMvcAutoConfiguration {
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
Integer cachePeriod = getSeconds(this.resourceProperties.getCache().getPeriod());
if (cachePeriod != null) {
registration.setCachePeriod(cachePeriod);
}
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (cacheControl != null) {
registration.setCacheControl(cacheControl);
}
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}
private Integer getSeconds(Duration cachePeriod) {
@Contract("!null -> !null")
private @Nullable Integer getSeconds(@Nullable Duration cachePeriod) {
return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null;
}
@@ -433,8 +445,9 @@ public final class WebMvcAutoConfiguration {
private final ListableBeanFactory beanFactory;
private final WebMvcRegistrations mvcRegistrations;
private final @Nullable WebMvcRegistrations mvcRegistrations;
@SuppressWarnings("NullAway.Init")
private ResourceLoader resourceLoader;
EnableWebMvcConfiguration(WebMvcProperties mvcProperties, WebProperties webProperties,
@@ -491,11 +504,13 @@ public final class WebMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
public LocaleResolver localeResolver() {
Locale locale = this.webProperties.getLocale();
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.webProperties.getLocale());
Assert.state(locale != null, "'locale' must not be null");
return new FixedLocaleResolver(locale);
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.webProperties.getLocale());
localeResolver.setDefaultLocale(locale);
return localeResolver;
}
@@ -513,7 +528,7 @@ public final class WebMvcAutoConfiguration {
return super.viewNameTranslator();
}
private Resource getIndexHtmlResource() {
private @Nullable Resource getIndexHtmlResource() {
for (String location : this.resourceProperties.getStaticLocations()) {
Resource indexHtml = getIndexHtmlResource(location);
if (indexHtml != null) {
@@ -527,11 +542,11 @@ public final class WebMvcAutoConfiguration {
return null;
}
private Resource getIndexHtmlResource(String location) {
private @Nullable Resource getIndexHtmlResource(String location) {
return getIndexHtmlResource(this.resourceLoader.getResource(location));
}
private Resource getIndexHtmlResource(Resource location) {
private @Nullable Resource getIndexHtmlResource(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && (resource.getURL() != null)) {
@@ -564,7 +579,9 @@ public final class WebMvcAutoConfiguration {
getClass().getClassLoader())) {
return super.mvcValidator();
}
return ValidatorAdapter.get(getApplicationContext(), getValidator());
ApplicationContext applicationContext = getApplicationContext();
Assert.state(applicationContext != null, "'applicationContext' must not be null");
return ValidatorAdapter.get(applicationContext, getValidator());
}
@Override
@@ -620,7 +637,7 @@ public final class WebMvcAutoConfiguration {
@Override
@ConditionalOnMissingBean(name = "mvcApiVersionStrategy")
public ApiVersionStrategy mvcApiVersionStrategy() {
public @Nullable ApiVersionStrategy mvcApiVersionStrategy() {
return super.mvcApiVersionStrategy();
}
@@ -642,7 +659,7 @@ public final class WebMvcAutoConfiguration {
interface WelcomePageHandlerMappingFactory<T extends AbstractUrlHandlerMapping> {
T create(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext,
Resource indexHtmlResource, String staticPathPattern);
@Nullable Resource indexHtmlResource, String staticPathPattern);
}
@@ -682,6 +699,7 @@ public final class WebMvcAutoConfiguration {
if (properties.getFixed().isEnabled()) {
String version = properties.getFixed().getVersion();
String[] paths = properties.getFixed().getPaths();
Assert.state(version != null, "'version' must not be null");
resolver.addFixedVersionStrategy(version, paths);
}
if (properties.getContent().isEnabled()) {
@@ -22,6 +22,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.http.MediaType;
@@ -45,7 +47,7 @@ public class WebMvcProperties {
/**
* Formatting strategy for message codes. For instance, 'PREFIX_ERROR_CODE'.
*/
private DefaultMessageCodesResolver.Format messageCodesResolverFormat;
private DefaultMessageCodesResolver.@Nullable Format messageCodesResolverFormat;
private final Format format = new Format();
@@ -100,11 +102,11 @@ public class WebMvcProperties {
private final Apiversion apiversion = new Apiversion();
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
public DefaultMessageCodesResolver.@Nullable Format getMessageCodesResolverFormat() {
return this.messageCodesResolverFormat;
}
public void setMessageCodesResolverFormat(DefaultMessageCodesResolver.Format messageCodesResolverFormat) {
public void setMessageCodesResolverFormat(DefaultMessageCodesResolver.@Nullable Format messageCodesResolverFormat) {
this.messageCodesResolverFormat = messageCodesResolverFormat;
}
@@ -202,13 +204,13 @@ public class WebMvcProperties {
* Amount of time before asynchronous request handling times out. If this value is
* not set, the default timeout of the underlying implementation is used.
*/
private Duration requestTimeout;
private @Nullable Duration requestTimeout;
public Duration getRequestTimeout() {
public @Nullable Duration getRequestTimeout() {
return this.requestTimeout;
}
public void setRequestTimeout(Duration requestTimeout) {
public void setRequestTimeout(@Nullable Duration requestTimeout) {
this.requestTimeout = requestTimeout;
}
@@ -282,26 +284,26 @@ public class WebMvcProperties {
/**
* Spring MVC view prefix.
*/
private String prefix;
private @Nullable String prefix;
/**
* Spring MVC view suffix.
*/
private String suffix;
private @Nullable String suffix;
public String getPrefix() {
public @Nullable String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
public void setPrefix(@Nullable String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
public @Nullable String getSuffix() {
return this.suffix;
}
public void setSuffix(String suffix) {
public void setSuffix(@Nullable String suffix) {
this.suffix = suffix;
}
@@ -318,7 +320,7 @@ public class WebMvcProperties {
/**
* Query parameter name to use when "favor-parameter" is enabled.
*/
private String parameterName;
private @Nullable String parameterName;
/**
* Map file extensions to media types for content negotiation. For instance, yml
@@ -340,11 +342,11 @@ public class WebMvcProperties {
this.favorParameter = favorParameter;
}
public String getParameterName() {
public @Nullable String getParameterName() {
return this.parameterName;
}
public void setParameterName(String parameterName) {
public void setParameterName(@Nullable String parameterName) {
this.parameterName = parameterName;
}
@@ -389,41 +391,41 @@ public class WebMvcProperties {
* Date format to use, for example 'dd/MM/yyyy'. Used for formatting of
* java.util.Date and java.time.LocalDate.
*/
private String date;
private @Nullable String date;
/**
* Time format to use, for example 'HH:mm:ss'. Used for formatting of java.time's
* LocalTime and OffsetTime.
*/
private String time;
private @Nullable String time;
/**
* Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'. Used for formatting
* of java.time's LocalDateTime, OffsetDateTime, and ZonedDateTime.
*/
private String dateTime;
private @Nullable String dateTime;
public String getDate() {
public @Nullable String getDate() {
return this.date;
}
public void setDate(String date) {
public void setDate(@Nullable String date) {
this.date = date;
}
public String getTime() {
public @Nullable String getTime() {
return this.time;
}
public void setTime(String time) {
public void setTime(@Nullable String time) {
this.time = time;
}
public String getDateTime() {
public @Nullable String getDateTime() {
return this.dateTime;
}
public void setDateTime(String dateTime) {
public void setDateTime(@Nullable String dateTime) {
this.dateTime = dateTime;
}
@@ -477,58 +479,58 @@ public class WebMvcProperties {
/**
* Whether the API version is required with each request.
*/
private Boolean required;
private @Nullable Boolean required;
/**
* Default version that should be used for each request.
*/
@Name("default")
private String defaultVersion;
private @Nullable String defaultVersion;
/**
* Supported versions.
*/
private List<String> supported;
private @Nullable List<String> supported;
/**
* Whether supported versions should be detected from controllers.
*/
private Boolean detectSupported;
private @Nullable Boolean detectSupported;
/**
* How version details should be inserted into requests.
*/
private final Use use = new Use();
public Boolean getRequired() {
public @Nullable Boolean getRequired() {
return this.required;
}
public void setRequired(Boolean required) {
public void setRequired(@Nullable Boolean required) {
this.required = required;
}
public String getDefaultVersion() {
public @Nullable String getDefaultVersion() {
return this.defaultVersion;
}
public void setDefaultVersion(String defaultVersion) {
public void setDefaultVersion(@Nullable String defaultVersion) {
this.defaultVersion = defaultVersion;
}
public List<String> getSupported() {
public @Nullable List<String> getSupported() {
return this.supported;
}
public void setSupported(List<String> supported) {
public void setSupported(@Nullable List<String> supported) {
this.supported = supported;
}
public Boolean getDetectSupported() {
public @Nullable Boolean getDetectSupported() {
return this.detectSupported;
}
public void setDetectSupported(Boolean detectSupported) {
public void setDetectSupported(@Nullable Boolean detectSupported) {
this.detectSupported = detectSupported;
}
@@ -541,44 +543,44 @@ public class WebMvcProperties {
/**
* Use the HTTP header with the given name to obtain the version.
*/
private String header;
private @Nullable String header;
/**
* Use the query parameter with the given name to obtain the version.
*/
private String queryParameter;
private @Nullable String queryParameter;
/**
* Use the path segment at the given index to obtain the version.
*/
private Integer pathSegment;
private @Nullable Integer pathSegment;
/**
* Use the media type parameter with the given name to obtain the version.
*/
private Map<MediaType, String> mediaTypeParameter = new LinkedHashMap<>();
public String getHeader() {
public @Nullable String getHeader() {
return this.header;
}
public void setHeader(String header) {
public void setHeader(@Nullable String header) {
this.header = header;
}
public String getQueryParameter() {
public @Nullable String getQueryParameter() {
return this.queryParameter;
}
public void setQueryParameter(String queryParameter) {
public void setQueryParameter(@Nullable String queryParameter) {
this.queryParameter = queryParameter;
}
public Integer getPathSegment() {
public @Nullable Integer getPathSegment() {
return this.pathSegment;
}
public void setPathSegment(Integer pathSegment) {
public void setPathSegment(@Nullable Integer pathSegment) {
this.pathSegment = pathSegment;
}
@@ -16,6 +16,8 @@
package org.springframework.boot.webmvc.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
@@ -44,7 +46,7 @@ public interface WebMvcRegistrations {
* processed by the MVC configuration.
* @return the custom {@link RequestMappingHandlerMapping} instance
*/
default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
default @Nullable RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return null;
}
@@ -53,7 +55,7 @@ public interface WebMvcRegistrations {
* processed by the MVC configuration.
* @return the custom {@link RequestMappingHandlerAdapter} instance
*/
default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
default @Nullable RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return null;
}
@@ -62,7 +64,7 @@ public interface WebMvcRegistrations {
* processed by the MVC configuration.
* @return the custom {@link ExceptionHandlerExceptionResolver} instance
*/
default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
default @Nullable ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
return null;
}
@@ -16,6 +16,8 @@
package org.springframework.boot.webmvc.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
@@ -32,11 +34,11 @@ final class WelcomePage {
*/
static final WelcomePage UNRESOLVED = new WelcomePage(null, false);
private final String viewName;
private final @Nullable String viewName;
private final boolean templated;
private WelcomePage(String viewName, boolean templated) {
private WelcomePage(@Nullable String viewName, boolean templated) {
this.viewName = viewName;
this.templated = templated;
}
@@ -45,7 +47,7 @@ final class WelcomePage {
* Return the view name of the welcome page.
* @return the view name
*/
String getViewName() {
@Nullable String getViewName() {
return this.viewName;
}
@@ -66,7 +68,7 @@ final class WelcomePage {
* @return a resolved {@link WelcomePage} instance or {@link #UNRESOLVED}
*/
static WelcomePage resolve(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
ApplicationContext applicationContext, @Nullable Resource indexHtmlResource, String staticPathPattern) {
if (indexHtmlResource != null && "/**".equals(staticPathPattern)) {
return new WelcomePage("forward:index.html", false);
}
@@ -22,6 +22,7 @@ import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
@@ -51,7 +52,7 @@ final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
private static final List<MediaType> MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL);
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
ApplicationContext applicationContext, @Nullable Resource indexHtmlResource, String staticPathPattern) {
setOrder(2);
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);
@@ -65,7 +66,7 @@ final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
}
@Override
public Object getHandlerInternal(HttpServletRequest request) throws Exception {
public @Nullable Object getHandlerInternal(HttpServletRequest request) throws Exception {
return (!isHtmlTextAccepted(request)) ? null : super.getHandlerInternal(request);
}
@@ -18,6 +18,7 @@ package org.springframework.boot.webmvc.autoconfigure;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
@@ -36,7 +37,7 @@ import org.springframework.web.servlet.mvc.Controller;
class WelcomePageNotAcceptableHandlerMapping extends AbstractUrlHandlerMapping {
WelcomePageNotAcceptableHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
ApplicationContext applicationContext, @Nullable Resource indexHtmlResource, String staticPathPattern) {
setOrder(LOWEST_PRECEDENCE - 10); // Before ResourceHandlerRegistry
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);
@@ -45,13 +46,13 @@ class WelcomePageNotAcceptableHandlerMapping extends AbstractUrlHandlerMapping {
}
}
private ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
private @Nullable ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
return null;
}
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
protected @Nullable Object getHandlerInternal(HttpServletRequest request) throws Exception {
return super.getHandlerInternal(request);
}
@@ -18,6 +18,8 @@ package org.springframework.boot.webmvc.autoconfigure.actuate.endpoint.web;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
@@ -56,7 +58,7 @@ public final class WebMvcHealthEndpointExtensionAutoConfiguration {
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
}
private static ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) {
private static @Nullable ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
return webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Spring MVC actuator web endpoint support.
*/
@NullMarked
package org.springframework.boot.webmvc.autoconfigure.actuate.endpoint.web;
import org.jspecify.annotations.NullMarked;
@@ -22,6 +22,7 @@ import java.util.Optional;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -39,7 +40,7 @@ class CompositeHandlerAdapter implements HandlerAdapter {
private final ListableBeanFactory beanFactory;
private List<HandlerAdapter> adapters;
private @Nullable List<HandlerAdapter> adapters;
CompositeHandlerAdapter(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
@@ -51,7 +52,7 @@ class CompositeHandlerAdapter implements HandlerAdapter {
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
public @Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Optional<HandlerAdapter> adapter = getAdapter(handler);
if (adapter.isPresent()) {
@@ -21,6 +21,7 @@ import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
@@ -46,11 +47,11 @@ class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
@Autowired
private ListableBeanFactory beanFactory;
private volatile List<HandlerExceptionResolver> resolvers;
private volatile @Nullable List<HandlerExceptionResolver> resolvers;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
public @Nullable ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) {
for (HandlerExceptionResolver resolver : getResolvers()) {
ModelAndView resolved = resolver.resolveException(request, response, handler, ex);
if (resolved != null) {
@@ -76,7 +77,7 @@ class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
return resolvers;
}
private void collectResolverBeans(List<HandlerExceptionResolver> resolvers, BeanFactory beanFactory) {
private void collectResolverBeans(List<HandlerExceptionResolver> resolvers, @Nullable BeanFactory beanFactory) {
if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
resolvers.addAll(listableBeanFactory.getBeansOfType(HandlerExceptionResolver.class).values());
}
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -39,10 +40,10 @@ class CompositeHandlerMapping implements HandlerMapping {
@Autowired
private ListableBeanFactory beanFactory;
private List<HandlerMapping> mappings;
private @Nullable List<HandlerMapping> mappings;
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping mapping : getMappings()) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Spring MVC-based actuator infrastructure.
*/
@NullMarked
package org.springframework.boot.webmvc.autoconfigure.actuate.web;
import org.jspecify.annotations.NullMarked;
@@ -23,6 +23,7 @@ import java.util.Map;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.webmvc.error.ErrorAttributes;
@@ -55,13 +56,14 @@ public abstract class AbstractErrorController implements ErrorController {
this(errorAttributes, null);
}
public AbstractErrorController(ErrorAttributes errorAttributes, List<ErrorViewResolver> errorViewResolvers) {
public AbstractErrorController(ErrorAttributes errorAttributes,
@Nullable List<ErrorViewResolver> errorViewResolvers) {
Assert.notNull(errorAttributes, "'errorAttributes' must not be null");
this.errorAttributes = errorAttributes;
this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
}
private List<ErrorViewResolver> sortErrorViewResolvers(List<ErrorViewResolver> resolvers) {
private List<ErrorViewResolver> sortErrorViewResolvers(@Nullable List<ErrorViewResolver> resolvers) {
List<ErrorViewResolver> sorted = new ArrayList<>();
if (resolvers != null) {
sorted.addAll(resolvers);
@@ -142,8 +144,8 @@ public abstract class AbstractErrorController implements ErrorController {
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
*/
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
protected @Nullable ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response,
HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
@@ -22,6 +22,7 @@ import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
@@ -97,7 +98,8 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
public @Nullable ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
@@ -105,7 +107,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
private @Nullable ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
@@ -115,7 +117,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
private @Nullable ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
@@ -157,7 +159,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream());
@@ -24,6 +24,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.BeansException;
@@ -65,6 +66,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.http.MediaType;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.BeanNameViewResolver;
@@ -173,9 +176,11 @@ public final class ErrorMvcAutoConfiguration {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ErrorTemplate Missing");
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(context.getClassLoader());
ClassLoader classLoader = context.getClassLoader();
Assert.state(classLoader != null, "'classLoader' must not be null");
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(classLoader);
TemplateAvailabilityProvider provider = providers.getProvider("error", context.getEnvironment(),
context.getClassLoader(), context.getResourceLoader());
classLoader, context.getResourceLoader());
if (provider != null) {
return ConditionOutcome.noMatch(message.foundExactly("template from " + provider));
}
@@ -194,8 +199,9 @@ public final class ErrorMvcAutoConfiguration {
private static final Log logger = LogFactory.getLog(StaticView.class);
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Assert.state(model != null, "'model' must not be null");
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
@@ -229,7 +235,8 @@ public final class ErrorMvcAutoConfiguration {
response.getWriter().append(builder.toString());
}
private String htmlEscape(Object input) {
@Contract("!null -> !null")
private @Nullable String htmlEscape(@Nullable Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}
@@ -19,6 +19,7 @@ package org.springframework.boot.webmvc.autoconfigure.error;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
@@ -39,6 +40,6 @@ public interface ErrorViewResolver {
* @param model the suggested model to be used with the view
* @return a resolved {@link ModelAndView} or {@code null}
*/
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
@Nullable ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Spring MVC error handling.
*/
@NullMarked
package org.springframework.boot.webmvc.autoconfigure.error;
import org.jspecify.annotations.NullMarked;
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Spring MVC.
*/
@NullMarked
package org.springframework.boot.webmvc.autoconfigure;
import org.jspecify.annotations.NullMarked;
@@ -26,6 +26,7 @@ import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.error.Error;
import org.springframework.boot.web.error.ErrorAttributeOptions;
@@ -81,8 +82,8 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
public @Nullable ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) {
storeErrorAttributes(request, ex);
return null;
}
@@ -139,7 +140,8 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
addErrorMessage(errorAttributes, webRequest, error);
}
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest,
@Nullable Throwable error) {
BindingResult bindingResult = extractBindingResult(error);
if (bindingResult != null) {
addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult);
@@ -166,7 +168,8 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
errorAttributes.put("errors", Error.wrapIfNecessary(result.getAllErrors()));
}
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest,
@Nullable Throwable error) {
errorAttributes.put("message", getMessage(webRequest, error));
}
@@ -183,7 +186,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
* @param error current error, if any
* @return message to include in the error attributes
*/
protected String getMessage(WebRequest webRequest, Throwable error) {
protected String getMessage(WebRequest webRequest, @Nullable Throwable error) {
Object message = getAttribute(webRequest, RequestDispatcher.ERROR_MESSAGE);
if (!ObjectUtils.isEmpty(message)) {
return message.toString();
@@ -194,14 +197,14 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
return "No message available";
}
private BindingResult extractBindingResult(Throwable error) {
private @Nullable BindingResult extractBindingResult(@Nullable Throwable error) {
if (error instanceof BindingResult bindingResult) {
return bindingResult;
}
return null;
}
private MethodValidationResult extractMethodValidationResult(Throwable error) {
private @Nullable MethodValidationResult extractMethodValidationResult(@Nullable Throwable error) {
if (error instanceof MethodValidationResult methodValidationResult) {
return methodValidationResult;
}
@@ -223,7 +226,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
@Override
public Throwable getError(WebRequest webRequest) {
public @Nullable Throwable getError(WebRequest webRequest) {
Throwable exception = getAttribute(webRequest, ERROR_INTERNAL_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
@@ -232,7 +235,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
@SuppressWarnings("unchecked")
private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
private <T> @Nullable T getAttribute(RequestAttributes requestAttributes, String name) {
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
@@ -19,6 +19,8 @@ package org.springframework.boot.webmvc.error;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;
@@ -52,6 +54,6 @@ public interface ErrorAttributes {
* @param webRequest the source request
* @return the {@link Exception} that caused the error or {@code null}
*/
Throwable getError(WebRequest webRequest);
@Nullable Throwable getError(WebRequest webRequest);
}
@@ -17,4 +17,7 @@
/**
* Spring MVC error handling infrastructure.
*/
@NullMarked
package org.springframework.boot.webmvc.error;
import org.jspecify.annotations.NullMarked;