Skip to content

Spring MVC 国际化机制详解:让你的应用支持全球用户 🌐

什么是国际化?为什么需要它?

想象一下,你开发了一个优秀的电商网站,但只支持中文。当美国用户访问时看到满屏的中文,德国用户看到的时间格式是"2024年1月1日"而不是他们熟悉的"01.01.2024",这会让用户感到困惑并可能流失。

NOTE

国际化(Internationalization,简称 i18n) 是指让应用程序能够适应不同语言、地区和文化的过程。Spring MVC 提供了强大的国际化支持,让你的应用能够自动识别用户的语言偏好并提供相应的本地化体验。

Spring MVC 国际化的核心原理

Spring MVC 的国际化机制基于一个简单而强大的设计哲学:自动检测用户的语言偏好,并在整个请求处理过程中保持这个上下文信息

TIP

核心思想:DispatcherServlet 就像一个智能的"翻译官",它会在每个请求到来时自动判断用户的语言偏好,然后确保整个处理链路都知道这个信息。

LocaleResolver:语言偏好的侦探 🔍

LocaleResolver 是 Spring MVC 国际化的核心接口,它负责从请求中"侦探"出用户的语言偏好。Spring 提供了多种"侦探"策略:

1. Header Resolver:从浏览器头部获取

这是最常见的方式,浏览器会自动发送用户的语言偏好。

kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
    
    @Bean
    fun localeResolver(): LocaleResolver {
        // 使用浏览器的 Accept-Language 头部
        return AcceptHeaderLocaleResolver().apply {
            supportedLocales = listOf(
                Locale.CHINESE,
                Locale.ENGLISH,
                Locale("ja", "JP") // 日语
            )
            defaultLocale = Locale.CHINESE 
        }
    }
}
kotlin
@RestController
class GreetingController {
    
    @GetMapping("/greeting")
    fun greeting(request: HttpServletRequest): String {
        val locale = RequestContextUtils.getLocale(request) 
        
        return when (locale.language) {
            "zh" -> "你好!欢迎访问我们的网站"
            "en" -> "Hello! Welcome to our website"
            "ja" -> "こんにちは!私たちのウェブサイトへようこそ"
            else -> "Hello! Welcome to our website"
        }
    }
}

WARNING

Header Resolver 有个限制:用户无法主动切换语言,因为它完全依赖浏览器设置。

当你希望用户能够主动选择语言并记住这个选择时,Cookie Resolver 是最佳选择。

kotlin
@Configuration
class LocaleConfig {
    
    @Bean
    fun localeResolver(): CookieLocaleResolver {
        return CookieLocaleResolver().apply {
            cookieName = "user_language"
            cookieMaxAge = 30 * 24 * 60 * 60 // 30天
            cookiePath = "/"
            defaultLocale = Locale.CHINESE
        }
    }
    
    @Bean
    fun localeChangeInterceptor(): LocaleChangeInterceptor {
        return LocaleChangeInterceptor().apply {
            paramName = "lang"
        }
    }
    
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(localeChangeInterceptor())
    }
}
kotlin
@Controller
class LanguageSwitchController {
    
    @GetMapping("/switch-language")
    fun switchLanguage(
        @RequestParam lang: String,
        request: HttpServletRequest,
        response: HttpServletResponse
    ): String {
        // 通过拦截器自动处理语言切换
        // URL: /switch-language?lang=en
        return "redirect:/"
    }
}
html
<!-- 语言切换按钮 -->
<div class="language-switcher">
    <a href="?lang=zh_CN">中文</a>
    <a href="?lang=en_US">English</a>
    <a href="?lang=ja_JP">日本語</a>
</div>

3. Session Resolver:会话级别的语言设置

适合需要在用户会话期间保持语言设置,但不需要长期记住的场景。

kotlin
@Configuration
class SessionLocaleConfig {
    
    @Bean
    fun localeResolver(): SessionLocaleResolver {
        return SessionLocaleResolver().apply {
            defaultLocale = Locale.CHINESE
        }
    }
}

IMPORTANT

选择建议

  • 🌐 Header Resolver:适合简单应用,无需用户主动切换语言
  • 🍪 Cookie Resolver:适合大多数 Web 应用,用户体验最佳
  • 📝 Session Resolver:适合对隐私要求较高的场景

时区支持:不只是语言,还有时间 ⏰

除了语言,时区也是国际化的重要组成部分。想象一个全球化的电商平台,订单时间显示必须符合用户当地时区。

kotlin
@RestController
class OrderController {
    
    @GetMapping("/orders/{id}")
    fun getOrder(@PathVariable id: Long, request: HttpServletRequest): OrderResponse {
        val order = orderService.findById(id)
        val timeZone = RequestContextUtils.getTimeZone(request) 
        
        return OrderResponse(
            id = order.id,
            // 自动转换为用户时区
            createdAt = order.createdAt.atZone(timeZone ?: ZoneId.systemDefault()), 
            status = order.status
        )
    }
}

data class OrderResponse(
    val id: Long,
    val createdAt: ZonedDateTime,
    val status: String
)

实战案例:构建多语言电商网站

让我们通过一个完整的例子来看看如何在实际项目中应用这些概念:

完整的多语言电商配置示例
kotlin
@Configuration
@EnableWebMvc
class InternationalizationConfig : WebMvcConfigurer {
    
    // 1. 配置语言解析器
    @Bean
    fun localeResolver(): LocaleResolver {
        return CookieLocaleResolver().apply {
            cookieName = "LOCALE_COOKIE"
            cookieMaxAge = 365 * 24 * 60 * 60 // 一年
            defaultLocale = Locale.CHINESE
            cookiePath = "/"
        }
    }
    
    // 2. 配置语言切换拦截器
    @Bean
    fun localeChangeInterceptor(): LocaleChangeInterceptor {
        return LocaleChangeInterceptor().apply {
            paramName = "locale"
            ignoreInvalidLocale = true
        }
    }
    
    // 3. 配置消息源
    @Bean
    fun messageSource(): MessageSource {
        return ReloadableResourceBundleMessageSource().apply {
            setBasename("classpath:messages/messages")
            setDefaultEncoding("UTF-8")
            setCacheSeconds(60) // 开发时设置较短,生产环境可以更长
        }
    }
    
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(localeChangeInterceptor())
    }
}

// 产品控制器
@RestController
@RequestMapping("/api/products")
class ProductController(
    private val messageSource: MessageSource
) {
    
    @GetMapping("/{id}")
    fun getProduct(
        @PathVariable id: Long,
        request: HttpServletRequest
    ): ProductResponse {
        val locale = RequestContextUtils.getLocale(request)
        val timeZone = RequestContextUtils.getTimeZone(request)
        
        val product = productService.findById(id)
        
        return ProductResponse(
            id = product.id,
            name = getLocalizedProductName(product, locale), 
            description = getLocalizedDescription(product, locale), 
            price = formatPrice(product.price, locale), 
            createdAt = product.createdAt.atZone(timeZone ?: ZoneId.systemDefault())
        )
    }
    
    private fun getLocalizedProductName(product: Product, locale: Locale): String {
        return messageSource.getMessage(
            "product.name.${product.id}", 
            null, 
            product.defaultName, 
            locale
        ) 
    }
    
    private fun getLocalizedDescription(product: Product, locale: Locale): String {
        return messageSource.getMessage(
            "product.description.${product.id}", 
            null, 
            product.defaultDescription, 
            locale
        )
    }
    
    private fun formatPrice(price: BigDecimal, locale: Locale): String {
        val formatter = NumberFormat.getCurrencyInstance(locale)
        return formatter.format(price) 
    }
}

data class ProductResponse(
    val id: Long,
    val name: String,
    val description: String,
    val price: String,
    val createdAt: ZonedDateTime
)

最佳实践与注意事项

1. 性能优化建议

kotlin
@Component
class LocaleAwareService {
    
    // 缓存本地化消息,避免重复查询
    private val messageCache = ConcurrentHashMap<String, String>() 
    
    fun getLocalizedMessage(key: String, locale: Locale): String {
        val cacheKey = "${key}_${locale.toLanguageTag()}"
        return messageCache.computeIfAbsent(cacheKey) { 
            messageSource.getMessage(key, null, locale)
        }
    }
}

2. 错误处理

kotlin
@ControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(LocaleNotSupportedException::class)
    fun handleUnsupportedLocale(
        ex: LocaleNotSupportedException,
        request: HttpServletRequest
    ): ResponseEntity<ErrorResponse> {
        val locale = RequestContextUtils.getLocale(request)
        val message = messageSource.getMessage(
            "error.locale.not.supported", 
            arrayOf(ex.requestedLocale), 
            locale
        ) 
        
        return ResponseEntity.badRequest()
            .body(ErrorResponse(message))
    }
}

3. 测试策略

kotlin
@SpringBootTest
class LocaleIntegrationTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @Test
    fun `should return Chinese content when locale is zh_CN`() {
        mockMvc.perform(
            get("/api/products/1")
                .cookie(Cookie("LOCALE_COOKIE", "zh_CN")) 
        )
        .andExpect(status().isOk)
        .andExpect(jsonPath("$.name").value("智能手机"))
    }
    
    @Test
    fun `should return English content when locale is en_US`() {
        mockMvc.perform(
            get("/api/products/1")
                .cookie(Cookie("LOCALE_COOKIE", "en_US")) 
        )
        .andExpect(status().isOk)
        .andExpect(jsonPath("$.name").value("Smartphone"))
    }
}

总结:国际化的价值与意义 🎯

Spring MVC 的国际化机制不仅仅是技术实现,更是一种用户体验哲学

TIP

核心价值

  • 🌍 全球化支持:让你的应用能够服务全球用户
  • 🔄 灵活切换:用户可以随时切换到熟悉的语言环境
  • 自动化处理:框架自动处理复杂的本地化逻辑
  • 🎨 开发友好:简单的配置即可实现强大的国际化功能

通过合理使用 LocaleResolver、LocaleChangeInterceptor 和 MessageSource,你可以构建出真正面向全球用户的应用程序。记住,好的国际化不仅仅是翻译文字,更要考虑文化差异、时区处理和用户习惯。

现在,你已经掌握了 Spring MVC 国际化的核心概念和实践方法。是时候让你的应用走向世界了! 🚀