1.spring为java项目提供国际化支持

message_zh_CN.properties(中文简体)
message_en_US.properties(美国英文)
等等中配置key=value
key可以是错误提示信息,页面显示信息等等。

使用配置的配置
@Autowired
private MessageSource messageSource;

messageSource.getMessage(key,null,"default value",Locale.getDefault()); //如果没有配置,会返回默认值
messageSource.getMessage(key,null,Locale.getDefault()); //如果没有配置会抛出异常

String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;

2.上述使用缺点

上述国际化使用适合做小型的web项目,配置key不多,或者项目的使用国家或地区很少。
如果配置项非常多的话,那么properties文件将会非常大,而且配置项非常多。如果支持全世界所有国家和地区时,那么就要有许多个properties文件,能够使用,但相对比较麻烦。
针对上述缺点,能否将上述的配置key及value写入数据中了?

3.具体解决方案

在数据库中定义表,表中基础字段国家或地区代表符号,如zh_CN(简体中文),en_US(美国英语)等,key及value,然后再增加一个basename,这个字段是参考spring源码做的,下面详细讲解。

创建sql

CREATE TABLE t_messages  (
  `basename` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `locale_lang` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `k` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `v` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ;

basename基础文件目录
locale_lan国家或地区代表符号
k key值
v value值

a方案
对待该表就想对待普通的业务表,针对该表建立MessageMapper文件,创建针对该表的增删改查方法,在service层控制事务管理及缓存等等,当任何一个controller需要国际化返回结果的时候,就可以通过调MessageService方法来得到对应的value.

此处mapper里面的方法仅仅是举例,详细的可根据项目需求自己修改
@Mapper
public interface MessageMapper {

    String columnList = " basename,locale_lan,k,v ";

    @Insert("insert into t_message(" +columnList+") values(#{basename},#{locale_lan},#{k},#{v})")
    boolean insertMessage(Message message);

    @Select("select " + columnList + "from t_message where basename = #{basename} and locale_lan = #{language}")
    AddressEntity getMessage(String basename,String language);
}

b方案
a方案是一种实现方法,但是spring既然已经实现properties文件的具体用法,对spring进行二次开发,开发适合自己项目的业务,岂不是更好吗?

spring 实现国际化源码简单分析

MessageSource接口

public interface MessageSource {
    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

HierarchicalMessageSource接口
其继承了MessageSource接口,增加了父MessageSource的设置方法
public interface HierarchicalMessageSource extends MessageSource {
    void setParentMessageSource(MessageSource parent);
    MessageSource getParentMessageSource();
}

DelegatingMessageSource类
该类主要是委派父MessageSource处理getMessage();
public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource;


    @Override
    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
        if (this.parentMessageSource != null) {
            return  **this.parentMessageSource.getMessage(code, args, defaultMessage, locale);**
        }
        else {
            **return renderDefaultMessage(defaultMessage, args, locale);**
        }
    }

    @Override
    public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
        if (this.parentMessageSource != null) {
            **return this.parentMessageSource.getMessage(code, args, locale);**
        }
        else {
            throw new NoSuchMessageException(code, locale);
        }
    }

    @Override
    public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
        if (this.parentMessageSource != null) {
            **return this.parentMessageSource.getMessage(resolvable, locale);**
        }
        else {
            if (resolvable.getDefaultMessage() != null) {
                **return renderDefaultMessage(resolvable.getDefaultMessage(), resolvable.getArguments(), locale);**
            }
            String[] codes = resolvable.getCodes();
            String code = (codes != null && codes.length > 0 ? codes[0] : null);
            throw new NoSuchMessageException(code, locale);
        }
    }

MessageSourceSupport 该类为获取字符串后的格式化处理的类

private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage = new HashMap();
格式化存储的结构类型,大Map的key为basename,嵌套的Map,key为Locale,value为MessageFormat,不同地区可以设置不同的格式化样式

核心方法:return messageFormat.format(this.resolveArguments(code, locale));

this.resolveArguments(code, locale)方法获取对应的value

AbstractMessageSource为MessageSource接口的核心实现类
核心方法的实现方法都是final的,不允许重写

public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) ;
public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;

AbstractResourceBasedMessageSource继承了AbstractMessageSource类

AbstractResourceBasedMessageSource的实现类有
1.ResourceBundleMessageSource
2.ReloadableResourceBundleMessageSource
3.ReloadableResourceBundleMessageSource

关注的主要是ResourceBundleMessageSource类
当调用ResourceBundleMessageSource 对象的getMessage()方法时候
其会调用其的protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
    return ResourceBundle.getBundle(basename, locale, this.getBundleClassLoader(), new ResourceBundleMessageSource.MessageSourceControl());
}方法,所以重写该方法可以实现自我配置的类。

b方案具体实现,具体的查库方法没有贴出来

public class DatabaseAndResourceBundleMessageSource
extends ResourceBundleMessageSource {

    private static final Logger log = LoggerFactory.getLogger(DatabaseAndResourceBundleMessageSource2.class);

    @Autowired
    DataSource dataSource;

    @Autowired
    MessageDBHandler databaseMessageHandler;

    @Override
    protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
        try {
            ResourceBundle delegate = super.doGetBundle(basename, locale);//父类MessageSource实现
            return new ResourceBundle() {
                @Override
                protected Object handleGetObject(String key) {
                    return Optional.<Object>ofNullable(databaseMessageHandler.loadMessageFromDb(basename, locale.getLanguage(), key)).orElseGet(() -> delegate.getObject(key)); //依据key获取value
                }

                @Override
                public Enumeration<String> getKeys() {
                    log.info("getKeys");
                    Set<String> l = databaseMessageHandler.loadKeysFromDb(basename, locale.getLanguage());
                    l.addAll(delegate.keySet());
                    return Collections.enumeration(l);
                }
            };
        } catch (MissingResourceException e) {
            return new ResourceBundle() {
                @Override
                protected Object handleGetObject(String key) {
                    return databaseMessageHandler.loadMessageFromDb(basename, locale.getLanguage(), key);
                }

                @Override
                public Enumeration<String> getKeys() {
                    return Collections.enumeration(databaseMessageHandler.loadKeysFromDb(basename, locale.getLanguage())); //获取同一basename和language下的所有keys
                }
            };
        }
    }
}


@Configuration
public class MessageConfig {

    @Value("message.basename:app/notice,app/mail")
    private String[] baseNames;


    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Bean("messageSource")
    public MessageSource myMessageSource(){
        ResourceBundleMessageSource messageSource = new DatabaseAndResourceBundleMessageSource();
        messageSource.setBasenames(baseNames);
        return messageSource;
    }
}

上述描述了一大堆,不好理解,下面的方便理解
spring解析properties文件使用的是Map<String,Map<Locale,ResourceBundle>>数据结构,ResourceBundle类中有Set<String> set = new HashSet<>();
这个set就是封装properties中对应的key-value,在ResourceBundleMessageSource对象中doGetBundle()方法中就是获取ResourceBundle对象的,所以重写doGetBundle()方法,就可以将数据库中的key-value加入到已经存在的key-value中了。如果仅仅以语言进行区别,可以使用Locale.getDefault().getLanguage()即可,如果想以国家和地区来区分就可以使用Locale.getDefault().toString(),两者是有区别的。

03-05 22:29