如何让Spring将Wrapper类透明代理为其content属性,处理属性路径访问问题?
你的问题本质是要让Spring的属性访问机制(包括ConstraintViolation路径解析)能够自动"穿透"你的Wrapper<T>包装类,直接访问其content属性的内部字段。下面提供两种可行的实现思路:
方案1:自定义SpEL PropertyAccessor(推荐)
Spring在处理属性路径(比如解析ConstraintViolation的属性路径时)通常会使用SpEL表达式引擎。我们可以通过自定义PropertyAccessor来告诉SpEL:当遇到Wrapper类型的对象时,自动将属性访问转发到其content字段,甚至可以直接跳过路径中对应的Wrapper节点。
步骤1:实现自定义PropertyAccessor
import org.springframework.expression.PropertyAccessor import org.springframework.expression.TypedValue import org.springframework.expression.EvaluationContext import kotlin.reflect.full.memberProperties class WrapperPropertyAccessor : PropertyAccessor { // 定义当前Accessor支持的类型:所有Wrapper的子类 override fun getSpecificTargetClasses(): MutableList<Class<*>> { return mutableListOf(Wrapper::class.java) } // 检查是否支持访问目标对象的指定属性 override fun canRead(context: EvaluationContext, target: Any, name: String): Boolean { val wrapper = target as Wrapper<*> // 两种情况:要么直接访问content,要么访问content内部的属性 return name == "content" || wrapper.content?.let { it::class.memberProperties.any { prop -> prop.name == name } } ?: false } // 读取属性值:如果是Wrapper,自动穿透到content再读取属性 override fun read(context: EvaluationContext, target: Any, name: String): TypedValue { val wrapper = target as Wrapper<*> return if (name == "content") { TypedValue(wrapper.content) } else { // 递归让SpEL处理content内部的属性 context.getPropertyAccessors() .firstOrNull { it != this && it.canRead(context, wrapper.content!!, name) } ?.let { it.read(context, wrapper.content!!, name) } ?: throw IllegalArgumentException("Cannot access property '$name' on Wrapper content") } } // Wrapper是只读数据类,这里不需要实现写操作 override fun canWrite(context: EvaluationContext, target: Any, name: String): Boolean { return false } override fun write(context: EvaluationContext, target: Any, name: String, newValue: Any?) { throw UnsupportedOperationException("Wrapper is immutable") } }
步骤2:注册自定义PropertyAccessor到Spring上下文
需要将这个PropertyAccessor注册到Spring的SpEL表达式上下文里。可以通过ApplicationContextInitializer来全局注册:
import org.springframework.context.ApplicationContextInitializer import org.springframework.context.ConfigurableApplicationContext import org.springframework.expression.spel.support.StandardEvaluationContext import org.springframework.validation.beanvalidation.SpringValidatorAdapter class WrapperPropertyAccessorInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> { override fun initialize(context: ConfigurableApplicationContext) { // 获取Spring的Validator适配器,它内部使用SpEL处理属性路径 val validator = context.getBean(SpringValidatorAdapter::class.java) val evaluationContext = validator.evaluationContext as StandardEvaluationContext // 将自定义Accessor添加到最前面,确保优先被使用 evaluationContext.addPropertyAccessor(WrapperPropertyAccessor()) } }
然后在application.properties中注册这个Initializer:
context.initializer.classes=com.yourpackage.WrapperPropertyAccessorInitializer
方案2:自定义BeanWrapper(适合更底层的属性访问场景)
如果你的场景中Spring直接使用BeanWrapper来访问属性(而不是SpEL),可以自定义BeanWrapper的实现,让它自动穿透Wrapper:
import org.springframework.beans.BeanWrapperImpl class WrapperAwareBeanWrapper(target: Any) : BeanWrapperImpl(target) { override fun getPropertyValue(propertyName: String): Any? { val currentTarget = wrappedObject return if (currentTarget is Wrapper<*>) { // 如果当前对象是Wrapper,先获取content,再递归处理剩余路径 val content = currentTarget.content if (content == null) { null } else { val remainingPath = propertyName.substringAfter('.', "") if (remainingPath.isEmpty()) { content } else { WrapperAwareBeanWrapper(content).getPropertyValue(remainingPath) } } } else { super.getPropertyValue(propertyName) } } }
然后在需要使用BeanWrapper的地方(比如处理ConstraintViolation的代码中),替换成这个自定义的实现。如果要全局替换Spring默认的BeanWrapper,需要自定义BeanWrapperFactory,复杂度较高,因此更推荐方案1。
额外提示:修正验证器生成的路径
如果你的自定义验证器生成的路径是name.first而不是name.content.first.content,也可以在验证完成后手动修正ConstraintViolation的路径:
import javax.validation.ConstraintViolation fun <T> fixWrapperViolationPaths(violations: Set<ConstraintViolation<T>>): Set<ConstraintViolation<T>> { return violations.map { violation -> val originalPath = violation.propertyPath.toString() // 将路径中的每个节点对应Wrapper的部分插入.content val fixedPath = originalPath.split('.').joinToString(".") { segment -> "$segment.content" }.removeSuffix(".content") // 处理最后一个节点不需要追加的情况 // 这里可以通过自定义ConstraintViolation包装类替换路径,或者用反射修改原对象(不推荐) violation }.toSet() }
不过这种事后修正的方式,不如前面两种方案从根源上解决问题来得优雅。
内容的提问来源于stack exchange,提问作者Aleksandar Dimitrov

