Spring国际化

国际化

  • 描述:跨国家地区的业务场景下,需要系统向前端返回地区相适配的数据

    1. 语言国际化(i18n):使应用支持多语言和地区文化需求,无需修改源代码
    2. 数据本地化(l10n):根据特定地区调整应用内容,如日期、货币格式
  • 国际化触发条件

    1. 默认情况下,根据浏览器的偏好信息选择语言
    2. 用户也可以手动点击语言,主动切换语言
  • Spring框架提供了国际化功能

    1. LocaleResolver:MVC区域解析器,在DispatcherServlet初始化时就自动解析Locale区域信息
    2. LocaleChangeInterceptor:MVC预置拦截器,重新解析Locale区域信息,用于用户手动切换语言场景
    3. MessageSource:加载资源文件接口,可以根据Locale区域加载预置的资源

获取Locale信息

  • LocaleResolverLocaleResolver 解析 → 存入 LocaleContextHolder → 业务层从 LocaleContextHolder 获取
  • LocaleChangeInterceptorLocaleChangeInterceptorMVC拦截 → LocaleResolver 解析 → 存入 LocaleContextHolder

配置区域解析器

  • LocaleResolver:MVC区域解析器,在DispatcherServlet初始化时就自动解析Locale区域信息,有多个实现类

    1. AcceptHeaderLocaleResolver(默认):从 HTTP 请求头 Accept-Language 中读取 Locale
    2. SessionLocaleResolver:优先读取Session中的区域信息,若未切换过,则自动解析请求头 Accept-Language 作为初始值
    3. 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-LanguageCookieSessionRequestParam中,可以自定义解析器
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);
    }
}

手动切换语言

  • 描述:用户手动传递区域信息时(例如点击"语言切换"),覆盖浏览器默认传递的区域信息,后续请求会复用此区域信息

  • 实现流程

    1. 区域信息拼接到URL中:"/..?lang=en_US"
    2. 后端LocaleChangeInterceptor拦截器识别并拦截
    3. 使用CookieLocaleResolver解析器,添加/修改区域信息,自动将cookies信息返回给前端
  • 技术选型

    1. AcceptHeaderLocaleResolver不支持后端修改请求头,否则会抛异常UnsupportedOperationException
    2. SessionLocaleResolver可以实现,但分布式场景下有Session失效问题,使用Redis储存部署成本高
    3. 自定义 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文件夹,并添加不同语言的资源文件,用于存储不同语言的文本内容

    1. .properties :Spring默认格式
    2. .json:Spring默认不支持,需通过自定义MessageSource实现
    3. .xml:不建议,XML解析时有性能损耗
  • 资源文件命名规则: basename + _language + _country + .properties,默认文件是basenem.properties

    1. 先匹配 Accept-Language 头指定的完整语言信息(Accept-Languag:en-US → messages_en_US.properties
    2. 只有语言没有地区也可以匹配(Accept-Languag:en → messages_en.properties
    3. 没有任何信息,或者信息无效,则加载默认文件( Accept-Languag:US → 无效格式 → messages.properties
  • 常见国家地区描述符

    1. en_US
    2. zh_CN
    3. ja_JP
    4. 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": "欢迎页"
  }
}
  • 国际化资源文件支持使用占位符和格式符,并在代码中动态传入参数(见下文)

    1. 简单占位符:使用 {0}, {1}, {2} 等数字占位符,数字代表参数的索引(从0开始)
    2. 命名占位符:使用 {name} 形式的命名占位符,需配合 MessageFormat 的扩展功能(需自定义配置)
    3. 格式化数字和日期:在占位符后添加格式化规则(如日期格式、数字精度)
    4. 不建议在配置文件中格式化,而是在代码中先格式化好,再将字符串注入到配置文件的占位符中
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接口提供了三个实现类

    1. ResourceBundleMessageSource:这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
    2. ReloadableResourceBundleMessageSource:支持定时刷新,实现了在不重启系统的情况下更新资源
    3. 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简化了国际化文本的获取过程

    1. 可以自动入参Locale,无需手动入参
    2. 资源文件中没有对应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
相关推荐
weixin_307779131 小时前
Jenkins ASM API 插件:详解与应用指南
java·运维·开发语言·后端·jenkins
程序员爱钓鱼1 小时前
Node.js 与前端 JavaScript 的区别:不仅仅是“运行环境不同”
后端·node.js
程序员爱钓鱼1 小时前
用 Go 做浏览器自动化?chromedp 带你飞!
后端·go·trae
ByteX1 小时前
springboot 项目某个接口响应特别慢排查
java·spring boot·后端
哈哈哈笑什么1 小时前
全面拆解离线→同步的10大核心问题【落地的完整方案(思路+架构+代码)】
后端
Java水解1 小时前
[Spring] Spring配置文件
后端·spring
稳住别浪1 小时前
DRF框架认证底层源码解析——简单易理解!
后端
马卡巴卡1 小时前
SpringBoot项目使用Redis对用户IP进行接口限流
后端
VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue酒店预约系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计