Four Content Negotiation Ways with Built-in Support of Spring MVC

Posted by kenslate on Tue, 27 Aug 2019 15:56:02 +0200

Every sentence

Ten bald heads, nine rich, and the last one will cut down trees

Preface

I wonder if you were "surprised" when using Spring Boot: the same interface (the same URL) in the case of interface error, if you use rest access, it returns you a json string; but if you use browser access, it returns you a section of html. Just the following example (Spring Book environment):

@RestController
@RequestMapping
public class HelloController {
    @GetMapping("/test/error")
    public Object testError() {
        System.out.println(1 / 0); // Forced throw exception
        return "hello world";
    }
}

Access with browser: http://localhost:8080/test/error

Use Postman to access:

The same root has different destiny. An important feature of RESTful services is that the same resource can be expressed in many ways, which is the theme of our article today: Content Negotiation.

HTTP Content Negotiation

Although this article is mainly about content negotiation mechanism in Spring MVC, it is necessary to understand what HTTP content negotiation is like before that (Spring MVC implements it and extends it more powerful ~).

Definition

A URL resource server can respond in many forms: the MIME (Media Type) media type. But for a client (browser, APP, Excel export...) it only needs one. so client and server have to have a mechanism to ensure this, which is content negotiation mechanism.

mode

There are roughly two ways of content negotiation in http:

  1. The server will send the list of available MIME types to the client, and the client will tell the server when it chooses. In this way, the server will return it according to the MIME told by the client. (Disadvantage: multiple network interactions, and the use of high demand for users, so this approach is generally not used)
  2. When a client sends a request, it specifies the MIME s needed (e.g. Http header: Accept). The server returns the appropriate form according to the request specified by the client, and explains it in the response header (e.g. Content-Type).
    1. If the MIME-type server required by the client can't provide it, then 406 is wrong.~

    Common request and response headers

    == Request header==
    Accept: Tell the server what MIME is needed (usually multiple, such as text/plain, application/json, etc.). / Represents any MIME resource)
    Accept-Language: Tell the server what language it needs (default in China is Chinese, but browsers can generally choose N languages, but support depends on whether the server can negotiate)
    Accept-Charset: Tell the server the required character set
    Accept-Encoding: Tell the server how to compress (gzip,deflate,br)
    == Response Head==
    Content-Type: The type of media that tells the client to respond (such as application/json, text/html, etc.)
    Content-Language: The language that tells the client to respond
    Content-Charset: Character set that tells the client to respond
    Content-Encoding: Tell the client how to compress the response (gzip)

    Difference between header Accept and Content-Type

    There are many rough explanations: Accept belongs to the request header, Content-Type belongs to the response header, which is actually inaccurate.
    In today's mainstream of separate front-end and back-end development, you should see that most of the front-end request requests have Content-Type: application/json;charset=utf-8, so it can be seen that Content-Type is not just a response header.

The format of HTTP protocol specification is as follows:

  1. request-line
  2. headers (request header)
  3. blank line
  4. request-body

Content-Type refers to the data format of the request message body, because both request and response can have message body, so it can be used in both request header and response header.
For more about Content-Type in Http, I recommend this article: Content-Type in Http Request

Spring MVC Content Negotiation

Spring MVC implements HTTP content negotiation and extends it at the same time. It supports four modes of consultation:

  1. HTTP Header Accept
  2. Extensions
  3. Request parameters
  4. Fixed type (producers)

Note: The following example is based on Spring, not Spring Boot.

Mode 1: HTTP header Accept

@RestController
@RequestMapping
public class HelloController {
    @ResponseBody
    @GetMapping("/test/{id}")
    public Person test(@PathVariable(required = false) String id) {
        System.out.println("id The value is:" + id);
        Person person = new Person();
        person.setName("fsx");
        person.setAge(18);
        return person;
    }
}

If this is the default, both browser access and Postman access will result in json strings.

But if you just add the following two packages to the pom:

<!-- Here you need to import databind Just pack it. jackson-annotations,jackson-core Neither need to show your imports-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>
<!-- jackson Default will only support json. If you want xml Support requires additional imports of the following packages -->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.8</version>
</dependency>

Then use browser / Postman to access, and the result is xml, like this:

Some articles say: browser is xml, postman is json. I try it myself: it's all xml.

But if we postman manually specifies this header: Accept: application/json, the return will be different from that of the browser (if not manually specified, Accept defaults to */*):

And we can see that the head information of response is compared as follows:
Accept: application/json is specified manually:

Accept (default */*):

Brief Analysis of Reasons

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3.
Because my example uses @ResponseBody, it does not return a view: it is handled by the message converter, so this is related to MediaType and weight.

The message will eventually be delivered to the AbstractMessageConverterMethodProcessor.writeWithMessageConverters() method:

// @since 3.1
AbstractMessageConverterMethodProcessor: 
    protected <T> void writeWithMessageConverters( ... ) {
        Object body;
        Class<?> valueType;
        Type targetType;
        ...
        HttpServletRequest request = inputMessage.getServletRequest();
        // Here we give contentNegotiation Manager. resolveMediaTypes () to find out what MediaTypes are acceptable to the client.~~~
        // Here is the sorted list (according to Q value, etc.)
        List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
        // This is the MediaType s that the server can provide.
        List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    
        // Consultation. After a certain sort and match, a suitable MediaType is finally matched.
        ...
        // Sort the things that need to be used again.
        MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

        // Find out the most appropriate and final use: Selected Media Type 
            for (MediaType mediaType : mediaTypesToUse) {
                if (mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
                    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                    break;
                }
            }
    }

Acceptable Types are notified by the client through Accept.
producibleTypes represent the types that the server can provide. Refer to the getProducibleMediaTypes() method:

AbstractMessageConverterMethodProcessor: 

    protected List<MediaType> getProducibleMediaTypes( ... ) {
        // The only thing it sets values is the @RequestMapping.producers attribute
        // Most of the time: we don't normally assign this property~~~
        Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<>(mediaTypes);
        }
        // In most cases: you go into this logic - > Match a suitable one from the message converter
        else if (!this.allSupportedMediaTypes.isEmpty()) {
            List<MediaType> result = new ArrayList<>();
            // Match one / more Lists < MediaType > results from all message converters
            // This means that all List < MediaType > that my server can support
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                if (converter instanceof GenericHttpMessageConverter && targetType != null) {
                    if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                }
                else if (converter.canWrite(valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            return result;
        } else { 
            return Collections.singletonList(MediaType.ALL);
        }
    }

You can see which MediaType s the server can ultimately provide, from the type support of the message converter HttpMessageConverter.
Phenomenon of this example: Initially, it returns a json string, just importing jackson-data format-xml and then returning xml. The reason is that when you join Mapping Jackson 2Xml HttpMessageConverter, you have this judgment:

    private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
    
        if (jackson2XmlPresent) {
            addPartConverter(new MappingJackson2XmlHttpMessageConverter());
        }

So by default, Spring MVC does not support the media format of application/xml, so if you do not import the package, the result will be: application/json.

By default, the priority is xml higher than json. Of course, there are usually xml packages, so it's json's turn.

Another point to note is that some partners say that they can achieve results by specifying Content-Type: application/json in the request header. Now you should know that it's obviously useless to do this (as for why it's useless, I hope the reader knows it well), and you can only use the Accept header to specify it.~~~

The first way to negotiate is that Spring MVC is based entirely on the HTTP Accept header. Spring MVC is supported by default and turned on by default.
Advantages and disadvantages:

  • Advantages: Ideal standard approach
  • Disadvantage: Due to the difference of browsers, the header of Accept Header may be different, so the result is not browser compatible.

    Mode 2: (Variable) Extensions

    Based on the above example: if I visit / test/1.xml and return xml, if I visit / test/1.json, return json; perfect~

This method is very convenient to use and does not depend on browsers. But I have summed up the following points for attention:

  1. Extensions must be extensions of variables. For example, if you visit test.json / test.xml, you will get 404~
  2. @ PathVariable parameter types can only use String/Object, because the value received is 1.json/1.xml, so using Integer to receive an error type conversion error~
    1. Tips: My personal suggestion is not to accept this part (this part does not use @PathVariable reception), but to use it only for content negotiation.
  3. Extensions have a higher priority than Accept (and are not related to using the Magic Horse browser)

Advantages and disadvantages:

  • Advantages: Flexible, not constrained by browsers
  • Disadvantage: Loss of multiple ways of presenting the same URL. It's still used more in the real world, because it's more in line with programmers'habits.

    Mode 3: Request parameters

    Spring MVC supports this negotiation method, but it is turned off by default and needs to be displayed on:
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        // Support request parameter negotiation
        configurer.favorParameter(true);
    }
}

Request URL: / test/1?format=xml returns xml; / test/1?format=json returns json. Similarly, I would like to summarize the following points for attention:

  1. The first two modes are turned on by default, but they need to be turned on manually.
  2. This method takes precedence over the extension (so if you want it to work in your test, remove the url suffix)

Advantages and disadvantages:

  • Advantages: Not constrained by browsers
  • Disadvantage: Additional format parameters are required, and URL s become redundant and cumbersome, lacking the simplicity of REST. Another disadvantage is that it needs to be turned on manually.

    Mode 4: fixed types (produces)
    It uses the @RequestMapping annotation property produces (you may be using it, but you don't know why):
@ResponseBody
@GetMapping(value = {"/test/{id}", "/test"}, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Person test() { ... }

Visit: / test/1 returns json; even if you have imported jackson's xml package, it still returns json.

It also has a very important note: the MediaType type specified by produces cannot conflict with suffixes, request parameters, Accept. For example, Bentley specifies the json format here. If you access / test/1.xml, or format=xml, or Accept is not application/json or */* you will not be able to complete content negotiation: http status code 406, the error is as follows:

Although the use of produces is relatively simple, for the above error 406 reasons, I simply explain as follows.

Reason:

1. Parse the media type of the request first: 1. The MediaType parsed by XML is application/xml.
2. When you take this Media Type (and of course the URL, request method, etc.) to match Handler Method, you will find that producers do not match.
3. If the match is not good, it is handled by RequestMapping InfoHandlerMapping. handleNoMatch ():

RequestMappingInfoHandlerMapping: 

    @Override
    protected HandlerMethod handleNoMatch(...) {
        if (helper.hasConsumesMismatch()) {
            ...
            throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
        }
        // Throw an exception: HttpMediaTypeNotAcceptable Exception
        if (helper.hasProducesMismatch()) {
            Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
            throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
        }
    }   

4. After throwing an exception, the exception is finally handed over to Dispatcher Servlet. processHandlerException () to handle the exception and convert to Http status code.

All handler Exception Resolvers are called to handle this exception, which is ultimately handled here by DefaultHandler Exception Resolver. The final processing code is as follows (406 status code):

    protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

        response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
        return new ModelAndView();
    }

The default exception handlers registered by Spring MVC are the following three:

principle

With a description of Accept's principles, it's easy to understand. Because the produces attribute is specified, the getProducibleMediaTypes() method takes the media type supported by the server:

protected List<MediaType> getProducibleMediaTypes( ... ){
    Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<>(mediaTypes);
    }
    ...
}

Because producers are set, the first sentence of the code gets the value (the negotiation mechanism behind is exactly the same).

Note: If the produces attribute you want to specify a lot, it is recommended that you use! xxx grammar, which supports this grammar (excluding grammar)~

Advantages and disadvantages:

  • Advantages: Easy to use, natural support
  • Disadvantage: Lack of flexibility for HandlerMethod processors

    Spring Boot default exception message processing

    Back to the beginning of Spring Boot, why does browser and postman show different abnormal messages? This is Spring Boot's default exception handling: it uses content negotiation based on fixed-type (produces) implementations.

When Spirng Boot has abnormal information, it accesses / error by default, and its processing class is Basic Error Controller.

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    ...
    // Processing Browser
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        ... 
        return (modelAndView != null ? modelAndView : new ModelAndView("error", model));
    }

    // Handling restful/json mode
    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<Map<String, Object>>(body, status);
    }
    ...
}

With the above explanation, there should be no blind spot in understanding this code.~

summary

Content negotiation is still a very important part of RESTful today. It plays an important role in improving user experience, improving efficiency and reducing maintenance costs. Note that its three priorities are: suffix > request parameters > HTTP header Accept.

Usually, we use Http-based content negotiation (Accept) for general purpose, but it is seldom used in practical application, because different browsers may lead to different behaviors (such as Chrome and Firefox are very different), so in order to ensure "stability", we usually choose to use scheme 2 or scheme. Three (for example, Spring's official doc).

Relevant Reading

Handler Mapping Source Details (2) - - Request Mapping Handler Mapping Series

Content Negotiation Content Negotiation Negotiation Mechanisms (1) -- Four Content Negotiation Modes Supported by Spring MVC Built-in
Content Negotiation Content Negotiation Negotiation Negotiation Mechanism (2) - - Implementation Principle and Custom Configuration of Spring MVC Content Negotiation
Content Negotiation Content Negotiation Negotiation Negotiation Content Negotiation View Resolver Deep Resolution

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
== If you are interested in Spring, Spring Boot, MyBatis and other source code analysis, you can add me wx: fsx641385712, invite you to join the group and fly together manually.==

Topics: Java JSON xml Spring Attribute