• SpringBoot 同时从数据库和properties文件中读取国际化信息


    参考资料

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


    一. 前期准备

    1.1 国际化信息

    ⏹messages_zh.properties

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

    ⏹messages_ja.properties

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

    在这里插入图片描述

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

    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 {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ⏹校验是否为半角数字

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    1.3 待校验form

    import lombok.Data;
    
    @Data
    public class Test9Form {
    
        @ValidateNotEmpty(msgArgs = "id")
        private String id;
    
        @ValidateHalfNumeric
        private String age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1.4 数据库查询接口

    import java.util.List;
    import com.example.jmw.entity.I18MessageEnttiy;
    
    public interface I18nMessageMapper {
    	
    	// 查询所有的国际化消息,封装到自定义的I18MessageEnttiy中
        List<I18MessageEnttiy> getAllLocaleMessage();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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">重新加载Messagebutton>
    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    二. 国际化相关配置

    2.1 指定i18n国际化文件路径

    spring:
      messages:
        basename: i18n/messages
        encoding: UTF-8
    
    • 1
    • 2
    • 3
    • 4

    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> 的数据格式
            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 的格式
                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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    三. 校验

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    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, "从数据库查询来的信息");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    StaticMessageSource的源码截图如下:

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

  • 相关阅读:
    【从零开始的Java开发】1-6-4 Java输入输出流:File类、绝对路径和相对路径、字节流、缓冲流、字符流、对象序列化
    吃货联盟系统简单实现(对象+数组)
    Java复习五:抽象类+模板设计方法+接口+三种工厂模式
    美国 地区 苹果 Apple ID获取
    Windows:Docker安装
    [SpringBoot]SpringBoot概述(简介、官网构建、快速启动)
    中国之声广播在线收听,云听“正直播”在路上
    centos7修改root用户密码
    用户认证技术
    笔记本电脑配置知识大全
  • 原文地址:https://blog.csdn.net/feyehong/article/details/126416875