Skip to content

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 对象之间自由穿梭,为用户提供更好的交互体验! 🚀