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
然后配置国际化内容
默认情况下,会根据后缀标识不同的国际化资源,比如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
在这里,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);
}
- 获取重定向的目标url
- 保存flash attributes
- 重定向到目标请求
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保存起来。
默认的FlashMapManager
为SessionFlashMapManager
,将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 注意事项
- 实际在Session中保存的FlashMap是
List<FlashMap>
类型,也就是说,一个Session可以保存多个FlashMap,一个FlashMap保存着一套Redirect转发所传递的参数; - FlashMap继承自HashMap,它除了具有HashMap的功能和设置有效期,还可以保存Redirect后的目标路径和通过url传递的参数,这两项内容主要用来从Session保存的多个FlashMap中查找当前请求的FlashMap
- FlashMapManager只是一个工具,真正的FlashMap是保存在session中的。FlashMapManager只是一个容器,这点和前面的SessionAttributes的设计异曲同工
3.3 流程总结
- 第一个请求处理时,会将
RedirectAttributes
中保存的信息设置到request的dispatchServlet.OUTPUT_FLASH_MAP_ATTRIBUTE
的属性中 - 紧接着在处理RedirectView时,会将这些数据进一步通过FlashMapManager保存到session中
- 第二个请求过来时,会直接通过FlashMapManager从session中获取数据,并保存到requesr的
dispatchServlet.INPUT_FLASH_MAP_ATTRIBUTE
的属性中 - 然后在ModelAndViewContainer生成时,会从request获取该属性并保存到当前Model中
这样,就实现了重定向的参数传递
其实,原理也很简单,就是把属性保存到session中,只是设计了FlashMapManager这样一个组件来完成。
四. 总结
整个springmvc的基本内容算是分析完了。总览springmvc,不得不感慨一下设计的精妙。对于web请求处理的整个流程,springmvc分别设计了不同的组件去处理每个阶段,总体流程清晰简洁,组件的分工协作很巧妙。这些都是我们值得学习的。
写程序就要像这样一样,各司其职,脉络清晰。并且通过接口和抽象类的设计,拓展功能也很强大。springmvc的几乎每个组件,我们都可以去拓展其中的逻辑。比如说拦截器,比如说参数解析器,比如说返回值处理器等等。都是可以定制的。
除了拓展和定制外,springmvc还提供了大量方便的注解,对付日常开发是绰绰有余。
学习springmvc,就是熟悉其中的九大组件,熟悉整个web处理的流程,熟悉springmvc对于组件化的设计,熟悉springmvc提供给我们的拓展方式。
熟悉了源码,对于日常开发中遇到的问题就能精确定位。并且有什么需求时,也能第一时间想到如何利用springmvc去解决问题。收获满满,日后回味!