深入 JAVA 系列之国际化
现在很多面试题比较烂。
比如说, HashMap
在 JAVA 8 中的 红黑树的引用,对于你代码的提升,其实是没有太多的感受的。
本期议题
- JAVA SE 国际化应用
- JAVA EE 国际化应用
- Spring 国际化应用
Java SE 国际化应用
Locale
ResourceBundle
ClassLoader
Control
(@since Java 6
)ResourceBundleControlProvider
(@since Java 8
)
Format
Java EE 国际化应用
- Servlet
ServletRequest
- JSP
- JSTL
- JSF(JSR-252)
- Bean Validation(JSR-303)
Spring 国际化应用
- Java SE
MessageSource
- Java EE
DispatcherServlet
LocaleContextHolder
LocaleResolver
Spring 是一个伟大的重复发明轮子。多了解 JAVA 规范以后,会对 Spring 的理解会加深很多,真正的 JAVA 高手,还是对底层非常的熟悉。包括 JDK 源码,和 JSR 规范。
1 Spring 如何利用 JAVA 的规范?
JAVA SE 国际化应用
Locale
ResourceBundle
ClassLoader
Control
(@since Java 6
)ResourceBundleControlProvider
(@since Java 8
)
Format
1
我们很多时候,在大多数场景使用很多很高级的框架,Spring 、Spring boot、Netty、Hadhoop ,但是我们对底层不是很了解。
- Language Country variant(语言,地区,语言变种类似于方言)
国内或是国际上一些把 Spring 讲清楚的人都很少。JAVA 更少。
1
2
3
4
5
6
7
8
9
/***
* {@link java.util.Locale}
*/
public class LocaleDemo {
public static void main(String[] args) {
// 输入默认的 Locale
System.out.println(Locale.getDefault());
}
}
1
zh_CN
和系统的语言跟时区有关系,是国际化的标准。
我本地的 Locale
可不可以改?
java.util.Locale#Locale(java.lang.String, java.lang.String, java.lang.String)
1
2
3
4
5
6
7
public Locale(String language, String country, String variant) {
if (language== null || country == null || variant == null) {
throw new NullPointerException();
}
baseLocale = BaseLocale.getInstance(convertOldISOCodes(language), "", country, variant);
localeExtensions = getCompatibilityExtensions(language, "", country, variant);
}
语言是和操作系统一脉相承。
user.language
en
-
user.region
- 输入默认的
Locale
- 硬编码调整
en_US
,无法做到一份代码,到处运行。 - 通过启动参数调整
-Dxxx
等同于System.setProperty();
System.setProperty()
可能更改不了与 安全 有关系。
- 安全
PropertyPermission
Integer
的缓存,低版本是可以调整的。
-
default.properties
1 2
# 默认配置 name=darian
-
default_zh_CN.properties
1 2
# 简体中文配置 name=小诸葛
代码
1
2
3
4
5
6
7
8
9
10
11
/***
* {@link java.util.ResourceBundle}
*/
public class ResourceBundleDemo {
public static void main(String[] args) {
// pachage(目录) + resource 名称(不包含 properties)
String baseName= "static.default";
ResourceBundle bundle = ResourceBundle.getBundle(baseName);
System.out.println("name: " + bundle.getString("name"));
}
}
1
2
name: 小诸葛
乱码解决方案
-Dfile.encoding=UTF-8无效的设置 native2ascii \uxxx -> Unicode- Control
native2ascii 能够将编码转化为 Unicode 编码。
每种技术的衍进肯定是要解决以前的一些技术的不足。
- 功能上的不足
- 便利上的不足
java.util.ResourceBundle.Control
1
2
3
4
5
6
public static class Control {
public static final List<String> FORMAT_DEFAULT = List.of("java.class", "java.properties");
}
不仅支持 .properties
还支持 .class
我们不得不写一个类,去做这么一个事情。
1
2
3
4
5
6
7
8
9
10
11
12
/***
* {@link java.util.ResourceBundle}
*/
public class ResourceBundleDemo {
public static void main(String[] args) {
// pachage(目录) + resource 名称(不包含 properties)
String baseName= "static.default";
ResourceBundle bundle = ResourceBundle.getBundle(baseName, Locale.ENGLISH);
System.out.println("name: " + bundle.getString("name"));
}
}
properties
加载步骤- 先找
default_en.properties
- 找不到,再找系统的语言
default_zh_CN.properties
- 最后再找
default.properties
- 先找
Since 1.6 ResourceBundle.Control
java.util.ResourceBundle.Control#getControl
1
2
3
4
5
6
7
8
9
10
11
12
13
public static final Control getControl(List<String> formats) {
if (formats.equals(Control.FORMAT_PROPERTIES)) {
return SingleFormatControl.PROPERTIES_ONLY;
}
if (formats.equals(Control.FORMAT_CLASS)) {
return SingleFormatControl.CLASS_ONLY;
}
if (formats.equals(Control.FORMAT_DEFAULT)) {
return Control.INSTANCE;
}
throw new IllegalArgumentException();
}
不同的调用不同的 Control
乱码怎么来的?
java.util.ResourceBundle.Control#newBundle
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
public ResourceBundle newBundle(String baseName, Locale locale, String format,
ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
/*
* Legacy mechanism to locate resource bundle in unnamed module only
* that is visible to the given loader and accessible to the given caller.
*/
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
if (format.equals("java.class")) {
//....
// 这里是 java.class 文件格式的加载。。。
//.....
} else if (format.equals("java.properties")) {
final String resourceName = toResourceName0(bundleName, "properties");
if (resourceName == null) {
return bundle;
}
final boolean reloadFlag = reload;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction<>() {
public InputStream run() throws IOException {
URL url = loader.getResource(resourceName);
if (url == null) return null;
URLConnection connection = url.openConnection();
if (reloadFlag) {
// Disable caches to get fresh data for
// reloading.
connection.setUseCaches(false);
}
return connection.getInputStream();
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (stream != null) {
try {
bundle = new PropertyResourceBundle(stream);
} finally {
stream.close();
}
}
} else {
throw new IllegalArgumentException("unknown format: " + format);
}
return bundle;
}
- 这又和
ClassLoader
有关系 - 它是通过
InputStream
去读取 PropertyResourceBundle
是ResourceBundle
的一个实现PropertyResourceBundle#PropertyResourceBundle(InputStream)
java.util.PropertyResourceBundle
1
2
3
4
5
6
7
8
9
10
11
12
13
// Check whether the strict encoding is specified.
// The possible encoding is either "ISO-8859-1" or "UTF-8".
private static final String encoding = GetPropertyAction
.privilegedGetProperty("java.util.PropertyResourceBundle.encoding", "")
.toUpperCase(Locale.ROOT);
/***
* JDK 11
*/
public PropertyResourceBundle (InputStream stream) throws IOException {
this(new InputStreamReader(stream, "ISO-8859-1".equals(encoding) ? StandardCharsets.ISO_8859_1.newDecoder() : new PropertyResourceBundleCharset("UTF-8".equals(encoding)).newDecoder()));
}
解决方法1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ResourceBundleDemo {
public static void main(String[] args) throws IOException {
// pachage(目录) + resource 名称(不包含 properties)
String baseName= "static.default";
ResourceBundle bundle = ResourceBundle.getBundle(baseName, Locale.ENGLISH);
System.out.println("[bundle] name: " + bundle.getString("name"));
ClassLoader classLoader = ResourceBundleDemo.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("static/default_zh_CN.properties");
InputStreamReader reader = new InputStreamReader(inputStream, "UTF-8");
PropertyResourceBundle propertyResourceBundle = new PropertyResourceBundle(reader);
System.out.println("[PropertyResourceBundle] name:"+ propertyResourceBundle.getString("name"));
}
}
1
2
3
[bundle] name: darian_en
[PropertyResourceBundle] name:小诸葛
- 扩展
Control
类 - 重写
Resourcebundle#newBundle
方法
native2ascii
可以解决乱码问题。
- 局限: 它不能直接作用于你的源代码。只能作用于你的
.class
文件。
我可以通过重载覆盖默认的方法 ResourceBundle.Control
的构造方法一劳永逸的解决乱码问题。
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
/***
* {@link ResourceBundle}
*/
public class CustomerResourceBundleDemo {
public static void main(String[] args) throws IOException {
// pachage(目录) + resource 名称(不包含 properties)
String baseName= "static.default";
ResourceBundle bundle = ResourceBundle.getBundle(baseName, new EncodedControl("UTF-8"));
System.out.println("[bundle] name: " + bundle.getString("name"));
}
private static void PropertyResourceBundleRender() throws IOException {
ClassLoader classLoader = CustomerResourceBundleDemo.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("static/default_zh_CN.properties");
InputStreamReader reader = new InputStreamReader(inputStream, "UTF-8");
PropertyResourceBundle propertyResourceBundle = new PropertyResourceBundle(reader);
System.out.println("[PropertyResourceBundle] name:"+ propertyResourceBundle.getString("name"));
}
public static 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.properties")) {
final String resourceName = toResourceName(bundleName, "properties");
if (resourceName == null) {
return bundle;
}
final boolean reloadFlag = reload;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction<>() {
public InputStream run() throws IOException {
URL url = loader.getResource(resourceName);
if (url == null) return null;
URLConnection connection = url.openConnection();
if (reloadFlag) {
// Disable caches to get fresh data for
// reloading.
connection.setUseCaches(false);
}
return connection.getInputStream();
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
Reader reader = new InputStreamReader(stream, encoding);
if (reader != null) {
try {
bundle = new PropertyResourceBundle(reader);
} finally {
reader.close();
stream.close();
}
}
} else {
throw new IllegalArgumentException("unknown format: " + format);
}
return bundle;
}
}
}
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 private static Locale initDefault() { String language, region, script, country, variant; Properties props = GetPropertyAction.privilegedGetProperties(); language = props.getProperty("user.language", "en"); // for compatibility, check for old user.region property region = props.getProperty("user.region"); if (region != null) { // region can be of form country, country_variant, or _variant int i = region.indexOf('_'); if (i >= 0) { country = region.substring(0, i); variant = region.substring(i + 1); } else { country = region; variant = ""; } script = ""; } else { script = props.getProperty("user.script", ""); country = props.getProperty("user.country", ""); variant = props.getProperty("user.variant", ""); } return getInstance(language, script, country, variant, getDefaultExtensions(props.getProperty("user.extensions", "")) .orElse(null)); }
总计
-
JAVA 通过
ResourceBundle
实现,查找properties
文件中的国际化内容。 -
PropertyResourceBundle
JDK 11 以后 如果设置了ISO-8859-1
就是ISO-8859-1
否则,就是UTF-8
. -
可以 采用 native2ascii 方法,将打包后的资源文件进行转移,而不是直接在源码方面解决。
我们如果项目很多人,张三搞一把,李四搞一把,如果有一个小朋友没有搞,那么项目就会乱码了。
-
而重写
ResourceBundle.Control
的方法需要基于JDK 1.6
以上, 如果 没有jdk 1.6
只能通过 native2ascii。- 缺点: 可移植性不强,你每次都需要去 显式 传递
new EncodedControl("UTF-8")
;
- 缺点: 可移植性不强,你每次都需要去 显式 传递
-
since JDK 1.8
实现java.util.spi.ResourceBundleControlProvider
,java 8 有ResourceBundleControlProvider
SPI
mercyblitz 的 实现: PropertyResourceBundleControl
java.util.ResourceBundle
java 11
1
2
private static ServiceLoader<ResourceBundleProvider> getServiceLoader(Module module,
String baseName)
我们不能保证每个人都传递 new Control(XXX)
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
/***
* {@link ResourceBundle}
*/
public class CustomerResourceBundleDemo {
public static void main(String[] args) throws IOException {
// pachage(目录) + resource 名称(不包含 properties)
String baseName= "static.default";
// 显示的传递 EncodedControl
ResourceBundle bundle = ResourceBundle.getBundle(baseName, new EncodedControl("UTF-8"));
System.out.println("[bundle] name: " + bundle.getString("name"));
// 使用默认的 ResourceBundleControlProvider SPI 机制
bundle = ResourceBundle.getBundle(baseName);
System.out.println("[ResourceBundleControlProvider] name: " + bundle.getString("name"));
}
public static class EncodedControl extends ResourceBundle.Control{
private final String encoding;
public EncodedControl(String encoding) {
this.encoding = encoding;
}
public EncodedControl() {
this("UTF-8");
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException {
// 。。。。。。
}
}
1
2
3
4
5
6
7
public class EncodingResourceBundleControlProvider implements ResourceBundleControlProvider {
@Override
public ResourceBundle.Control getControl(String baseName) {
return new CustomerResourceBundleDemo.EncodedControl();
}
}
1
2
[bundle] name: 小诸葛
[ResourceBundleControlProvider] name: 小诸葛
一个工程师,或者说架构师,要了解设计,API 的设计,对于设计模式也好,通用的框架也好,真的很重要。
1 2 3 好的设计可以提高开发效率。不好的设计,一坑十,十坑百。 要重视 API 的设计,比如说 API 的设计。
- JAVA 的 SPI 是没有名称,控制不了顺序。
- Dubbo 的 SPI 利用的是 key, value 的方式来做。
Dubbo 和 JAVA 的 SPI 各有利弊,不要说如何如何好,要考虑到不好,才是关键。
Format
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/***
* {@link java.text.SimpleDateFormat}
*/
public class DateFormatDemo implements Runnable{
public static void main(String[] args) {
new Thread(new DateFormatDemo()).start();
new Thread(new DateFormatDemo()).start();
}
@Override
public void run() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(dateFormat.format(new Date()));
}
}
这个代码是线程安全的。
什么叫重进入。
java.util.concurrent.locks.ReentrantLock
一个方法不停的进入,是线程安全的,因为数据 dateFormat
没有暴漏出去,所以它是线程安全的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/***
* {@link java.text.SimpleDateFormat}
*/
public class DateFormatDemo implements Runnable {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
new Thread(new DateFormatDemo()).start();
new Thread(new DateFormatDemo()).start();
}
@Override
public void run() { // 重进入
System.out.println(dateFormat.format(new Date()));
}
}
这样也没问题,因为你每次都是 new
的新的对象,也没问题。
只有你这个对象,能够被所有的线程所看到,然后同时读和写的时候,才是线程不安全的。
1 理解深刻,就不会有那么多问题了。
1
2
3
4
5
6
7
8
9
10
11
/***
*{@link java.text.NumberFormat} 实例
*/
public class NumberFormatDemo {
public static void main(String[] args) {
NumberFormat numberFormat = NumberFormat.getNumberInstance();
System.out.println(numberFormat.format(10000));
numberFormat = NumberFormat.getNumberInstance(Locale.FRANCE);
System.out.println(numberFormat.format(10000));
}
}
1
2
10,000
10 000
当你发现
new
方法报错的时候,你就要思考,它是不是抽象类。那么它必然会有很多实现类,还有静态工厂方法。windows 的数字格式等等,都可以去设置。
Locale
只是一个入口,只是一个 API ,它控制了非常多的东西,比如说:
ResourceBundle
、Format
国际化的运用很多你就不会发愁 国际货币的表达方式了。
基础很重要。只研究高层框架,很难发展。
三个方法:
- native2ascii
- 而重写
ResourceBundle.Control
since 1.6
- 实现
java.util.spi.ResourceBundleControlProvider
since JDK 1.8
SPI
JAVA EE 国际化的运用
servlet 运用
培养技术的能力不能靠面试。面试只能告诉你市场上的需求,但是如果没有一个背景的话,你是不可能理解的很透彻的。
1 2 3 不要太浮躁,要注重积累。 Spring 的设计,很简单的。
1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet(urlPatterns = "/local")
public class LocaleServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Locale locale = request.getLocale();
long num = Long.parseLong(request.getParameter("num"));
Format format = NumberFormat.getNumberInstance(locale);
response.getWriter().println(format.format(num));
}
}
通过切换,语言,实现不同的格式。
这是 HTTP 协议的规范。
windows 也可以设置,这是国际的通用标准。
JSTL
它是串起来的。
JSF
JavaServer Faces Specification
本地化和国际化。
国际化是对服务端而言的。本地化是对用户而言的。
Bean Validation
1 2 3 4 5 public interface MessageInterpolator{ //。。。。。。 String interpolate(String messageTemplate, Context context, Locale locale); //。。。。。。 }
我们可以进行国际化的设置。
javax.validation.constraints.NotNull.message
同一个文案可以让你在不同的 properties
里边有, JSR 303 都是允许你去拓展的。
Spring
org.springframework.context.MessageSource
1
2
3
4
5
6
7
8
9
public interface MessageSource {
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
传进来 Locale
MessageFormatDemo
1
2
3
4
5
6
7
8
9
10
11
/***
* {@link java.text.MessageFormat}
*/
public class MessageFormatDemo {
public static void main(String[] args) {
// Formatter 是 JAVA 5 里边的。
MessageFormat format = new MessageFormat("Hello,{0} , {1}!");
System.out.println(format.format(new String[]{"world","darian"}));
}
}
1
Hello,world , darian!
ResourceBundleMessageSourceDemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/***
* {@link org.springframework.context.support.ResourceBundleMessageSource} 实例
*/
public class ResourceBundleMessageSourceDemo {
public static void main(String[] args) {
String baseName = "static.default";
// ResourceBundle + MessageFormat > MessageSource
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename(baseName);
System.out.println(messageSource.getMessage("message",
new Object[]{"darian"},
Locale.CHINA));
}
}
Spring 的 MessageSource
就是重复发明轮子。
DispatcherServlet
它的国际化是怎么去处理的?
org.springframework.web.servlet.View
1
2
3
4
5
6
7
8
9
10
11
12
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
@Nullable
default String getContentType() {
return null;
}
void render(@Nullable Map<String, ?> var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception;
}
渲染上下文是知道我的编码的。
org.springframework.web.servlet.LocaleResolver
1
2
3
4
5
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest var1);
void setLocale(HttpServletRequest var1, @Nullable HttpServletResponse var2, @Nullable Locale var3);
}
我要设置这个 Locale
org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
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
public class AcceptHeaderLocaleResolver implements LocaleResolver {
@Nullable
private Locale defaultLocale;
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
}
@Nullable
public Locale getDefaultLocale() {
return this.defaultLocale;
}
会有很多很多请求的实现。
还可能放到 cookie
的实现,
我是在美国,我的语言设置成中文,我用 Locale
来操作。我用 LocaleContextResolver
有很多实现,我可以实现自动的装配。
如何当前的 Locale
是一定的呢?需要和 ThreadLocal
结合。
1
2
3
4
5
6
7
public final class LocaleContextHolder {
private static final ThreadLocal<LocaleContext> localeContextHolder =
new NamedThreadLocal<>("LocaleContext");
private static final ThreadLocal<LocaleContext> inheritableLocaleContextHolder =
new NamedInheritableThreadLocal<>("LocaleContext");
每次都去里边去存。
org.springframework.context.i18n.LocaleContext
1
2
3
4
public interface LocaleContext {
@Nullable
Locale getLocale();
}
每个线程都去里边取出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = (LocaleResolver)context.getBean("localeResolver", LocaleResolver.class);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Detected " + this.localeResolver);
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
}
} catch (NoSuchBeanDefinitionException var3) {
this.localeResolver = (LocaleResolver)this.getDefaultStrategy(context, LocaleResolver.class);
if (this.logger.isTraceEnabled()) {
this.logger.trace("No LocaleResolver 'localeResolver': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
}
}
}
我们在这里把 LocaleResolver
进行实例化。
Spring MVC 实现了多种实现。对视图。
1
我们页面也可能是国际化的。我们页面模板相同的时候。我们可以替换页面。我们当前的页面在不同的国家,不同的一样。处理页面的时候,我会把 `Locale` 去传进去。