自定义SpringBoot默认MVC配置?好几个坑,这篇文章必须珍藏

程序员小跃

共 9601字,需浏览 20分钟

 ·

2021-01-19 19:52

大家好,我是跃哥。平时学习的时候,一再强调,SpringBoot 是Java 学习中很重要的一环,所以理解 SpringBoot 的一些细节,也就成了重中之重。那今天,我们就来看看默认 MVC 配置的学习有哪些坑。走起!!!


前言




前些天,在项目实践过程中出现了一个奇怪的状况,Spring Boot的参数绑定失效了。而起因只是因为同事把参数上的@RequestParam注解去掉了。我们都知道,如果参数名称和Controller的方法名相同时,根本不需要@RequestParam注解的,Spring Boot会自动帮我们完成参数的绑定。

但为什么自动绑定机制失效了呢?本篇文章会为大家揭开谜底,在此过程中也会全面讲解如何在Spring Boot项目中自定义配置WebMvc,以及这其中的很多坑。


SpringBoot自定义WebMvc




Spring Boot为Spring MVC提供了默认的配置主要包括视图解析器、静态资源处理、类型转化器与格式化器、HTTP消息转换器、静态主页支持等,可谓简单易用。

但实践中,难免需要进行个性化的配置,因此自定义Web MVC配置在所难免。

Spring Boot先后提供了WebMvcConfigurerAdapter、WebMvcConfigurationSupport、WebMvcConfigurer、@EnableWebMvc等形式来实现Web MVC的自定义配置。下面我们来逐一学习。


被废弃的WebMvcConfigurerAdapter




在Spring Boot1.0+中,可以使用WebMvcConfigurerAdapter来扩展Spring MVC的功能。WebMvcConfigurerAdapter是WebMvcConfigurer的一个抽象实现类,该抽象类中所有的方法实现都为空,子类需要哪些功能就实现哪些功能。

到了Spring 5.0之后,也就是在Spring Boot2.0版本中,JDK基于Java8来实现了,而在Java8中可以将接口的方法定义为default。接口中被定义为default的方法子类可以不进行实现。

而接口WebMvcConfigurer便运用了Java8的特性,因此WebMvcConfigurerAdapter存在的意义没有了。于是,在Spring Boot2.0版本中,WebMvcConfigurerAdapter这个类被弃用了。

@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {

查看WebMvcConfigurerAdapter的实现,你会发现它就是把接口的所有方法实现为一个空的方法而已,Java8的default特性完全覆盖掉此功能。

如果你是基于Spring Boot 2.x进行开发,通过extends(继承)WebMvcConfigurerAdapter来实现功能,那么可以直接替换为implements(实现)WebMvcConfigurer的形式了。关于具体实现,我们后面会讲到。


会覆盖的WebMvcConfigurationSupport




WebMvcConfigurerAdapter被废弃了,那么我们还可以通过继承WebMvcConfigurationSupport来实现Spring MVC的拓展。

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {...}

这个类很特殊,实现了ApplicationContextAware和ServletContextAware接口, 提供了一些默认实现,同时提供了很多@Bean 方法,但是并没有提供@Configureation注解,因此这些@Bean并不会生效,所以我们需要继承这个类,并在提供的类上提供@Configureation注解才能生效。

WebMvcConfigurationSupport中不仅定义了Bean,还提供了大量add、config开头的方法。对照WebMvcConfigurer的方法定义,会发现几乎WebMvcConfigurer有的在WebMvcConfigurationSupport中都有。需要注意的是,某些方法在WebMvcConfigurationSupport中也并未实现具体功能。

比如常见的addInterceptors和addViewControllers:

/**
* Override this method to add Spring MVC interceptors for
* pre- and post-processing of controller invocation.
* @see InterceptorRegistry
 */
protected void addInterceptors(InterceptorRegistry registry) {
}

/**
 * Override this method to add view controllers.
 * @see ViewControllerRegistry
*/
protected void addViewControllers(ViewControllerRegistry registry) {
}

继承WebMvcConfigurationSupport之后,可以使用方法来添加自定义的拦截器、视图解析器等功能,示例如下:

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/login.html").setViewName("login");
    }
}

此时,你以为万事大吉,可以开心的写业务逻辑了?其实此时问题刚刚开始。当通过继承WebMvcConfigurationSupport的形式来实现MVC配置时,会对Spring Boot默认的MVC配置进行顶替。

一旦进行顶替,Spring Boot默认提供的那些约定优于配置的功能可能就会失效,比如静态资源访问不到、返回数据不成功,当然还有开篇提到的参数绑定失效的问题。

那么,为什么继承WebMvcConfigurationSupport会顶替到Spring Boot默认的MVC配置呢?先来看一下Spring Boot中对WEB MVC相关组件自动装配的实现:

@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 {...}

Spring Boot通过WebMvcAutoConfiguration配置类来对MVC的默认参数(约定)进行设置,但WebMvcAutoConfiguration生效是有限制条件的。

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})指定了,当Spring容器中不存在类型为WebMvcConfigurationSupport的bean的时候,才会进行默认配置。

一定自定义了WebMvcConfigurationSupport,那么将导致WebMvcAutoConfiguration无法实例化,进而内部初始化配置将全部无法实例化。

这种情况下,相关的配置都需要自己去实现了,除非对代码有极好的把控能力,或者大量特殊化定制,才会考虑此种形式。否则,一些列的约定便不复存在,可能会出现一些莫名其妙的问题。


终极杀手锏WebMvcConfigurer




讲了上述两种不可行或有坑的方式之后,按照剧情的发展,当然该出现最终的解决方案了,那就是实现WebMvcConfigurer接口。

但在学习了WebMvcConfigurationSupport的方式之后,你是否心有余悸,会不会也出现覆盖的情况?很显然,WebMVC自动配置类中并没有WebMvcConfigurer的Bean是否存在的限制条件。

因此,并不会因为实现了该接口而导致默认配置失效。不仅如此,Spring Boot还支持存在多个WebMvcConfigurer的实现类。

上面已经提到,Spring Boot2.x是基于Java8的,Java8有个重大的改变就是接口中可以有default方法,而default方法是不需要强制实现的。

上述的WebMvcConfigurerAdapter类就是实现了WebMvcConfigurer,所以我们不需要继承WebMvcConfigurerAdapter类,可以直接实现WebMvcConfigurer接口,用法与继承适配类是一样的。如:

@Configuration
public class MyMVCConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/user").setViewName("success");
    }
}

如果还不确定,你可以实践一下,会发现实现WebMvcConfigurer接口的方式,Spring Boot的自动配置并不会失效。通常,也是建议大家通过这种形式来实现Web MVC的自定义的。

下面在深入分析一下为什么通过实现WebMvcConfigurer接口的方式能保持保持自定义和默认配置同时生效。回到前面的自动配置类WebMvcAutoConfiguration,在该类内部会初始化一个内部类EnableWebMvcConfiguration:

@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {...}

该内部类继承了DelegatingWebMvcConfiguration类,DelegatingWebMvcConfiguration是对Spring MVC进行配置的一个代理类,它结合缺省配置和用户配置来定义Spring MVC运行时最终使用的配置。

DelegatingWebMvcConfiguration继承自WebMvcConfigurationSupport,而WebMvcConfigurationSupport为Spring MVC提供缺省配置。

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
   @Autowired(required = false)
   public void setConfigurers(List configurers) {
      if (!CollectionUtils.isEmpty(configurers)) {
         this.configurers.addWebMvcConfigurers(configurers);
      }
   }
   //...
}

DelegatingWebMvcConfiguration本身使用了注解@Configuration,所以它是一个配置类,会被作为一个单例bean注册到容器和被容器注入相应的依赖。WebMvcConfigurer注入到DelegatingWebMvcConfiguration是通过setConfigurers(List configurers)方法来实现的。该方法使用了注解@Autowired(required = false),会收集所有的WebMvcConfigurer实现类中的配置组合起来,组成一个超级配置。

因此,只要我们实现了WebMvcConfigurer接口的类都会通过setConfigurers方法被注入,多个WebMvcConfigurer实例会以List形式存储。

所以,直接实现WebMvcConfigurer接口的形式不会覆盖掉原有的默认配置,还可以新增客户自定义的配置。那么,使用实现WebMvcConfigurer接口的形式就OK了吗?如果你在网上找一些示例代码,还会有一些坑等着你,请继续往下看。


@EnableWebMvc使用场景




在很多代码示例中,你还会看到它不仅实现了WebMvcConfigurer接口,还在实现类上使用了@EnableWebMvc注解,此时你需要注意了。@EnableWebMvc注解会引起新一轮的默认WebMVC配置失效。

早期官方版本中有类似如下说明:如果想保持Spring Boot默认提供的WebMVC特性,然后新增额外的功能,只需要继承WebMvcConfigurerAdapter或实现WebMvcConfigurer接口,然后在实现类上通过@Configuration注解进行实例化即可,不需要使用@EnableWebMvc。如果你想完全控制Spring MVC,你可以在实现类上再添加上@EnableWebMvc注解。

也就是说@EnableWebMvc注解并不是必须配置,只有在要完全覆盖默认配置的情况下才会使用。而且该注解的源码注释中也明确指明,整个项目中只能有一个类使用@EnableWebMvc注解,而不像WebMvcConfigurer接口的实现类可以有多个。

所以,一旦代码中使用了@EnableWebMvc注解,就意味着Spring MVC的自动配置会失效,所有的东西都需要我们自动配置。使用示例如下:

@Configuration
@EnableWebMvc
public class WebDispatcherServletConfigure implements WebMvcConfigurer {
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resourceViewResolver = new InternalResourceViewResolver();
        resourceViewResolver.setViewClass(JstlView.class);
        resourceViewResolver.setPrefix("/WEB-INF/jsp/");
        resourceViewResolver.setSuffix(".jsp");
        return resourceViewResolver;
    }
}

那么,为什么使用@EnableWebMvc注解会导致默认生效呢?我们来看一下它的源码:

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

在注解EnableWebMvc上通过@Import导入了DelegatingWebMvcConfiguration类。这个类是不是似曾相识?对的,就是上面我们讲到的继承了WebMvcConfigurationSupport的类。

也就是说,一旦使用@EnableWebMvc注解,便会导入一个WebMvcConfigurationSupport,进而产生了一个Bean。此时,又回到前面的结论,WebMvcAutoConfiguration发现已经存在这么一个Bean,变不会进行实例化操作了。

汇总一下就是WebMvcConfigurationSupport有三种实现形式:

第一,用户自定义WebMvcConfigurationSupport的实现类,此时会导致WebMvcAutoConfiguration不会实例化;


第二,使用@EnableWebMvc注解,等于扩展了WebMvcConfigurationSupport,但没有重写任何方法,此时同样会导致WebMvcAutoConfiguration不会实例化;

第三,采用默认配置,WebMvcAutoConfiguration来实例化对应的WebMvcConfigurationSupport的实现类。

因此,通常我们采用实现WebMvcConfigurer接口,然后让Spring Boot采用默认的配置。


WebMvc常见配置




上面了解了各类配置形式的区别以及其中的坑,选择好适合的WebMVC配置形式之后,再来看看通常可以有哪些配置,主要围绕接口的实现类。


静态资源配置




重写addResourceHandlers来配置路径访问等,Spring Boot中默认使用ResourceHttpRequestHandler来映射类路径下的/static、/public、/resources和/METAINF/resources目录或者ServletContext的根目录中的静态文件直接映射为 /****。

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { 
    //静态资源路径 css,js,img等
    registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/");
    //视图
    registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
    //mapper.xml
    registry.addResourceHandler("/mapper/**").addResourceLocations("classpath:/mapper/");
    super.addResourceHandlers(registry);  
}  


拦截器配置




重写addInterceptors() 方法来配置拦截器(实现了HandlerInterceptor接口)等。这里实现的addInterceptors方法对应的是xml文件中配置。

@Autowired
private MyInteceptor myInteceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) { 
    //注册自定义拦截器,添加拦截路径和排除拦截路径
    registry.addInterceptor(myInteceptor) //添加拦截器
               .addPathPatterns("/**") //添加拦截路径
               .excludePathPatterns(//排除拦截路径
                       "/statics/**/*.*",
                       );
    super.addInterceptors(registry);  
}


跨域配置




重写addCorsMappings方法实现配置cors跨域限制等。

@Override
public void addCorsMappings(CorsRegistry registry) {  
    registry.addMapping("/**")//配置允许跨域的路径
        .allowedOrigins("*")//配置允许访问的跨域资源的请求域名
        .allowedMethods("PUT,POST,GET,DELETE,OPTIONS")//配置允许访问该跨域资源服务器的请求方法,如:POST、GET、PUT、DELETE等
        .allowedHeaders("*"); //配置允许请求header的访问,如 :X-TOKEN
    super.addCorsMappings(registry);
}


视图控制器配置




重写addViewControllers方法配置view视图映射等。

@Override
public void addViewControllers(ViewControllerRegistry registry) { 
    registry.addViewController("/").setViewName("/home");//默认视图跳转
    registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    super.addViewControllers(registry);  
}


消息转换器配置




重写configureMessageConverters方法来对消息进行转换。MessageConverter用于对http请求的返回结果进行转换,以fastjon、编码格式application/json;charset=UTF-8进行转换。

@Override
public void configureMessageConverters(List> converters) {
    StringHttpMessageConverter converter  = new StringHttpMessageConverter(Charset.forName("UTF-8"));
    converters.add(converter);  


数据格式化器配置




重写addFormatters方法来添加数据格式化器,比如将字符串转换为日期类型,可通过DateFormatter类来实现自动转换。

formatters和converters用于对日期格式进行转换,默认已注册了Number和Date类型的formatters,支持@NumberFormat和@DateTimeFormat注解,需要自定义formatters和converters可以实现addFormatters方法。

@Override
public void addFormatters(FormatterRegistry registry) {
    super.addFormatters(registry);
    registry.addFormatter(new DateFormatter("yyyy-MM-dd"));
}


视图解析器配置




重写configureViewResolvers()方法来配置视图解析器,主要是配置视图的前后缀。

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setPrefix("");
    viewResolver.setSuffix(".html");
    viewResolver.setCache(false);
    viewResolver.setContentType("text/html;charset=UTF-8");
    viewResolver.setOrder(0);  
    registry.viewResolver(viewResolver);
   super.configureViewResolvers(registry);
}

也提供了采用简化版本,比如:

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.jsp("/"".jsp");
}


小结




通过本篇文章的学习,想必大家对Spring Boot默认的配置,如何自定义配置,以及具体方法的实现都有了一个详细的了解。最关键的是通过不同的表现形式,不断追踪到底层实现,最终达到从底层原理到上层应用融会贯通的效果。所以,在实践的过程中我们不要忽略掉任何一个小的异常或bug,深入追加一下就打开一片新的天地。

参考文章:https://www.cnblogs.com/guoxiangyue/p/12123468.html https://juejin.cn/post/6844903960906563591





0、重磅!两万字长文总结,梳理 Java 入门进阶哪些事(推荐收藏)

1、程序员小跃 2020 原创汇总来袭,全年干货一网打尽


浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报