Custom WebMvc Configurer for Spring MVC Source Series

Posted by rxero on Fri, 02 Aug 2019 09:58:11 +0200

Centralized Registry for All Components in Spring MVC

What does a component centralized registry mean? To put it bluntly, if you start the project with pure annotations, the components registered in xml will be transferred to the implementation of the WebMvcConfigurer class.
The following is an explanation of the configuration of all components in combination with source code and examples. See also the configuration of all components. MvcConfig.java See the source code for details
spring-mvc module in current directory

What components are there?

  1. Path Match Configurer
  2. Content Negotiation Strategy configuration

Path Match Configurer

Here I posted the PathMatch Configurer configuration code

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    // Postpositional fuzzy matching is not supported, / ABC matching is normal, but / abc. * returns 404
    configurer.setUseSuffixPatternMatch(true);
    // Backward oblique lines are not supported, such as / abc matching is normal, / abc / return 404
    configurer.setUseTrailingSlashMatch(false);
    AntPathMatcher pathMatcher = new AntPathMatcher();
    // Spring MVC is case-sensitive by default and is set to insensitive here
    pathMatcher.setCaseSensitive(false);
    configurer.setPathMatcher(pathMatcher);
}

If you are not satisfied with spring's default way of parsing URL s, such as / ABC /=/ abc, / ABC!=/abc, we can configure PathMatch Configurer to do this, of course, as explained here.
If we don't configure suffix pattern / trailing Slash and so on, spring will have its own default configuration. For example, spring will be in WebMvc Configuration Support
Get the PathMatcher/URLPathHelper configured in PathMatch Configurer, and if not, it will store a new object in the spring container.
Other options (suffix pattern/trailing Slash) default configuration you will find that there is no settings in PathMatch Configurer, their default settings refer to Request Mapping Handler Mapping.

Content Negotiation Strategy configuration

What is Content Negotiation Strategy? What role does it play in Spring MVC?
In order to better see spring error information, the log4j framework is introduced here. Note: Since spring uses commons-logging, here I use slf4j, so here
Need to import the following three packages, do not know about this can be referred to Introduction to java log components (common-logging, log4j, slf4j, logback):

<!-- slf4j Core package -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>
<!-- commons logging and slf4j Bridge Library -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
</dependency>
<!-- Solve slf4j NOP problem -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
</dependency>

Also remember to put these packages under tomcat classpath, Idea in artifact.
stay IndexController Add an interface to return data in json format.
stay Front page Add an Ajax request interface to customize the request.
The normal Ajax request is as follows, URL: http://localhost:8080/mvc/grb:

$("#get-random-json").click(function () {
    $.ajax({
        url: "${pageContext.servletContext.contextPath}/grb",
        type: "GET",
        success: function (data) {
            var name = data.name;
            var age = data.age;
            $("#random-json-text").text("name:" + name + ", age:" + age);
        }
    })
})

The results are as follows:

Note: The URL requested above does not have any suffix.
URL:http://localhost:8080/mvc/grb.xml.
The results are as follows:

As you can see, spring has already reported a mistake at this time, so how does this URL resolve? Here is the spring mvc request process stack:

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:184)  // Converting the return value to Accept format using MessageConverter
	  at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:174)
	  at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81) //Handling the Handler Method return value corresponding to the RequestMapping annotation
	  at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:113)
	  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
	  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
	  at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
	  at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
	  at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
	  at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	  at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
	  at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
	  at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	  at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
	  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
	  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
	  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
	  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
	  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
	  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
	  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:94)
	  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
	  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
	  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
	  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620)
	  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
	  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:502)
	  at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1132)
	  at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684)
	  at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2521)
	  at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2510)
	  - locked <0x1710> (a org.apache.tomcat.util.net.AprEndpoint$AprSocketWrapper)
	  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	  at java.lang.Thread.run(Thread.java:748)

The top of the stack is spring mvc's request result processing logic. The following is the core code for processing RequestMapping return value parsing:

protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    Object outputValue;
    Class<?> valueType;
    Type declaredType;

    if (value instanceof CharSequence) {
        outputValue = value.toString();
        valueType = String.class;
        declaredType = String.class;
    }
    else {
        outputValue = value;
        valueType = getReturnValueType(outputValue, returnType);
        declaredType = getGenericType(returnType);
    }

    HttpServletRequest request = inputMessage.getServletRequest();
    // Get the type of return value received in Accept or RequParameter in the request header in HttpServletRequest
    List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
    // Get all data types that can be parsed in the current spring mvc environment
    List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

    if (outputValue != null && producibleMediaTypes.isEmpty()) {
        throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
    }

    Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
    // If the requested Accept data type is "application/xml" and spring mvc can only parse "application/json", the returned compatibleMediaTypes are empty
    for (MediaType requestedType : requestedMediaTypes) {
        for (MediaType producibleType : producibleMediaTypes) {
            if (requestedType.isCompatibleWith(producibleType)) {
                compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
            }
        }
    }
    // If the appropriate parser is not registered, the HttpMediaTypeNotAcceptable Exception exception will be thrown
    if (compatibleMediaTypes.isEmpty()) {
        if (outputValue != null) {
            throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
        }
        return;
    }
    ...
}

The getAcceptable MediaTypes (request) implementation logic in the code above is as follows:

private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
    // Resolve incoming requests using contentNegotiation Manager
    List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}

The contentNegotiation Manager above calls ContentNegotiation Strategy to parse the http request and retrieve the data format it wants to return from HttpServletRequest.
There are two default parsing strategies for spring mvc:

  1. Servlet Path Extension Content Negotiation Strategy: According to the suffix such as:. xml or the format parameter carried in the request, which format to use, such as:? format=xml
  2. Header Content Negotiation Strategy: Judged by Accept parameters in the request header

Next, how about the format of the returned data? Look at the getProducibleMediaTypes method invocation process:

protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
    Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<MediaType>(mediaTypes);
    }
    else if (!this.allSupportedMediaTypes.isEmpty()) {
        List<MediaType> result = new ArrayList<MediaType>();
        // Get all message converter s registered in spring mvc
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            // GenericHttpMessageConverter translates the request result into the target object and writes it into http response
            if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            else if (converter.canWrite(valueClass, null)) {
                result.addAll(converter.getSupportedMediaTypes());
            }
        }
        return result;
    }
    else {
        return Collections.singletonList(MediaType.ALL);
    }
}

So the core question here is what are the default message converter s for spring mvc? Since we don't configure any converters in our application, how does spring MVC know the data format needed for the conversion request?
At this point, you can see the default converter registry of spring mvc: WebMvc Configuration Support# addDefaultHttpMessageConverters method, code as follows:

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setWriteAcceptCharset(false);

    messageConverters.add(new ByteArrayHttpMessageConverter());
    messageConverters.add(stringConverter);
    messageConverters.add(new ResourceHttpMessageConverter());
    messageConverters.add(new SourceHttpMessageConverter<Source>());
    messageConverters.add(new AllEncompassingFormHttpMessageConverter());

    if (romePresent) {
        messageConverters.add(new AtomFeedHttpMessageConverter());
        messageConverters.add(new RssChannelHttpMessageConverter());
    }

    // If the classpath contains jackson-data format-xml dependencies, load the XML format converter
    if (jackson2XmlPresent) {
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
    }
    else if (jaxb2Present) {
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }

    // If there is a jackson-databind dependency in the classpath, register the Mapping Jackson 2HttpMessageConverter converter, and if there is a gson dependency, register the gson converter
    if (jackson2Present) {
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
        messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
    }
    else if (gsonPresent) {
        messageConverters.add(new GsonHttpMessageConverter());
    }
}

To sum up, the tomcat 406 error is that there is no XML corresponding converter in spring mvc, in other words, there is no jackson-data format-xml dependency. At this time, the dependency is added to pom.xml.
And added to tomcat classpath, redeploy will find that the interface has returned successfully. The previous Ajax results are as follows:

What custom configurations can ContentNegotiation Configurer do? Here I wrote a custom message converter DEMO, source code and explanatory reference two articles:
Implementation of yaml Format Exchange by Spring MVC

Topics: Java Spring Apache xml