Appearance
Spring MVC 类型转换:让数据在不同格式间自由穿梭 🔄
🎯 什么是类型转换?为什么需要它?
想象一下这样的场景:用户在网页表单中输入了一个日期 "2024-01-15"
,但你的 Java 代码需要的是 LocalDate
对象。或者用户输入了价格 "99.99"
,而你需要的是 BigDecimal
类型。这就是类型转换要解决的核心问题!
IMPORTANT
类型转换的本质:在 Web 应用中,HTTP 请求中的所有数据都是字符串形式,但我们的业务逻辑需要各种 Java 类型。类型转换就是这两者之间的"翻译官"。
🏗️ Spring MVC 的类型转换体系
内置支持的类型转换
Spring MVC 默认已经为我们准备了丰富的类型转换器:
开箱即用的转换器
- 数字类型:String ↔ Integer, Long, Double, BigDecimal 等
- 日期时间:String ↔ LocalDate, LocalDateTime, Date 等
- 布尔值:String ↔ Boolean
- 枚举类型:String ↔ Enum
注解驱动的格式化
Spring 提供了三个强大的注解来控制格式化:
kotlin
data class OrderRequest(
@NumberFormat(pattern = "#,###.##") // 支持千分位格式
val price: BigDecimal,
@DateTimeFormat(pattern = "yyyy-MM-dd") // 自定义日期格式
val orderDate: LocalDate,
@DurationFormat(unit = ChronoUnit.MINUTES) // 时长格式化
val processingTime: Duration
)
🛠️ 自定义类型转换器
场景:处理自定义的商品编码
假设我们有一个特殊的商品编码格式:PROD-12345-ELECTRONICS
,需要转换为自定义的 ProductCode
对象。
kotlin
// 自定义的商品编码类
data class ProductCode(
val prefix: String, // PROD
val id: Long, // 12345
val category: String // ELECTRONICS
) {
override fun toString(): String = "$prefix-$id-$category"
companion object {
fun parse(code: String): ProductCode {
val parts = code.split("-")
require(parts.size == 3) { "Invalid product code format: $code" }
return ProductCode(
prefix = parts[0],
id = parts[1].toLong(),
category = parts[2]
)
}
}
}
kotlin
// 自定义转换器
@Component
class ProductCodeConverter : Converter<String, ProductCode> {
override fun convert(source: String): ProductCode {
return try {
ProductCode.parse(source)
} catch (e: Exception) {
throw IllegalArgumentException("无法解析商品编码: $source", e)
}
}
}
注册自定义转换器
kotlin
@Configuration
class WebConfiguration : WebMvcConfigurer {
@Autowired
private lateinit var productCodeConverter: ProductCodeConverter
override fun addFormatters(registry: FormatterRegistry) {
// 注册自定义转换器
registry.addConverter(productCodeConverter)
// 也可以直接注册 Lambda 表达式
registry.addConverter<String, ProductCode> { source ->
ProductCode.parse(source)
}
}
}
在 Controller 中使用
kotlin
@RestController
@RequestMapping("/api/products")
class ProductController {
// 路径参数自动转换
@GetMapping("/{productCode}")
fun getProduct(@PathVariable productCode: ProductCode): ResponseEntity<Product> {
// productCode 已经是 ProductCode 对象了!
return ResponseEntity.ok(productService.findByCode(productCode))
}
// 请求参数自动转换
@GetMapping
fun searchProducts(@RequestParam code: ProductCode): List<Product> {
return productService.findSimilar(code)
}
}
📅 日期时间处理的最佳实践
处理 HTML5 日期输入
当使用 HTML5 的 <input type="date">
时,浏览器会发送固定的 ISO 格式日期。
kotlin
@Configuration
class DateTimeWebConfiguration : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar().apply {
setUseIsoFormat(true)
// 这将使用 ISO 格式:yyyy-MM-dd
}
registrar.registerFormatters(registry)
}
}
多种日期格式支持
kotlin
@Configuration
class FlexibleDateConfiguration : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// 支持多种日期格式
registry.addFormatter(object : Formatter<LocalDate> {
private val formatters = listOf(
DateTimeFormatter.ofPattern("yyyy-MM-dd"),
DateTimeFormatter.ofPattern("dd/MM/yyyy"),
DateTimeFormatter.ofPattern("MM-dd-yyyy")
)
override fun parse(text: String, locale: Locale): LocalDate {
for (formatter in formatters) {
try {
return LocalDate.parse(text, formatter)
} catch (e: DateTimeParseException) {
// 尝试下一个格式
continue
}
}
throw IllegalArgumentException("无法解析日期: $text")
}
override fun print(date: LocalDate, locale: Locale): String {
return date.format(formatters[0]) // 使用第一个格式输出
}
})
}
}
🔧 高级应用:自定义格式化注解
创建货币格式化注解
kotlin
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class CurrencyFormat(
val currency: String = "CNY"
)
实现格式化器工厂
kotlin
@Component
class CurrencyFormatterFactory : AnnotationFormatterFactory<CurrencyFormat> {
override fun getFieldTypes(): Set<Class<*>> {
return setOf(BigDecimal::class.java)
}
override fun getPrinter(annotation: CurrencyFormat, fieldType: Class<*>): Printer<BigDecimal> {
return Printer { value, locale ->
val currency = Currency.getInstance(annotation.currency)
val format = NumberFormat.getCurrencyInstance(locale)
format.currency = currency
format.format(value)
}
}
override fun getParser(annotation: CurrencyFormat, fieldType: Class<*>): Parser<BigDecimal> {
return Parser { text, locale ->
// 移除货币符号,只保留数字
val cleanText = text.replace(Regex("[^0-9.]"), "")
BigDecimal(cleanText)
}
}
}
使用自定义注解
kotlin
data class PriceRequest(
@CurrencyFormat(currency = "USD")
val price: BigDecimal,
@CurrencyFormat(currency = "CNY")
val originalPrice: BigDecimal
)
⚠️ 常见问题与解决方案
问题1:转换失败时的优雅处理
kotlin
@ControllerAdvice
class ConversionExceptionHandler {
@ExceptionHandler(ConversionFailedException::class)
fun handleConversionError(ex: ConversionFailedException): ResponseEntity<ErrorResponse> {
val error = ErrorResponse(
code = "CONVERSION_ERROR",
message = "数据格式错误: ${ex.value} 无法转换为 ${ex.targetType.simpleName}",
timestamp = LocalDateTime.now()
)
return ResponseEntity.badRequest().body(error)
}
}
问题2:性能优化
TIP
对于频繁使用的转换器,考虑添加缓存机制:
kotlin
@Component
class CachedProductCodeConverter : Converter<String, ProductCode> {
private val cache = ConcurrentHashMap<String, ProductCode>()
override fun convert(source: String): ProductCode {
return cache.computeIfAbsent(source) {
ProductCode.parse(it)
}
}
}
🎉 总结
Spring MVC 的类型转换系统为我们提供了强大而灵活的数据转换能力:
✅ 开箱即用:内置常用类型的转换器
✅ 注解驱动:通过注解轻松控制格式化
✅ 高度可扩展:支持自定义转换器和格式化器
✅ 性能优秀:转换过程高效,支持缓存优化
NOTE
类型转换不仅仅是技术实现,更是用户体验的重要组成部分。合理的类型转换策略能让你的 API 更加友好和健壮!
通过掌握这些技能,你就能让数据在字符串和 Java 对象之间自由穿梭,为用户提供更好的交互体验! 🚀