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

如何为ProxyProjectionFactory添加自定义转换器支持接口式投影?

问题描述

升级至Spring Boot 3.2及对应的Hibernate 6.2后,数据库枚举值的列类型默认从integer变为smallint。我们将数据库列改为smallint后,Hibernate会将其映射为short类型,这导致包含枚举的接口式投影出现转换异常:

示例投影接口:

public interface MyProjection { 
  MyEnum getEnum();
}

抛出的错误:

java.lang.UnsupportedOperationException: Cannot project java.lang.Short to com.bbraun.nemo.enums.MyEnum; Target type is not an interface and no matching Converter found

at org.springframework.data.projection.ProjectingMethodInterceptor.potentiallyConvertResult(ProjectingMethodInterceptor.java:110)
at org.springframework.data.projection.ProjectingMethodInterceptor.invoke(ProjectingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.projection.ProxyProjectionFactory$TargetAwareMethodInterceptor.invoke(ProxyProjectionFactory.java:243)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:249)

已尝试的无效方案

编写了Short转枚举的自定义转换器工厂:

@Component
final class ShortToEnumConverterFactory implements ConverterFactory<Short, Enum> {

  @Override
  public <T extends Enum> Converter<Short, T> getConverter(Class<T> targetType) {
    return new ShortToEnum(targetType);
  }

  private static class ShortToEnum<T extends Enum> implements Converter<Short, T> {

    private final Class<T> enumType;

    public ShortToEnum(Class<T> enumType) {
      this.enumType = enumType;
    }

    @Override
    public T convert(Short source) {
      return this.enumType.getEnumConstants()[source];
    }
  }
}

并通过两种方式注册:

  1. 在配置类中添加到FormatterRegistry:
@Configuration
...

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverterFactory(new ShortToEnumConverterFactory());   
  }

...
  1. 添加到DefaultConversionService共享实例:
DefaultConversionService sharedInstance = (DefaultConversionService)DefaultConversionService.getSharedInstance();
sharedInstance.addConverterFactory(new ShortToEnumConverterFactory());

以上操作均无效,原因是ProxyProjectionFactory内部使用了自身创建的静态DefaultConversionService实例,而非共享实例,导致自定义转换器无法被加载。

解决方案

提供几种可行的解决思路:

方案1:自定义ProxyProjectionFactory并注入转换器

创建自定义的ProxyProjectionFactory,通过反射将自定义转换器添加到它的内部静态转换服务,再注册为Spring Bean:

@Configuration
public class ProjectionConfig {

    @Bean
    public ProjectionFactory projectionFactory() {
        ProxyProjectionFactory factory = new ProxyProjectionFactory();
        try {
            Field conversionServiceField = ProxyProjectionFactory.class.getDeclaredField("CONVERSION_SERVICE");
            conversionServiceField.setAccessible(true);
            GenericConversionService conversionService = (GenericConversionService) conversionServiceField.get(null);
            conversionService.addConverterFactory(new ShortToEnumConverterFactory());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException("Failed to add converter to ProxyProjectionFactory's conversion service", e);
        }
        return factory;
    }
}

注意:该方案依赖反射访问私有静态字段,若Spring Data Commons后续修改字段定义可能失效。

方案2:强制Hibernate将枚举映射为Integer

在枚举字段上显式指定Hibernate类型为Integer,即使数据库列是smallint,也会以Integer类型映射,避免Short到枚举的转换问题:

@Entity
public class MyEntity {

    @Enumerated(EnumType.ORDINAL)
    @Column(columnDefinition = "smallint")
    @Type(type = "org.hibernate.type.IntegerType")
    private MyEnum myEnum;

    // 其他字段和方法
}

方案3:改用类式投影

替换接口式投影为类式投影,Spring Data会直接通过构造函数或Setter注入值,无需依赖投影转换服务:

public class MyProjection { 
  private final MyEnum myEnum;

  public MyProjection(MyEnum myEnum) {
    this.myEnum = myEnum;
  }

  public MyEnum getEnum() {
    return myEnum;
  }
}

方案4:通过BeanPostProcessor扩展转换服务

实现BeanPostProcessor,在ProxyProjectionFactory初始化后,将自定义转换器添加到它的内部转换服务:

@Component
public class ProjectionConversionServicePostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ProxyProjectionFactory) {
            try {
                Field conversionServiceField = ProxyProjectionFactory.class.getDeclaredField("CONVERSION_SERVICE");
                conversionServiceField.setAccessible(true);
                GenericConversionService conversionService = (GenericConversionService) conversionServiceField.get(null);
                conversionService.addConverterFactory(new ShortToEnumConverterFactory());
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RuntimeException("Failed to customize ProxyProjectionFactory conversion service", e);
            }
        }
        return bean;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 13:42:03