Spring源码阅读目录
第一部分——IOC篇
第一章 Spring之最熟悉的陌生人——IOC
第二章 Spring之假如让你来写IOC容器——加载资源篇
第三章 Spring之假如让你来写IOC容器——解析配置文件篇
第四章 Spring之假如让你来写IOC容器——XML配置文件篇
第五章 Spring之假如让你来写IOC容器——BeanFactory和FactoryBean
第六章 Spring之假如让你来写IOC容器——Scope和属性填充
第七章 Spring之假如让你来写IOC容器——属性填充特别篇:SpEL表达式
第八章 Spring之假如让你来写IOC容器——拓展篇
第九章 Spring之源码阅读——环境搭建篇
第十章 Spring之源码阅读——IOC篇
第二部分——AOP篇
第十一章 Spring之不太熟的熟人——AOP
第十二章 Spring之不得不了解的内容——概念篇
第十三章 Spring之假如让你来写AOP——AOP联盟篇
第十四章 Spring之假如让你来写AOP——雏形篇
第十五章 Spring之假如让你来写AOP——Joinpoint(连接点)篇
第十六章 Spring之假如让你来写AOP——Pointcut(切点)篇
第十七章 Spring之假如让你来写AOP——Advice(通知)上篇
第十八章 Spring之假如让你来写AOP——Advice(通知)下篇
第十九章 Spring之假如让你来写AOP——番外篇:Spring早期设计
第二十章 Spring之假如让你来写AOP——Aspect(切面)篇
第二十一章 Spring之假如让你来写AOP——Weaver(织入器)篇
第二十二章 Spring之假如让你来写AOP——Target Object(目标对象)篇
第二十三章 Spring之假如让你来写AOP——融入IOC容器篇
第二十四章 Spring之源码阅读——AOP篇
第三部分——事务篇
第二十五章 Spring之曾经的老朋友——事务
第二十六章 Spring之假如让你来写事务——初稿篇
第二十七章 Spring之假如让你来写事务——铁三角篇
第二十八章 Spring之假如让你来写事务——属性篇
第二十九章 Spring之假如让你来写事务——状态篇
第三十章 Spring之假如让你来写事务——管理篇
第三十一章 Spring之假如让你来写事务——融入IOC容器篇
第三十二章 Spring之源码阅读——事务篇
第四部分——MVC篇
第三十三章 Spring之梦开始的地方——MVC
第三十四章 Spring之假如让你来写MVC——草图篇
第三十五章 Spring之假如让你来写MVC——映射器篇
第三十六章 Spring之假如让你来写MVC——拦截器篇
第三十七章 Spring之假如让你来写MVC——控制器篇
第三十八章 Spring之假如让你来写MVC——适配器篇
第三十九章 Spring之假如让你来写MVC——番外篇:类型转换
第四十章 Spring之假如让你来写MVC——ModelAndView篇
第四十一章 Spring之假如让你来写MVC——番外篇:数据绑定
第四十二章 Spring之假如让你来写MVC——视图篇
第四十三章 Spring之假如让你来写MVC——上传文件篇
第四十四章 Spring之假如让你来写MVC——异常处理器篇
第四十五章 Spring之假如让你来写MVC——国际化篇
第四十六章 Spring之假如让你来写MVC——主题解析器篇
第四十七章 Spring之假如让你来写MVC——闪存管理器篇
第四十八章 Spring之假如让你来写MVC——请求映射视图篇
第四十九章 Spring之假如让你来写MVC——番外篇:属性操作
第五十章 Spring之假如让你来写MVC——融入IOC容器篇
第五十一章 Spring之源码阅读——MVC篇
文章目录
前言
对于Spring一直都是既熟悉又陌生,说对它熟悉吧,平时用用没啥问题,但面试的时候被问的一脸懵逼,就很尴尬,都不好意思在简历上写着熟悉Spring了
所以决定花点时间研究研究Spring的源码。主要参考的书籍是:《Spring源码深度解析(第2版)》、《Spring揭秘》、《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》
书接上回,在上篇 四十四章 Spring之假如让你来写MVC——异常处理器篇 中,A君 已经完成了 异常处理器 部分的功能了。接下来看看 A君 会有什么骚操作吧
尝试动手写IOC容器
出场人物:A君(苦逼的开发)、老大(项目经理)
背景:老大 要求 A君在一周内开发个简单的 IOC容器
前情提要:A君 已经完成了 异常处理器 部分的功能了 。。。
第三十九版 国际化
“可以可以,A君 。我看你在写测试的时候,似乎有意在回避什么?” 老大 笑道
“额。是!我在写测试的时候全用的是英文,没用中文。” A君 没想到,这点小心思也被 老大 看穿了,只能老实回应道
“果然!那这就是今天要做的事情了,相信你之前也有接触过!” 老大 依旧笑意不减,对 A君 说到
“国际化!” A君 惊呼道
“对!既然你已经知道了,之前也接触过了,我就不在多说什么了,你自己发挥!” 老大 挥了挥手,结束了这场谈话
JDK自带的国际化
又是逝去的记忆,A君 之前玩 国际化 的时候,都不知道多久之前的事情了。东翻翻,西看看。A君 总算回想起来一些东西了,所谓的 国际化,说到底就是显示的不同文字罢了,那如果支持多国语言??AI?既要实时翻译,又得符合语境,现在的AI还没厉害到这种地步,既然AI不太合适,那这时候还是得依靠人来,人要怎么解决呢?肯定不能让一个人在那里盯着实时翻译(笑)。其实要显示的文字基本上在系统成型的时候就已经定好了,这时候预先把要显示的文字定义好,而后再根据地区获取对应的配置就行了。这部分内容 JDK 其实有自带的工具了。A君 简单的写个例子,首先需要定义资源文件,里边配置了各种内容:
而后,就可以根据API直接使用了,代码如下:
@Test
public void v39() {
Locale defaultLocale = Locale.getDefault();
System.out.println("Default Locale: " + defaultLocale);
// 加载对应 Locale 的资源文件
ResourceBundle bundle = ResourceBundle.getBundle("messages", defaultLocale);
// 获取国际化消息
String message = bundle.getString("welcome.message");
System.out.println("不带参数的国际化:" + message);
String messageTemplate = bundle.getString("greeting");
// 替换占位符 {0},这里使用 "张三" 作为参数
message = MessageFormat.format(messageTemplate, "张三");
System.out.println("带参数的国际化:" + message);
}
这时候就会根据对应的资源名称+地区去找对应的文件,来读取要显示的内容。结果如下:
国际化信息
国际化 这部分内容显然不止在Web环境上能使用,各个环境都可以使用,这部分可以先提取出来。为啥不直接使用 JDK 自带的,原因还是一样的,需要对其进行更高层次的封装,提供更为丰富的API。为此,A君 打算先把国际化信息封装成一个对象,方便操作。定义MessageSourceResolvable
接口,代码如下:
/**
* 国际化信息
*/
@FunctionalInterface
public interface MessageSourceResolvable {
/**
* 获取国际化的key,或者说是code
*
* @return
*/
String[] getCodes();
/**
* 获取国际化参数
*
* @return
*/
default Object[] getArguments() {
return null;
}
/**
* 获取默认值
*
* @return
*/
default String getDefaultMessage() {
return null;
}
}
在提供个默认实现,就是一个简单的 JavaBean。DefaultMessageSourceResolvable
代码如下:
/**
* 封装国际化包含的信息,如:
* code
* 参数
* 默认值
*/
public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable {
private final String[] codes;
private final Object[] arguments;
private final String defaultMessage;
public DefaultMessageSourceResolvable(
String[] codes, Object[] arguments, String defaultMessage) {
this.codes = codes;
this.arguments = arguments;
this.defaultMessage = defaultMessage;
}
//省略get/set方法。。。
}
公共国际化
封装好国际化信息之后,接下来就可以定义真正的 国际化 接口了。大体功能和 JDK 自带的差不多,但是之前的例子中,如果Message
存在变量还得用户再次调用相关的类进行转换,这个显然太不方便了。于是,A君 对其进行了改造,新增MessageSource
接口,代码如下:
/**
* 国际化接口
*/
public interface MessageSource {
/**
* 获取信息,带默认值
*
* @param code
* @param args
* @param defaultMessage
* @param locale
* @return
*/
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
/**
* 获取信息,不带默认值
*
* @param code
* @param args
* @param locale
* @return
* @throws NoSuchMessageException
*/
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
/**
* 功能类似,不过MessageSourceResolvable将国际化信息封装成一个类
*
* @param resolvable
* @param locale
* @return
* @throws NoSuchMessageException
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
OK,其实这个接口大致功能足够了,再玩的花一点,让它有层级结构。A君 定义HierarchicalMessageSource
接口,代码如下:
/**
* MessageSource子接口,使其拥有层级结构
*/
public interface HierarchicalMessageSource extends MessageSource {
/**
* 获取父级解析器
*
* @return
*/
MessageSource getParentMessageSource();
/**
* 设置父级解析器
*
* @param parent
*/
void setParentMessageSource(MessageSource parent);
}
接下来就是实现类,NO,NO,NO!和之前类型转换一样的道理,MessageSource
可能有多个实现,但是 国际化 信息转换有那么点内容,没必要每个实现类都写一遍,把这些公共的转换封装成一个类就行了。A君 新增MessageSourceSupport
类,代码如下:
/**
* 提取国际化公共转换内容
*/
public abstract class MessageSourceSupport {
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage = new HashMap<>();
private boolean alwaysUseMessageFormat = false;
protected String formatMessage(String msg, Object[] args, Locale locale) {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
MessageFormat messageFormat = null;
synchronized (this.messageFormatsPerMessage) {
/**
* 获取缓存
*/
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
if (messageFormatsPerLocale != null) {
messageFormat = messageFormatsPerLocale.get(locale);
} else {
messageFormatsPerLocale = new HashMap<>();
this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
}
if (messageFormat == null) {
try {
//创建格式化对象
messageFormat = createMessageFormat(msg, locale);
} catch (IllegalArgumentException ex) {
if (isAlwaysUseMessageFormat()) {
throw ex;
}
messageFormat = INVALID_MESSAGE_FORMAT;
}
messageFormatsPerLocale.put(locale, messageFormat);
}
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
synchronized (messageFormat) {
return messageFormat.format(resolveArguments(args, locale));
}
}
//省略其他方法。。。
}
现在可以开始提取抽象类,这回 A君 还是不知道具体实现方式是啥,虽然 JDK 自带的 国际化 已经满足大部分情况,但是也不能保证实在特殊的情况,那这个抽象类就只能定义一些最基本的功能,比如:当前信息获取不到,就去父级获取、地区为空时,获取默认地区等。A君 定义AbstractMessageSource
类,代码如下:
/**
* 抽象国际化处理,用以定义处理顺序
*/
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
/**
* 父级消息处理
*/
private MessageSource parentMessageSource;
/**
* 公共配置
*/
private Properties commonMessages;
private boolean useCodeAsDefaultMessage = false;
@Override
public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
return getDefaultMessage(code);
}
return renderDefaultMessage(defaultMessage, args, locale);
}
protected String getMessageInternal(String code, Object[] args, Locale locale) {
/**
* code为空,直接返回
*/
if (code == null) {
return null;
}
/**
* 地区为空,获取默认地区
*/
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
/**
* 关闭格式化且不包含参数
*/
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
} else {
//处理参数
argsToUse = resolveArguments(args, locale);
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
/**
* 还没有获取到,则尝试去全局配置获取
*/
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
/**
* 还是没有,则去父级获取
*/
return getMessageFromParent(code, argsToUse, locale);
}
}
获取国际化信息的流程已经提取完成了,接着就可以提取 JDK国际化 公共代码了。像什么资源名、缓存之类的可以抽象出来。A君 定义AbstractResourceBasedMessageSource
,代码如下:
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {
/**
* 资源前缀
*/
private final Set<String> basenameSet = new LinkedHashSet<>(4);
/**
* 默认编码
*/
private String defaultEncoding;
private boolean fallbackToSystemLocale = true;
private Locale defaultLocale;
private long cacheMillis = -1;
public void setBasenames(String... basenames) {
this.basenameSet.clear();
addBasenames(basenames);
}
public void addBasenames(String... basenames) {
if (!ObjectUtils.isEmpty(basenames)) {
for (String basename : basenames) {
this.basenameSet.add(basename.trim());
}
}
}
//省略其他代码。。。
}
接下来就是真正的实现了,老样子,既然是依赖于 JDK,这里要做的事情就少了很多了。创建ResourceBundle
进行处理就行了。A君 新增ResourceBundleMessageSource
类,代码如下:
public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware {
/**
* 缓存
*/
private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
new ConcurrentHashMap<>();
/**
* 格式化缓存
*/
private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats =
new ConcurrentHashMap<>();
/**
* 资源类加载器
*/
private ClassLoader bundleClassLoader;
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
private volatile MessageSourceControl control = new MessageSourceControl();
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
/**
* 缓存是否过期
*/
if (getCacheMillis() >= 0) {
return doGetBundle(basename, locale);
} else {
//缓存中获取
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
return bundle;
} catch (MissingResourceException ex) {
return null;
}
}
}
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
ClassLoader classLoader = getBundleClassLoader();
MessageSourceControl control = this.control;
if (control != null) {
try {
return ResourceBundle.getBundle(basename, locale, classLoader, control);
} catch (UnsupportedOperationException ex) {
this.control = null;
}
}
return ResourceBundle.getBundle(basename, locale, classLoader);
}
//省略其他方法。。。
}
好了,现在公共的 国际化 功能算是提取完毕了
地区和时区
在前面弄 事务 时,A君 已经见识过了 Connection Passing(即连接对象需要一直当成参数传递),在这里 A君 可不想重蹈覆辙,在方法执行的过程中,地区、时区很可能有多个方法都要使用,这时候一一当成参数传递可不是个好主意。既然之前已经遇到过了,那么解决方法也是一样的,和线程绑定即可。不过在此之前,需要先把接口定义出来。A君 新增LocaleContext
接口,代码如下:
/**
* 地区信息,和一般和线程绑定
*/
public interface LocaleContext {
/**
* 获取地区
*
* @return
*/
Locale getLocale();
}
给他个简单的实现类,新增SimpleLocaleContext
类,代码如下:
public class SimpleLocaleContext implements LocaleContext {
private final Locale locale;
public SimpleLocaleContext(Locale locale) {
this.locale = locale;
}
@Override
public Locale getLocale() {
return this.locale;
}
}
接着就是时区相关的内容,与上面类似,A君 新增TimeZoneAwareLocaleContext
接口,代码如下:
/**
* 和线程绑定
*/
public interface TimeZoneAwareLocaleContext extends LocaleContext {
/**
* 获取时区
*
* @return
*/
TimeZone getTimeZone();
}
还是给他个默认实现,新增SimpleTimeZoneAwareLocaleContext
类,代码如下:
ublic class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implements TimeZoneAwareLocaleContext {
/**
* 时区
*/
private final TimeZone timeZone;
public SimpleTimeZoneAwareLocaleContext(Locale locale, TimeZone timeZone) {
super(locale);
this.timeZone = timeZone;
}
@Override
public TimeZone getTimeZone() {
return this.timeZone;
}
}
剩下的线程绑定工具类和之前类似,这里就不在说明了
Web环境下的国际化
A君 已经把 国际化 的公共代码提取出来了,接下来就得回归主题了,Web环境下如何支持 国际化?既然是Web环境,又是HTTP接口,那信息自然由客户端提供了,这可不能直接获取服务器的地区。既然如此,A君 新增LocaleResolver
接口,代码如下:
/**
* web环境国际化接口
*/
public interface LocaleResolver {
/**
* 查找国际化
*
* @param request
* @return
*/
Locale resolveLocale(HttpServletRequest request);
/**
* 设置国际化
*
* @param request
* @param response
* @param locale
*/
void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}
接着,提取抽象类可难倒 A君 了,这玩意有什么公共的地方,无奈,A君 只能弄个默认地区。要是地区为空,可以获取默认地区。AbstractLocaleResolver
代码如下;
public abstract class AbstractLocaleResolver implements LocaleResolver {
/**
* 默认时区
*/
private Locale defaultLocale;
protected Locale getDefaultLocale() {
return this.defaultLocale;
}
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
}
接下来需要对其LocaleResolver
进行拓展,和之前一样,不可能将request
或者locale
当成参数一层层传递下去,这时候就需要线程绑定了,新增LocaleContextResolver
接口,代码如下:
/**
* 线程绑定地区
*/
public interface LocaleContextResolver extends LocaleResolver {
LocaleContext resolveLocaleContext(HttpServletRequest request);
void setLocaleContext(HttpServletRequest request, HttpServletResponse response,
LocaleContext localeContext);
}
接着,就是抽象类了,和AbstractLocaleResolver
一样,不过这回是时区。AbstractLocaleContextResolver
代码如下:
public abstract class AbstractLocaleContextResolver extends AbstractLocaleResolver implements LocaleContextResolver {
/**
* 默认时区
*/
private TimeZone defaultTimeZone;
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = resolveLocaleContext(request).getLocale();
return (locale != null ? locale : request.getLocale());
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}
}
接着就是实现类了,虽说世界上有很多国家和地区,但是也不排除用户只针对国内市场,这种情况下时区、地区都是固定的,没必要进行额外的配置。想到这里,A君 新增FixedLocaleResolver
类,代码如下:
/**
* 固定地区
*/
public class FixedLocaleResolver extends AbstractLocaleContextResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = getDefaultLocale();
if (locale == null) {
locale = Locale.getDefault();
}
return locale;
}
@Override
public LocaleContext resolveLocaleContext(HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
return getDefaultLocale();
}
@Override
public TimeZone getTimeZone() {
return getDefaultTimeZone();
}
};
}
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response,
LocaleContext localeContext) {
throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}
}
这里提供的全是默认时区、地区信息,且不能修改。在现在全球化的潮流下,固定地区显然还是比较少见的,面对全球的用户必然是要提供对应语言进行显示的。但是这里有一个问题:用户每次访问都需要带上地区信息吗?显然答案是否定的,那么有地方可以存储吗?也有,Session
或者Cookie
都可以进行存储,起码一段时间内不用操心。这里,A君 就以Session
为例,Cookie
与之类似。新增SessionLocaleResolver
类,代码如下:
/**
* 基于Session的国际化
*/
public class SessionLocaleResolver extends AbstractLocaleContextResolver {
public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE";
public static final String TIME_ZONE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".TIME_ZONE";
private String localeAttributeName = LOCALE_SESSION_ATTRIBUTE_NAME;
private String timeZoneAttributeName = TIME_ZONE_SESSION_ATTRIBUTE_NAME;
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);
if (locale == null) {
locale = determineDefaultLocale(request);
}
return locale;
}
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
Locale locale = (Locale) WebUtils.getSessionAttribute(request, localeAttributeName);
if (locale == null) {
locale = determineDefaultLocale(request);
}
return locale;
}
@Override
public TimeZone getTimeZone() {
TimeZone timeZone = (TimeZone) WebUtils.getSessionAttribute(request, timeZoneAttributeName);
if (timeZone == null) {
timeZone = determineDefaultTimeZone(request);
}
return timeZone;
}
};
}
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response,
LocaleContext localeContext) {
Locale locale = null;
TimeZone timeZone = null;
if (localeContext != null) {
locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext) {
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
}
}
WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);
WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
}
protected Locale determineDefaultLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
if (defaultLocale == null) {
defaultLocale = request.getLocale();
}
return defaultLocale;
}
protected TimeZone determineDefaultTimeZone(HttpServletRequest request) {
return getDefaultTimeZone();
}
}
整体逻辑并不复杂,就是把地区、时区设置到Session
中去就行了
地区拦截器
到此为止,国际化 大部分内容已经完成了,还剩一个问题:在哪里获取对应的地区信息?在DispatcherServlet
中吗?A君 一阵皱眉,似乎不太合适,DispatcherServlet
只负责请求处理,要它再去处理请求参数,这职责似乎又耦合了。那还有哪里可以处理?拦截器 !A君 眼前一亮,再也没有比 拦截器 更合适了,它可以在请求前期就获取到地区信息进行处理,于是,A君 新增LocaleChangeInterceptor
类,代码如下:
public class LocaleChangeInterceptor implements HandlerInterceptor {
public static final String DEFAULT_PARAM_NAME = "lang";
private String paramName = DEFAULT_PARAM_NAME;
private String[] httpMethods;
private boolean ignoreInvalidLocale = false;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
String newLocale = request.getParameter(getParamName());
if (newLocale != null) {
if (checkHttpMethod(request.getMethod())) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
} catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
ex.printStackTrace();
} else {
throw ex;
}
}
}
}
return true;
}
private boolean checkHttpMethod(String currentMethod) {
String[] configuredMethods = getHttpMethods();
if (ObjectUtils.isEmpty(configuredMethods)) {
return true;
}
for (String configuredMethod : configuredMethods) {
if (configuredMethod.equalsIgnoreCase(currentMethod)) {
return true;
}
}
return false;
}
protected Locale parseLocaleValue(String localeValue) {
return StringUtils.parseLocale(localeValue);
}
}
这个 拦截器 只需要根据请求中的地区参数,设置对应的内容即可
修改DispatcherServlet
拦截器 定义完成之后,还需要DispatcherServlet
提供LocaleResolver
对象,毕竟整个处理信息都是由DispatcherServlet
提供,国际化 也不例外。DispatcherServlet
新增initLocaleResolver
方法,代码如下:
protected void initStrategies() {
/**
*1.初始化文件上传
*/
initMultipartResolver();
/**
* 2.初始化国际化
*/
initLocaleResolver();
/**
* 初始化url处理器
*/
initHandlerMappings();
/**
* 初始化适配器
*/
initHandlerAdapters();
/**
* 初始化异常处理
*/
initHandlerExceptionResolvers();
/**
* 初始化视图
*/
initViewResolvers();
}
private void initLocaleResolver() {
this.localeResolver = new SessionLocaleResolver();
}
接下来,需要把它设置到requst
中,拦截器 才能获取的到。新增doService
方法,代码如下:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
//清理include
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
//设置国际化处理器
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
doDispatch(request, response);
}
好了,DispatcherServlet
也处理完毕了
测试
折腾了半天,总算完成了。接下来,又到了愉快地测试环节了。A君 新增index.jsp
页面,用来模拟不同地区的请求,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="getLogin?lang=zh">中文</a>
<br />
<a href="getLogin?lang=en">英文</a>
<br />
<a href="getLogin?lang=fr">法语</a>
</body>
</html>
接着就是需要国际化显示的页面,新增home.jsp
页面,代码如下:
<%@page isELIgnored="false"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
还需要一些 国际化 的配置,如下:
好了,最后在定义个 控制器 进行处理即可,新增HelloController
类,代码如下:
@Controller
public class HelloController {
private static final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
static {
messageSource.getBasenameSet().add("messages");
}
@RequestMapping(value = "/index")
public String index() {
return "index";
}
@RequestMapping(value = "/getLogin")
public String home(HttpServletRequest request, HttpServletResponse response,
Model model, Locale locale) throws UnsupportedEncodingException {
String encoding = StandardCharsets.UTF_8.toString();
request.setCharacterEncoding(encoding);
response.setCharacterEncoding(encoding);
// 获取国际化消息
String message = messageSource.getMessage("welcome.message", null, locale);
model.addAttribute("message", message);
System.out.println(message);
return "home";
}
}
一切都准备好之后,接下来就可以编写测试代码了。如下:
@Test
public void v39() throws LifecycleException {
System.out.println("############# 第三十九版: 国际化篇 #############");
Tomcat tomcat = new Tomcat();
//设置端口
tomcat.setPort(8082);
//设置静态资源路径
String webApp = new File("src/main/resources/v39").getAbsolutePath();
Context context = tomcat.addWebapp("/test/", webApp);
tomcat.start();
//挂起
tomcat.getServer().await();
}
测试结果如下:
中国:
英国:
好嘞,总算搞定了。总算没有辜负一整天的努力。A君 也准备收拾包裹回家咯
总结
正所谓树欲静而风不止,欲知后事如何,请看下回分解(✪ω✪)
标签:return,第四十五章,locale,Spring,Locale,MVC,public,String From: https://blog.csdn.net/weixin_42789334/article/details/144760064