Spring入門到放棄篇(1)-國際化

Java原生國際化

文檔地址

java官方文檔html

參考官方文檔java

自定義國際化案例

public class LocaleDemo {
    public static void main(String[] args) {
        System.out.println(Locale.getDefault());
    }
}

獲取本地方言git

配置本地方言

  • 經過啓動參數-D命令配置github

    • 可是這種方式只適合本機
  • Locale.setDefault(Locale.US);

國際化數字

public class NumberFormatDemo {

  public static void main(String[] args) {
    NumberFormat numberFormat = NumberFormat.getNumberInstance();
    System.out.println(numberFormat.format(10000));//10,000

    numberFormat = NumberFormat.getNumberInstance(Locale.FRANCE);
    System.out.println(numberFormat.format(10000));//10 000
  }
}

經過不一樣的方言來決定數字的顯示方式web

ResourceBundle國際化

建立一個demo_zh_CN.properties在resources目錄spring

name=測試
world=你好,{0}
public class ResourceBundleDemo {
  public static final String BUNDLE_NAME = "demo";

  public static void main(String[] args) {
    getEn();
    getZhCn();
  }
  private static void getZhCn() {
    Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
    ResourceBundle demo2 = ResourceBundle.getBundle(BUNDLE_NAME);

    //由於當前沒有使用unicode來寫,默認是iso_8859_1,因此轉化,避免亂碼
    System.out.println(new String(demo2.getString("name").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
  }


  private static void getEn() {
    Locale.setDefault(Locale.ENGLISH);
    ResourceBundle demo = ResourceBundle.getBundle(BUNDLE_NAME);
    String test = demo.getString("name");
    System.out.println(test);
  }
}

​ 上述代碼中經過java.util.ResourceBundle來作國際化轉化,可是由於properties文件中的國際化內容默認採用的是ISO 8895-1因此只要出現的是中文就會亂碼。當前咱們使用的是經過字符串編解碼來轉化的。shell

國際化亂碼問題

​ 從上述案例中咱們能夠看到中文會亂碼。json

解決方式有如下三種:windows

  • 能夠採用jdk自帶的工具 native2ascii 方法,將打包後的資源文件進行轉移,而不是直接在源碼方面解決
  • 擴展 ResourceBundle.Controlapi

    • 缺點:可移植性不強,不得不顯示地傳遞
  • 實現 ResourceBundleControlProvider

jdk自帶的native2ascii

工具文檔地址

java支持的編碼

 native2ascii demo_zh_CN.properties demo_zh_CN_ascii.properties

轉化後文件內容以下

name=\u6d4b\u8bd5
world=\u4f60\u597d,{0}

擴展java.util.ResourceBundle.Control

java.util.ResourceBundle.Control#newBundle能夠看到java.util.ResourceBundle是從這裏生產出來的。

核心代碼以下

final String resourceName = toResourceName0(bundleName, "properties");
if (resourceName == null) {
  return bundle;
}
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream stream = null;
try {
  //權限檢查
  stream = AccessController.doPrivileged(
    new PrivilegedExceptionAction<InputStream>() {
      public InputStream run() throws IOException {
        InputStream is = null;
        if (reloadFlag) {
          URL url = classLoader.getResource(resourceName);
          if (url != null) {
            URLConnection connection = url.openConnection();
            if (connection != null) {
              // Disable caches to get fresh data for
              // reloading.
              connection.setUseCaches(false);
              is = connection.getInputStream();
            }
          }
        } else {
          is = classLoader.getResourceAsStream(resourceName);
        }
        return is;
      }
    });
} catch (PrivilegedActionException e) {
  throw (IOException) e.getException();
}
if (stream != null) {
  try {
    //把讀取到的流裝載到PropertyResourceBundle中
    bundle = new PropertyResourceBundle(stream);
  } finally {
    stream.close();
  }
}

java.util.PropertyResourceBundle#PropertyResourceBundle(java.io.InputStream)

public PropertyResourceBundle (InputStream stream) throws IOException {
  Properties properties = new Properties();
  properties.load(stream);
  lookup = new HashMap(properties);
}

image-20200408194407695

斷點查看,在Peroerties加載stream的時候出現了亂碼。

因此咱們能夠在獲取到流的時候,直接定義流的編碼就好了

因此照葫蘆畫瓢,修改代碼以下

public class EncodedControl extends ResourceBundle.Control {
  private final String encoding;

  public EncodedControl(String encoding) {
    this.encoding = encoding;
  }

  @Override
  public ResourceBundle newBundle(String baseName, Locale locale, String format,
                                  ClassLoader loader, boolean reload)
    throws IllegalAccessException, InstantiationException, IOException {
    String bundleName = toBundleName(baseName, locale);
    ResourceBundle bundle = null;
    if (format.equals("java.class")) {
      try {
        @SuppressWarnings("unchecked")
        Class<? extends ResourceBundle> bundleClass
          = (Class<? extends ResourceBundle>) loader.loadClass(bundleName);

        // If the class isn't a ResourceBundle subclass, throw a
        // ClassCastException.
        if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
          bundle = bundleClass.newInstance();
        } else {
          throw new ClassCastException(bundleClass.getName()
                                       + " cannot be cast to ResourceBundle");
        }
      } catch (ClassNotFoundException e) {
      }
    } else if (format.equals("java.properties")) {
      final String resourceName = toResourceName0(bundleName, "properties");
      if (resourceName == null) {
        return bundle;
      }
      final ClassLoader classLoader = loader;
      final boolean reloadFlag = reload;
      InputStream stream = null;
      try {
        stream = AccessController.doPrivileged(
          new PrivilegedExceptionAction<InputStream>() {
            @Override
            public InputStream run() throws IOException {
              InputStream is = null;
              if (reloadFlag) {
                URL url = classLoader.getResource(resourceName);
                if (url != null) {
                  URLConnection connection = url.openConnection();
                  if (connection != null) {
                    // Disable caches to get fresh data for
                    // reloading.
                    connection.setUseCaches(false);
                    is = connection.getInputStream();
                  }
                }
              } else {
                is = classLoader.getResourceAsStream(resourceName);
              }
              return is;
            }
          });
      } catch (PrivilegedActionException e) {
        throw (IOException) e.getException();
      }
      Reader reader = null;
      if (stream != null) {
        try {
          //增長轉碼
          reader = new InputStreamReader(stream, encoding);

          bundle = new PropertyResourceBundle(reader);
        } finally {
          reader.close();
          stream.close();
        }
      }
    } else {
      throw new IllegalArgumentException("unknown format: " + format);
    }
    return bundle;
  }

  private String toResourceName0(String bundleName, String suffix) {
    // application protocol check
    if (bundleName.contains("://")) {
      return null;
    } else {
      return toResourceName(bundleName, suffix);
    }
  }
}

修改代碼

/**
     * 基於 Java 1.6
     * 顯示地傳遞 EncodedControl
     */
    private static void extendControl() {
        Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
        ResourceBundle resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME, new EncodedControl("utf8"));
        System.out.println("resourceBundle.name : " + resourceBundle.getString("name"));
    }

測試,發現成功了。

可是這種方式可移植性不強,不得不顯示地傳遞 ResourceBundle.Control因此咱們採用下面這種方式

實現ResourceBundleControlProvider

static {
  List<ResourceBundleControlProvider> list = null;
  ServiceLoader<ResourceBundleControlProvider> serviceLoaders
    = ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);
  for (ResourceBundleControlProvider provider : serviceLoaders) {
    if (list == null) {
      list = new ArrayList<>();
    }
    list.add(provider);
  }
  providers = list;
}

這裏能夠看到,當咱們ResourceBundle初始化的時候會基於SPI自動加載provider,在java.util.ResourceBundle#getDefaultControl這裏能夠看到

private static Control getDefaultControl(String baseName) {
  if (providers != null) {
    for (ResourceBundleControlProvider provider : providers) {
      Control control = provider.getControl(baseName);
      if (control != null) {
        return control;
      }
    }
  }
  return Control.INSTANCE;
}

獲取默認的java.util.ResourceBundle.Control前會嘗試從java.util.spi.ResourceBundleControlProvider中獲取,因此咱們能夠自定義java.util.spi.ResourceBundleControlProvider來生成對應的control

SPI

SPI官方地址

spi原理具體見java.util.ServiceLoader.LazyIterator#hasNextService

private static final String PREFIX = "META-INF/services/";

編寫代碼

public class EncodingResourceBundleControlProvider implements ResourceBundleControlProvider {

  @Override
  public ResourceBundle.Control getControl(String baseName) {
    return new EncodedControl();
  }
}

而後按照文檔

META-INF/services建立java.util.spi.ResourceBundleControlProvider文件

內容爲

com.zzjson.se.provider.EncodingResourceBundleControlProvider

最後測試

可是發現失效!!!

緣由resourceBundle中spi調用的是java.util.ServiceLoader#loadInstalled這裏面不會加載項目中的配置

Spring國際化

MessageSource-消息轉化的頂層接口

Spring-messageSource,介紹文檔地址

public interface MessageSource {

  //用於從MessageSource檢索消息的基本方法。 若是找不到指定語言環境的消息,則使用默認消息。 使用標準庫提供的MessageFormat功能,傳入的全部參數都將成爲替換值。
    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);

    //與先前的方法基本相同,但有一個區別:沒法指定默認消息;默認值爲0。 若是找不到消息,則拋出NoSuchMessageException。
    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;

    //前述方法中使用的全部屬性也都包裝在一個名爲MessageSourceResolvable的類中,您能夠在此方法中使用該類。
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}

加載ApplicationContext時,它將自動搜索在上下文中定義的MessageSource bean。

​ Bean必須具備名稱messageSource。 若是找到了這樣的bean,則對先前方法的全部調用都將委派給消息源。

​ 若是找不到消息源,則ApplicationContext嘗試查找包含同名bean的父級。 若是是這樣,它將使用該bean做爲MessageSource。

​ 若是ApplicationContext找不到任何消息源,則將實例化一個空的org.springframework.context.support.DelegatingMessageSource,以便可以接受對上述方法的調用。

MessageSourceResolvable

org.springframework.context.MessageSourceResolvable

image-20200409124246636

public interface MessageSourceResolvable {

  String[] getCodes();

  Object[] getArguments();

  String getDefaultMessage();

}

類圖

image-20200409101500140

當前咱們只須要關注這一塊就好了

image-20200409112828009

HierarchicalMessageSource

public interface HierarchicalMessageSource extends MessageSource {

    void setParentMessageSource(MessageSource parent);

    MessageSource getParentMessageSource();
}

MessageSourceSupport

MessageFormat

MessageFormat是java提供的他的包在java.text,他能幫咱們格式化文本

MessageSourceSupport和MessageFormat密切相關咱們先看看MessageFormat的案例

public class MessageFormatDemo {
  /**
    * @param args
    * @see ResourceBundleMessageSource#resolveCode(java.lang.String, java.util.Locale)
    */
  public static void main(String[] args) {
    MessageFormat format = new MessageFormat("Hello,{0}!");
    System.out.println(format.format(new Object[]{"World"}));
  }

}
  • MessageFormat可以幫咱們填充參數java.text.MessageFormat#subformat

​ 回到org.springframework.context.support.MessageSourceSupport能夠看到其提供了標準的java.text.MessageFormat功能查看其核心代碼

public abstract class MessageSourceSupport {
  private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");


  private boolean alwaysUseMessageFormat = false;

  private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage =
    new HashMap<String, Map<Locale, MessageFormat>>();

  //使用緩存的MessageFormats格式化給定的消息字符串。默認狀況下,將爲傳入的默認消息調用,以解析在其中找到的全部參數佔位符。
  protected String formatMessage(String msg, Object[] args, Locale locale) {
    if (msg == null || (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args))) {
      return msg;
    }
    MessageFormat messageFormat = null;
    synchronized (this.messageFormatsPerMessage) {
      Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
      if (messageFormatsPerLocale != null) {
        messageFormat = messageFormatsPerLocale.get(locale);
      }
      else {
        messageFormatsPerLocale = new HashMap<Locale, MessageFormat>();
        this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
      }
      if (messageFormat == null) {
        try {
          messageFormat = createMessageFormat(msg, locale);
        }
        catch (IllegalArgumentException ex) {
          // Invalid message format - probably not intended for formatting,
          // rather using a message structure with no arguments involved...
          if (isAlwaysUseMessageFormat()) {
            throw ex;
          }
          // Silently proceed with raw message if format not enforced...
          messageFormat = INVALID_MESSAGE_FORMAT;
        }
        messageFormatsPerLocale.put(locale, messageFormat);
      }
    }
    if (messageFormat == INVALID_MESSAGE_FORMAT) {
      return msg;
    }
    synchronized (messageFormat) {
      return messageFormat.format(resolveArguments(args, locale));
    }
  }

    //爲給定的消息和語言環境建立一個MessageFormat。
  protected MessageFormat createMessageFormat(String msg, Locale locale) {
    return new MessageFormat((msg != null ? msg : ""), locale);
  }
}

從代碼中可見org.springframework.context.support.MessageSourceSupport主要提供了一下幾個功能

  • 使用建立對應的Messageformat
  • 緩存語言環境和對應的MessageFormat

AbstractMessageSource模板類

org.springframework.context.support.AbstractMessageSource 實現消息的通用處理,從而能夠輕鬆地針對具體的MessageSource實施特定策略。

先看AbstractMessageSource對於MessageSource的默認實現

@Override
public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
  String msg = getMessageInternal(code, args, locale);
  if (msg != null) {
    return msg;
  }
  if (defaultMessage == null) {
    String fallback = getDefaultMessage(code);
    if (fallback != null) {
      return fallback;
    }
  }
  return renderDefaultMessage(defaultMessage, args, locale);
}

@Override
public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
  String msg = getMessageInternal(code, args, locale);
  if (msg != null) {
    return msg;
  }
  String fallback = getDefaultMessage(code);
  if (fallback != null) {
    return fallback;
  }
  throw new NoSuchMessageException(code, locale);
}

@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
  String[] codes = resolvable.getCodes();
  if (codes != null) {
    for (String code : codes) {
      String message = getMessageInternal(code, resolvable.getArguments(), locale);
      if (message != null) {
        return message;
      }
    }
  }
  String defaultMessage = getDefaultMessage(resolvable, locale);
  if (defaultMessage != null) {
    return defaultMessage;
  }
  throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : null, locale);
}

結合前面說的MessageSource接口的定義咱們不難看出這裏有兩個核心的方法

  • org.springframework.context.support.AbstractMessageSource#getMessageInternal

    • 在給定的語言環境中將給定的代碼和參數解析爲消息
  • org.springframework.context.support.AbstractMessageSource#getDefaultMessage(org.springframework.context.MessageSourceResolvable, java.util.Locale)

    • 若是上述解析出來的Message是空的,則經過此方法獲取默認消息

getDefaultMessage

protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
  String defaultMessage = resolvable.getDefaultMessage();
  String[] codes = resolvable.getCodes();
  if (defaultMessage != null) {
    if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) {
      // Never format a code-as-default-message, even with alwaysUseMessageFormat=true
      return defaultMessage;
    }
    //調用前面說到的`org.springframework.context.support.MessageSourceSupport#renderDefaultMessage`
    return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);
  }
  return (!ObjectUtils.isEmpty(codes) ? getDefaultMessage(codes[0]) : null);
}

​ 從這裏能夠看到就是把參數傳遞給了咱們前面說的MessageSourceSupport中的方法而後對傳入的參數基於語言環境進行了格式化

getMessageInternal

org.springframework.context.support.AbstractMessageSource#getMessageInternal

protected String getMessageInternal(String code, Object[] args, Locale locale) {
  if (code == null) {
    return null;
  }
  if (locale == null) {
    locale = Locale.getDefault();
  }
  Object[] argsToUse = args;

  if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
    // 當前代碼可能須要優化,由於咱們並不須要參數所以不須要涉及MessageFormat。可是實際上仍是使用了MessageFormat去格式化消息
    //注意,默認實現仍使用MessageFormat; 
    //這能夠在特定的子類中覆蓋

    String message = resolveCodeWithoutArguments(code, locale);
    if (message != null) {
      return message;
    }
  }

  else {
    //對於在父MessageSource中定義了消息
    //而在子MessageSource中定義了可解析參數的狀況,直接子MessageSource就解析參數。
    //把須要解析的參數封裝到數組中
    argsToUse = resolveArguments(args, locale);

    MessageFormat messageFormat = resolveCode(code, locale);
    if (messageFormat != null) {
      synchronized (messageFormat) {
        //使用消息格式化器來格式
        return messageFormat.format(argsToUse);
      }
    }
  }

  //若是上面都沒有找到合適的解析器,即子類沒有返回MessageFormat,則從語言環境無關的公共消息中的給定消息代碼
  //private Properties commonMessages;
 // 當前commonMessage就是Properties
  Properties commonMessages = getCommonMessages();
  if (commonMessages != null) {
    String commonMessage = commonMessages.getProperty(code);
    if (commonMessage != null) {
      return formatMessage(commonMessage, args, locale);
    }
  }

  //若是都沒有找到,就從父節點找
  return getMessageFromParent(code, argsToUse, locale);
}

@Override
//把須要解析的參數封裝到數組中
    protected Object[] resolveArguments(Object[] args, Locale locale) {
        if (args == null) {
            return new Object[0];
        }
        List<Object> resolvedArgs = new ArrayList<Object>(args.length);
        for (Object arg : args) {
            if (arg instanceof MessageSourceResolvable) {
                resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));
            }
            else {
                resolvedArgs.add(arg);
            }
        }
        return resolvedArgs.toArray(new Object[resolvedArgs.size()]);
    }

protected String resolveCodeWithoutArguments(String code, Locale locale) {
  //直接調用子類的解析方式
  MessageFormat messageFormat = resolveCode(code, locale);
  if (messageFormat != null) {
    synchronized (messageFormat) {
      return messageFormat.format(new Object[0]);
    }
  }
  return null;
}

protected abstract MessageFormat resolveCode(String code, Locale locale);

上述從代碼中能夠看出來模板類主要作了如下幾件事情和提出了一個將來版本或者子類重寫須要優化的地方

  • 提供了模板方法解析消息

    • 對於沒有args的而且參數爲空的直接交給子類重寫的org.springframework.context.support.AbstractMessageSource#resolveCodeWithoutArguments去解析
    • 其餘的則先org.springframework.context.support.AbstractMessageSource#resolveArguments把參數變成參數數組,而後調用子類的org.springframework.context.support.AbstractMessageSource#resolveCode獲取到MessageFormat
    • 若是都沒有返回對應的MessageFormat則直接從Properties中獲取
    • 最後若是當前層仍是沒有獲取到,則利用org.springframework.context.HierarchicalMessageSource來遞歸調用父類的org.springframework.context.support.AbstractMessageSource#getMessageInternal

image-20200409174652437

查看子類能夠看到其有三個子類

ResourceBundleMessageSource

當前類是基於JDK的java.util.ResourceBundle來實現的

查看org.springframework.context.support.ResourceBundleMessageSource.MessageSourceControl能夠看到,其自定義了一個Control來解析國際化,以及增長了編解碼的功能,爲了解決國際化亂碼的問題

if (stream != null) {
  String encoding = getDefaultEncoding();
  if (encoding == null) {
    encoding = "ISO-8859-1";
  }
  try {
    return loadBundle(new InputStreamReader(stream, encoding));
  }
  finally {
    stream.close();
  }
}
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
  Set<String> basenames = getBasenameSet();
  for (String basename : basenames) {
    
    ResourceBundle bundle = getResourceBundle(basename, locale);
    if (bundle != null) {
      String result = getStringOrNull(bundle, code);
      if (result != null) {
        return result;
      }
    }
  }
  return null;
}

/**
 * Resolves the given message code as key in the registered resource bundles,
 * using a cached MessageFormat instance per message code.
 */
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
  Set<String> basenames = getBasenameSet();
  for (String basename : basenames) {
    ResourceBundle bundle = getResourceBundle(basename, locale);
    if (bundle != null) {
      MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
      if (messageFormat != null) {
        return messageFormat;
      }
    }
  }
  return null;
}
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
        if (getCacheMillis() >= 0) {
            // Fresh ResourceBundle.getBundle call in order to let ResourceBundle
            // do its native caching, at the expense of more extensive lookup steps.
            return doGetBundle(basename, locale);
        }
        else {
            // Cache forever: prefer locale cache over repeated getBundle calls.
            synchronized (this.cachedResourceBundles) {
                Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
                if (localeMap != null) {
                    ResourceBundle bundle = localeMap.get(locale);
                    if (bundle != null) {
                        return bundle;
                    }
                }
                try {
                    ResourceBundle bundle = doGetBundle(basename, locale);
                    if (localeMap == null) {
                        localeMap = new HashMap<Locale, ResourceBundle>();
                        this.cachedResourceBundles.put(basename, localeMap);
                    }
                    localeMap.put(locale, bundle);
                    return bundle;
                }
                catch (MissingResourceException ex) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
                    }
                    // Assume bundle not found
                    // -> do NOT throw the exception to allow for checking parent message source.
                    return null;
                }
            }
        }
    }

查看上述代碼org.springframework.context.support.ResourceBundleMessageSource#resolveCodeWithoutArguments可知其從basenames位置獲取了國際化信息,拿到告終果

org.springframework.context.support.ResourceBundleMessageSource#resolveCode中能夠見到返回了java.text.MessageFormat而且設置了國際化信息

org.springframework.context.support.ResourceBundleMessageSource#getResourceBundle中作了幾件事情

  • 若是指定了了本地緩存的時間則會超時後從新獲取
  • 若是沒有指定本地緩存時間則直接都存儲在了org.springframework.context.support.ResourceBundleMessageSource#cachedResourceBundles

當前類缺點也是很明顯,只能從類路徑讀取,不能指定外部文件

ReloadableResourceBundleMessageSource

​ 當前類支持相同的包文件格式,但比基於標準JDK的ResourceBundleMessageSource實現更靈活。

​ 特別是,它容許從任何Spring資源位置讀取文件(不只僅是從類路徑),並支持熱重載bundle屬性文件(同時在二者之間有效地緩存它們)。

默認的重載方法

@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
  if (getCacheMillis() < 0) {
    PropertiesHolder propHolder = getMergedProperties(locale);
    String result = propHolder.getProperty(code);
    if (result != null) {
      return result;
    }
  }
  else {
    for (String basename : getBasenameSet()) {
      List<String> filenames = calculateAllFilenames(basename, locale);
      for (String filename : filenames) {
        PropertiesHolder propHolder = getProperties(filename);
        String result = propHolder.getProperty(code);
        if (result != null) {
          return result;
        }
      }
    }
  }
  return null;
}

/**
 * Resolves the given message code as key in the retrieved bundle files,
 * using a cached MessageFormat instance per message code.
 */
@Override
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;
}
protected PropertiesHolder getMergedProperties(Locale locale) {
  PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
  if (mergedHolder != null) {
    return mergedHolder;
  }
  Properties mergedProps = newProperties();
  long latestTimestamp = -1;
  String[] basenames = StringUtils.toStringArray(getBasenameSet());
  for (int i = basenames.length - 1; i >= 0; i--) {
    List<String> filenames = calculateAllFilenames(basenames[i], locale);
    for (int j = filenames.size() - 1; j >= 0; j--) {
      String filename = filenames.get(j);
      PropertiesHolder propHolder = getProperties(filename);
      if (propHolder.getProperties() != null) {
        mergedProps.putAll(propHolder.getProperties());
        if (propHolder.getFileTimestamp() > latestTimestamp) {
          latestTimestamp = propHolder.getFileTimestamp();
        }
      }
    }
  }
  mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
  PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
  if (existing != null) {
    mergedHolder = existing;
  }
  return mergedHolder;
}

protected List<String> calculateAllFilenames(String basename, Locale locale) {
        Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
        if (localeMap != null) {
            List<String> filenames = localeMap.get(locale);
            if (filenames != null) {
                return filenames;
            }
        }
        List<String> filenames = new ArrayList<String>(7);
        filenames.addAll(calculateFilenamesForLocale(basename, locale));
        if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) {
            List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
            for (String fallbackFilename : fallbackFilenames) {
                if (!filenames.contains(fallbackFilename)) {
                    // Entry for fallback locale that isn't already in filenames list.
                    filenames.add(fallbackFilename);
                }
            }
        }
        filenames.add(basename);
        if (localeMap == null) {
            localeMap = new ConcurrentHashMap<Locale, List<String>>();
            Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
            if (existing != null) {
                localeMap = existing;
            }
        }
        localeMap.put(locale, filenames);
        return filenames;
    }

//計算給定包基本名稱和語言環境的文件名
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
  List<String> result = new ArrayList<String>(3);
  String language = locale.getLanguage();
  String country = locale.getCountry();
  String variant = locale.getVariant();
  StringBuilder temp = new StringBuilder(basename);

  temp.append('_');
  if (language.length() > 0) {
    temp.append(language);
    result.add(0, temp.toString());
  }

  temp.append('_');
  if (country.length() > 0) {
    temp.append(country);
    result.add(0, temp.toString());
  }

  if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
    temp.append('_').append(variant);
    result.add(0, temp.toString());
  }

  return result;
}

//
protected PropertiesHolder getMergedProperties(Locale locale) {
  PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
  if (mergedHolder != null) {
    return mergedHolder;
  }
  Properties mergedProps = newProperties();
  long latestTimestamp = -1;
  String[] basenames = StringUtils.toStringArray(getBasenameSet());
  for (int i = basenames.length - 1; i >= 0; i--) {
    List<String> filenames = calculateAllFilenames(basenames[i], locale);
    for (int j = filenames.size() - 1; j >= 0; j--) {
      String filename = filenames.get(j);
      PropertiesHolder propHolder = getProperties(filename);
      if (propHolder.getProperties() != null) {
        mergedProps.putAll(propHolder.getProperties());
        if (propHolder.getFileTimestamp() > latestTimestamp) {
          latestTimestamp = propHolder.getFileTimestamp();
        }
      }
    }
  }
  mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
  PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
  if (existing != null) {
    mergedHolder = existing;
  }
  return mergedHolder;
}

ReloadableResourceBundleMessageSource.PropertiesHolder用於緩存。

核心加載配置的代碼

protected Properties loadProperties(Resource resource, String filename) throws IOException {
  InputStream is = resource.getInputStream();
  Properties props = newProperties();
  try {
    if (resource.getFilename().endsWith(XML_SUFFIX)) {
      if (logger.isDebugEnabled()) {
        logger.debug("Loading properties [" + resource.getFilename() + "]");
      }
      this.propertiesPersister.loadFromXml(props, is);
    }
    else {
      String encoding = null;
      if (this.fileEncodings != null) {
        encoding = this.fileEncodings.getProperty(filename);
      }
      if (encoding == null) {
        encoding = getDefaultEncoding();
      }
      if (encoding != null) {
        if (logger.isDebugEnabled()) {
          logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
        }
        this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
      }
      else {
        if (logger.isDebugEnabled()) {
          logger.debug("Loading properties [" + resource.getFilename() + "]");
        }
        this.propertiesPersister.load(props, is);
      }
    }
    return props;
  }
  finally {
    is.close();
  }
}

org.springframework.context.support.ReloadableResourceBundleMessageSource#loadProperties 這裏經過org.springframework.context.support.ReloadableResourceBundleMessageSource#calculateAllFilenames以及org.springframework.context.support.ReloadableResourceBundleMessageSource#calculateFilenamesForLocale計算出來的對應方言的路徑加載到properties中,而後把獲取到的properties放到org.springframework.context.support.ReloadableResourceBundleMessageSource.PropertiesHolder中持有,當前類會存儲源文件的最後修改的時間戳,而後判斷最後修改的時間戳和當前時間差值比較,判斷是否超過了容許的最大緩存時間。

使用
public class SpringI18nDemo {
  public static final String BUNDLE_NAME = "demo";

  public static void main(String[] args) {
    // ResourceBundle + MessageFormat => MessageSource
    // ResourceBundleMessageSource 不能重載
    // ReloadableResourceBundleMessageSource
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setDefaultEncoding("utf-8");
    messageSource.setBasename(BUNDLE_NAME);
    String name = messageSource
      .getMessage("world", new Object[]{"World"}, Locale.SIMPLIFIED_CHINESE);
    System.out.println(name);
  }
}

StaticMessageSource

​ StaticMessageSource不多使用,相比之下就比較簡單了

@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
  return this.messages.get(code + '_' + locale.toString());
}

@Override
protected MessageFormat resolveCode(String code, Locale locale) {
  String key = code + '_' + locale.toString();
  String msg = this.messages.get(key);
  if (msg == null) {
    return null;
  }
  synchronized (this.cachedMessageFormats) {
    MessageFormat messageFormat = this.cachedMessageFormats.get(key);
    if (messageFormat == null) {
      messageFormat = createMessageFormat(msg, locale);
      this.cachedMessageFormats.put(key, messageFormat);
    }
    return messageFormat;
  }
}

只是很簡單的從靜態map中獲取值

經常使用api

Locale存儲器-LocaleContext

public interface LocaleContext {

  /**
     * Return the current Locale, which can be fixed or determined dynamically,
     * depending on the implementation strategy.
     * @return the current Locale, or {@code null} if no specific Locale associated
     */
  Locale getLocale();

}
public interface TimeZoneAwareLocaleContext extends LocaleContext {

  /**
    * Return the current TimeZone, which can be fixed or determined dynamically,
    * depending on the implementation strategy.
    * @return the current TimeZone, or {@code null} if no specific TimeZone associated
    */
  TimeZone getTimeZone();

}

image-20200409094645088

查看上述可知TimeZoneAwareLocaleContext增長了時區的概念。

像這種存儲器大部分都是寫的關於TimeZoneAwareLocaleContext的匿名類

例如org.springframework.web.servlet.i18n.FixedLocaleResolver#resolveLocaleContext

@Override
public LocaleContext resolveLocaleContext(HttpServletRequest request) {
  return new TimeZoneAwareLocaleContext() {
    @Override
    public Locale getLocale() {
      return getDefaultLocale();
    }
    @Override
    public TimeZone getTimeZone() {
      return getDefaultTimeZone();
    }
  };
}

Locale線程關聯器-LocaleContextHolder

​ 能夠經過LocaleContextHolder類將LocaleContext實例與線程關聯。

org.springframework.context.i18n.LocaleContextHolder

Locale解析器-LocaleResolver

官方localeresolver

public interface LocaleResolver {


  Locale resolveLocale(HttpServletRequest request);


  void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);

}

​ 咱們可使用客戶端的語言環境自動解析器org.springframework.web.servlet.LocaleResolver來自動解析消息。

image-20200409094055651

​ 如上圖所述Spring提供了幾個獲取國際化信息的解析器:

  • org.springframework.web.servlet.i18n.SessionLocaleResolver
  • org.springframework.web.servlet.i18n.CookieLocaleResolver
  • org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

    • 此解析器檢查客戶端(例如,web瀏覽器)發送的請求中的accept-language頭。
    • 一般這個頭字段包含客戶操做系統的語言環境。
    • 請注意,此解析器不支持時區信息
  • org.springframework.web.servlet.i18n.FixedLocaleResolver

CookieLocaleResolver

org.springframework.web.servlet.i18n.CookieLocaleResolver

CookieLocaleResolver文檔地址

​ 此區域設置解析器檢查客戶端上可能存在的Cookie,以查看是否指定了區域設置或時區。若是是,則使用指定的詳細信息。使用此區域設置解析器的屬性,能夠指定cookie的名稱以及存活時間。下面是定義CookieLocaleResolver的一個示例。

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

  <property name="cookieName" value="clientlanguage"/>

  <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
  <property name="cookieMaxAge" value="100000"/>

</bean>

SessionLocaleResolver

SessionLocaleResolver文檔地址

org.springframework.web.servlet.i18n.SessionLocaleResolver

​ SessionLocaleResolver容許咱們從可能與用戶請求關聯的會話中檢索Locale和TimeZone。

​ 與CookieLocaleResolver相比,此策略將本地選擇的語言環境設置存儲在Servlet容器的HttpSession中。

​ 所以,這些設置對於每一個會話來講都是臨時的,所以在每一個會話終止時都會丟失。請注意,與外部會話管理機制(如Spring Session項目)沒有直接關係。

​ 該SessionLocaleResolver將僅根據當前的HttpServletRequest評估並修改相應的HttpSession屬性。

FixedLocaleResolver

org.springframework.web.servlet.i18n.FixedLocaleResolver

​ 指定固定的方言和時區,不容許修改修改會報錯

@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
  throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}

獲取Locale

​ 當咱們收到請求時,DispatcherServlet會查找語言環境解析器,若是找到了它,則嘗試使用它來設置語言環境。 使 用RequestContext.getLocale方法,您始終能夠檢索由語言環境解析器解析的語言環境。

​ 語言環境解析器和攔截器在org.springframework.web.servlet.i18n包中定義,並以常規方式在應用程序上下文中進行配置。 這是Spring中包含的語言環境解析器的一部分。

org.springframework.web.servlet.support.RequestContext#getLocale

獲取時區信息

​ LocaleContextResolver接口提供了LocaleResolver的擴展,該擴展容許解析程序提供更豐富的LocaleContext,其中可能包含時區信息。

​ 若是可用,則可使用RequestContext.getTimeZone()方法獲取用戶的TimeZone。

​ 在Spring的ConversionService中註冊的日期/時間轉換器和格式化程序對象將自動使用時區信息。

更改國際化

​ 咱們除了自動的語言環境解析以外,您還能夠在處理程序映射上附加攔截器LocaleChangeInterceptor以在特定狀況下更改語言環境。

LocaleChangeInterceptor

​ 咱們可以很方便的更改國際化,經過參數來更改咱們的國際化內容,經過增長一個LocaleChangeInterceptor攔截器給一個handler mapping,這個攔截器會監測請求參數,而且更改locale。

文檔地址

案例配置

當前若是是*.view的資源包含有siteLanguate參數的都會更改國際化。以下請求路徑就會更改語言環境爲荷蘭語

https://www.sf.net/home.view?siteLanguage=nl

<bean id="localeChangeInterceptor"
        class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>

裝載方式

JavaConfig
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
    }

}
xml
<mvc:interceptors>
  <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
</mvc:interceptors>

源碼分析

Spring 國際化初始化的地方

org.springframework.web.servlet.DispatcherServlet#initLocaleResolver

tomcat國際化

咱們能夠直接調用javax.servlet.ServletRequest#getLocale獲取請求的Locale

國際化-字符

參考文檔地址

參考地址2

基本概念

字符

​ 各類文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。

​ 也就是說,它是一個信息單位,一個數字是一個字符,一個文字是一個字符,一個標點符號也是一個字符。

字節

​ 字節是一個8bit的存儲單元,取值範圍是0x00~0xFF。

​ 根據字符編碼的不一樣,一個字符能夠是單個字節的,也能夠是多個字節的。

字符集

​ 字符的集合就叫字符集。不一樣集合支持的字符範圍天然也不同,譬如ASCII只支持英文,GB18030支持中文等等

​ 在字符集中,有一個碼錶的存在,每個字符在各自的字符集中對應着一個惟一的碼。可是同一個字符在不一樣字符集中的碼是不同的,譬如字符「中」在Unicode和GB18030中就分別對應着不一樣的碼(2001354992)。

字符編碼

​ 定義字符集中的字符如何編碼爲特定的二進制數,以便在計算機中存儲。 字符集和字符編碼通常一一對應(有例外)

​ 譬如GB18030既能夠表明字符集,也能夠表明對應的字符編碼,它爲了兼容ASCII碼,編碼方式爲code大於255的採用兩位字節(或4字節)來表明一個字符,不然就是兼容模式,一個字節表明一個字符。(簡單一點理解,將它認爲是如今用的的中文編碼就好了)

​ 字符集與字符編碼的一個例外就是Unicode字符集,它有多種編碼實現(UTF-8,UTF-16,UTF-32等)

字符集和字符編碼

字符集(Charset):

​ 是一個系統支持的全部抽象字符的集合。字符是各類文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。

字符編碼(Character Encoding):

​ 是一套法則,使用該法則可以對天然語言的字符的一個集合(如字母表或音節表),與其餘東西的一個集合(如號碼或電脈衝)進行配對。即在符號集合與數字系統之間創建對應關係,它是信息處理的一項基本技術。一般人們用符號集合(通常狀況下就是文字)來表達信息。而以計算機爲基礎的信息處理系統則是利用元件(硬件)不一樣狀態的組合來存儲和處理信息的。元件不一樣狀態的組合能表明數字系統的數字,所以字符編碼就是將符號轉換爲計算機能夠接受的數字系統的數,稱爲數字代碼。

經常使用字符集和字符編碼

​ 常見字符集名稱:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。

​ 計算機要準確的處理各類字符集文字,須要進行字符編碼,以便計算機可以識別和存儲各類文字。

Ascii字符集&編碼

ASCII美國信息交換標準代碼是基於拉丁字母的一套電腦編碼系統。它主要用於顯示現代英語,而其擴展版本EASCII則能夠勉強顯示其餘西歐語言。它是現今最通用的單字節編碼系統(可是有被Unicode追上的跡象),並等同於國際標準ISO/IEC 646

只能顯示26個基本拉丁字母、阿拉伯數目字和英式標點符號,所以只能用於顯示現代美國英語(並且在處理英語當中的外來詞如naïve、café、élite等等時,全部重音符號都不得不去掉,即便這樣作會違反拼寫規則)。而EASCII雖然解決了部份西歐語言的顯示問題,但對更多其餘語言依然無能爲力。

所以如今的蘋果電腦已經拋棄ASCII而轉用Unicode

GBXXXX字符集&編碼

​ 天朝專家把那些127號以後的奇異符號們(即EASCII)取消掉,規定:

​ 一個小於127的字符的意義與原來相同,但兩個大於127的字符連在一塊兒時,就表示一個漢字,前面的一個字節(他稱之爲高字節)從0xA1用到 0xF7,後面一個字節(低字節)從0xA1到0xFE,這樣咱們就能夠組合出大約7000多個簡體漢字了。

​ 在這些編碼裏,還把數學符號、羅馬希臘的 字母、日文的假名們都編進去了,連在ASCII裏原本就有的數字、標點、字母都通通從新編了兩個字節長的編碼,這就是常說的"全角"字符,而原來在127號如下的那些就叫"半角"字符了。

字符集與字符編碼的快速區分

  • ASCII碼是一個字符集,同時它的實現也只有一種,所以它也能夠指代這個字符集對應的字符編碼
  • GB18030是一個字符集,主要是中國人爲了解決中文而發明制定的,因爲它的實現也只有一種,因此它也能夠指代這個字符集對應的字符編碼
  • Unicode是一個字符集,爲了解決不一樣字符集碼錶不一致而推出的,統一了全部字符對應的碼,所以在這個規範下,全部字符對應的碼都是一致的(統一碼),可是統一碼只規定了字符與碼錶的一一對應關係,卻沒有規定該如何實現,所以這個字符集有多種實現方式(UTF-8,UTF-18,UTF-32),所以這些實現就是對應的字符編碼。 也就是說,Unicode統一約定了字符與碼錶直接一一對應的關係,而UTF-8是Unicode字符集的一種字符編碼實現方式,它規定了字符該如何編碼成二進制,存儲在計算機中。

字符集與字符編碼發展簡史

歐美的單字節字符編碼發展

  • 美國人發明了計算機,使用的是英文,因此一開始就設計了一個幾乎只支持英文的字符集ASCII碼(1963 發佈),有128個碼位,用一個字節便可表示,範圍爲00000000-01111111
  • 後來發現碼位不夠,因而在這基礎上進行拓展,256個字符,取名爲EASCII(Extended ASCII),也能一個字節表示,範圍爲00000000-11111111
  • 後來傳入歐洲,發現這個標準並不適用於一些歐洲語言,因而在ASCII(最原始的ASCII)的基礎上拓展,造成了ISO-8859標準(國際標準,1998年發佈),跟EASCII相似,兼容ASCII。而後,根據歐洲語言的複雜特性,結合各自的地區語言造成了N個子標準,ISO-8859-一、ISO-8859-二、...。 兼容性簡直使人髮指。

亞洲,只能雙字節了

​ 計算機傳入亞洲後,國際標準已被徹底不夠用,東亞語言隨便一句話就已經超出範圍了,也是這時候亞洲各個國家根據本身的地區特點,有發明了本身地圖適用的字符集與編碼,譬如中國大陸的GB2312,中國臺灣的BIG5,日本的Shift JIS等等 這些編碼都是用雙字節來進行存儲,它們對外有一個統稱(ANSI-American National Standards Institute),也就是說GB2312或BIG5等都是ANSI在各自地區的不一樣標準

Unicode,一統天下

  • 到了全球互聯網時代,不一樣國家,不一樣地區須要進行交互,這時候因爲各自編碼標準都不同,彼此之間都是亂碼,沒法良好的溝通交流,因而這時候ISO組織與統一碼聯盟分別推出了UCS(Universal Multiple-Octet Coded Character Set)與Unicode。後來,二者意識到沒有必要用兩套字符集,因而進行了一次整合,到了Unicode2.0時代,Nnicode的編碼和UCS的編碼都基本一致(因此後續爲了簡便會贊成用Unicode指代),這時候全部的字符均可以採用同一個字符集,有着相同的編碼,能夠愉快的進行交流了。
  • 須要注意的是UCS標準有本身的格式,如UCS-2(雙字節),UCS-4(四字節)等等 而Unicode也有本身的不一樣編碼實現,如UTF-8,UTF-16,UTF-32等等 其中UTF-16能夠認爲是UCS-2的拓展,UTF-32能夠認爲是UCS-4的拓展,而Unicode能夠認爲是Unicode最終用來制霸互聯網的一種編碼格式。

在中國,GB系列的發展

  • 在計算機傳入中國後,1980年,中國國家標準總局發佈了第一個漢字編碼國家標準GB2312(2312是標準序號),採用雙字節編碼,裏面包括了大部分漢字,拉丁字母,日文假名以及全角字符等。
  • 然而,隨着程序的發展,逐漸發現GB2312已經不知足需求了,因而1993年又推出了一個GBK編碼(漢字國標擴展碼),徹底兼容GB2312標準。而且包括了BIG5的全部漢字,與1995年發佈。 同時GBK也涵蓋了Unicode全部CJK漢字,因此也能夠和Unicode作一一對應。
  • 後來到了2000年,又推出了一個全新的標準 GB 18030,它不只拓展了新的字符,如支持中國少數名族文字等,並且它採用了單字節,雙字節,四字節三種編碼方式,因此徹底兼容ASCII碼與GBK碼。 到了2005年,這一標準有進行了拓展,推出了GB18030-2005,劇本涵蓋全部漢字,也就是說,如今使用的國標標準碼就是GB18030-2005了。

不一樣字符編碼的字符是如何進行轉換的

  • 若是是相同字符集,因爲相同字符集中的碼都是同樣的,因此只須要針對不一樣的編碼方式轉變而已。譬如UTF-16轉UTF-8,首先會取到當前須要轉換的字符的Unicode碼,而後將當前的編碼方式由雙字節(有4字節的拓展就不贅述了),變爲變長的1,2,3等字節
  • 若是是不一樣的字符集,因爲不一樣字符集的碼是不同的,因此須要各自的碼錶才能進行轉換。譬如UTF-16轉GBK,首先須要取到當前須要轉換的字符的Unicode碼,而後根據Unicode和GBK碼錶一一對應的關係(只有部分共同都有的字符才能在碼錶中查到),找到它對應的GBK碼,而後用GBK的編碼方式(雙字節)進行編碼
相關文章
相關標籤/搜索