Appearance
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 有个限制:用户无法主动切换语言,因为它完全依赖浏览器设置。
2. Cookie 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 国际化的核心概念和实践方法。是时候让你的应用走向世界了! 🚀