In this tutorial, we will understand how content negotiation works in Spring Web MVC.
The ContentNegotiationStrategy interface
It is a strategy interface for resolving the requested media types of an HTTP request.
Definition of ContentNegotiationStrategyVersion: 7.0.6 package org.springframework.web.accept;
@FunctionalInterface
public interface ContentNegotiationStrategy {
List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws
HttpMediaTypeNotAcceptableException; 1
}
Let's see which ContentNegotiationStrategies are registered by default.
The default ContentNegotiationStrategies
In the following controller, we are going to print the ContentNegotiationManager registered as a bean by default. It is a Central class to determine requested media types for a request. It does so by delegating to a list of configured ContentNegotiationStrategy instances.
package com.logicbig.example;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
@Controller
public class MyController {
@Autowired
ApplicationContext context;
@RequestMapping("/")
@ResponseBody
public String handleRequest() {
Map<String, ContentNegotiationStrategy> map =
BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, ContentNegotiationStrategy.class,
true, false);
System.out.println("-- list of ContentNegotiationStrategies --");
map.forEach((k, v) ->
System.out.printf("%s=%s%n",
k,
v.getClass().getSimpleName()));
System.out.println("-- list of ContentNegotiationStrategies configured "
+ "with ContentNegotiationManager --");
ContentNegotiationManager m = (ContentNegotiationManager) map.get(
"mvcContentNegotiationManager");
List<ContentNegotiationStrategy> strategies = m.getStrategies();
strategies.forEach(s -> System.out.println(
s.getClass().getName()));
return "response";
}
}
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Accessing http://localhost:8080/:
$ curl -s "http://localhost:8080/" response
Server Output -- list of ContentNegotiationStrategies -- mvcContentNegotiationManager=ContentNegotiationManager -- list of ContentNegotiationStrategies configured with ContentNegotiationManager -- org.springframework.web.accept.HeaderContentNegotiationStrategy
Understanding ContentNegotiationManager
The ContentNegotiationManager is the central class that determines the requested media types for a request. Its resolveMediaTypes() method does not resolve the media types itself. Instead, it delegates to a list of configured ContentNegotiationStrategies. Above output show only one HeaderContentNegotiationStrategy is registered in latest version of Spring.
Since Spring Framework 7.0, HeaderContentNegotiationStrategy is the only strategy registered by default.
HeaderContentNegotiationStrategy
It reads the 'Accept' request header to resolve the media types. If the header is absent, it returns MediaType.ALL (*/*), which means the client accepts any media type.
Removed: path extension strategies
Older versions of Spring (5.2 and earlier) also registered ServletPathExtensionContentNegotiationStrategy by default. It resolved the file extension in the request path (for example, /report.json) to a media type. This approach was deprecated in Spring 5.2.4 and disabled by default in 5.3, mainly because of security concerns (such as reflected file download attacks) and ambiguity in URL matching. It has been removed completely in Spring Framework 7.0, together with the related PathExtensionContentNegotiationStrategy, the favorPathExtension and ignoreUnknownPathExtensions options, and the suffix pattern matching options.
If we run above code in older version (pre 5.3), the server side will show following output (we need to uncomment the related code as well)
-- list of ContentNegotiationStrategies -- mvcContentNegotiationManager=ContentNegotiationManager -- list of ContentNegotiationStrategies configured with ContentNegotiationManager -- org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy org.springframework.web.accept.HeaderContentNegotiationStrategy -- file extensions configured with ServletPathExtensionContentNegotiationStrategy -- xml
If you still depend on extension-based negotiation, use the 'Accept' header instead. Alternatively, you can enable the query parameter strategy (for example, /report?format=json) with ContentNegotiationConfigurer#favorParameter(true).
The order of ContentNegotiationStrategies
The order of the strategies matters only when more than one strategy is configured. Strategies are applied in the order in which they are registered. When we add more strategies, the order is applied as specified
Media type matching criteria
For an HTTP request, if a strategy returns MEDIA_TYPE_ALL_LIST (a list containing only */*), the next strategy is attempted, until a more specific media type is resolved. If no strategy resolves a specific media type, MEDIA_TYPE_ALL_LIST is returned, which means any media type is acceptable.
How are the resolved media types used?
Let's find out how the default instance of ContentNegotiationManager is used at configuration time. The manager is registered as a bean by the factory method WebMvcConfigurationSupport#mvcContentNegotiationManager(). These are the callers of this method (screenshot from IntelliJ):
As seen in the above screenshot, the following components of Spring MVC are configured with the default instance of ContentNegotiationManager:
RequestMappingHandlerMapping For RequestMappingHandlerMapping, the media types returned by ContentNegotiationManager#resolveMediaTypes() are matched against the value(s) specified in the produces or headers elements of @RequestMapping (and its shortcut annotations such as @GetMapping) on the handler methods, until a match is selected.
RequestMappingHandlerAdapter Handler's returned object to HTTP response body resolution RequestMappingHandlerAdapter uses the media types returned by ContentNegotiationManager#resolveMediaTypes() to select a suitable HttpMessageConverter. The write() method of the selected HttpMessageConverter is then called with the handler method's returned value to perform the conversion. HTTP request body to handler method argument resolution Based on the request 'Content-Type' header, a suitable HttpMessageConverter is selected, and then its read() method is called to convert the request body to a Java object. RequestMappingHandlerAdapter delegates this process to an instance of HandlerMethodArgumentResolver. The selection logic is implemented in AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(). Note that this direction is driven by the 'Content-Type' header, not by ContentNegotiationStrategies.
ResourceHandlerRegistry It applies the ContentNegotiationStrategies to static resources such as images and CSS files.
ViewResolverRegistry It applies the ContentNegotiationStrategies to ContentNegotiatingViewResolver, which uses the media types returned by the ContentNegotiationStrategies to select a suitable View for a request during view rendering.
ExceptionHandlerExceptionResolver Similar to RequestMappingHandlerAdapter, ExceptionHandlerExceptionResolver uses the ContentNegotiationStrategies to select a suitable HttpMessageConverter to write the response (from the method annotated with @ExceptionHandler).
In the next couple of tutorials, we will explore examples of customizing the default ContentNegotiationStrategies and of writing a custom ContentNegotiationStrategy.
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.3.0.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- JDK 25
- Maven 3.9.11
|