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

一、概述

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

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

1、使用场景

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

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

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

4Web 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);

        // 重置 FormatFormat类即可
        // 根据参数索引来设置 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 BeanSpringboot启动时已经注册了
  • 默认內建实现 - 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 核心编程思想》

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring

“Spring国际化详解,Spring国家化实例及源码详解” 的相关文章