如何让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
相关产品推荐
相关产品推荐

