You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让SpEL表达式支持@JsonProperty指定的JSON字段名?

解决方案

要让SpEL支持通过@JsonProperty指定的JSON字段名访问对象属性,核心是实现自定义PropertyAccessor,让SpEL解析时能将JSON字段名映射到实际的Kotlin类成员变量名。以下是完整实现方案,支持嵌套对象且性能友好:

1. 自定义JsonPropertyAwarePropertyAccessor

这个Accessor会自动扫描类的@JsonProperty注解,建立JSON字段名与实际属性名的映射,并在SpEL求值时完成转换,同时支持嵌套对象的递归解析。

import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.expression.AccessException
import org.springframework.expression.EvaluationContext
import org.springframework.expression.PropertyAccessor
import org.springframework.expression.TypedValue
import org.springframework.expression.spel.support.ReflectivePropertyAccessor
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap

class JsonPropertyAwarePropertyAccessor : ReflectivePropertyAccessor() {
    // 缓存类的JSON字段名 -> 属性名映射,避免重复反射解析
    private val jsonPropertyMappingCache = ConcurrentHashMap<Class<*>, Map<String, String>>()

    override fun getProperty(context: EvaluationContext, target: Any?, name: String): TypedValue {
        target ?: return TypedValue.NULL
        val targetClass = target.javaClass
        // 获取当前类的JSON字段映射
        val propertyMapping = getJsonPropertyMapping(targetClass)
        // 查找实际属性名:如果name是JSON字段名,替换为实际属性名;否则直接用原name
        val actualPropertyName = propertyMapping[name] ?: name
        return try {
            // 调用父类的反射逻辑获取属性值,自动支持嵌套属性
            super.getProperty(context, target, actualPropertyName)
        } catch (e: AccessException) {
            // 如果原name不是JSON字段,也不是属性名,抛出原异常
            throw e
        }
    }

    override fun canRead(context: EvaluationContext, target: Any?, name: String): Boolean {
        target ?: return false
        val targetClass = target.javaClass
        val propertyMapping = getJsonPropertyMapping(targetClass)
        val actualPropertyName = propertyMapping[name] ?: name
        return super.canRead(context, target, actualPropertyName)
    }

    // 解析类的@JsonProperty注解,建立JSON字段到属性名的映射
    private fun getJsonPropertyMapping(clazz: Class<*>): Map<String, String> {
        return jsonPropertyMappingCache.computeIfAbsent(clazz) {
            val mapping = mutableMapOf<String, String>()
            // 遍历所有字段(包括父类)
            var currentClass: Class<*>? = clazz
            while (currentClass != null && currentClass != Any::class.java) {
                currentClass.declaredFields.forEach { field: Field ->
                    val jsonProperty = field.getAnnotation(JsonProperty::class.java)
                    jsonProperty?.let {
                        mapping[it.value] = field.name
                    }
                    // 同时处理getter方法上的@JsonProperty(如果有的话)
                    val getterMethod = try {
                        currentClass.getMethod("get${field.name.replaceFirstChar { it.uppercaseChar() }}")
                    } catch (_: NoSuchMethodException) {
                        // 处理布尔类型的is开头getter
                        try {
                            currentClass.getMethod("is${field.name.replaceFirstChar { it.uppercaseChar() }}")
                        } catch (_: NoSuchMethodException) {
                            null
                        }
                    }
                    getterMethod?.let { method ->
                        val methodJsonProperty = method.getAnnotation(JsonProperty::class.java)
                        methodJsonProperty?.let {
                            mapping[it.value] = field.name
                        }
                    }
                }
                currentClass = currentClass.superclass
            }
            mapping
        }
    }
}

2. 在SpEL求值中使用自定义Accessor

修改你的testSpEL方法,创建StandardEvaluationContext并添加自定义的PropertyAccessor,这样SpEL就能识别JSON字段名了:

import org.springframework.expression.spel.standard.SpelExpressionParser
import org.springframework.expression.spel.support.StandardEvaluationContext
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class TestController {

    data class Person(
        @JsonProperty("first_name")
        var firstName: String,
        @JsonProperty("last_name")
        var lastName: String,
        @JsonProperty("age")
        var age: Int,
        @JsonProperty("contact_info")
        var contactInfo: ContactInfo
    )

    data class ContactInfo(
        @JsonProperty("phone_number")
        var phoneNumber: String,
        @JsonProperty("city_name")
        var cityName: String
    )

    @GetMapping("/test")
    fun testSpEL(): ResponseEntity<Boolean> {
        val person = Person(
            "Tom",
            "Black",
            26,
            ContactInfo("123456789", "New York")
        )

        val parser = SpelExpressionParser()
        // 创建StandardEvaluationContext并添加自定义Accessor
        val context = StandardEvaluationContext(person)
        context.addPropertyAccessor(JsonPropertyAwarePropertyAccessor())

        // 使用JSON字段名的表达式,包括嵌套属性
        val expression = parser.parseExpression("age > 18 && first_name == 'Tom' && contact_info.city_name == 'New York'")
        val result = expression.getValue(context, Boolean::class.java)

        println("##################### $result")

        return ResponseEntity(result, HttpStatus.OK)
    }
}

3. 关键特性说明

  • 自动映射JSON字段名:通过扫描@JsonProperty注解,将first_name这类JSON字段名自动转换为Kotlin的firstName属性名。
  • 支持嵌套对象:由于继承了ReflectivePropertyAccessor,父类的递归属性访问逻辑会自动处理嵌套对象(如contact_info.city_name会先解析contact_info到contactInfo,再解析city_name到cityName)。
  • 性能优化:用ConcurrentHashMap缓存类的字段映射,避免每次求值都反射解析注解,提升性能。
  • 用户友好:最终用户可以直接使用JSON风格的字段名编写表达式,无需关心后端类的成员变量命名。

内容的提问来源于stack exchange,提问作者kinglite

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 15:47:27