Special customization of SpringMvc in SpringBoot

There are many @ beans in the automatic configuration classes of SpringMvc. SpringBoot automatically configures some default properties of SpringMvc according to these classes. We can override this @ Bean to customize some of our own special things.
Auto configuration class name: webmvcoautoconfiguration
In most cases, SpringBoot marks many @ conditionalonmissingbeans (xxx.class) in the automatic configuration (meaning that if there is no @ Bean in the IOC container, the current @ Bean will take effect). If we configure XXX ourselves Class, the default configuration of SpringBoot will be overwritten.
Implement a custom webmvcoautoconfiguration class, implement it in the WebMvcConfigurer interface and register it in the IOC container. Writing the corresponding code in it will overwrite the default configuration.

1. Custom interceptor

package cool.ale.config;

import cool.ale.interceptor.TimeInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class MyWebMvcConfiguration implements WebMvcConfigurer {

    /**
     * Add interceptor
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TimeInterceptor())      // Add an interceptor
            .addPathPatterns("/**");     // Intercept mapping rules to intercept all requests
                //. excludePathPatterns("") / / set excluded mapping rules
    }
}

Specific classes to implement interceptors:

package cool.ale.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.LocalDateTime;

public class TimeInterceptor implements HandlerInterceptor {

    Logger logger = LoggerFactory.getLogger(TimeInterceptor.class);

    LocalDateTime start;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // start time
        start = LocalDateTime.now();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // End time
        LocalDateTime end = LocalDateTime.now();

        // Calculate two time differences
        Duration between = Duration.between(start, end);

        // Get the difference in milliseconds
        long millis = between.toMillis();

        logger.info("Current request:" + request.getRequestURI() + ": Execution time:" + millis + "millisecond.");
    }
}

2. CORS cross domain request

2.1. Global configuration

For example, the request before our two system modules is called cross domain request.
First, I write an ajax request in one module to request another module. This module currently sets other ports:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>rest home page</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
    <button id="restButton">Request cross domain request</button>
</body>

    <script type="text/javascript">
        $("#restButton").click(function () {
            $.ajax({
                url:'http://localhost:8080/user/1',
                type:'GET',
                success:function (result) {
                    alert(result)
                }
            });
        })
    </script>
</html>

Then we start the service and report an error according to the way in the picture, as follows:

If the access is successful, we need to add the configuration of cross domain access to the module after the jump. The steps are as follows:
Rewrite the method of cross domain request in our customized SpringMvc class, as follows:

/**
 * Cross domain request processing (global configuration)
 * @param registry
 */
@Override
public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/user/*")            // Map which http interfaces in this server can make cross domain requests
            .allowedOrigins("http://localhost:8101 ") / / configure which sources have permission to cross domain
            .allowedMethods("GET","POST","DELETE","PUT");      // Configure methods to allow cross domain requests
    }

You can succeed!

2.2. Single configuration

Based on the above, we can remove the cross domain request configuration in the custom class and add @ CrossOrigin annotation to the corresponding controller method.

/**
 * query
 * @param id
 * @return
 */
@GetMapping("/{id}")
@ApiOperation("According to user id Query corresponding user information")
@CrossOrigin
public Result getUser(@PathVariable Integer id){
    User user = userService.getUser(id);
    return new Result<>(200,"Query succeeded!",user);
}

3,Json

SpringBoot provides the inheritance of three JSON mapping libraries (Gson, Jackson and JSON-B) by default. Jackson is the default.

3.1 use of Jackson

annotationmeaning
@JsonIgnoreExcluding json serialization and marking it on the attribute will not be json formatted
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh")Format date
@JsonInclude(JsonInclude.Include.NON_NULL)When this field is not null, it is json serialized
@JsonProperty("ha")You can use this property to set aliases for fields

3.2. Serialization and deserialization of custom json

First, serialized classes must be annotated @ JsonComponent.
Then specify the object to be serialized and directly specify it on the generic type of the static class. The code example is as follows:

package cool.ale.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.*;
import cool.ale.entity.User;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.jackson.JsonObjectDeserializer;
import org.springframework.boot.jackson.JsonObjectSerializer;

import java.io.IOException;

@JsonComponent
public class UserJsonCustom {
    public static class Serializer extends JsonObjectSerializer<User> {

        @Override
        protected void serializeObject(User user, JsonGenerator jgen, SerializerProvider provider) throws IOException {
            jgen.writeObjectField("userId",user.getId());
            jgen.writeObjectField("addR",user.getAddress());
        }
    }

    public static class deserializer extends JsonObjectDeserializer<User>{

        @Override
        protected User deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree) throws IOException {
            User user = new User();
            // The deserialization here is equivalent to that the requested id will be json encapsulated
            user.setId(tree.findValue("id").asInt());
            return user;
        }
    }
}

4. Internationalization

We generally have two ways of Internationalization: one is to internationalize according to the language currently set by the browser, and the other is to pass the corresponding internationalization identification parameters to realize internationalization.
Both of the following are explained

4.1. Obtain the language parameters set by the browser in the request header to realize internationalization

To operate internationalized text in SpringBoot, we need to carry out the following steps:

4.1.1. Add international resource file

4.1.2 configure messageResource and set internationalization text

MessageSourceAutoConfiguration is provided in SpringBoot, so we don't need to configure messageResource.
But it doesn't take effect. We can see the following information on the console by setting the debug=true parameter

Let's analyze why it doesn't work through the code:

@Configuration(
    proxyBeanMethods = false
)
// If you configure a @ Bean bean named messageSource, you will use a custom Bean
@ConditionalOnMissingBean(
    name = {"messageSource"},
    search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(-2147483648)
// @If the Conditional user-defined conditions match, a ResourceBundleCondition class that implements the Condition interface will be passed in
// ResourceBundleCondition will override the matches method and customize the matching rules. If the method returns true, it means that the matching is successful
@Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

Call the following matches method and return the matching result

public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    String classOrMethodName = getClassOrMethodName(metadata);

    try {
    	// The specific matching rules are in the getMatchOutcome method
        ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
        this.logOutcome(classOrMethodName, outcome);
        this.recordEvaluation(context, classOrMethodName, outcome);
        return outcome.isMatch();
    } catch (NoClassDefFoundError var5) {
        throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
    } catch (RuntimeException var6) {
        throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
    }
}

The getMatchOutcome method code is as follows:

public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
	// Get spring. In the configuration file messages. The value of the basename attribute. The default value is messages
    String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
    ConditionOutcome outcome = (ConditionOutcome)cache.get(basename);
    if (outcome == null) {
        outcome = this.getMatchOutcomeForBasename(context, basename);
        cache.put(basename, outcome);
    }

    return outcome;
}

The code of getMatchOutcomeForBasename method is as follows:

private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
    Builder message = ConditionMessage.forCondition("ResourceBundle", new Object[0]);
    String[] var4 = StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename));
    int var5 = var4.length;

    for(int var6 = 0; var6 < var5; ++var6) {
        String name = var4[var6];
        Resource[] var8 = this.getResources(context.getClassLoader(), name);
        int var9 = var8.length;
		// Get the resource files of all properties under the classpath according to message
		// If there is an attribute resource file under this path, the matching result will be true
        for(int var10 = 0; var10 < var9; ++var10) {
            Resource resource = var8[var10];
            if (resource.exists()) {
                return ConditionOutcome.match(message.found("bundle").items(new Object[]{resource}));
            }
        }
    }

    return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}

Therefore, according to the above code tracking, MessageSourceAutoConfiguration does not match because the system does not find the corresponding international text attribute resource file. Therefore, according to the logic of the code just now, there are two solutions:

1. Change the name of the folder i18n to messages.
2. Configure spring.com in the configuration file messages. The basename property specifies the location of the i18n folder.

We restart the observation console again, as shown below:

4.1.3. Resolve the accept language in the request header or the url parameter? local=

In the webmvcoautoconfiguration class, there is an accept language method localeResolver in the parse request header

@Bean
@ConditionalOnMissingBean(
    name = {"localeResolver"}
)
public LocaleResolver localeResolver() {
	// When there is spring in the configuration file mvc. locale-resolver=fixed
	// Will go to the configuration file to find spring mvc. Locale, which is equivalent to setting a fixed value
    if (this.webProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.webProperties.getLocale());
    } else if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    } else {
    	// If fixed is not set above, it will be resolved with the AcceptHeaderLocaleResolver class
    	// Take this parameter in the configuration file as a default value. If there is no request header, this default value will be taken
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        Locale locale = this.webProperties.getLocale() != null ? this.webProperties.getLocale() : this.mvcProperties.getLocale();
        localeResolver.setDefaultLocale(locale);
        return localeResolver;
    }
}

The parsing method of AcceptHeaderLocaleResolver class is as follows:

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // Go to the accept language parameter of the request header first. If not, the default one in the configuration file will be found
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    } else {
        Locale requestLocale = request.getLocale();
        List<Locale> supportedLocales = this.getSupportedLocales();
        if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
            Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
            if (supportedLocale != null) {
                return supportedLocale;
            } else {
                return defaultLocale != null ? defaultLocale : requestLocale;
            }
        } else {
            return requestLocale;
        }
    }
}

4.1.4. Call directly through MessageSource class

First inject the MessageSource class into the class to be used, and then call the corresponding international text information through this class.

package cool.ale.controller;

import cool.ale.entity.Result;
import cool.ale.entity.User;
import cool.ale.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Api("User control class")
public class UserController {

    @Autowired
    UserService userService;

    @Autowired
    MessageSource messageSource;

    /**
     * query
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("According to user id Query corresponding user information")
    public Result getUser(@PathVariable Integer id){
        String message = messageSource.getMessage("user.query.success",null, LocaleContextHolder.getLocale());
        User user = userService.getUser(id);
        return new Result<>(200,message,user);
    }
}

4.2. Realize internationalization according to the form of transmission parameters

The form of passing parameters is similar to making a drop-down list on the page. When selecting Chinese page, Chinese will be displayed, and when selecting English page, English will be displayed.
Based on the above steps, we need to:

4.2.1. Add an internationalization interceptor (customized in MyWebMvcConfiguration class)

/**
 * Add interceptor
 * @param registry
 */
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // Add internationalization interceptor
    registry.addInterceptor(new LocaleChangeInterceptor())
            .addPathPatterns("/**");    // Interception mapping rule
}

4.2.2. Overwrite the previous method of obtaining internationalization ID (customized in MyWebMvcConfiguration class)

/**
 * Rewrite the localeResolver method to take parameters in the url and set the internationalized text
 * @return
 */
@Bean
public LocaleResolver localeResolver() {
    CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
    // Set the expiration time and design one month
    cookieLocaleResolver.setCookieMaxAge(60*60*24*30);
    cookieLocaleResolver.setCookieName("locale");
    return cookieLocaleResolver;
}

4.2.3 test

http://localhost:8080/user/2?locale=en_US

10. Others

10.1 principle of WebMvcConfigurer

In fact, there is a static class in the automatic configuration class webmvcoautoconfiguration of SpringMvc, which also implements the WebMvcConfigurer class. It also extends some default configurations through the same configuration as above. We only need to extend some other things to improve our system.
However, there is a problem now. Although we have rewritten this interface, the configuration of SpringBoot is still effective. How these configurations cooperate with each other needs to be studied next.
On the webmvcaconfiguration class, we imported an EnableWebMvcConfiguration class, as shown below:

@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})

The EnableWebMvcConfiguration class inherits from the DelegatingWebMvcConfiguration class. In this class, all classes that implement the WebMvcConfigurer interface form a list and store it in the delegates delegate.

private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

public DelegatingWebMvcConfiguration() {
}

@Autowired(
    required = false
)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
    if (!CollectionUtils.isEmpty(configurers)) {
        this.configurers.addWebMvcConfigurers(configurers);
    }

}

When this method is called, it will loop through the delegator just stored and get it. As follows:

public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    Iterator var2 = this.delegates.iterator();

    while(var2.hasNext()) {
        WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
        delegate.configureDefaultServletHandling(configurer);
    }

}

Note: we must not add the @ EnableWebMvc annotation on the custom SpringMvc class, because the extension method in the webmvcoautoconfiguration class will fail after we add this annotation.

Failure principle:
When we enter the implementation of @ EnableWebMvc annotation, we will find that it imports the DelegatingWebMvcConfiguration class

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

However, DelegatingWebMvcConfiguration inherits from the WebMvcConfigurationSupport class.
Again, our webmvccautoconfiguration class takes effect only when the WebMvcConfigurationSupport class does not exist in the IOC container. Therefore, the webmvccautoconfiguration class fails at this time.

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {

Keywords: Java Spring Spring Boot Back-end

Added by desmond_ckl on Mon, 07 Feb 2022 05:20:16 +0200