SpringBoot中对SpringMvc的特殊定制

在SpringMvc的自动配置类中有很多的@Bean,SpringBoot就是根据这些类去自动配置一些SpringMvc的默认属性,我们可以覆盖掉这个@Bean去自定义一些我们自己特殊化的东西。
自动配置类名称:WebMvcAutoConfiguration
在大多数情况下,SpringBoot在自动配置中标记了很多@ConditionalOnMissingBean(xxx.class)(意思就是如果IOC容器中没有,当前的@Bean才会生效),如果我们自己配置了xxx.class,就会覆盖掉SpringBoot的默认配置。
实现一个自定义WebMvcAutoConfiguration类,让其实现于WebMvcConfigurer接口并注册到IOC容器中,在其中写相应代码就会覆盖掉默认配置。

1、自定义拦截器

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 {

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TimeInterceptor())      // 添加一个拦截器
            .addPathPatterns("/**");     // 拦截映射规则,拦截所有请求
                //.excludePathPatterns("")  // 设置排除的映射规则
    }
}

实现拦截器具体的类:

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 = LocalDateTime.now();
        return true;
    }

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

        // 计算两个时间差
        Duration between = Duration.between(start, end);

        // 获得相差毫秒
        long millis = between.toMillis();

        logger.info("当前请求:" + request.getRequestURI() + ":执行时间:" + millis + "毫秒。");
    }
}

2、CORS跨域请求

2.1、全局配置

就比如说我们两个系统模块之前的请求,就称之为跨域请求。
首先我在一个模块写一个ajax请求来请求另一个模块,本模块目前设置其它端口:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>rest首页</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
    <button id="restButton">请求跨域请求</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>

然后我们启动服务,按照图片中的方式操作时报错,如下:

在这里插入图片描述

如果我们需要访问成功的话,我们需要在跳转后的模块添加上跨域访问的配置,步骤如下:
在我们自定义的SpringMvc类中重写跨域请求的方法,如下操作:

/**
 * 跨域请求处理(全局的配置)
 * @param registry
 */
@Override
public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/user/*")            // 映射本服务器中哪些http接口可以进行跨域请求
            .allowedOrigins("http://localhost:8101")           // 配置哪些来源有权限跨域
            .allowedMethods("GET","POST","DELETE","PUT");      // 配置允许跨域请求的方法
    }

就可以成功了!

2.2、单个配置

在上面的基础上,我们可以去掉自定义类中的跨域请求配置,在相应的controller方法上添加@CrossOrigin注解即可。

/**
 * 查询
 * @param id
 * @return
 */
@GetMapping("/{id}")
@ApiOperation("根据用户id查询相应的用户信息")
@CrossOrigin
public Result getUser(@PathVariable Integer id){
    User user = userService.getUser(id);
    return new Result<>(200,"查询成功!",user);
}

3、Json

SpringBoot默认提供了三种JSON映射库(Gson、Jackson、JSON-B)的继承,默认的是jackson。

3.1、Jackson的使用

注解含义
@JsonIgnore排除json序列化,将它标注在属性上将不会进行json格式化
@JsonFormat(pattern = “yyyy-MM-dd hh:mm:ss”, locale = “zh”)进行日期格式化
@JsonInclude(JsonInclude.Include.NON_NULL)当这个字段不为null时,给它进行json序列化
@JsonProperty(“ha”)可以通过这个属性给字段设置别名

3.2、自定义json的序列化与反序列化

首先顶一个一个序列化的类,必须加上注解@JsonComponent。
接着指定需要序列化的对象,在静态类的泛型上直接指定即可,代码示例如下:

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();
            // 这里的反序列化相当于是请求进来的id会进行json封装
            user.setId(tree.findValue("id").asInt());
            return user;
        }
    }
}

4、国际化

国际化我们一般有两种方式,一种是根据浏览器目前设置的语言去国际化,还有一种是传递相应的国际化标识参数来实现国际化。
下面两种都进行说明一下

4.1、通过浏览器设置的语言参数,在请求头中获取实现国际化

在SpringBoot中进行国际化文本的操作我们需要进行以下几个步骤:

4.1.1、添加国际化资源文件

在这里插入图片描述

4.1.2、配置messageResource 设置国际化文本

SpringBoot中提供了MessageSourceAutoConfiguration,所以我们不需要去配置messageResource。
但是他并没有生效,我们可以通过设置debug=true参数在控制台看到以下信息

在这里插入图片描述

下面我们通过代码分析一下为什么没有生效:

@Configuration(
    proxyBeanMethods = false
)
// 如果自己配置了@Bean名字叫messageSource的bean,就会用自定义的
@ConditionalOnMissingBean(
    name = {"messageSource"},
    search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(-2147483648)
// @Conditional自定义条件匹配,会传入一个实现了Condition接口的类ResourceBundleCondition
// ResourceBundleCondition 会重写matches方法,自定义匹配规则,如果该方法返回true,则代表匹配成功
@Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

调用下面的matches方法,然后返回匹配结果

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

    try {
    	// 具体的匹配规则在 getMatchOutcome 方法里面
        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);
    }
}

getMatchOutcome方法代码如下:

public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
	// 获取配置文件中 spring.messages.basename 属性的值,默认值为 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;
}

getMatchOutcomeForBasename方法代码如下:

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;
		// 根据 message 获取 该类路径下的所有 properties 的资源文件
		// 当如果这个路径下有属性资源文件时,匹配结果将会是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());
}

所以,根据以上的代码跟踪,MessageSourceAutoConfiguration没有匹配到是因为系统没有找到相应的国际化文本属性资源文件,所以,根据刚才代码的逻辑,有以下两种解决方式:

1、将文件夹i18n的名称改为messages。
2、在配置文件中配置spring.messages.basename属性,指定i18n文件夹的位置。

我们再次重启观察控制台,如下所示:

在这里插入图片描述

4.1.3、通过去解析请求头中的accept-language 或者 解析url参数中?local=

在WebMvcAutoConfiguration类中有一个解析请求头中的accept-language方法localeResolver

@Bean
@ConditionalOnMissingBean(
    name = {"localeResolver"}
)
public LocaleResolver localeResolver() {
	// 当配置文件中有 spring.mvc.locale-resolver=fixed
	// 就会去配置文件里面找 spring.mvc.locale,这里相当于设置了一个固定值
    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 {
    	// 如果上面没有设置fixed,则会以AcceptHeaderLocaleResolver 类去解析
    	// 将配置文件里面的这个参数当成一个默认值,如果请求头没有的话,则会去取这个默认值
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        Locale locale = this.webProperties.getLocale() != null ? this.webProperties.getLocale() : this.mvcProperties.getLocale();
        localeResolver.setDefaultLocale(locale);
        return localeResolver;
    }
}

AcceptHeaderLocaleResolver类的解析方法,代码如下:

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // 先去请求头的Accept-Language参数去找,如果没有,则会找配置文件里面默认的
    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、直接通过MessageSource类调用

在需要使用的类中先注入MessageSource类,然后通过这个类调用相应的国际化文本信息。

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("用户控制类")
public class UserController {

    @Autowired
    UserService userService;

    @Autowired
    MessageSource messageSource;

    /**
     * 查询
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据用户id查询相应的用户信息")
    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、根据传递参数的形式实现国际化

传递参数的形式也就类似于在页面搞一个下拉列表,当选择中文页面就显示中文,当选择英文页面就展示英文。
我们在上面的基础上有以下几个步骤需要操作:

4.2.1、添加国际化拦截器(在MyWebMvcConfiguration类中,自定义的)

/**
 * 添加拦截器
 * @param registry
 */
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 添加国际化拦截器
    registry.addInterceptor(new LocaleChangeInterceptor())
            .addPathPatterns("/**");    // 拦截映射规则
}

4.2.2、覆盖掉之前的获取国际化标识的方法(在MyWebMvcConfiguration类中,自定义的)

/**
 * 重写localeResolver方法,让其在 url 中取参数设置国际化文本
 * @return
 */
@Bean
public LocaleResolver localeResolver() {
    CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
    // 设置过期时间,设计一个月
    cookieLocaleResolver.setCookieMaxAge(60*60*24*30);
    cookieLocaleResolver.setCookieName("locale");
    return cookieLocaleResolver;
}

4.2.3、测试

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

10、其它

10.1、WebMvcConfigurer原理

其实在SpringMvc的自动配置类WebMvcAutoConfiguration里面有一个静态类也实现了WebMvcConfigurer类,也是通过与上面同样的配置来对一些默认配置的扩展,我们自己只需要扩展一些其它的东西来完善我们的系统。
但是现在有一个问题就是,我们虽然重写了这个接口,但是SpringBoot的配置依然生效,那么这些配置相互之前是如何配合的,需要接下来来研究。
在WebMvcAutoConfiguration类上我们导入了一个EnableWebMvcConfiguration类,如下所示:

@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})

EnableWebMvcConfiguration类又继承于DelegatingWebMvcConfiguration类,在这个类中会将所有的实现了WebMvcConfigurer接口的类组成一个list存储到delegates委派器中。

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

}

在调用这个方法时,会去循环刚才存储的委派器,进行获取。如下所示:

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

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

}

注意:我们在自定义的SpringMvc类上千万不能加上@EnableWebMvc注解,因为当我们加上这个注解之后,WebMvcAutoConfiguration类中的扩展方法将会失效。

失效原理:
当我们进入@EnableWebMvc注解的实现的时候,会发现它导入了DelegatingWebMvcConfiguration类

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

然而DelegatingWebMvcConfiguration又继承于WebMvcConfigurationSupport类。
而再次看到我们的WebMvcAutoConfiguration类只有在IOC容器中不存在WebMvcConfigurationSupport类的时候才生效,所以,这个时候WebMvcAutoConfiguration类就失效了。

@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 {
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值