参考资料

  1. springboot项目实现数据库读取国际化配置
  2. 路人甲博客-国际化详解
  3. Spring源码分析-MessageSource
  4. Spring Boot 架构中的国际化支持实践—— Spring Boot 全球化解决方案


一. 前期准备

1.1 国际化信息

⏹messages_zh.properties

1001E=请输入{msgArgs}。
1005E=请输入半角数字。

⏹messages_ja.properties

1001E={msgArgs}を入力してください。
1005E=半角数字を入力してください。

在这里插入图片描述

⏹数据库
在这里插入图片描述

1.2 自定义校验注解

⏹校验是否为空

import javax.validation.Constraint;
import javax.validation.constraints.NotEmpty;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.*;

@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@NotEmpty
@ReportAsSingleViolation
public @interface ValidateNotEmpty {

    String msgArgs() default "";

	String message() default "{1001E}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

⏹校验是否为半角数字

import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.lang.annotation.*;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
@Constraint(validatedBy = {})
@ConstraintComposition(CompositionType.AND)
@NotEmpty
@Pattern(regexp = "[0-9]*")
@ReportAsSingleViolation
// 标记该注解是否可重复使用
@Repeatable(ValidateHalfNumeric.List.class)
public @interface ValidateHalfNumeric {

	String message() default "{1005E}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

    @Target({ FIELD })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        ValidateHalfNumeric[] value();
    }
}

1.3 待校验form

import lombok.Data;

@Data
public class Test9Form {

    @ValidateNotEmpty(msgArgs = "id")
    private String id;

    @ValidateHalfNumeric
    private String age;
}

1.4 数据库查询接口

import java.util.List;
import com.example.jmw.entity.I18MessageEnttiy;

public interface I18nMessageMapper {
	
	// 查询所有的国际化消息,封装到自定义的I18MessageEnttiy中
    List<I18MessageEnttiy> getAllLocaleMessage();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.jmw.mapper.I18nMessageMapper">
    <select id="getAllLocaleMessage" resultType="com.example.jmw.entity.I18MessageEnttiy">
        SELECT
          code
          , locale
          , item
        FROM
          i18message
    </select>
</mapper>

1.5 前台html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
    <script type="text/javascript" th:src="@{/js/common/common.js}"></script>
    <title>test9页面</title>
</head>
<body>
    <button id="btn1">发送请求,进行校验</button>

    <!-- 国际化语言展示区域 -->
    <div>[[#{M004}]]</div>
    <div>[[#{M002}]]</div>
    <div>[[#{M003}]]</div>
    <hr>

    <button id="reloadMessage">重新加载Message</button>
</body>
<script>
    $("#btn1").click(() => {

        const param = {
            id: null,
            age: "测试年龄"
        };
		
		// 读取url中的参数,指定当前页面的校验语言
        const languageParam = new URL(window.location.href).searchParams.get("language");
        const url = `http://localhost:8080/test9/validate?language=${languageParam}`;
        doAjax(url, param, function(data) { });
    });

    // 当数据库修改数据之后,需要手动触发 重新加载国际化消息方法
    $("#reloadMessage").click(() => {
        const url = `http://localhost:8080/test9/reloadMessage`;
        doAjax(url, null, function(data) {});
    });
</script>
</html>

二. 国际化相关配置

2.1 指定i18n国际化文件路径

spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8

2.2 自定义MessageSource类整合国际化消息

要点

  • ResourceBundle.getBundle() 读取国际化文件消息
  • Collectors.groupingBy() 分组
  • Collectors.toMap() 收集为Map
  • LocaleContextHolder.getLocale() 获取设置的当前地区Locale
import com.example.jmw.entity.I18MessageEnttiy;
import com.example.jmw.mapper.I18nMessageMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

// @Component("messageSource"): 也可以在此处指明bean的名称为 messageSource
public class CustomMessageSource extends AbstractMessageSource implements InitializingBean {

    // 这个是用来缓存数据库中获取到的配置的 数据库配置更改的时候可以调用reload方法重新加载
    private static final Map<String, Map<String, String>> LOCAL_CACHE = new ConcurrentHashMap<>();
	
	// 注入查询接口对象
    @Resource
    private I18nMessageMapper i18nMessageMapper;

    // 程序启动之后,会自动加载
    @Override
    public void afterPropertiesSet() {
        this.reload();
    }

    // 重新加载消息到该类的Map缓存中
    public void reload() {

        // 清除该类的缓存
        LOCAL_CACHE.clear();

        // 加载所有的国际化资源
        Map<String, Map<String, String>> localeMsgMap = this.loadAllMessageResources();
        LOCAL_CACHE.putAll(localeMsgMap);
    }

    // 加载所有的国际化消息资源
    private Map<String, Map<String, String>> loadAllMessageResources() {

        // 从数据库中查询所有的国际化资源
        List<I18MessageEnttiy> allLocaleMessage = i18nMessageMapper.getAllLocaleMessage();
        if (ObjectUtils.isEmpty(allLocaleMessage)) {
            allLocaleMessage = new ArrayList<>();
        }

        // 将查询到的国际化资源转换为 Map<地区码, Map<code, 信息>> 的数据格式
        Map<String, Map<String, String>> localeMsgMap = allLocaleMessage
                // stream流
                .stream()
                // 分组
                .collect(Collectors.groupingBy(
                        // 根据国家地区分组
                        I18MessageEnttiy::getLocale,
                        // 收集为Map,key为code,value为信息
                        Collectors.toMap(
                                I18MessageEnttiy::getCode
                                , I18MessageEnttiy::getItem
                        )
                ));

        // 获取国家地区List
        List<Locale> localeList = localeMsgMap.keySet().stream().map(Locale::new).collect(Collectors.toList());
        for (Locale locale : localeList) {

            // 按照国家地区来读取本地的国际化资源文件,我们的国际化资源文件放在i18n文件夹之下
            ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n/messages", locale);
            // 获取国际化资源文件中的key和value
            Set<String> keySet = resourceBundle.keySet();

            // 将 code=信息 格式的数据收集为 Map<code,信息> 的格式
            Map<String, String> msgFromFileMap = keySet.stream()
                    .collect(
                        Collectors.toMap(
                            Function.identity(),
                            resourceBundle::getString
                        )
                    );

            // 将本地的国际化信息和数据库中的国际化信息合并
            Map<String, String> localeFileMsgMap = localeMsgMap.get(locale.getLanguage());
            localeFileMsgMap.putAll(msgFromFileMap);
            localeMsgMap.put(locale.getLanguage(), localeFileMsgMap);
        }

        return localeMsgMap;
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {

        String msg = this.getSourceFromCacheMap(code, locale);
        return new MessageFormat(msg, locale);
    }

    @Override
    protected String resolveCodeWithoutArguments(String code, Locale locale) {
        return this.getSourceFromCacheMap(code, locale);
    }

    // 缓存Map中加载国际化资源
    private String getSourceFromCacheMap(String code, Locale locale) {

        String language = ObjectUtils.isEmpty(locale)
                ? LocaleContextHolder.getLocale().getLanguage() : locale.getLanguage();

        // 获取缓存中对应语言的所有数据项
        Map<String, String> propMap = LOCAL_CACHE.get(language);
        if (!ObjectUtils.isEmpty(propMap) && propMap.containsKey(code)) {
            // 如果对应语言中能匹配到数据项,那么直接返回
            return propMap.get(code);
        }

        // 如果找不到国际化消息,就直接返回code
        return code;
    }
}

2.3 国际化配置类

要点

  • 自定义MessageSource类要交给Spring管理,bean名称一定要叫messageSource
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@Configuration
public class InternationalConfig implements WebMvcConfigurer {

    // 默认解析器,用来设置当前会话默认的国际化语言
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        // 指定当前项目的默认语言是中文
        sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return sessionLocaleResolver;
    }

    // 默认拦截器,用来指定切换国际化语言的参数名
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {

        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        /*
            设置国际化请求参数为language
            设置完成之后,URL中的 ?language=zh 表示读取国际化文件messages_zh.properties
         */
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }

    /**
     * Spring在启动的时候,加载上下文的时候,会查询查询是否存在容器名称为messageSource的bean
     * 如果没有就会创建一个名为messageSource的bean,然后放在上下文中
     * 我们手动创建一个名为messageSource的bean,替代Spring为我们自动创建
     */
    @Bean
    public MessageSource messageSource() {
        return new CustomMessageSource();
    }

    // 将我们自定义的国际化语言参数拦截器放入Spring MVC的默认配置中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

三. 校验

3.1 校验Controller

import com.example.jmw.common.config.CustomMessageSource;
import com.example.jmw.form.Test9Form;
import org.springframework.stereotype.Controller;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import java.util.Set;

@Controller
@RequestMapping("/test9")
public class Test9Controller {
    
    // 注入校验validator
    @Resource
    private LocalValidatorFactoryBean validator;
    
    // 注入自定义MessageSource
    @Resource
    private CustomMessageSource customMessageSource;

    @GetMapping("/init")
    public ModelAndView init() {

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("test9");
        return  modelAndView;
    }

    @PostMapping("/validate")
    @ResponseBody
    public void validate(@RequestBody Test9Form form) {

        Set<ConstraintViolation<Test9Form>> validateSet = validator.validate(form);
        for (ConstraintViolation<Test9Form> violation : validateSet) {
            System.out.println(violation.getMessage());
        }
    }
    
    // 重新加载国际化消息
    @PostMapping("/reloadMessage")
    @ResponseBody
    public void reloadMessage() {

        customMessageSource.reload();
    }
}

3.2 效果

✅✅✅进入国际化页面后,点击校验按钮
在这里插入图片描述
在这里插入图片描述

✅✅✅切换语言为日语,然后点击校验按钮
在这里插入图片描述
在这里插入图片描述
✅✅✅修改数据库数据后,点击重新加载Message按钮后,刷新页面
可以看到,我们修改的数据,已经被反映到页面上
在这里插入图片描述
在这里插入图片描述
✅✅✅将 M004 ja 这条数据删除(修改为M005,就相当于删除)之后,点击重新加载Message按钮后,刷新页面.可以看到只有code显示在页面上.
在这里插入图片描述
在这里插入图片描述

3.3 StaticMessageSource说明

  • 在前面的文章中,我们自定义了一个CustomMessageSource 类继承了AbstractMessageSource 类实现了InitializingBean接口,从而实现了从数据库获取到的国际化消息和本地properties文件中的国际化消息整合的功能。
  • StaticMessageSource类也能实现同样的功能,但是不推荐在生产环境中使用,并且不支持国际化消息删除
  • StaticMessageSource适合国际化消息测试,支持硬编码的方法添加国际化消息
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.StaticMessageSource;
import java.util.Locale;

@Component("messageSource")
public class CustomMessageSource extends StaticMessageSource implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        
        // addMessage为 StaticMessageSource 父类中的方法
        this.addMessage("key", Locale.CHINA, "从数据库查询来的信息");
    }
}

StaticMessageSource的源码截图如下:

在这里插入图片描述
源码的注释也提到了
Intended for testing rather than for use in production systems.
翻译为中文就是用于测试而不是用于生产系统.
并且所有的国际化消息最终都会缓存到messageMap中.
由于StaticMessageSource并没有提供清除map数据的方法,
因此只有当程序重启,数据库删除的国际化消息才能被反映到messageMap中.

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐