九. springmvc之其他组件

springmvc之其他组件

一. LocaleResolver

LocaleResolver的作用是使用request解析出Locale。主要是配合视图渲染来使用的,在DispatchServlet中有如下的代码

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  // Determine locale for request and apply it to the response.
  Locale locale =
    (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
  response.setLocale(locale);

如果配置了LocaleResolver,那么使用LocaleResolver从request中解析出Local,否则使用request的Local

然后把Locale设置到response中。

我们前面在FrameworkServlet中见到过另一个类

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

这里是在LocaleContextHolder中放入了Locale信息,方便调用

1.1 概述

首先来看看Locale是什么?

Locale表示了特定的地理、政治和文化地区 。即国际化配置,定义了某个语言环境,我们可以根据不同的语言环境,输出不同的语言。

在springmvc中,我们可以根据不同的Locale进行不同的操作。比如如果是中国,返回"你好";如果是美国,返回"Hello"。

LocaleResolver则是springmvc提供的一套用来通过request解析Locale的组件。我们怎么知道用户的语言环境呢,自然是request请求中携带了语言环境信息,LocaleResolver就是用来解析request中携带的语言信息。

默认情况下,当我们什么都不配置的时候,使用的是AcceptHeaderLocaleResolver

1.2 AcceptHeaderLocaleResolver

WebMvcAutoConfiguration 中,springboot为我们自动配置了如下的localResolver

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
  if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
    return new FixedLocaleResolver(this.mvcProperties.getLocale());
  }
  AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
  localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
  return localeResolver;
}

这个类的核心方法如下:

@Override
public Locale resolveLocale(HttpServletRequest request) {
  Locale defaultLocale = getDefaultLocale();
  if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
    return defaultLocale;
  }
  Locale requestLocale = request.getLocale();
  List<Locale> supportedLocales = getSupportedLocales();
  if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
    return requestLocale;
  }
  Locale supportedLocale = findSupportedLocale(request, supportedLocales);
  if (supportedLocale != null) {
    return supportedLocale;
  }
  return (defaultLocale != null ? defaultLocale : requestLocale);
}

首先获取配置的defaultLocale,默认情况下为null,可以通过spring.mvc.locale配置

如果不为null,切request中没有Accept-Language,那么返回默认的

否则从request中获取,request中获取的逻辑默认是从Accept-Language获取的。

1.3 使用技巧

1.3.1 i18n国际化配置

springboot自动注入了关于国际化配置相关的类ResourceBundleMessageSource

MessageSourceAutoConfiguration中有如下代码:

@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
  return new MessageSourceProperties();
}

@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
  ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
  if (StringUtils.hasText(properties.getBasename())) {
    messageSource.setBasenames(StringUtils
                               .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
  }
  if (properties.getEncoding() != null) {
    messageSource.setDefaultEncoding(properties.getEncoding().name());
  }
  messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
  Duration cacheDuration = properties.getCacheDuration();
  if (cacheDuration != null) {
    messageSource.setCacheMillis(cacheDuration.toMillis());
  }
  messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
  messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
  return messageSource;
}

我们可以在配置文件中指定国际化资源的位置:

# 国际化设置
spring.messages.basename=i18n/practice
spring.messages.cache-duration=120

然后配置国际化内容

image-20210622180422227

默认情况下,会根据后缀标识不同的国际化资源,比如practice_zh_CN.properties,会根据_进行拆分,取后面的信息作为Locale

在开发中,一般我们也会自定义一个LocaleResolver来实现根据不同request参数而不是Accept-Language来解析Locale

@Bean
public LocaleResolver localeResolver() {
  return new LocaleResolver() {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
      String l = request.getParameter("l");
      Locale locale = Locale.getDefault();
      if (!StringUtils.isEmpty(l)) {
        String[] s = l.split("_");
        locale = new Locale(s[0], s[1]);
      }
      return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
  };
}

只需要在容器中定义一个localeResolver,就会覆盖默认的,这个类主要实现根据请求参数中的l来获取local

比如http://localhost:8081/source/test10?l=en_US,会将en_US作为Locale返回

这样,在业务中,我们就能通过这个信息,返回不同的数据

@Autowired
private MessageSource messageSource;

@RequestMapping("/test10")
@ResponseBody
public ResponseResult<String> test10() {
  Locale locale = LocaleContextHolder.getLocale();
  String message = messageSource.getMessage("login.btn", null, locale);
  return ResponseResult.ok(message);
}

甚至可以在全局的ResponseBodyAdvice中,统一针对此,设置返回内容,这里不再赘述

二. ThemeResolver

ThemeResolver根据request解析Theme,这个组件用的不是很多,先不做分析

三. FlashMapManager

​ FlashMapManager用来管理FlashMap,FlashMap用于在redirect时传递参数

3.1 源码分析

在DispatchServlet中,关于此组件的代码在doService中:

if (this.flashMapManager != null) {
  FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
  if (inputFlashMap != null) {
    request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
  }
  request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
  request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

这步是给request中设置相应的属性,方便日后获取。具体属性在前面已经分析过,不再赘述。需要注意的是,这里面的INPUT_FLASH_MAP_ATTRIBUTE则只有redirect参数传递时才会生效

下面结合一个具体案例说明该组件所起的作用:

Controller

@RequestMapping("/test5")
public String test5(RedirectAttributes modelMap, Model model) {
  //使用RedirectAttributes的addFlashAttribute方法,可以在重定向之间传递参数
  modelMap.addFlashAttribute("redirect", "redirect");
  model.addAttribute("default", "default");
  return "redirect:test6";
}

@RequestMapping("/test6")
@ResponseBody
public String test6(ModelMap modelMap) {
  System.out.println(modelMap.getAttribute("default"));
  System.out.println(modelMap.getAttribute("redirect"));
  return "success";
}

/test5请求重定向到/test6,并且设置了一个重定向参数redirect

首先来追踪一下test5

image-20210622191631968

在这里,test5中并没有inputFlashMap,因此只给request设置了其余3个属性。

请求接下来来到RequestMappingHandlerAdapter中,如下

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

这里从RequestContextUtils.getInputFlashMap(request))也没有取到,因为前面没有设置。下面来到getModelAndView()

if (model instanceof RedirectAttributes) {
  Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
  HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
  if (request != null) {
    RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
  }
}

这里可以看到将我们在Model中设置的重定向参数设置到了request的OUTPUT_FLASH_MAP_ATTRIBUTE属性中

接下来,请求到了渲染视图这一块,首先看看AbstractView的代码:

@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
                   HttpServletResponse response) throws Exception {

  if (logger.isDebugEnabled()) {
    logger.debug("View " + formatViewName() +
                 ", model " + (model != null ? model : Collections.emptyMap()) +
                 (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
  }

  Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
  prepareResponse(request, response);
  renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

在最后一步renderMergedOutputModel中,由于我们前面返回视图实际类型为RedirectView,因此来到RedirectView中

@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
                                       HttpServletResponse response) throws IOException {

  String targetUrl = createTargetUrl(model, request);
  targetUrl = updateTargetUrl(targetUrl, model, request, response);

  // Save flash attributes
  RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);

  // Redirect
  sendRedirect(request, response, targetUrl, this.http10Compatible);
}
  1. 获取重定向的目标url
  2. 保存flash attributes
  3. 重定向到目标请求
public static void saveOutputFlashMap(String location, HttpServletRequest request, HttpServletResponse response) {
  FlashMap flashMap = getOutputFlashMap(request);
  if (CollectionUtils.isEmpty(flashMap)) {
    return;
  }

  UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build();
  flashMap.setTargetRequestPath(uriComponents.getPath());
  flashMap.addTargetRequestParams(uriComponents.getQueryParams());

  FlashMapManager manager = getFlashMapManager(request);
  Assert.state(manager != null, "No FlashMapManager. Is this a DispatcherServlet handled request?");
  manager.saveOutputFlashMap(flashMap, request, response);
}

这里,由于前面已经设置过了,因此可以从request中获取flashMap,然后获取容器中的FlashMapManager,将FlashMap保存起来。

默认的FlashMapManagerSessionFlashMapManager,将flashMap保存在session中,结构如下:

private static final String FLASH_MAPS_SESSION_ATTRIBUTE = SessionFlashMapManager.class.getName() + ".FLASH_MAPS";

protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
  HttpSession session = request.getSession(false);
  return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);
}

如上,整个test5的请求就结束了,下面看看test6

test6就很简单了,由于前面已经保存FlashMap到session中,因此这里可以直接获取到

FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
  request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}

后面在mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));中会把属性放入mavContainer中,这样我们就能获取到这些属性了。

3.2 注意事项

  1. 实际在Session中保存的FlashMap是List<FlashMap>类型,也就是说,一个Session可以保存多个FlashMap,一个FlashMap保存着一套Redirect转发所传递的参数;
  2. FlashMap继承自HashMap,它除了具有HashMap的功能和设置有效期,还可以保存Redirect后的目标路径和通过url传递的参数,这两项内容主要用来从Session保存的多个FlashMap中查找当前请求的FlashMap
  3. FlashMapManager只是一个工具,真正的FlashMap是保存在session中的。FlashMapManager只是一个容器,这点和前面的SessionAttributes的设计异曲同工

3.3 流程总结

  1. 第一个请求处理时,会将RedirectAttributes中保存的信息设置到request的dispatchServlet.OUTPUT_FLASH_MAP_ATTRIBUTE的属性中
  2. 紧接着在处理RedirectView时,会将这些数据进一步通过FlashMapManager保存到session中
  3. 第二个请求过来时,会直接通过FlashMapManager从session中获取数据,并保存到requesr的dispatchServlet.INPUT_FLASH_MAP_ATTRIBUTE的属性中
  4. 然后在ModelAndViewContainer生成时,会从request获取该属性并保存到当前Model中

这样,就实现了重定向的参数传递

其实,原理也很简单,就是把属性保存到session中,只是设计了FlashMapManager这样一个组件来完成。

四. 总结

​ 整个springmvc的基本内容算是分析完了。总览springmvc,不得不感慨一下设计的精妙。对于web请求处理的整个流程,springmvc分别设计了不同的组件去处理每个阶段,总体流程清晰简洁,组件的分工协作很巧妙。这些都是我们值得学习的。

​ 写程序就要像这样一样,各司其职,脉络清晰。并且通过接口和抽象类的设计,拓展功能也很强大。springmvc的几乎每个组件,我们都可以去拓展其中的逻辑。比如说拦截器,比如说参数解析器,比如说返回值处理器等等。都是可以定制的。

​ 除了拓展和定制外,springmvc还提供了大量方便的注解,对付日常开发是绰绰有余。

​ 学习springmvc,就是熟悉其中的九大组件,熟悉整个web处理的流程,熟悉springmvc对于组件化的设计,熟悉springmvc提供给我们的拓展方式。

​ 熟悉了源码,对于日常开发中遇到的问题就能精确定位。并且有什么需求时,也能第一时间想到如何利用springmvc去解决问题。收获满满,日后回味!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值