Content Negotiation Content Negotiation Negotiation Mechanism-Implementation Principle and Custom Configuration of Spring MVC Content Negotiation

Keywords: Spring xml JSON Attribute

Every sentence

In the face of absolute power, all skills are floating clouds

Preface

Above This paper introduces some concepts of Http content negotiation, and introduces the use of four negotiation modes built in Spring MVC. This paper mainly focuses on Spring MVC content negotiation: understanding from the step and principle level, and finally achieving the effect of expanding the negotiation mode by oneself.

First of all, it is necessary to introduce the principle analysis of the four negotiation strategies supported by Spring MVC by default.

ContentNegotiationStrategy

This interface is Spring MVC's policy interface for content negotiation:

// A strategy for resolving the requested media types for a request.
// @since 3.2
@FunctionalInterface
public interface ContentNegotiationStrategy {
	// @since 5.0.5
	List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);

	// Resolve a given request into a list of media types
	// The returned lists are sorted first by the specificity parameter, and secondly by the quality parameter.
	// Throw an HttpMediaTypeNotAcceptable Exception exception if the requested media type cannot be parsed
	List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException;
}

To put it bluntly, the policy interface is to know what type of data List the client needs for its request. from Above We know that Spring MVC supports four different negotiation mechanisms, which are all related to this policy interface.
Its succession tree:

From the name of the implementation class, we can see that it corresponds exactly to the four ways mentioned above (except Content Negotiation Manager).

Spring MVC loads two implementation classes of the policy interface by default:
Servlet Path Extension Content Negotiation Strategy --> Based on File Extensions (RESTful support).
Header Content Negotiation Strategy --> According to the Accept field in HTTP Header (supporting Http).

HeaderContentNegotiationStrategy

Accept Header parsing: It negotiates based on the request header Accept.

public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
	
		// My Chrome browser values are: [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
		// The value of postman is: [*/*]
		String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
		if (headerValueArray == null) {
			return MEDIA_TYPE_ALL_LIST;
		}

		List<String> headerValues = Arrays.asList(headerValueArray);
		try {
			List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
			// sort
			MediaType.sortBySpecificityAndQuality(mediaTypes);
			// Finally, the List of Chrome browser is as follows:
			// 0 = {MediaType@6205} "text/html"
			// 1 = {MediaType@6206} "application/xhtml+xml"
			// 2 = {MediaType@6207} "image/webp"
			// 3 = {MediaType@6208} "image/apng"
			// 4 = {MediaType@6209} "application/signed-exchange;v=b3"
			// 5 = {MediaType@6210} "application/xml;q=0.9"
			// 6 = {MediaType@6211} "*/*;q=0.8"
			return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
		} catch (InvalidMediaTypeException ex) {
			throw new HttpMediaTypeNotAcceptableException("Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
		}
	}
}

As you can see, if Accept is not passed, MediaType.ALL is used by default.*/*

AbstractMappingContentNegotiationStrategy

An Abstract implementation class negotiated through file extension/query param. Before you know it, it's necessary to jump in and learn about MediaTypeFile Extension Resolver first.

MediaTypeFileExtension Resolver: An interface between MediaType and path extension resolution policies, such as parsing. json into application/json or reverse parsing

// @since 3.2
public interface MediaTypeFileExtensionResolver {

	// Returns a set of file extensions based on the specified mediaType
	List<String> resolveFileExtensions(MediaType mediaType);
	// Returns all extensions registered with the interface
	List<String> getAllFileExtensions();
}

The succession tree is as follows:

Obviously, we only need to explain its direct implementation subclass Mapping MediaTypeFileExtension Resolver:

MappingMediaTypeFileExtensionResolver
public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver {

	// key is lowerCase Extension and value is the corresponding mediaType
	private final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<>(64);
	// Contrary to the above, key is mediaType and value is lowerCase Extension (apparently using a multi-valued map)
	private final MultiValueMap<MediaType, String> fileExtensions = new LinkedMultiValueMap<>();
	// All extensions (List is not set oh ~)
	private final List<String> allFileExtensions = new ArrayList<>();

	...
	public Map<String, MediaType> getMediaTypes() {
		return this.mediaTypes;
	}
	// protected method
	protected List<MediaType> getAllMediaTypes() {
		return new ArrayList<>(this.mediaTypes.values());
	}
	// Add a corresponding mediaType to extension
	// Concurrent Map is used to avoid consistency problems caused by concurrency
	protected void addMapping(String extension, MediaType mediaType) {
		MediaType previous = this.mediaTypes.putIfAbsent(extension, mediaType);
		if (previous == null) {
			this.fileExtensions.add(mediaType, extension);
			this.allFileExtensions.add(extension);
		}
	}

	// Interface method: Get the extensions corresponding to the specified mediaType~
	@Override
	public List<String> resolveFileExtensions(MediaType mediaType) {
		List<String> fileExtensions = this.fileExtensions.get(mediaType);
		return (fileExtensions != null ? fileExtensions : Collections.emptyList());
	}
	@Override
	public List<String> getAllFileExtensions() {
		return Collections.unmodifiableList(this.allFileExtensions);
	}

	// protected method: Find a MediaType ~ (which may not be found, of course) based on the extension
	@Nullable
	protected MediaType lookupMediaType(String extension) {
		return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
	}
}

This abstract class maintains some Map s as well as methods to provide operations. It maintains a two-way lookup table for file extensions and MediaType. The corresponding relationship between extension and MediaType:

  1. A MediaType corresponds to N extensions
  2. An extension can only belong to one MediaType at most~

Continue back to AbstractMapping Content Negotiation Strategy.

// @ since 3.2 is an abstract implementation of negotiation policy and also has the ability to extend + MediaType correspondence
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver implements ContentNegotiationStrategy {

	// Whether to only use the registered mappings to look up file extensions,
	// or also to use dynamic resolution (e.g. via {@link MediaTypeFactory}.
	// Spring framework. http. MediaTypeFactory is a factory class provided by Spring 5.0
	// It reads the file / org / spring framework / HTTP / mime. types, which records the corresponding relationships.
	private boolean useRegisteredExtensionsOnly = false;
	// Whether to ignore requests with unknown file extension. Setting this to
	// Default false: If you know an unknown extension, throw an exception: HttpMediaTypeNotAcceptable Exception
	private boolean ignoreUnknownExtensions = false;

	// Unique constructor
	public AbstractMappingContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
		super(mediaTypes);
	}

	// Implementing Policy Interface Method
	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
		// getMediaTypeKey: An abstract method (let subclasses provide the extension key)
		return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
	}

	public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException {
		if (StringUtils.hasText(key)) {
			// Call the parent method: Find a MediaType based on the key
			MediaType mediaType = lookupMediaType(key); 
			// handleMatch is the empty method of protected ~~subclass that is not implemented at present.
			if (mediaType != null) {
				handleMatch(key, mediaType); // Callback
				return Collections.singletonList(mediaType);
			}

			// If there is no corresponding MediaType, it is handled by handleNoMatch (the default is to throw an exception, see below).
			// Note: If handleNoMatch is found through the factory, add Mapping () is saved (equivalent to registration).
			mediaType = handleNoMatch(webRequest, key);
			if (mediaType != null) {
				addMapping(key, mediaType);
				return Collections.singletonList(mediaType);
			}
		}
		return MEDIA_TYPE_ALL_LIST; // Default values: all
	}

	// This method subclass ServletPathExtensionContentNegotiation Strategy is replicated
	@Nullable
	protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException {

		// If it's not just from the registry, go to the Media Type Factory and see if ~~is found and return.
		if (!isUseRegisteredExtensionsOnly()) {
			Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + key);
			if (mediaType.isPresent()) {
				return mediaType.get();
			}
		}

		// Ignore not finding, return null or throw an exception: HttpMediaTypeNotAcceptable Exception
		if (isIgnoreUnknownExtensions()) {
			return null;
		}
		throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
	}
}

The abstract class implements the template processing flow.
It's up to subclasses to decide whether your extension comes from the parameters of the URL or from the path.

ParameterContentNegotiationStrategy

The subclass of the abstract class above is implemented concretely, and you can see from the name that the extension comes from the param parameter.

public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
	// The default key for the request parameter is format, which you can set and change. (set method)
	private String parameterName = "format";

	// Unique structure
	public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
		super(mediaTypes);
	}
	... // Raw Road get/set

	// Little Tips: Here we call getParameterName() instead of using the attribute name directly. It is suggested that we use the same design framework in the future. Although the effect is the same in many cases, it is more in line with the usage specifications.
	@Override
	@Nullable
	protected String getMediaTypeKey(NativeWebRequest request) {
		return request.getParameter(getParameterName());
	}
}

The requested MediaType is judged by a query parameter, which defaults to format.

Note: Although Spring MVC supports this strategy based on param, it is open by default. If you want to use it, you need to display it manually.

PathExtensionContentNegotiationStrategy

Its extension needs to be analyzed from Path.

public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {

	private UrlPathHelper urlPathHelper = new UrlPathHelper();

	// It additionally provides an empty structure.
	public PathExtensionContentNegotiationStrategy() {
		this(null);
	}
	// Parametric structure
	public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
		super(mediaTypes);
		setUseRegisteredExtensionsOnly(false);
		setIgnoreUnknownExtensions(true); // Note: This value is set to true
		this.urlPathHelper.setUrlDecode(false); // No need to decode (url, no Chinese)
	}

	// @ As you can see from 4.2.8, Spring MVC allows you to define your own parsing logic
	public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
		this.urlPathHelper = urlPathHelper;
	}


	@Override
	@Nullable
	protected String getMediaTypeKey(NativeWebRequest webRequest) {
		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
		if (request == null) {
			return null;
		}

		// Resolve extensions from URL s with urlPathHelper and UriUtils
		String path = this.urlPathHelper.getLookupPathForRequest(request);
		String extension = UriUtils.extractFileExtension(path);
		return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
	}

	// Subclass ServletPathExtensionContentNegotiation Strategy has usage and duplication
	// Its purpose is to find the corresponding MediaType for the Resource~
	@Nullable
	public MediaType getMediaTypeForResource(Resource resource) { ... }
}

Determine the requested MediaType based on the extension part of the file resource requested in the request URL path (with the help of UrlPathHelper and UriUtils to parse the URL).

ServletPathExtensionContentNegotiationStrategy

It is an extension of Path Extension Content Negotiation Strategy, which is related to Servlet containers. Because Servlet additionally provides this method: ServletContext#getMimeType(String) to handle file extensions.

public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy {
	private final ServletContext servletContext;
	... // Ellipsis constructor

	// Bottom line: Before you go to the factory, go to this. servletContext. getMimeType ("file."+ extension). Find it and return directly. Otherwise, go back to the factory.
	@Override
	@Nullable
	protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension) throws HttpMediaTypeNotAcceptableException { ... }

	//  Same: First this.servletContext.getMimeType(resource.getFilename()) is handed over to the parent class for processing.
	@Override
	public MediaType getMediaTypeForResource(Resource resource) { ... }

	// Both call the parent class conditionally: mediaType == null | MediaType. Application_OCTET_STREAM. equals (mediaType)
}

Description: Servlet Path Extension Content Negotiation Strategy is Spring MVC's default enablement support policy, without manual enablement.

FixedContentNegotiationStrategy

Fixed type resolution: Returns a fixed MediaType.

public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {
	private final List<MediaType> contentTypes;

	// Constructor: MediaType must be specified
	// Typically specified by the annotation attribute @RequestMapping.produces (multiple can be specified)
	public FixedContentNegotiationStrategy(MediaType contentType) {
		this(Collections.singletonList(contentType));
	}
	// @since 5.0
	public FixedContentNegotiationStrategy(List<MediaType> contentTypes) {
		this.contentTypes = Collections.unmodifiableList(contentTypes);
	}
}

Fixed parameter types are very simple, constructors pass in and return what (not null).

ContentNegotiationManager

After introducing the negotiation strategy in the above 4, we begin to introduce the negotiation "container".
This manager has a special function like the container management class xxxComposite described earlier. The general idea is management and delegation. It's very simple to understand from the previous foundation.

//  It manages not only a List of strategies, but also a Set of resolvers.
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
	private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();
	private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>();
	
	...
	// If not specifically specified, at least this strategy is included: Header Content Negotiation Strategy
	public ContentNegotiationManager() {
		this(new HeaderContentNegotiationStrategy());
	}
	... // Because of its simplicity, other code is omitted.
}

It is a Content Negotiation Strategy container as well as a MediaType File Extension Resolver container. At the same time, it realizes the two interfaces.

ContentNegotiationManagerFactoryBean

As the name implies, it is a FactoryBean specifically designed to create a Content Negotiation Manager.

// @ since 3.2 also implements ServletContextAware to get the current servlet container context
public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
	
	// The default is to open support for suffixes
	private boolean favorPathExtension = true;
	// param support is not enabled by default
	private boolean favorParameter = false;
	// Accept support is also enabled by default
	private boolean ignoreAcceptHeader = false;

	private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
	private boolean ignoreUnknownPathExtensions = true;
	// Jaf is a data processing framework that can be ignored
	private Boolean useJaf;
	private String parameterName = "format";
	private ContentNegotiationStrategy defaultNegotiationStrategy;
	private ContentNegotiationManager contentNegotiationManager;
	private ServletContext servletContext;
	... // Eliminate common get/set

	// Note that what is passed in here is that Properties denotes the corresponding relationship between suffix and MediaType
	public void setMediaTypes(Properties mediaTypes) {
		if (!CollectionUtils.isEmpty(mediaTypes)) {
			for (Entry<Object, Object> entry : mediaTypes.entrySet()) {
				String extension = ((String)entry.getKey()).toLowerCase(Locale.ENGLISH);
				MediaType mediaType = MediaType.valueOf((String) entry.getValue());
				this.mediaTypes.put(extension, mediaType);
			}
		}
	}
	public void addMediaType(String fileExtension, MediaType mediaType) {
		this.mediaTypes.put(fileExtension, mediaType);
	}
	...
	
	// A lot of default logic is handled here.
	@Override
	public void afterPropertiesSet() {
		List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();

		// The default favorPathExtension=true supports the path suffix pattern
		// The servlet environment uses the Servlet Path Extension Content Negotiation Strategy, otherwise it uses the Path Extension Content Negotiation Strategy.
		// 
		if (this.favorPathExtension) {
			PathExtensionContentNegotiationStrategy strategy;
			if (this.servletContext != null && !isUseJafTurnedOff()) {
				strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
			} else {
				strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
			}
			strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
			if (this.useJaf != null) {
				strategy.setUseJaf(this.useJaf);
			}
			strategies.add(strategy);
		}

		// Default favorParameter=false wood has open drops
		if (this.favorParameter) {
			ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
			strategy.setParameterName(this.parameterName);
			strategies.add(strategy);
		}

		// Note that there is a!, so the default Accept is also supported.
		if (!this.ignoreAcceptHeader) {
			strategies.add(new HeaderContentNegotiationStrategy());
		}

		// If you like, you can set a default Negotiation Strategy that will eventually be add ed in.
		if (this.defaultNegotiationStrategy != null) {
			strategies.add(this.defaultNegotiationStrategy);
		}

		// What I need to note in this section is that ArrayList is used here, so the order you add is the last execution order of u.
		// So if you specify defaultNegotiation Strategy, it's at the end.
		this.contentNegotiationManager = new ContentNegotiationManager(strategies);
	}

	// Three Interface Methods
	@Override
	public ContentNegotiationManager getObject() {
		return this.contentNegotiationManager;
	}
	@Override
	public Class<?> getObjectType() {
		return ContentNegotiationManager.class;
	}
	@Override
	public boolean isSingleton() {
		return true;
	}
}

Here's the explanation. This article The order (suffix > request parameter > HTTP header Accept) phenomenon. Spring MVC uses it to create Content Negotiation Manager to manage negotiation strategies.

Configuration Content Negotiation Configurer

Although Spring's negotiation support by default covers most of our application scenarios, there are still times when we need to personalize Spring, so this section explains the personalized configuration of Spring.~

ContentNegotiationConfigurer

It is used to "collect" configuration items and create a Content Negotiation Manager based on the configuration items you provide.

public class ContentNegotiationConfigurer {

	private final ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
	private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();

	public ContentNegotiationConfigurer(@Nullable ServletContext servletContext) {
		if (servletContext != null) {
			this.factory.setServletContext(servletContext);
		}
	}
	// @since 5.0
	public void strategies(@Nullable List<ContentNegotiationStrategy> strategies) {
		this.factory.setStrategies(strategies);
	}
	...
	public ContentNegotiationConfigurer defaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) {
		this.factory.setDefaultContentTypeStrategy(defaultStrategy);
		return this;
	}

	// Create a Content Negotiation Manager manually. This method is protected 
	// The only call is: WebMvc Configuration Support
	protected ContentNegotiationManager buildContentNegotiationManager() {
		this.factory.addMediaTypes(this.mediaTypes);
		return this.factory.build();
	}
}

Content Negotiation Configurer can be thought of as providing an entry to set up Content Negotiation Manager FactoryBean (its own content is new with an instance of it), which is ultimately submitted to WebMvc Configuration Support to register the Bean in the container:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
	...
	// Note that the BeanName is: mvcContentNegotiation Manager
	// If you really need it, you can cover it.~~~~
	@Bean
	public ContentNegotiationManager mvcContentNegotiationManager() {
		if (this.contentNegotiationManager == null) {
			ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
			configurer.mediaTypes(getDefaultMediaTypes()); // Suffix names supported by default on the server side - > MediaTypes~~~

			// This method calls back the protected method of our custom configuration~~~~
			configureContentNegotiation(configurer);
		
			// Call methods to generate a manager
			this.contentNegotiationManager = configurer.buildContentNegotiationManager();
		}
		return this.contentNegotiationManager;
	}


	// Negotiating MediaType s Supported by Default~~~~
	protected Map<String, MediaType> getDefaultMediaTypes() {
		Map<String, MediaType> map = new HashMap<>(4);
		// Hardly needed
		if (romePresent) {
			map.put("atom", MediaType.APPLICATION_ATOM_XML);
			map.put("rss", MediaType.APPLICATION_RSS_XML);
		}
		// If you import the package jackson supports for xml, it will be supported
		if (jaxb2Present || jackson2XmlPresent) {
			map.put("xml", MediaType.APPLICATION_XML);
		}
		// jackson.databind supports json, so it's generally satisfied here
		// Additionally, gson and jsonb are supported. Hope to have built-in support for fastjson in the near future
		if (jackson2Present || gsonPresent || jsonbPresent) {
			map.put("json", MediaType.APPLICATION_JSON);
		}
		if (jackson2SmilePresent) {
			map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
		}
		if (jackson2CborPresent) {
			map.put("cbor", MediaType.valueOf("application/cbor"));
		}
		return map;
	}
	...
}

Tips: WebMvc Configuration Support is imported by @Enable WebMvc.

Practice of Content Negotiation Configuration

With the support of the above theory, the best practice configuration for negotiating with Spring MVC can be referred to as follows (in most cases, no configuration is required):

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true)
        //.parameterName("formatter")
        //Default Content Type Strategy (new...)// Customize a default content negotiation policy
        //defaultContentType(...)// Its effect is that the new Fixed Content Negotiation Strategy (contentTypes) adds support for fixed policies.
        //.strategies(list);
        //.useRegisteredExtensionsOnly() //PathExtensionContentNegotiationStrategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
        ;
    }
}

summary

This paper analyses the management, use and open configuration of Spring MVC's content negotiation strategy in principle. The aim is to have a good idea of how to expand the content negotiation strategy better, safer and more convenient. It is very helpful to understand the content negotiation view below, and is interested in sustainable attention.~

Relevant Reading

Content Negotiation Content Negotiation Negotiation Mechanisms-Four Content Negotiation Modes Supported by Spring MVC Built-in

Knowledge exchange

The last: If you think this is helpful to you, you might as well give a compliment. Of course, sharing your circle of friends so that more small partners can see it is also authorized by the author himself.~

If you are interested in technical content, you can join the wx group: Java Senior Engineer and Architect Group.
If the group two-dimensional code fails, Please add wx number: fsx641385712 (or scan the wx two-dimensional code below). And note: "java into the group" words, will be manually invited to join the group

Posted by davelr459 on Mon, 26 Aug 2019 01:10:11 -0700