为什么 MyBatis-Plus 的 ServiceImpl<Mapper, Entity> 需要同时指定两个泛型?
在使用 MyBatis-Plus 开发过程中,你可能遇到过这样一个写法:
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture> implements PictureService {
// 业务代码
}
其中 PictureMapper 继承了 BaseMapper<Picture>,而 Picture 是实体类。这里有一个常见疑问:
既然
PictureMapper已经指定了实体类型Picture,为什么ServiceImpl还要额外显式指定Picture?不能自动从PictureMapper泛型中推断出来吗?
本文将从 Java 泛型机制和 MyBatis-Plus 设计角度,帮你全面解析这个问题。
一、Java 泛型擦除导致无法自动推断
Java 的泛型在编译时会做类型擦除(Type Erasure),这意味着:
- 编译生成的字节码中,不包含泛型参数的具体类型信息。
- 运行时无法通过反射直接获得泛型参数类型。
举例来说:
public interface PictureMapper extends BaseMapper<Picture> { }
在运行时,PictureMapper 类的泛型参数 Picture 已经被擦除,变成了 BaseMapper 的原始类型,没法反射得到具体类型。
二、ServiceImpl<M, T> 的设计必须明确两个泛型
MyBatis-Plus 中 ServiceImpl 定义如下(简化):
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected M baseMapper;
public T getById(Serializable id) {
return baseMapper.selectById(id);
}
// 其他 CRUD 方法...
}
这里:
M是 Mapper 类型,要求继承BaseMapper<T>。T是实体类型,ServiceImpl需要知道实体类型,才能正确实现接口IService<T>的方法签名,比如:
T getById(Serializable id);
boolean save(T entity);
如果不显式指定 T,编译器无法确认这些方法的参数和返回类型。
三、为什么不能只指定 M,由它自动推断 T?
理论上,如果 Java 泛型推断更智能,ServiceImpl<M extends BaseMapper<?>> 可以通过反射获得泛型参数类型。但实际上:
- Java 反射在运行时拿不到
M的泛型参数T。 - 编译器也无法通过泛型链自动推断
T。 - 泛型擦除限制了泛型的自动推断能力。
因此,MyBatis-Plus 设计时只能显式让开发者告诉它 T 是什么。
四、设计权衡带来的好处
这种写法的好处:
- 类型安全:显式声明保证编译器校验准确,避免类型混淆。
- IDE 支持友好:自动补全和类型提示更准确。
- 代码可读性高:一目了然哪个 Mapper 操作哪个实体。
- 扩展性强:方便二次开发和定制业务逻辑。
五、类比说明
可以类比成:
快递公司(
ServiceImpl)需要知道“快递员是谁”(Mapper)和“快递是什么东西”(实体),虽然快递员负责快递,但公司不能仅凭快递员的身份就推断快递类型,必须明确告知两者对应关系。
六、源码中如何通过反射获取实体类(辅助功能)
虽然无法编译时自动推断,但 ServiceImpl 里通常会写辅助方法反射获取泛型参数:
@SuppressWarnings("unchecked")
protected Class<T> currentModelClass() {
Type genType = getClass().getGenericSuperclass();
if (!(genType instanceof ParameterizedType)) {
return (Class<T>) Object.class;
}
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
return (Class<T>) params[1];
}
这要求子类必须显式写出泛型参数。
对这个问题是不是有更好的理解呢