《学会 SpringBoot · 定制 SpringMVC》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 近期刚转战 CSDN,会严格把控文章质量,绝不滥竽充数,如需交流,欢迎留言评论。👍

CSDN.gif

写在前面的话

使用SpringBoot作为Java后端开发框架,基本是大多数企业的标配,这边把实际企业开发中,一些常用的 SpringBoot 操作做一个整理,将持续更新。


定制 MVC 功能(前言)

Spring Boot 为 Spring MVC 提供了默认的配置主要包括视图解析器、静态资源处理、类型转化器与格式化器、HTTP消息转换器、静态主页支持等,可谓简单易用。但实践中,难免需要进行个性化的配置,因此自定义Web MVC配置在所难免。
Spring Boot 先后提供了 WebMvcConfigurerAdapter、WebMvcConfigurationSupport、WebMvcConfigurer、@EnableWebMvc 等形式来实现Web MVC的自定义配置。

WebMvcConfigurerAdapter 废弃方式

SB1.x,可使用WebMvcConfigurerAdapter来扩展Spring MVC的功能,它是WebMvcConfigurer的一个抽象实现类,该抽象类中所有的方法实现都为空,子类需要哪些功能就实现哪些功能。
SB2.x,基于Java8实现,可将接口的方法定义为default,接口中被定义为default的方法子类可以不进行实现。而接口WebMvcConfigurer便运用了Java8的特性,因此WebMvcConfigurerAdapter存在的意义没有了。

@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {}

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

WebMvcConfigurationSupport 覆盖方式

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

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

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

/**
* 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会顶替到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接口,这种方式不会影响未覆盖的方法逻辑,这也是推荐的稳妥方式。

@Configuration
public class MyMVCConfig implements WebMvcConfigurer {

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

定制 MVC 功能(正篇)

Spring2.0版本后,推荐使用实现WebMvcConfigurer的方式 来全局定制SpringMVC特性。
也可以继承WebMvcConfigurationSupport实现,但会覆盖默认行为,具体参考前面分析专栏。

Tips:早期1.x版本是使用 extends WebMvcConfigurerAdapter 的方式,暂不需要了解。

@Configuration    
public class WebMvcConfg implements WebMvcConfigurer {}

可以做什么?

/* 拦截器配置 */
void addInterceptors(InterceptorRegistry var1);

/* 视图跳转控制器 */
void addViewControllers(ViewControllerRegistry registry);

/* 静态资源处理 */
void addResourceHandlers(ResourceHandlerRegistry registry);

/* 默认静态资源处理器 */
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);

/* 这里配置视图解析器 */
void configureViewResolvers(ViewResolverRegistry registry);

/* 配置内容裁决的一些选项*/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);

/** 解决跨域问题 **/
public void addCorsMappings(CorsRegistry registry) ;

静态资源配置

重写 addResourceHandlers 来配置路径访问等,SB 中默认使用 ResourceHttpRequestHandler 来映射类路径下的/static、/public、/resources 等路径中的静态文件直接映射为 /****。

/**
 * 配置静态访问资源
 * addResoureHandler:指的是对外暴露的访问路径
 * addResourceLocations:指的是内部文件放置的目录
 * 范例:http://localhost:8868/my/a1.jpg
 * 等同:http://localhost:8868/img/a1.jpg
 */
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/my/**").addResourceLocations("classpath:/static/img/");
}

@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文件中mvc:interceptors配置。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**");
}

@Autowired
private MyInteceptor myInteceptor;

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

参数解析器

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(currentUserMethodArgumentResolver());
}

@Bean
CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
    return new CurrentUserMethodArgumentResolver();
}

跨域拦截器

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

/**
 * 配置跨域拦截器
 *
 * @param registry 跨域拦截器
 */
@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowCredentials(true)
            .allowedHeaders("*")
            .allowedOrigins("*")
            .allowedMethods("*");
}

@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);
}

/**
 * 此种设置跨域的方式,在自定义拦截器的情况下可能导致跨域失效
 * 原因:当跨越请求在跨域请求拦截器之前的拦截器处理时就异常返回了,那么响应的response报文头部关于跨域允许的信息就没有被正确设置,导致浏览器认为服务不允许跨域,而造成错误。
 * 解决:自定义跨域过滤器解决跨域问题(该过滤器最好放在其他过滤器之前)
 * @param registry
 */
@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowedOrigins("*")
            .allowedHeaders("*")
            .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
            .allowCredentials(true)
            .maxAge(3600);
}

消息转换器

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

/**
 * 添加自定义消息转换器
 * 自定义消息转化器的第二种方法
 * 默认也会注册utf-8的该转换器
 */
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> 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) {
    registry.addFormatter(new DateFormatter("yyyy-MM-dd"));
}

@Override
public void addFormatters(FormatterRegistry registry) {
    //注册ConverterFactory(类型转换器工厂)
    registry.addConverterFactory(new BaseEnumConverterFactory());
}

//具体实例参考上方博客
@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(booleanFormatter());// 布尔格式化器
    registry.addConverter(stringToDateConverter());// 字符串转日期转化器
}

@Bean
BooleanFormatter booleanFormatter() {
    return new BooleanFormatter();
}

@Bean
StringToDateConverter stringToDateConverter() {
    return new StringToDateConverter();
}

路径匹配规则

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    // 设置是否模糊匹配,默认真。例如/user是否匹配/user.*。如果真,也就是说"/user.html"的请求会被"/user"的Controller所拦截。
    configurer.setUseSuffixPatternMatch(false);
    // 设置是否自动后缀模式匹配,默认真。如/user是否匹配/user/。如果真,也就是说, "/user"和"/user/"都会匹配到"/user"的Controller。
    configurer.setUseTrailingSlashMatch(true);
}

内容协商策略

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    // 自定义策略
    configurer.favorPathExtension(true)// 是否通过请求Url的扩展名来决定mediaType,默认true
            .ignoreAcceptHeader(true)// 不检查Accept请求头
            .parameterName("mediaType")
            .defaultContentType(MediaType.TEXT_HTML)// 设置默认的MediaType
            .mediaType("html", MediaType.TEXT_HTML)// 请求以.html结尾的会被当成MediaType.TEXT_HTML
            .mediaType("json", MediaType.APPLICATION_JSON)// 请求以.json结尾的会被当成MediaType.APPLICATION_JSON
            .mediaType("xml", MediaType.APPLICATION_ATOM_XML);// 请求以.xml结尾的会被当成MediaType.APPLICATION_ATOM_XML
    
    // 或者下面这种写法
    Map<String, MediaType> map = new HashMap<>();
    map.put("html", MediaType.TEXT_HTML);
    map.put("json", MediaType.APPLICATION_JSON);
    map.put("xml", MediaType.APPLICATION_ATOM_XML);
    // 指定基于参数的解析类型
    ParameterContentNegotiationStrategy negotiationStrategy = new ParameterContentNegotiationStrategy(map);
    // 指定基于请求头的解析
    configurer.strategies(Arrays.asList(negotiationStrategy));
}

异步调用支持

@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    // 注册callable拦截器
    configurer.registerCallableInterceptors(timeoutInterceptor());
    // 注册deferredResult拦截器
    configurer.registerDeferredResultInterceptors();
    // 异步请求超时时间
    configurer.setDefaultTimeout(1000);
    // 设定异步请求线程池callable等, spring默认线程不可重用
    configurer.setTaskExecutor(new ThreadPoolTaskExecutor());
}

@Bean
public TimeoutCallableProcessingInterceptor timeoutInterceptor() {
    return new TimeoutCallableProcessingInterceptor();
}

//测试接口
@GetMapping("test1")
public Callable<String> test1() {
    Callable<String> callable = () -> {
        Thread.sleep(60000);
        return "test";
    };
    return callable;
}


静态资源处理器

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
    configurer.enable("defaultServletName");
}

此时会注册一个默认的Handler:DefaultServerHttpRequestHandler,这个Handler也会用来处理静态文件的,它会尝试映射/*。当DispatcherServlet映射/时(/ 和/*是有区别的),并且没有找到合适的Handler来处理请求时,就会交给DefaultServletHttpRequestHandler来处理。注意:这里的静态资源是放置在web根目录下,而非WEB_INF下。
举例说明:在webroot目录下有一个图片a.png,我们知道Servelt规范中web根目录webroot下的文件可以直接访问的,但是由于DispatcherServlet配置了映射路径是:/,它几乎把所有的请求都拦截了,从而导致a.png访问不到,这时注册一个DefaultServletHttpRequestHandler就可以解决这个问题,其实可以理解为DispatchServlet破坏了Servler的一个特性(就是根目录下的文件可以直接访问),DefaultServletHttpRequestHandler是帮助回归这个特性的。


总结陈词

此篇文章介绍了SpringBoot 项目中如何常见的SpringMVC定制能力,仅供学习参考。
💗 后续将持续更新,请多多支持!!

CSDN_END.gif

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值