Automatic configuration of web application
As analyzed in previous articles, @ SpringBootApplication will use the @ Import annotation to introduce AutoConfigurationImportSelector
AutoConfigurationImportSelector will return the autoconfiguration class to be loaded through the spi mechanism
These include dispatcher servlet autoconfiguration and webmvca autoconfiguration
The former is used for servlet related configuration, while the latter is used for mvc related component configuration
DispatcherServletAutoConfiguration
First, let's take a look at the annotations used by this class
// Used to specify the order of automatic initialization @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // Specify the current class as the configuration class @Configuration(proxyBeanMethods = false) // Load only when the application type is servlet @ConditionalOnWebApplication(type = Type.SERVLET) // Only load when there is DispatcherServlet in the classpath @ConditionalOnClass(DispatcherServlet.class) // Load after the ServletWebServerFactoryAutoConfiguration is loaded @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { }
In addition, there are several internal classes in this class
DispatcherServletConfiguration
// Specify the current class as the configuration class @Configuration(proxyBeanMethods = false) // Load only when the DefaultDispatcherServletCondition is met @Conditional(DefaultDispatcherServletCondition.class) // Only when ServletRegistration appears in the classpath can it be loaded @ConditionalOnClass(ServletRegistration.class) // Validate WebMvcProperties @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { // Initialize dispatcherServlet. The name of the bean is dispatcherServlet @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); // Configures dispatcherSerlvet through injected webMvcProperties dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; } // Initialize MultipartResolver to upload files // The loading condition here is that there is a bean of multipartresolver in the context, but the name of the bean is not multipartresolver // The name of this bean will be named multipartResolver here @Bean @ConditionalOnBean(MultipartResolver.class) @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) public MultipartResolver multipartResolver(MultipartResolver resolver) { // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } }
Let's take a look at the class WebMvcProperties
@ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { }
As you can see, this class is mainly used to receive spring Configuration starting with MVC
DefaultDispatcherServletCondition
Next, take a look at the DefaultDispatcherServletCondition used to represent the DispatcherServletConfiguration loading condition
@Order(Ordered.LOWEST_PRECEDENCE - 10) private static class DefaultDispatcherServletCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage.forCondition("Default DispatcherServlet"); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); // Get the name of the bean of type DispatcherServlet from beanFactory List<String> dispatchServletBeans = Arrays .asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false)); if (dispatchServletBeans.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { // If a bean with the name dispatcherSerlet and the type DispatcherServlet is already included, there is no match at this time return ConditionOutcome .noMatch(message.found("dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); } if (beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { // If it already contains a bean named dispatcherSerlvet, there is no match at this time return ConditionOutcome.noMatch( message.found("non dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); } if (dispatchServletBeans.isEmpty()) { // Currently, there is no bean of type DispatcherServlet, so the matching return ConditionOutcome.match(message.didNotFind("dispatcher servlet beans").atAll()); } // Although there are bean s of type dispatcherServlet, but the name is not dispatcherServlet, they still match through return ConditionOutcome.match(message.found("dispatcher servlet bean", "dispatcher servlet beans") .items(Style.QUOTE, dispatchServletBeans) .append("and none is named " + DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); } }
DispatcherServletRegistrationConfiguration
The main function is to register dispatcherServlet and configure some properties
// The current class is a configuration class @Configuration(proxyBeanMethods = false) // Only when the DispatcherServletRegistrationCondition is satisfied can it be loaded @Conditional(DispatcherServletRegistrationCondition.class) // Only when there is ServletRegistration in the classpath can it be loaded @ConditionalOnClass(ServletRegistration.class) // Enable automatic property injection of WebMvcProperties @EnableConfigurationProperties(WebMvcProperties.class) // Import DispatcherServletConfiguration @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { // Returns a bean with the name dispatcherServletRegistration and the type dispatcherServletRegistration @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) // Load only when there are both beans of type dispatcherservlet and name dispatcherservlet in beanFactory @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { // The default path is/ DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); // Set the configuration of uploading files multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; } }
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet> implements DispatcherServletPath { private final String path; /** * Create a new {@link DispatcherServletRegistrationBean} instance for the given * servlet and path. * @param servlet the dispatcher servlet * @param path the dispatcher servlet path */ public DispatcherServletRegistrationBean(DispatcherServlet servlet, String path) { super(servlet); Assert.notNull(path, "Path must not be null"); this.path = path; // Add urlMappings super.addUrlMappings(getServletUrlMapping()); } @Override public String getPath() { return this.path; } // Manual modification of urlMapping is not supported @Override public void setUrlMappings(Collection<String> urlMappings) { throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration"); } @Override public void addUrlMappings(String... urlMappings) { throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration"); } }
Let's take a look at getServletUrlMapping
default String getServletUrlMapping() { if (getPath().equals("") || getPath().equals("/")) { return "/"; } if (getPath().contains("*")) { return getPath(); } if (getPath().endsWith("/")) { return getPath() + "*"; } return getPath() + "/*"; }
DispatcherServletRegistrationCondition
The main function of this class is to determine whether to load DispatcherServletRegistrationConfiguration
@Order(Ordered.LOWEST_PRECEDENCE - 10) private static class DispatcherServletRegistrationCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); ConditionOutcome outcome = checkDefaultDispatcherName(beanFactory); if (!outcome.isMatch()) { return outcome; } return checkServletRegistration(beanFactory); } private ConditionOutcome checkDefaultDispatcherName(ConfigurableListableBeanFactory beanFactory) { // Gets the name of the bean of type DispatcherServlet in beanFactory List<String> servlets = Arrays .asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false)); // Determine whether there is a bean named dispatcherServlet boolean containsDispatcherBean = beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); // If there is a bean with the name dispatcherservlet but the type is not dispatcherservlet, there is no match if (containsDispatcherBean && !servlets.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { return ConditionOutcome.noMatch( startMessage().found("non dispatcher servlet").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); } // Otherwise, a match is returned return ConditionOutcome.match(); } private ConditionOutcome checkServletRegistration(ConfigurableListableBeanFactory beanFactory) { ConditionMessage.Builder message = startMessage(); // Gets the bean of type ServletRegistrationBean in beanFactory List<String> registrations = Arrays .asList(beanFactory.getBeanNamesForType(ServletRegistrationBean.class, false, false)); // Determine whether there is a bean named dispatcherServletRegistration boolean containsDispatcherRegistrationBean = beanFactory .containsBean(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME); if (registrations.isEmpty()) { if (containsDispatcherRegistrationBean) { // There is no bean of type ServletRegistrationBean, but there is a bean named dispatcherServletRegistration, then there is no match return ConditionOutcome.noMatch(message.found("non servlet registration bean") .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); } // If there is neither a bean of type ServletRegistrationBean nor a bean of name dispatcherServletRegistration, then the matching return ConditionOutcome.match(message.didNotFind("servlet registration bean").atAll()); } if (registrations.contains(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)) { // If there is a bean with the name dispatcherServletRegistration and the type is ServletRegistrationBean, then it does not match return ConditionOutcome.noMatch(message.found("servlet registration bean") .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); } if (containsDispatcherRegistrationBean) { // There is a bean with the name dispatcherServletRegistration, which does not match return ConditionOutcome.noMatch(message.found("non servlet registration bean") .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); } return ConditionOutcome.match(message.found("servlet registration beans").items(Style.QUOTE, registrations) .append("and none is named " + DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); } private ConditionMessage.Builder startMessage() { return ConditionMessage.forCondition("DispatcherServlet Registration"); } }
WebMvcAutoConfiguration
First, let's take a look at the annotations used by webmvcoautoconfiguration
// The current class is a configuration class @Configuration(proxyBeanMethods = false) // Load only if the current application is a servlet application @ConditionalOnWebApplication(type = Type.SERVLET) // Configure only when baokukou Servlet DispatcherServlet WebMvcConfigurer is in the classpath @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) // Configure only when WebMvcConfigurationSupport does not exist @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) // After initializing the taskexecutionconfiguration, execute the autovalidationconfiguration @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { }
This class mainly includes the following internal classes, which are analyzed below
WebMvcAutoConfigurationAdapter
// The current class is a configuration class @Configuration(proxyBeanMethods = false) // Introducing EnableWebMvcConfiguration @Import(EnableWebMvcConfiguration.class) // Perform the binding configured in WebMvcProperties and ResourceProperties // Used to parse spring MVC and spring Configuration item with resources as prefix @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { }
viewResolver
@Bean // Only when there is a bean of ViewResolver type can it be loaded @ConditionalOnBean(ViewResolver.class) // When there is no bean with the name of viewResolver and the type of ContentNegotiatingViewResolver @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class) public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); // Settings Manager resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class)); // ContentNegotiatingViewResolver uses all the other view resolvers to locate // a view so it should have a high precedence // set priority resolver.setOrder(Ordered.HIGHEST_PRECEDENCE); return resolver; }
Although the ContentNegotiatingViewResolver inherits the ViewResolver, it does not directly parse the view, but finds the appropriate view parser through the context
ContentNegotiatingViewResolver
The following code will get the bean that implements the ViewResolver interface from beanFactory and put it into viewResolvers
@Override protected void initServletContext(ServletContext servletContext) { // Find the bean that implements the ViewResolver interface from the spring context Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values(); // Add the found bean s to viewsolvers if (this.viewResolvers == null) { this.viewResolvers = new ArrayList<>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans) { if (this != viewResolver) { this.viewResolvers.add(viewResolver); } } } else { for (int i = 0; i < this.viewResolvers.size(); i++) { ViewResolver vr = this.viewResolvers.get(i); if (matchingBeans.contains(vr)) { continue; } String name = vr.getClass().getName() + i; obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name); } } AnnotationAwareOrderComparator.sort(this.viewResolvers); this.cnmFactoryBean.setServletContext(servletContext); }
public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { // Use the parser in viewResolvers to parse the current view List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); // Select a best match View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : ""; if (this.useNotAcceptableStatusCode) { if (logger.isDebugEnabled()) { logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo); } return NOT_ACCEPTABLE_VIEW; } else { logger.debug("View remains unresolved" + mediaTypeInfo); return null; } }
It can also be seen from the above code that ContentNegotiatingViewResolver will not directly parse the view, but will hand over the parsing work to the implementation class bean of ViewResolver in beanFactory
BeanNameViewResolver
BeanNameViewResolver matches beans of View type in beanFactory by name
@Bean // Only bean s of View type are loaded @ConditionalOnBean(View.class) // Beans of type bean nameviewresolver do not exist before loading @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; }
BeanNameViewResolver
BeanNameViewResolver also inherits the ViewResolver, so it also has the ability to resolve the View. However, unlike the ContentNegotiatingViewResolver above, this class looks for a bean with the same name and type of View in beanFactory through the name of the View
public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered /** * Specify the order value for this ViewResolver bean. * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered. * @see org.springframework.core.Ordered#getOrder() */ public void setOrder(int order) { this.order = order; } @Override public int getOrder() { return this.order; } @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws BeansException { ApplicationContext context = obtainApplicationContext(); // Find a bean of type View whose name matches the View name if (!context.containsBean(viewName)) { // Allow for ViewResolver chaining... return null; } if (!context.isTypeMatch(viewName, View.class)) { if (logger.isDebugEnabled()) { logger.debug("Found bean named '" + viewName + "' but it does not implement View"); } // Since we're looking into the general ApplicationContext here, // let's accept this as a non-match and allow for chaining as well... return null; } return context.getBean(viewName, View.class); } }
Static resource processing
Webmvcoautoconfigurationadapter implements the WebMvcConfigurer interface, which is mainly used as a callback to customize the configuration
The processing of static resources is mainly in the addResourceHandlers method
public void addResourceHandlers(ResourceHandlerRegistry registry) { // The configuration items in resourceProperties are spring Resources prefix // Whether to enable the default static resource processing. The default value is true. If enabled, the following code will be executed to configure the resource processing if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); // Judge whether there is mapping processing for / webjars / * * or not. If there is no mapping configuration below if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } // The static pattern here defaults to/** // path can be configured here String staticPathPattern = this.mvcProperties.getStaticPathPattern(); // Judge whether there is a corresponding mapping. The default mapping location is [/ META-INF/resources/, /resources/, /static/, /public /] // Therefore, by default, we can put static resources under the above directories if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }
@EnableWebMvc
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { }
You can see the @ EnableWebMvc annotation, which mainly imports DelegatingWebMvcConfiguration
Let's take a look at the DelegatingWebMvcConfiguration inheritance hierarchy
WebMvcConfigurationSupport
First, take a look at WebMvcConfigurationSupport, which implements ApplicationContextAware and ServletContextAware, so you can get ApplicationContext and ServletContext
In addition, this class has many methods for injecting bean s
@Bean public BeanNameUrlHandlerMapping beanNameHandlerMapping( @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; }
And there are many methods of empty protected modifiers
protected void addViewControllers(ViewControllerRegistry registry) { }
The methods of these empty protected modifiers will be called in the above bean injection methods. The methods of these empty protected modifiers are provided to subclasses for customization operations
DelegatingWebMvcConfiguration
Let's take a look at the implementation class DelegatingWebMvcConfiguration. From the following code, we can see that DelegatingWebMvcConfiguration is a configuration class first
Secondly, the bean of WebMvcConfigurer type is injected through setConfigurers method
Then it implements the extension point method of customized mvc provided by WebMvcConfigurationSupport
In the extension point method, all injected WebMvcConfigurer type bean s will be traversed, and their corresponding methods will be called to execute their respective customization logic
@Configuration(proxyBeanMethods = false) public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } @Override protected void configurePathMatch(PathMatchConfigurer configurer) { this.configurers.configurePathMatch(configurer); } @Override protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) { this.configurers.configureContentNegotiation(configurer); } @Override protected void configureAsyncSupport(AsyncSupportConfigurer configurer) { this.configurers.configureAsyncSupport(configurer); } @Override protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { this.configurers.configureDefaultServletHandling(configurer); } @Override protected void addFormatters(FormatterRegistry registry) { this.configurers.addFormatters(registry); } @Override protected void addInterceptors(InterceptorRegistry registry) { this.configurers.addInterceptors(registry); } @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); } @Override protected void addCorsMappings(CorsRegistry registry) { this.configurers.addCorsMappings(registry); } @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); } @Override protected void configureViewResolvers(ViewResolverRegistry registry) { this.configurers.configureViewResolvers(registry); } @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { this.configurers.addArgumentResolvers(argumentResolvers); } @Override protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { this.configurers.addReturnValueHandlers(returnValueHandlers); } @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { this.configurers.configureMessageConverters(converters); } @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { this.configurers.extendMessageConverters(converters); } @Override protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { this.configurers.configureHandlerExceptionResolvers(exceptionResolvers); } @Override protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { this.configurers.extendHandlerExceptionResolvers(exceptionResolvers); } @Override @Nullable protected Validator getValidator() { return this.configurers.getValidator(); } @Override @Nullable protected MessageCodesResolver getMessageCodesResolver() { return this.configurers.getMessageCodesResolver(); } }
Customized mvc configuration
If you need to customize mvc configuration, you can use the following methods. The simplest way to retain the spring boot automatic configuration function is to implement a WebMvcConfigurer and inject
implements WebMvcConfigurer
The corresponding type of mvspring configuration will be automatically configured as mvboot, and the corresponding type of mvspring configuration will be automatically customized
Customization achieved in this way will not override the configuration of @ EnableAutoConfiguration about webmvcoautoconfiguration
@EnableWebMvc + implements WebMvcConfigurer
Because @ EnableWebMvc will introduce DelegatingWebMvcConfiguration, and DelegatingWebMvcConfiguration itself inherits WebMvcConfigurationSupport
The premise of Spring boot for mvc related automatic configuration is that there is no bean of WebMvcConfigurationSupport type
Therefore, using this method will override the configuration of @ EnableAutoConfiguration about webmvcoautoconfiguration
Extensions webmvcconfigurationsupport or extensions delegatingwebmvcconfiguration
The reason is the same as above, which will overwrite the configuration of @ EnableAutoConfiguration about webmvcoautoconfiguration