为何ProjectionFactory将null转为空Optional?升级Spring Boot后如何恢复旧行为
问题根因
Spring Data Commons 从2.4.0版本开始(对应Spring Boot 2.4.x及更高版本),修改了SpelAwareProxyProjectionFactory的默认处理逻辑:当投影接口的方法返回类型为Optional时,会自动将底层查询返回的null值包装为Optional.empty(),旧版本不会做这个自动包装,这就是升级后两种场景返回结果一致的根本原因,官方没有提供直接关闭这个行为的配置项。
解决方案
方案1:自定义投影工厂(推荐,侵入性最小)
通过自定义ProjectionFactory重写Optional类型的转换逻辑,仅当原始返回值非空时才做Optional包装,空值直接保留null:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import java.util.Optional; @Configuration public class CustomProjectionConfig { @Bean public SpelAwareProxyProjectionFactory projectionFactory() { return new SpelAwareProxyProjectionFactory() { @Override protected Object convertResult(Object result, Class<?> targetType) { // 目标类型为Optional且原始结果为null时,直接返回null不做包装 if (targetType == Optional.class && result == null) { return null; } // 其他情况走原有默认逻辑 return super.convertResult(result, targetType); } }; } }
如果上述配置没有生效,说明你的项目使用了自定义的Repository工厂,需要将自定义投影工厂注入到RepositoryFactory中:
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import jakarta.persistence.EntityManager; public class CustomJpaRepositoryFactoryBean<T, ID> extends JpaRepositoryFactoryBean<T, ID> { private final SpelAwareProxyProjectionFactory projectionFactory; public CustomJpaRepositoryFactoryBean(Class<T> repositoryInterface, SpelAwareProxyProjectionFactory projectionFactory) { super(repositoryInterface); this.projectionFactory = projectionFactory; } @Override protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager); factory.setProjectionFactory(projectionFactory); return factory; } }
然后在启动类上指定使用自定义的Repository工厂Bean:
@SpringBootApplication @EnableJpaRepositories(repositoryFactoryBeanClass = CustomJpaRepositoryFactoryBean.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
方案2:替换Optional为自定义包装类
如果不想修改Spring的核心组件逻辑,可以自定义一个和Optional功能类似的包装类,Spring不会对自定义类型做自动空包装,用法和原Optional一致,仅需要修改投影接口的返回类型即可:
// 自定义包装类 public class NullableWrapper<T> { private final T value; private NullableWrapper(T value) { this.value = value; } public static <T> NullableWrapper<T> of(T value) { return new NullableWrapper<>(value); } public static <T> NullableWrapper<T> empty() { return new NullableWrapper<>(null); } public T get() { return value; } public boolean isPresent() { return value != null; } } // 投影接口修改为 public interface MyProjection { @JsonInclude(JsonInclude.Include.NON_NULL) NullableWrapper<String> getMyAttribute(); }
注意事项
自定义SpelAwareProxyProjectionFactory依赖Spring Data的内部非公开API,后续升级Spring Boot版本时需要验证该方法的兼容性。
内容的提问来源于stack exchange,提问作者Steffen Bobek
相关产品推荐
相关产品推荐

