首页 > 其他分享 >第四十五章 Spring之假如让你来写MVC——国际化篇

第四十五章 Spring之假如让你来写MVC——国际化篇

时间:2025-01-20 09:28:28浏览次数:3  
标签:return 第四十五章 locale Spring Locale MVC public String

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

在提供个默认实现,就是一个简单的 JavaBeanDefaultMessageSourceResolvable代码如下:

/**
 * 封装国际化包含的信息,如:
 * 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

相关文章

  • Shell SpringBoot 操作
    通过shell脚本来操作SpringBoot,检查程序是否在运行,启动程序,停止程序,重启程序,输出程序状态#!/bin/bash#这里可替换为你自己的执行程序,其他代码无需更改APP_NAME="$2"APP_DIR=/application#APP_DIR=`pwd`#使用说明,用来提示输入参数usage(){echo"Usage:shxxx.sh[......
  • Spring Security 6.X + JWT + RBAC 权限管理实战教程(上)
    前言本教程基于SpringBoot3.x+SpringSecurity6.x实现,采用JWT+Redis的认证方案,结合RBAC权限模型,实现了一个完整的权限管理系统。一、项目依赖配置关键依赖说明: <!--SpringWeb--> <dependency> <groupId>org.springframework.boot</groupId> <arti......
  • 基于SpringBoot+Vue的智慧党建平台设计与实现
    ......
  • spring +fastjson 的 rce
    前言众所周知,spring下是不可以上传jsp的木马来rce的,一般都是控制加载class或者jar包来rce的,我们的fastjson的高版本正好可以完成这些,这里来简单分析一手一、环境搭建<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-s......
  • 【详解】JavaSpringMVC+MyBitis+多数据源切换
    目录JavaSpringMVC+MyBatis+多数据源切换1.环境准备2.添加依赖3.配置多数据源4.创建数据源配置类5.动态数据源切换5.1动态数据源类5.2数据源上下文持有者5.3切面管理数据源选择5.4自定义注解6.使用示例6.1UserMapper6.2OrderMapper6.3Service......
  • 2025毕设springboot 基于web的家教管理系统论文+源码
    系统程序文件列表开题报告内容研究背景在当今社会,随着教育需求的多元化和个性化发展,家教服务逐渐成为众多家庭补充学校教育、提升孩子学习成绩的重要途径。然而,传统的家教市场存在信息不对称、管理不规范、服务质量参差不齐等问题,给家长和家教老师带来了诸多不便。随着互联......
  • 2025毕设springboot 基于Web的多功能游戏平台设计与实现论文+源码
    系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展和普及,网络游戏已成为人们休闲娱乐的重要方式之一。传统的游戏平台大多局限于单一的游戏类型或服务商,无法满足用户多样化的游戏需求。与此同时,随着Web技术的不断进步,基于Web的游戏平台凭借其跨平台、易访问、......
  • 计算机毕业设计Springboot大学生创新教育平台 基于Springboot的大学生创新创业教育平
    计算机毕业设计Springboot大学生创新教育平台3ky241z7(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着全球经济和科技的快速发展,创新已成为推动社会进步的重要动力。大学生作为未来社会的主要建设者和创新者,其创新能力的培养和......
  • 计算机毕业设计Springboot服装租赁系统 基于Spring Boot框架的服装租赁平台开发 Sprin
    计算机毕业设计Springboot服装租赁系统4jrvt1zd(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着时尚潮流的快速更迭,人们对于服装的需求日益多样化,但购买大量服装不仅成本高昂,还可能造成资源浪费。服装租赁系统应运而生,它为用户......
  • Spring,Spring Ioc,Bean详解
    Spring框架Spring框架是Java应用最广的框架,其的成功来自于理念,并非是技术,其中几个理念非常重要,例如IoC(控制反转),AOP(面向切面编程)Spring的优势低耦合/低侵入(解耦)Spring通过IoC(控制反转)和DI(依赖注入)来实现低耦合高内聚声明式事务管理Spring基于AOP的方......