Spring 国际化

Spring 国际化使用场景

• 普通国际化文案
• Bean Validation 校验国际化文案
• Web 站点页面渲染
• Web MVC 错误消息提示

Spring 国际化接口

• 核心接口

  • org.springframework.context.MessageSource

• 主要概念

  • 文案模板编码(code)
  • 文案模板参数(args)
  • 区域(Locale)

层次性 MessageSource

• Spring 层次性接口回顾

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

• Spring 层次性国际化接口

  • org.springframework.context.HierarchicalMessageSource

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)

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
/**
 * {@link MessageFormat} 示例
 */
public class MessageFormatDemo {

    public static void main(String[] args) {

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

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

        // 重置 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
        // 根据参数索引来设置 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);
    }
}

MessageSource 开箱即用实现

• 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现

  • org.springframework.context.support.ResourceBundleMessageSource

• 可重载 Properties + MessageFormat 组合 MessageSource 实现

  • org.springframework.context.support.ReloadableResourceBundleMessageSource

MessageSource 內建依赖

• MessageSource 內建 Bean 可能来源

  • 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean
  • 默认內建实现 - DelegatingMessageSource
    • 层次性查找 MessageSource 对象

课外资料

• Spring Boot 为什么要新建 MessageSource Bean?

  • AbstractApplicationContext 的实现决定了 MessageSource 內建实现
  • Spring Boot 通过外部化配置简化 MessageSource Bean 构建
  • Spring Boot 基于 Bean Validation 校验非常普遍
/**
 * Spring Boot 场景下自定义 {@link MessageSource} Bean
 */
@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo { // @Configuration Class

    /**
     * 在 Spring Boot 场景中,Primary Configuration Sources(Classes) 高于 *AutoConfiguration
     * @return
     */
    @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();
    }
}

在这里插入图片描述

面试题

Spring 国际化接口有哪些?
答:
• 核心接口 - MessageSource
• 层次性接口 - org.springframework.context.HierarchicalMessageSource

Spring 有哪些 MessageSource 內建实现?
答:
• org.springframework.context.support.ResourceBundleMessageSource
• org.springframework.context.support.ReloadableResourceBundleMessageSource
• org.springframework.context.support.StaticMessageSource
• org.springframework.context.support.DelegatingMessageSource

如何实现配置自动更新 MessageSource?
答:
主要技术
• Java NIO 2:java.nio.file.WatchService
• Java Concurrency : java.util.concurrent.ExecutorService
• Spring:org.springframework.context.support.AbstractMessageSource

/**
 * 动态(更新)资源 {@link MessageSource} 实现
 * <p>
 * 实现步骤:
 * <p>
 * 1. 定位资源位置( Properties 文件)
 * 2. 初始化 Properties 对象
 * 3. 实现 AbstractMessageSource#resolveCode 方法
 * 4. 监听资源文件(Java NIO 2 WatchService)
 * 5. 使用线程池处理文件变化
 * 6. 重新装载 Properties 对象
 */
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);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值