当前位置: 首页 > news >正文

Spring国际化详解,Spring国家化实例及源码详解

文章目录

  • 一、概述
    • 1、使用场景
  • 二、Java 国际化标准实现
    • 1、Java文本格式化
  • 三、Spring 国际化接口
    • 1、层次性MessageSource
    • 2、MessageSource 开箱即用实现
      • ResourceBundleMessageSource
      • ReloadableResourceBundleMessageSource
    • 3、MessageSource 內建依赖
      • 源码分析
    • 4、Spring Boot 为什么要新建 MessageSource Bean?
    • 5、实现配置自动更新 MessageSource
  • 参考资料

一、概述

Spring国际化相关的,似乎平时接触的也很少,也很少用到这部分的技术。

但是做架构、框架封装的时候,又不得不考虑。

1、使用场景

(1)普通国际化文案
就是我们网页上显示的和我们输入的一些支持多语言的字体或文案。

(2)Bean Validation 校验国际化文案
Springboot场景用的比较多的、默认加载的Bean Validation(用于Bean校验的JSR标准)。

(3)Web 站点页面渲染
比如说一个web页面,不同的国家ip或者其他信息,会收到不同的页面渲染,文字、布局会发生变化。

(4)Web MVC 错误消息提示
国际化场景下访问一些链接、API,会有一些文字性的描述,可能会存在国际化的提示。

二、Java 国际化标准实现

核心接口:

  • 抽象实现 - java.util.ResourceBundle
  • Properties 资源实现 - java.util.PropertyResourceBundle
  • 例举实现 - java.util.ListResourceBundle

ResourceBundle 核心特性:

  • Key-Value 设计
  • 层次性设计
  • 缓存设计
  • 字符编码控制 - java.util.ResourceBundle.Control(@since 1.6)
  • Control SPI 扩展 - java.util.spi.ResourceBundleControlProvider(@since 1.8)

1、Java文本格式化

核心接口:java.text.MessageFormat(非线程安全)

基本用法:

  • 设置消息格式模式- new MessageFormat(…)
  • 格式化 - format(new Object[]{…})

消息格式模式:

  • 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))}
  • FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一
  • FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、percent

高级特性:

  • 重置消息格式模式
  • 重置 java.util.Locale
  • 重置 java.text.Format

import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * {@link MessageFormat} 示例
 * @see MessageFormat
 */
public class MessageFormatDemo {

    public static void main(String[] args) {

        int planet = 7;
        String event = "a disturbance in the Force";

        // {0} 、 {1} 等代表占位符
        String messageFormatPattern = "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.";
        MessageFormat messageFormat = new MessageFormat(messageFormatPattern);
        String result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);

        // 也可以使用静态方法,其实也是用的new方式创建的
        String formatResult = MessageFormat.format(messageFormatPattern, new Object[]{planet, new Date(), event});

        // 重置 MessageFormatPattern
        // applyPattern
        messageFormatPattern = "This is a text : {0}, {1}, {2}";
        messageFormat.applyPattern(messageFormatPattern);
        result = messageFormat.format(new Object[]{"Hello,World", "666"});
        System.out.println(result);

        // 重置 Locale
        messageFormat.setLocale(Locale.ENGLISH);
        messageFormatPattern = "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.";
        messageFormat.applyPattern(messageFormatPattern);
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);

        // 重置 Format,Format类即可
        // 根据参数索引来设置 Pattern
        messageFormat.setFormat(1,new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"));
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);
    }
}

MessageFormat类的doc注释中有着大量的实例,这里就不一一列举了。

三、Spring 国际化接口

核心接口:org.springframework.context.MessageSource
主要概念:文案模板编码(code)、文案模板参数(args)、区域(Locale)

Locale类主要是靠语言、国家、语言变种的方式来定位。

MessageSource接口有两个开箱即用的实现(与java国际化类ResourceBundle密不可分):
org.springframework.context.support.ResourceBundleMessageSource, org.springframework.context.support.ReloadableResourceBundleMessageSource

1、层次性MessageSource

Spring 层次性接口:

  • org.springframework.beans.factory.HierarchicalBeanFactory
  • org.springframework.context.ApplicationContext
  • org.springframework.beans.factory.config.BeanDefinition

有关Spring层次性接口更多详情请看(3、层次性依赖查找(接口 - HierarchicalBeanFactory)):
spring依赖查找、依赖注入深入学习及源码分析

Spring 层次性国际化接口:

  • org.springframework.context.HierarchicalMessageSource

HierarchicalMessageSource接口继承了MessageSource接口,增加了对parent的操作。

2、MessageSource 开箱即用实现

ResourceBundleMessageSource

基于 ResourceBundle + MessageFormat 组合 MessageSource 实现:
org.springframework.context.support.ResourceBundleMessageSource

关键方法源码分析:

// org.springframework.context.support.AbstractMessageSource#getMessage(java.lang.String, java.lang.Object[], java.lang.String, java.util.Locale)
@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable 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);
}
// org.springframework.context.support.AbstractMessageSource#getMessageInternal
@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
	if (code == null) {
		return null;
	}
	if (locale == null) {
		locale = Locale.getDefault();
	}
	Object[] argsToUse = args;

	if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
		// Optimized resolution: no arguments to apply,
		// therefore no MessageFormat needs to be involved.
		// Note that the default implementation still uses MessageFormat;
		// this can be overridden in specific subclasses.
		String message = resolveCodeWithoutArguments(code, locale);
		if (message != null) {
			return message;
		}
	}

	else {
		// Resolve arguments eagerly, for the case where the message
		// is defined in a parent MessageSource but resolvable arguments
		// are defined in the child MessageSource.
		argsToUse = resolveArguments(args, locale);
		// 通过code关联模板,然后通过java的MessageFormat 进行翻译
		MessageFormat messageFormat = resolveCode(code, locale);
		if (messageFormat != null) {
			synchronized (messageFormat) {
				return messageFormat.format(argsToUse);
			}
		}
	}

	// Check locale-independent common messages for the given message code.
	Properties commonMessages = getCommonMessages();
	if (commonMessages != null) {
		String commonMessage = commonMessages.getProperty(code);
		if (commonMessage != null) {
			return formatMessage(commonMessage, args, locale);
		}
	}

	// Not found -> check parent, if any.
	return getMessageFromParent(code, argsToUse, locale);
}

此时resolveCode方法就是在ResourceBundleMessageSource实现的:

// org.springframework.context.support.ResourceBundleMessageSource#resolveCode
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
	Set<String> basenames = getBasenameSet();// 使用LinkedHashSet缓存
	for (String basename : basenames) {
		ResourceBundle bundle = getResourceBundle(basename, locale);
		if (bundle != null) {
			//使用ConcurrentHashMap缓存,只读MessageFormat,不可以set
			MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
			if (messageFormat != null) {
				return messageFormat;
			}
		}
	}
	return null;
}

ReloadableResourceBundleMessageSource

可重载 Properties + MessageFormat 组合 MessageSource 实现:
org.springframework.context.support.ReloadableResourceBundleMessageSource

ReloadableResourceBundleMessageSource的getMessage同样是父类AbstractMessageSource处理的,与ResourceBundleMessageSource处理逻辑相同,只是在resolveCode方法中有所不同。

// org.springframework.context.support.ReloadableResourceBundleMessageSource#resolveCode
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
	if (getCacheMillis() < 0) {
		PropertiesHolder propHolder = getMergedProperties(locale);
		MessageFormat result = propHolder.getMessageFormat(code, locale);
		if (result != null) {
			return result;
		}
	}
	else {
		for (String basename : getBasenameSet()) {
			List<String> filenames = calculateAllFilenames(basename, locale);
			for (String filename : filenames) {
				PropertiesHolder propHolder = getProperties(filename);
				MessageFormat result = propHolder.getMessageFormat(code, locale);
				if (result != null) {
					return result;
				}
			}
		}
	}
	return null;
}

ReloadableResourceBundleMessageSource加载配置文件有个重新加载的功能,判断文件的上次修改时间是否有变化:
(这个实现其实没多大用处,1、配置文件通常都在classPath下,通常不会变;2、lastModified不一定全部存在,有可能全部返回-1)

// org.springframework.context.support.ReloadableResourceBundleMessageSource#refreshProperties
protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) {
	long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());

	Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
	if (!resource.exists()) {
		resource = this.resourceLoader.getResource(filename + XML_SUFFIX);
	}

	if (resource.exists()) {
		long fileTimestamp = -1;
		if (getCacheMillis() >= 0) {
			// Last-modified timestamp of file will just be read if caching with timeout.
			try {
				fileTimestamp = resource.lastModified();
				if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
					if (logger.isDebugEnabled()) {
						logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
					}
					propHolder.setRefreshTimestamp(refreshTimestamp);
					return propHolder;
				}
			}
			catch (IOException ex) {
				// Probably a class path resource: cache it forever.
				if (logger.isDebugEnabled()) {
					logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex);
				}
				fileTimestamp = -1;
			}
		}
		try {
			Properties props = loadProperties(resource, filename);
			propHolder = new PropertiesHolder(props, fileTimestamp);
		}
		catch (IOException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex);
			}
			// Empty holder representing "not valid".
			propHolder = new PropertiesHolder();
		}
	}

	else {
		// Resource does not exist.
		if (logger.isDebugEnabled()) {
			logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");
		}
		// Empty holder representing "not found".
		propHolder = new PropertiesHolder();
	}

	propHolder.setRefreshTimestamp(refreshTimestamp);
	this.cachedProperties.put(filename, propHolder);
	return propHolder;
}

3、MessageSource 內建依赖

MessageSource 內建 Bean 可能来源:

  • 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean(Springboot启动时已经注册了)
  • 默认內建实现 - DelegatingMessageSource:层次性查找 MessageSource 对象

源码分析

IOC容器启动时,会调用initMessageSource方法初始化。
关于IOC容器启动流程请移步:
spring系列-注解驱动原理及源码-spring容器创建流程

// org.springframework.context.support.AbstractApplicationContext#initMessageSource
protected void initMessageSource() {
	ConfigurableListableBeanFactory beanFactory = getBeanFactory();
	if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { // 只在当前beanFactory找,并不在parent找
		this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
		// Make MessageSource aware of parent MessageSource.
		if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
			HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
			if (hms.getParentMessageSource() == null) {
				// Only set parent context as parent MessageSource if no parent MessageSource
				// registered already.
				hms.setParentMessageSource(getInternalParentMessageSource());
			}
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Using MessageSource [" + this.messageSource + "]");
		}
	}
	else { // 如果找不到MessageSource,新建一个
		// Use empty MessageSource to be able to accept getMessage calls.
		DelegatingMessageSource dms = new DelegatingMessageSource();
		dms.setParentMessageSource(getInternalParentMessageSource()); // paremt
		this.messageSource = dms;
		beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); // 注册singleton Bean
		if (logger.isTraceEnabled()) {
			logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
		}
	}
}

4、Spring Boot 为什么要新建 MessageSource Bean?

  • AbstractApplicationContext 的实现决定了 MessageSource 內建实现。
  • Spring Boot 通过外部化配置简化 MessageSource Bean 构建。
  • Spring Boot 基于 Bean Validation 校验非常普遍

Springboot通过MessageSourceAutoConfiguration自动化装配MessageSource,通过外部化配置ResourceBundleMessageSource的方式创建MessageSource。

我们也可以自定义MessageSource来替换默认自动装配的MessageSource:


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

/**
 * Spring Boot 场景下自定义 {@link MessageSource} Bean
 *
 * @see MessageSource
 * @see MessageSourceAutoConfiguration
 * @see ReloadableResourceBundleMessageSource
 */
@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo { // @Configuration Class


    /**
     * 在 Spring Boot 场景中,Primary Configuration Sources(Classes) 高于 *AutoConfiguration
     */
    @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
    public MessageSource messageSource() {
        return new ReloadableResourceBundleMessageSource();
    }

    public static void main(String[] args) {

        ConfigurableApplicationContext applicationContext =
                // Primary Configuration Class
                new SpringApplicationBuilder(CustomizedMessageSourceBeanDemo.class)
                        .web(WebApplicationType.NONE)
                        .run(args);

        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();

        if (beanFactory.containsBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)) {
            // 查找 MessageSource 的 BeanDefinition
            System.out.println(beanFactory.getBeanDefinition(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME));
            // 查找 MessageSource Bean
            MessageSource messageSource = applicationContext.getBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
            System.out.println(messageSource);
        }

        // 关闭应用上下文
        applicationContext.close();
    }
}

注意:需要在resources目录下创建文件messages.properties。

5、实现配置自动更新 MessageSource


import org.springframework.context.MessageSource;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.*;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

/**
 * 动态(更新)资源 {@link MessageSource} 实现
 * <p>
 * 实现步骤:
 * <p>
 * 1. 定位资源位置( Properties 文件)
 * 2. 初始化 Properties 对象
 * 3. 实现 AbstractMessageSource#resolveCode 方法
 * 4. 监听资源文件(Java NIO 2 WatchService)
 * 5. 使用线程池处理文件变化
 * 6. 重新装载 Properties 对象
 *
 * @see MessageSource
 * @see AbstractMessageSource
 * @since
 */
public class DynamicResourceMessageSource extends AbstractMessageSource implements ResourceLoaderAware {

    private static final String resourceFileName = "msg.properties";

    private static final String resourcePath = "/META-INF/" + resourceFileName;

    private static final String ENCODING = "UTF-8";

    private final Resource messagePropertiesResource;

    private final Properties messageProperties;

    private final ExecutorService executorService;

    private ResourceLoader resourceLoader;


    public DynamicResourceMessageSource() {
        this.messagePropertiesResource = getMessagePropertiesResource();
        this.messageProperties = loadMessageProperties();
        this.executorService = Executors.newSingleThreadExecutor();
        // 监听资源文件(Java NIO 2 WatchService)
        onMessagePropertiesChanged();
    }

    private void onMessagePropertiesChanged() {
        if (this.messagePropertiesResource.isFile()) { // 判断是否为文件
            // 获取对应文件系统中的文件
            try {
                File messagePropertiesFile = this.messagePropertiesResource.getFile();
                Path messagePropertiesFilePath = messagePropertiesFile.toPath();
                // 获取当前 OS 文件系统
                FileSystem fileSystem = FileSystems.getDefault();
                // 新建 WatchService
                WatchService watchService = fileSystem.newWatchService();
                // 获取资源文件所在的目录
                Path dirPath = messagePropertiesFilePath.getParent();
                // 注册 WatchService 到 dirPath,并且关心修改事件
                dirPath.register(watchService, ENTRY_MODIFY);
                // 处理资源文件变化(异步)
                processMessagePropertiesChanged(watchService);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 处理资源文件变化(异步)
     *
     * @param watchService
     */
    private void processMessagePropertiesChanged(WatchService watchService) {
        executorService.submit(() -> {
            while (true) {
                WatchKey watchKey = watchService.take(); // take 发生阻塞
                // watchKey 是否有效
                try {
                    if (watchKey.isValid()) {
                        for (WatchEvent event : watchKey.pollEvents()) {
                            Watchable watchable = watchKey.watchable();
                            // 目录路径(监听的注册目录)
                            Path dirPath = (Path) watchable;
                            // 事件所关联的对象即注册目录的子文件(或子目录)
                            // 事件发生源是相对路径
                            Path fileRelativePath = (Path) event.context();
                            if (resourceFileName.equals(fileRelativePath.getFileName().toString())) {
                                // 处理为绝对路径
                                Path filePath = dirPath.resolve(fileRelativePath);
                                File file = filePath.toFile();
                                Properties properties = loadMessageProperties(new FileReader(file));
                                synchronized (messageProperties) {
                                    messageProperties.clear();
                                    messageProperties.putAll(properties);
                                }
                            }
                        }
                    }
                } finally {
                    if (watchKey != null) {
                        watchKey.reset(); // 重置 WatchKey
                    }
                }

            }
        });
    }

    private Properties loadMessageProperties() {
        EncodedResource encodedResource = new EncodedResource(this.messagePropertiesResource, ENCODING);
        try {
            return loadMessageProperties(encodedResource.getReader());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Properties loadMessageProperties(Reader reader) {
        Properties properties = new Properties();
        try {
            properties.load(reader);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return properties;
    }

    private Resource getMessagePropertiesResource() {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(resourcePath);
        return resource;
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        String messageFormatPattern = messageProperties.getProperty(code);
        if (StringUtils.hasText(messageFormatPattern)) {
            return new MessageFormat(messageFormatPattern, locale);
        }
        return null;
    }

    private ResourceLoader getResourceLoader() {
        return this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader();
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    public static void main(String[] args) throws InterruptedException {
        DynamicResourceMessageSource messageSource = new DynamicResourceMessageSource();
        for (int i = 0; i < 10000; i++) {
            String message = messageSource.getMessage("name", new Object[]{}, Locale.getDefault());
            System.out.println(message);
            Thread.sleep(1000L);
        }
    }
}

参考资料

极客时间-《小马哥讲 Spring 核心编程思想》

相关文章:

  • 自己做网站公司/推广软件有哪些
  • 电子商务网站环境建设/关键词搜索站长工具
  • 四川手机网上营业厅/站长之家seo查找
  • 株洲市建设局官方网站/全球热门网站排名
  • wordpress 后台添加文章 没编辑功能/只需要手机号的广告
  • iis 5 如何添加网站/公司推广方法有哪些
  • 解决Windows Server远程断开后自动锁屏问题
  • 系分 - 案例分析 - 系统设计
  • 基于有向图的邻接矩阵计算其割点、割边、压缩图,并用networkx可视化绘制
  • 【进阶】Spring更简单的读取和存储对象
  • C++内存分配方法new与placement new使用方法详解
  • [ACTF2020 新生赛]BackupFile
  • 自动化测试 | 这些常用测试平台,你们公司在用的是哪些呢?
  • Android大厂面试100题,涵盖测试技术、环境搭建、人力资源
  • 【QT5 实现“上图下文”,带图标的按键样式-toolbutton-学习笔记-记录-基础样例】实现方式之一
  • Android 9.0系统源码_SystemUI(八)PhoneWindow更新状态栏和导航栏背景颜色的流程解析
  • ElementUI中树形表格下拉卡死的问题解决
  • AppScan扫描报告