国际化
-
描述:跨国家地区的业务场景下,需要系统向前端返回地区相适配的数据
- 语言国际化(i18n):使应用支持多语言和地区文化需求,无需修改源代码
- 数据本地化(l10n):根据特定地区调整应用内容,如日期、货币格式
-
国际化触发条件
- 默认情况下,根据浏览器的偏好信息选择语言
- 用户也可以手动点击语言,主动切换语言
-
Spring框架提供了国际化功能
LocaleResolver:MVC区域解析器,在DispatcherServlet初始化时就自动解析Locale区域信息LocaleChangeInterceptor:MVC预置拦截器,重新解析Locale区域信息,用于用户手动切换语言场景MessageSource:加载资源文件接口,可以根据Locale区域加载预置的资源
获取Locale信息
LocaleResolver:LocaleResolver解析 → 存入LocaleContextHolder→ 业务层从LocaleContextHolder获取LocaleChangeInterceptor:LocaleChangeInterceptorMVC拦截 →LocaleResolver解析 → 存入LocaleContextHolder
配置区域解析器
-
LocaleResolver:MVC区域解析器,在DispatcherServlet初始化时就自动解析Locale区域信息,有多个实现类AcceptHeaderLocaleResolver(默认):从 HTTP 请求头Accept-Language中读取 LocaleSessionLocaleResolver:优先读取Session中的区域信息,若未切换过,则自动解析请求头Accept-Language作为初始值CookieLocaleResolver:优先读取Cookie中的区域信息,若未切换过,则自动解析请求头Accept-Language作为初始值
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
// ========== 1. 核心:命名为 localeResolver,作为默认解析器 ==========
@Bean("localeResolver") // 名称固定为 localeResolver,优先级最高
public LocaleResolver acceptHeaderLocaleResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.US);
return resolver;
}
// ========== 2. 其他解析器:自定义名称,仅作为普通 Bean 存在 ==========
@Bean("cookieLocaleResolver") // 自定义名称,不会自动生效
public LocaleResolver cookieLocaleResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.CHINA);
resolver.setCookieName("user_language");
resolver.setCookieMaxAge(365 * 24 * 60 * 60); // Cookie 有效期1年
return resolver;
}
@Bean("sessionLocaleResolver") // 自定义名称,不会自动生效
public LocaleResolver sessionLocaleResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.CHINA);
return resolver;
}
// 其他Web配置
...
}
- 如果地区信息不在
Accept-Language、Cookie、Session、RequestParam中,可以自定义解析器
java
/**
* 自定义 LocaleResolver:从请求头 "X-Language" 读取 Locale
*/
public class CustomLocaleResolver extends AbstractLocaleResolver {
private Locale defaultLocale = Locale.CHINA;
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 1. 从自定义请求头读取语言参数
String lang = request.getHeader("X-Language");
if (lang != null && !lang.isEmpty()) {
return Locale.forLanguageTag(lang.replace("_", "-"));
}
// 2. 无自定义头,返回默认 Locale
return defaultLocale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// 如需支持手动设置,实现该方法(如写入 Header/数据库)
if (locale != null) {
response.setHeader("X-Language", locale.toLanguageTag());
}
}
@Override
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
@Override
public Locale getDefaultLocale() {
return this.defaultLocale;
}
}
// 注册为 Bean
@Configuration
public class WebConfig {
@Bean
public LocaleResolver localeResolver() {
CustomLocaleResolver resolver = new CustomLocaleResolver();
resolver.setDefaultLocale(Locale.CHINA);
return resolver;
}
}
配置区域拦截器
LocaleChangeInterceptor:预置拦截器,当区域信息存在于请求参数中时,重新解析Locale区域信息,例如用户手动切换语言
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean("localeResolver") // 名称固定为 localeResolver,优先级最高
public LocaleResolver acceptHeaderLocaleResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.US);
return resolver;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor(); // MVC内置Locale拦截器
localeInterceptor.setParamName("lang"); // 请求参数含有lang=xxx时拦截
registry.addInterceptor(localeInterceptor)
.addPathPatterns("/**")
.order(-1);
}
}
手动切换语言
-
描述:用户手动传递区域信息时(例如点击"语言切换"),覆盖浏览器默认传递的区域信息,后续请求会复用此区域信息
-
实现流程
- 区域信息拼接到URL中:
"/..?lang=en_US" - 后端
LocaleChangeInterceptor拦截器识别并拦截 - 使用
CookieLocaleResolver解析器,添加/修改区域信息,自动将cookies信息返回给前端
- 区域信息拼接到URL中:
-
技术选型
AcceptHeaderLocaleResolver不支持后端修改请求头,否则会抛异常UnsupportedOperationExceptionSessionLocaleResolver可以实现,但分布式场景下有Session失效问题,使用Redis储存部署成本高- 自定义
LocaleResolver是更灵活的方案 ,完全掌控 Locale 的解析逻辑(优先读请求头→再读 Cookie→...)
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthTokenInterceptor authTokenInterceptor;
@Bean("localeResolver")
public LocaleResolver localeResolver() {
// 入参即是区域信息对应的cookie key
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver("lang");
// 兜底策略:使用后端默认的 Locale,触发添加如下
// - 首次访问,浏览器无lang Cookie,且请求头Accept-Language为空/解析失败;
// - URL传参?lang=xxx是非法值(比如?lang=abc);
cookieLocaleResolver.setDefaultLocale(Locale.CHINA);
// 设置有效期后,会暂时更改浏览器的默认语言偏好
cookieLocaleResolver.setCookieMaxAge(Duration.ofHours(1L));
// Cookie作用域:全站生效(所有接口都能读取)
cookieLocaleResolver.setCookiePath("/");
// 允许前端JS读取Cookie(如需前端展示当前语言,设为false;否则设为true更安全)
cookieLocaleResolver.setCookieHttpOnly(false);
// CookieLocaleResolver 会自动处理 Cookie 的写入和返回,后端在响应时会自动往 HTTP 响应头中添加 Set-Cookie 字段
return cookieLocaleResolver;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 区域拦截器
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor)
.addPathPatterns("/**")
.order(-1);
}
}
配置国际化资源
创建资源文件
-
在
src/main/resources目录下创建i18n文件夹,并添加不同语言的资源文件,用于存储不同语言的文本内容.properties:Spring默认格式.json:Spring默认不支持,需通过自定义MessageSource实现.xml:不建议,XML解析时有性能损耗
-
资源文件命名规则:
basename+_language+_country+.properties,默认文件是basenem.properties- 先匹配
Accept-Language头指定的完整语言信息(Accept-Languag:en-US → messages_en_US.properties) - 只有语言没有地区也可以匹配(
Accept-Languag:en → messages_en.properties) - 没有任何信息,或者信息无效,则加载默认文件(
Accept-Languag:US → 无效格式 → messages.properties)
- 先匹配
-
常见国家地区描述符
- en_US
- zh_CN
- ja_JP
- fr_FR
properties
# messages.properties(默认语言,如英文)
welcome.message=Welcome, {0}! Today is {1,date,yyyy-MM-dd}
welcome.title=Login Page
# messages_zh_CN.properties(简体中文)
welcome.message=您好, {0}! 今天是 {1,date,yyyy-MM-dd}
welcome.title=欢迎页
json
// messages.json(默认语言,如英文)
{
"welcome": {
"message": "Welcome, {0}! Today is {1,date,yyyy-MM-dd}",
"title": "Welcome Page"
}
}
// messages_zh_CN.json(简体中文)
{
"welcome": {
"message": "您好, {0}! 今天是 {1,date,yyyy-MM-dd}",
"title": "欢迎页"
}
}
-
国际化资源文件支持使用占位符和格式符,并在代码中动态传入参数(见下文)
- 简单占位符:使用
{0},{1},{2}等数字占位符,数字代表参数的索引(从0开始) - 命名占位符:使用
{name}形式的命名占位符,需配合MessageFormat的扩展功能(需自定义配置) - 格式化数字和日期:在占位符后添加格式化规则(如日期格式、数字精度)
- 不建议在配置文件中格式化,而是在代码中先格式化好,再将字符串注入到配置文件的占位符中
- 简单占位符:使用
properties
# {0,number,currency}
# {0}:第一个参数(索引从0开始),对应代码中传入的 Object[] 数组的第一个值
# number:指定参数类型为数字
# currency:格式化规则,表示将数字渲染为货币格式(如 $1,234.56 或 €1.234,56,具体取决于 Locale)
##############################################################################################
# {1,number,0.##}%
# {1}:第二个参数(如税率 20.5)
# number:参数类型为数字
# 0.##:格式化规则,0表示显示整数部分,##表示最多显示两位小数,但不强制补零
# %表示在格式化后的数字后追加百分号
order.total=钱币格式化: {0,number,currency} , 数值格式化: {1,number,0.##}%
# {0}:第一个参数(索引从0开始),对应代码中传入的 Object[] 数组的第一个值
# date:指定参数类型为日期
# yyyy-MM-dd:格式化模式,表示将日期显示为 年-月-日
event.date=The event will start on {0,date,yyyy-MM-dd}
配置MessageSource
-
描述:将
MessageSource和国际化资源文件绑定,后续通过此接口获取国际化资源数据 -
MessageSource接口提供了三个实现类
ResourceBundleMessageSource:这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源ReloadableResourceBundleMessageSource:支持定时刷新,实现了在不重启系统的情况下更新资源StaticMessageSource:它允许通过编程的方式提供国际化信息,可以据此实现存储国际化信息到数据库
java
@Configuration
public class I18nConfig {
/*
* ReloadableResourceBundleMessageSource支持不停机更新
**/
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// 1. 指定国际化文件基础路径(核心),加载顺序:先加载 i18n/messages,再加载 i18n/errors
messageSource.setBasenames(
"classpath:i18n/messages", // 主文案文件(如 messages_zh_CN.properties)
"classpath:i18n/error" // 后配置的文件会覆盖先配置的同 Key 文案
);
// 2. 设置编码(必须,解决中文乱码)
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
// 3. 缓存时间(秒):开发环境设 0(实时刷新),生产环境设 3600(1小时)
messageSource.setCacheSeconds(0);
// 4. 默认语言(LocaleContextHolder找不到对应语言或者入参Locale找不到时,匹配US的资源文件)
messageSource.setDefaultLocale(Locale.US);
// 5. 关闭系统Locale兜底(避免服务器语言干扰)
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
/*
* 配置 MessageSourceAccessor:简化代码中获取消息的逻辑
* MessageSourceAccessor初始化时就可以设置默认Locale,后续调用 getMessage(key) 时无需手动传 Locale
* getMessage(key)没有指定 key 时,默认返回 key 本身(而非抛 NoSuchMessageException)
* key不存在时也可以自定义兜底逻辑,重写getMessage方法即可
*
**/
@Bean
public MessageSourceAccessor messageSourceAccessor(MessageSource messageSource) {
// 显式指定默认Locale为中文,更可控
return new MessageSourceAccessor(messageSource);
}
}
MessageSource适配Json格式
- Spring默认只支持properties文件,如果使用Json格式文件,则需要手动解析Json以获取信息
java
/**
* JSON 解析工具类
*/
public class JsonMessageLoader {
// 缓存不同语言的 JSON 节点
private final Map<Locale, JsonNode> localeJsonCache = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
private final String basePath;
public JsonMessageResourceLoader(String basePath) {
this.basePath = basePath;
// 初始化加载默认语言
loadLocale(Locale.CHINA);
}
// 加载指定语言的 JSON 文件
public void loadLocale(Locale locale) {
String filename = basePath + "_" + locale.toString() + ".json";
try {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource resource = resolver.getResource(filename);
if (resource.exists()) {
JsonNode jsonNode = objectMapper.readTree(resource.getInputStream());
localeJsonCache.put(locale, jsonNode);
}
} catch (IOException e) {
throw new RuntimeException("加载国际化 JSON 文件失败:" + filename, e);
}
}
// 获取指定 key 的消息(支持参数替换)
public String getMessage(String key, Locale locale, Object... args) {
// 优先用指定语言,无则用默认语言
JsonNode jsonNode = localeJsonCache.getOrDefault(locale, localeJsonCache.get(Locale.CHINA));
if (jsonNode == null) {
return key; // 找不到返回 key 本身
}
// 支持嵌套键,如 "user.login.success" 解析为 jsonNode.get("user").get("login").get("success")
String[] keyParts = key.split("\\.");
JsonNode currentNode = jsonNode;
for (String part : keyParts) {
currentNode = currentNode.get(part);
if (currentNode == null) {
return key;
}
}
// 替换参数(如 {0})
String message = currentNode.asText();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
message = message.replace("{" + i + "}", args[i].toString());
}
}
return message;
}
}
java
/**
* Spring默认不支持解析Json,需要手动实现 MessageSource 接口的 getMessage 方法
*/
@Component
public class JsonResourceBundleMessageSource implements MessageSource {
private JsonMessageResourceLoader resourceLoader;
// 初始化加载 JSON 资源
@Autowired
public CustomJsonMessageSource() {
this.resourceLoader = new JsonMessageResourceLoader("classpath:i18n/messages");
}
@Override
public String getMessage(String key, Object[] args, String defaultMessage, Locale locale) {
try {
return getMessage(key, args, locale);
} catch (NoSuchMessageException e) {
return defaultMessage;
}
}
@Override
public String getMessage(String key, Object[] args, Locale locale) throws NoSuchMessageException {
String message = resourceLoader.getMessage(key, locale, args);
if (message.equals(key)) {
throw new NoSuchMessageException(key, locale);
}
return message;
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
// 处理可解析的消息(如绑定异常),按需实现
String[] codes = resolvable.getCodes();
for (String code : codes) {
try {
return getMessage(code, resolvable.getArguments(), locale);
} catch (NoSuchMessageException e) {
continue;
}
}
throw new NoSuchMessageException(resolvable.getDefaultMessage(), locale);
}
}
StaticMessageSource
StaticMessageSource可以不依赖外部文件,可以直接从内存、数据库等方式加载对应信息
java
// 直接内存注入信息
@Configuration
public class MessageSourceConfig {
@Bean
public MessageSource messageSource() {
StaticMessageSource messageSource = new StaticMessageSource();
// 添加单条消息
messageSource.addMessage(
"welcome.message",
Locale.ENGLISH,
"Welcome, {0}!"
);
messageSource.addMessage(
"welcome.message",
Locale.CHINESE,
"欢迎,{0}!"
);
// 批量添加消息(通过Map)
Map<String, String> messages = new HashMap<>();
messages.put("error.notFound", "Resource not found");
messages.put("error.notFound", "资源未找到"); // 中文版本(需额外处理Locale)
// 注意:批量添加时需自行处理Locale,通常需调用多次addMessage或封装方法
return messageSource;
}
}
java
// 从数据库中加载信息
@Configuration
public class DatabaseMessageSource {
@Autowired
private MessageRepository messageRepository;
@Bean
public MessageSource messageSource() {
StaticMessageSource messageSource = new StaticMessageSource();
List<MessageEntity> messages = messageRepository.findAll();
for (MessageEntity message : messages) {
messageSource.addMessage(
message.getCode(),
new Locale(message.getLanguage()),
message.getText()
);
}
return messageSource;
}
}
国际化文本
- 创建资源文件
properties
test={0}:欢迎
test={0}:hello
MessageSource
| 参数 | 作用 | 示例 |
|---|---|---|
code |
国际化文案的唯一标识(对应 properties 文件中的 Key) | "user.welcome" |
args |
文案中的占位符参数(如 user.welcome=你好,{0}! 中的 {0}) |
new Object[]{"张三"} |
defaultMessage |
兜底默认值(找不到 code 时返回) | "欢迎访问系统" |
locale |
目标语言(如 Locale.CHINA/Locale.US) |
LocaleContextHolder.getLocale() |
java
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private MessageSource messageSource;
@GetMapping("/cn")
public String test1() {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage("test", new Object[]{locale.toString()}, locale);
}
}
MessageSourceAccessor
-
MessageSourceAccessor简化了国际化文本的获取过程- 可以自动入参
Locale,无需手动入参 - 资源文件中没有对应Key时,提供了降级机制
- 可以自动入参
java
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private MessageSourceAccessor messageSourceAccessor;
@GetMapping("/us")
public String test2() {
Locale locale = LocaleContextHolder.getLocale();
return messageSourceAccessor.getMessage("test",new Object[]{locale.toString()});
}
}
国际化时间
- 通过
DateFormat/DateTimeFormatter实现时间类型的格式转换
DateFormat
DateFormat处理Date类型数据DateFormat线程不安全,实例需每次重新创建或用ThreadLocal封装全局复用
| 方法 | 作用 | 示例 |
|---|---|---|
DateFormat.getDateInstance() |
默认日期格式(中等样式) | 美式:Oct 28, 2025 中式:2025年10月28日 |
DateFormat.getDateInstance(int style) |
指定样式: SHORT/MEDIUM/LONG/FULL |
FULL 样式:Friday, October 28, 2025 |
DateFormat.getDateInstance(int style, Locale locale) |
指定样式 + 地区 | 精准控制本地化格式 |
DateFormat.getTimeInstance() |
默认时间格式 | 美式:3:45:30 PM 中式:15:45:30 |
DateFormat.getDateTimeInstance() |
默认日期 + 时间格式 | 美式:Oct 28, 2025, 3:45:30 PM 中式:2025年10月28日 15:45:30 |
java
Date now = new Date();
// 1. 中文(中国)格式:中等样式
DateFormat cnDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
String cnDate = cnDateFormat.format(now);
System.out.println("中文日期:" + cnDate); // 输出:2025年10月28日
// 2. 英文(美国)格式:完整样式
DateFormat usDateFormat = DateFormat.getDateInstance(DateFormat.FULL, Locale.US);
String usDate = usDateFormat.format(now);
System.out.println("英文日期:" + usDate); // 输出:Friday, October 28, 2025
// 3. 日期+时间(中文)
DateFormat cnDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.CHINA);
String cnDateTime = cnDateTimeFormat.format(now);
System.out.println("中文日期时间:" + cnDateTime); // 输出:2025年10月28日 15:45:30
DateTimeFormatter
DateTimeFormatter处理LocalDateTime类型数据DateTimeFormatter是更现代的替代方案,线程安全,推荐优先使用
| 方法/构造方式 | 作用 |
|---|---|
DateTimeFormatter.ofLocalizedDate(FormatStyle style) |
本地化日期格式(仅日期部分) |
DateTimeFormatter.ofLocalizedDate(FormatStyle style).withLocale(Locale locale) |
指定样式 + 地区 |
DateTimeFormatter.ofLocalizedTime(FormatStyle style) |
本地化时间格式(仅时间部分) |
DateTimeFormatter.ofLocalizedDateTime(FormatStyle dateStyle, FormatStyle timeStyle) |
本地化日期 + 时间组合格式 |
DateTimeFormatter.ofPattern(String pattern) |
自定义格式模式 |
DateTimeFormatter.ofPattern(String pattern, Locale locale) |
自定义模式 + 地区 |
java
LocalDateTime now = LocalDateTime.now();
// 中式日期(MEDIUM 样式)
DateTimeFormatter cnDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.CHINA);
System.out.println("中文日期:" + now.format(cnDateFormatter)); // 输出:2025年10月28日
// 英文日期(FULL 样式)
DateTimeFormatter enDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.US);
System.out.println("英文日期:" + now.format(enDateFormatter)); // 输出:Tuesday, October 28, 2025
// 中式日期时间(MEDIUM 样式)
DateTimeFormatter cnDateFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM)
.withLocale(Locale.CHINA);
System.out.println("中文日期时间:" + now.format(cnDateFormatter)); // 输出:2025年10月28日 15:45:30
// 自定义模式(年-月-日 时:分:秒)
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss", Locale.CHINA);
System.out.println("自定义格式:" + now.format(customFormatter)); // 输出:2025年10月28日 15:45:30
国际化数字/货币
- 通过
DateFormat/DateTimeFormatter实现数值/货币类型的格式转换
NumberFormat
NumberFormat线程不安全,实例需每次重新创建或用ThreadLocal封装全局复用
| 方法 | 作用 | 示例(Locale.US/ Locale.CHINA) |
|---|---|---|
NumberFormat.getNumberInstance() |
默认数字格式 | 美式:123,456.789 中式:123,456.789(数字分隔符通用) |
NumberFormat.getNumberInstance(Locale locale) |
指定地区数字格式 | 适配不同地区的小数 / 千分位规则 |
NumberFormat.getCurrencyInstance() |
货币格式 | 美式:$123,456.79 中式:¥123,456.79 |
NumberFormat.getPercentInstance() |
百分比格式 | 美式 / 中式:78.9%(仅符号位置差异) |
java
// 美式数字格式(千分位分隔)
NumberFormat usNumFormat = NumberFormat.getNumberInstance(Locale.US);
usNumFormat.setMaximumFractionDigits(2); // 保留2位小数
String usNum = usNumFormat.format(num);
System.out.println(usNum); // 输出:123,456.79(四舍五入)
// 中式数字格式(同美式,但适配其他地区时差异明显)
NumberFormat cnNumFormat = NumberFormat.getNumberInstance(Locale.CHINA);
cnNumFormat.setMinimumFractionDigits(3); // 至少保留3位小数
String cnNum = cnNumFormat.format(num);
System.out.println(cnNum); // 输出:123,456.789
// 人民币(中国)
NumberFormat cnCurrency = NumberFormat.getCurrencyInstance(Locale.CHINA);
String cnMoney = cnCurrency.format(amount);
System.out.println(cnMoney); // 输出:¥123,456.79
// 美元(美国)
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
String usMoney = usCurrency.format(amount);
System.out.println(usMoney); // 输出:$123,456.79
// 德国百分比格式
NumberFormat percentFormat = NumberFormat.getPercentInstance(Locale.GERMAN);
percentFormat.setMaximumFractionDigits(1); // 保留1位小数
String percent = percentFormat.format(0.789d);
System.out.println(percent); // 输出:78,9 %
NumberFormatter
NumberFormatter基于NumberFormat,仍为非线程安全,如果需要全局复用建议ThreadLocal封装
java
// 推荐:ThreadLocal 封装(线程安全)
private static final ThreadLocal<NumberFormatter> CURRENCY_FORMATTER = ThreadLocal.withInitial(() -> {
NumberFormat format = NumberFormat.getCurrencyInstance(Locale.CHINA);
return new NumberFormatter(format);
});
NumberFormatter既兼容NumberFormat的本地化能力,又补充了类型安全、解析 / 格式化统一、与 Swing/JFX 绑定等特性
| 方法 | 作用 |
|---|---|
NumberFormatter(NumberFormat format) |
基于本地化规则初始化 |
NumberFormatter(String pattern) |
基于自定义格式串初始化 |
format(Object obj) |
数字 → 本地化字符串 |
parse(String text) |
字符串 → Number 对象 |
parse(String text, ParsePosition pos) |
容错解析(不抛异常) |
setValueClass(Class<?> cls) |
指定解析结果类型 |
setLocale(Locale locale) |
指定本地化规则 |
setParseIntegerOnly(boolean parseIntegerOnly) |
仅解析整数 |
setLenient(boolean lenient) |
宽松解析模式 |
setFormat(NumberFormat newFormat) |
动态切换格式化规则 |
java
// 1. 初始化:人民币格式(强制解析为 BigDecimal,避免精度丢失)
NumberFormat cnCurrency = NumberFormat.getCurrencyInstance(Locale.CHINA);
NumberFormatter currencyFormatter = new NumberFormatter(cnCurrency);
currencyFormatter.setValueClass(BigDecimal.class); // 指定解析类型
// 2. 格式化 BigDecimal(推荐处理货币)
BigDecimal amount = new BigDecimal("999999.99");
String cnMoney = currencyFormatter.format(amount);
System.out.println("人民币格式化:" + cnMoney); // 输出:¥999,999.99
// 3. 解析人民币字符串为 BigDecimal
String inputMoney = "¥999,999.99";
BigDecimal parsedMoney = (BigDecimal) currencyFormatter.parse(inputMoney);
System.out.println("解析后金额:" + parsedMoney); // 输出:999999.99
// 4. 切换为美元格式
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
currencyFormatter.setFormat(usCurrency); // 动态切换格式
String usMoney = currencyFormatter.format(amount);
System.out.println("美元格式化:" + usMoney); // 输出:$999,999.99