Java11使用Dozer自定义BeanMapper出现泛型capture编译错误如何修复
请注意: 尽管本问题中提及Dozer框架,但问题本质属于纯Java泛型范畴。或许存在Dozer专属的解决方案,但只要是熟练掌握Java(11)泛型、通配符捕获、类型擦除相关知识的开发者均可以提供帮助。
当前技术栈为Java 11与Dozer:Dozer非常适合基于字段名匹配应用默认Bean映射规则,但当需要实现特殊的自定义映射逻辑时,必须实现Dozer的CustomConverter接口并完成注册。该方案的问题在于Dozer的CustomConverter API未做泛型化设计,属于单体式结构,会产生如下臃肿的代码:
public class MyMonolithicConverter implements CustomConverter { @Override public Object convert(Object destination, Object source, Class<?> destinationClass, Class<?> sourceClass) { if (sourceClass.isAssignableFrom(Widget.class)) { Widget widget = (Widget)source; if (destinationClass.isAssignableFrom(Fizz.class)) { Fizz fizz = (Fizz)destination; // 此处编写widget -> fizz的映射逻辑 } else if (destinationClass.isAssignableFrom(Foo.class)) { // 此处编写widget -> foo的映射逻辑 } ... 其他分支 } else if (sourceClass.isAssignableFrom(Baz.class)) { // 此处编写baz -> 所有目标类型的if-else-if分支与映射逻辑 } } }
如上所示:这种实现是单体结构、无泛型支持,会产生大量复杂的嵌套if-else-if代码块,可维护性极差。
我尝试对该实现进行优化,提升代码易用性,首先定义泛型抽象映射基类:
public abstract class BeanMapper<SOURCE,TARGET> { private Class<SOURCE> sourceClass; private Class<TARGET> targetClass; public abstract TARGET map(SOURCE source); public boolean matches(Class<?> otherSourceClass, Class<?> otherTargetClass) { return sourceClass.equals(otherSourceClass) && targetClass.equals(otherTargetClass); } }
以下是该抽象类的使用示例:
public class SignUpRequestToAccountMapper extends BeanMapper<SignUpRequest, Account> { private PlaintextEncrypter encrypter; public SignUpRequestToAccountMapper(PlaintextEncrypter encrypter) { this.encrypter = encrypter; } @Override public Account map(SignUpRequest signUpRequest) { return Account.builder() .username(signUpRequest.getRequestedName()) .email(signUpRequest.getEmailAddr()) .givenName(signUpRequest.getFirstName()) .surname(signUpRequest.getLastName()) .dob(DateUtils.toDate(signUpRequest.getBirthDate())) .passwordEnc(encrypter.saltPepperAndEncrypt(signUpRequest.getPasswordPlaintext())) .build(); } }
之后我实现了在Dozer转换器中匹配并调用对应源-目标Mapper的逻辑:
public class DozerConverter implements CustomConverter { private Set<BeanMapper> beanMappers; @Override public Object convert(Object destination, Object source, Class<?> destinationClass, Class<?> sourceClass) { BeanMapper<?,?> mapper = beanMappers.stream() .filter(beanMapper -> beanMapper.matches(sourceClass, destinationClass)) .findFirst() .orElseThrow(); // 此处出现编译错误: return mapper.map(source); } }
我非常认可这个设计/API方案的简洁性,但在最后mapper.map(source)行出现了编译错误:
Required type: capture of ?; Provided: Object
我并不强制要求必须使用当前的API/方案,但相比Dozer原生强制的MyMonolithicConverter实现方式,我更偏好当前设计的简洁性。需要说明的是,我已经在项目其他位置使用Dozer完成简单Bean映射,因此更倾向于通过实现CustomConverter复用Dozer的能力,而非引入其他依赖/库来处理这些自定义复杂映射。如果Dozer本身提供其他解决方案我也可以接受,核心诉求是修复这个泛型通配符捕获(capture)问题。
报错的核心原因是通配符泛型的类型不确定性:当变量声明为BeanMapper<?,?>时,编译器无法确认两个通配符代表的具体类型,既不允许传入Object类型的source参数,也无法验证调用的类型安全性。可以通过泛型辅助方法做通配符捕获解决,同时需要补全原代码中缺失的类型字段赋值逻辑,否则匹配规则永远无法生效。
第一步:补全BeanMapper构造逻辑
原BeanMapper中存储源/目标类型的字段没有赋值入口,matches方法永远无法正常匹配,需要补充构造方法要求子类传入具体类型的Class令牌:
public abstract class BeanMapper<SOURCE,TARGET> { private final Class<SOURCE> sourceClass; private final Class<TARGET> targetClass; // 新增构造方法,子类初始化时必须传入源/目标类型 public BeanMapper(Class<SOURCE> sourceClass, Class<TARGET> targetClass) { this.sourceClass = sourceClass; this.targetClass = targetClass; } public abstract TARGET map(SOURCE source); public boolean matches(Class<?> otherSourceClass, Class<?> otherTargetClass) { return sourceClass.equals(otherSourceClass) && targetClass.equals(otherTargetClass); } }
对应的子类Mapper需要调整构造方法,传入类型令牌:
public class SignUpRequestToAccountMapper extends BeanMapper<SignUpRequest, Account> { private PlaintextEncrypter encrypter; public SignUpRequestToAccountMapper(PlaintextEncrypter encrypter) { super(SignUpRequest.class, Account.class); this.encrypter = encrypter; } @Override public Account map(SignUpRequest signUpRequest) { return Account.builder() .username(signUpRequest.getRequestedName()) .email(signUpRequest.getEmailAddr()) .givenName(signUpRequest.getFirstName()) .surname(signUpRequest.getLastName()) .dob(DateUtils.toDate(signUpRequest.getBirthDate())) .passwordEnc(encrypter.saltPepperAndEncrypt(signUpRequest.getPasswordPlaintext())) .build(); } }
第二步:添加通配符捕获辅助方法
在DozerConverter中增加私有泛型方法,通过类型参数绑定捕获通配符的实际类型,完成类型安全的转换调用:
public class DozerConverter implements CustomConverter { private Set<BeanMapper<?, ?>> beanMappers; @Override public Object convert(Object destination, Object source, Class<?> destinationClass, Class<?> sourceClass) { BeanMapper<?,?> mapper = beanMappers.stream() .filter(beanMapper -> beanMapper.matches(sourceClass, destinationClass)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("未找到匹配的映射器: " + sourceClass + " -> " + destinationClass)); return doMap(mapper, sourceClass, source); } /** * 泛型辅助方法捕获通配符实际类型,匹配逻辑已保证类型一致,强转安全 */ @SuppressWarnings("unchecked") private <S, T> T doMap(BeanMapper<S, T> mapper, Class<?> sourceClass, Object source) { S typedSource = (S) sourceClass.cast(source); return mapper.map(typedSource); } }
该方案完全保留了原设计的简洁性,不需要引入额外依赖,所有自定义映射逻辑都可以拆分为独立的BeanMapper实现,无需再维护臃肿的嵌套if-else分支。Class.cast()做类型转换比直接强转更严谨,能在转换前做类型校验,避免隐式类型转换异常。
内容的提问来源于stack exchange,提问作者hotmeatballsoup

