Spring泛型依赖注入机制解析

小豆丁 3天前 ⋅ 13 阅读
聊天记录
头像

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> { protected Log log = LogFactory.getLog(this.getClass()); @Autowired protected M baseMapper; 这里Spring 可以M可以正确注入吗?Spring是怎么处理的?

头像

这是一个很好的问题!Spring 可以正确注入 M baseMapper,但需要满足一些条件。让我详细解释一下 Spring 的处理机制:

Spring 的泛型依赖注入机制

1. 类型擦除与泛型信息保留

虽然 Java 在运行时会有类型擦除,但 Spring 通过反射可以获取到泛型的具体类型信息:

JAVA
// 示例:具体的实现类
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {
    // Spring 可以通过反射获取到泛型信息:
    // M = UserMapper, T = User
}

2. Spring 的处理方式

Spring 会:

  1. 通过反射获取实际类的泛型参数
  2. 根据泛型类型在容器中查找匹配的 Bean
  3. 进行依赖注入
JAVA
// Spring 内部大致逻辑(简化)
Class<?> actualClass = userServiceImpl.getClass();
Type genericSuperclass = actualClass.getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
    Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
    // 获取到 M 的具体类型:UserMapper
    Class<?> mapperClass = (Class<?>) actualTypeArguments[0];
    // 从容器中获取 UserMapper 类型的 Bean
    Object mapperBean = applicationContext.getBean(mapperClass);
}

3. 必要条件

要使注入正常工作,需要:

a. 有具体的实现类:

JAVA
// 必须有一个具体的子类指定泛型参数
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    implements IUserService<User> {
    // 此时 Spring 知道 M = UserMapper, T = User
}

b. 对应的 Mapper 必须被 Spring 管理:

JAVA
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 必须被 @Mapper 扫描到或被 @Repository 标记
}

c. 不能直接使用泛型类:

JAVA
// 这样会失败 - Spring 无法确定具体的泛型类型
@Service
public class MyService extends ServiceImpl<BaseMapper<SomeEntity>, SomeEntity> {
    // 缺少具体的 Mapper 类型
}

4. 完整的正确示例

JAVA
// 实体类
@Data
public class User {
    private Long id;
    private String name;
}

// Mapper 接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 自定义方法
}

// 具体的服务实现
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    implements IUserService<User> {
    
    // 此时 Spring 能正确注入 UserMapper
    // baseMapper 的实际类型是 UserMapper
}

// 使用
@RestController
public class UserController {
    @Autowired
    private UserServiceImpl userService; // 正常使用
}

5. 常见问题排查

如果注入失败,检查以下几点:

  • 是否有具体的子类指定了泛型参数
  • Mapper 接口是否被正确扫描(@Mapper 或 @MapperScan)
  • 泛型参数是否正确对应
  • 是否存在多个相同类型的 Bean 导致歧义

总结:Spring 可以正确注入,但依赖于具体的子类来提供泛型类型信息。这是 MyBatis-Plus 等框架能够正常工作的基础机制。

头像

通过反射可以获取到泛型的具体类型信息?@autowied private ServiceImpl<UserMapper, User> service; service可以获取类型参数吗?

头像

答案是不行。让我详细解释为什么:

1. 字段声明 vs 实际实例的泛型信息

JAVA
@Autowired 
private ServiceImpl<UserMapper, User> service;

在这种情况下:

  • 字段声明确实有泛型信息:ServiceImpl&lt;UserMapper, User&gt;
  • 实际注入的实例的泛型信息在运行时被擦除了

2. 类型擦除的实际影响

JAVA
// 编译时:有完整的泛型信息
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
}

// 运行时:类型被擦除
UserService userService = new UserService();
// 运行时只知道是 UserService,不知道父类的泛型参数

3. 验证示例

JAVA
@RestController
public class TestController {
    
    @Autowired
    private ServiceImpl<UserMapper, User> userService;
    
    @GetMapping("/test")
    public void test() {
        // 方法1:通过字段获取泛型信息(可以)
        Field field = this.getClass().getDeclaredField("userService");
        Type genericType = field.getGenericType();
        
        if (genericType instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) genericType;
            Type[] actualTypes = pt.getActualTypeArguments();
            // 这里可以获取到 UserMapper 和 User
            System.out.println("M: " + actualTypes[0]); // UserMapper
            System.out.println("T: " + actualTypes[1]); // User
        }
        
        // 方法2:通过实例获取泛型信息(不可以)
        Class<?> runtimeClass = userService.getClass();
        Type genericSuperclass = runtimeClass.getGenericSuperclass();
        
        // 这里获取到的是 ServiceImpl<M, T>,不是具体的 UserMapper, User
        // 因为运行时类型信息被擦除了
    }
}

4. 关键区别

方式 能否获取具体泛型 原因
字段声明 ✅ 可以 编译时保留了泛型信息
运行时实例 ❌ 不可以 类型擦除,只有原始类型

5. 实际场景演示

JAVA
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
}

@RestController
public class DemoController {
    
    // 情况1:字段声明有具体泛型
    @Autowired
    private ServiceImpl<UserMapper, User> userService;
    
    // 情况2:字段声明使用原始类型
    @Autowired
    private ServiceImpl rawService;
    
    @PostConstruct
    public void checkGenericInfo() throws Exception {
        // 检查字段声明的泛型
        Field field = DemoController.class.getDeclaredField("userService");
        Type fieldType = field.getGenericType();
        System.out.println("字段类型: " + fieldType); 
        // 输出: ServiceImpl<UserMapper, User>
        
        // 检查运行时实例的泛型
        Type instanceType = userService.getClass().getGenericSuperclass();
        System.out.println("实例父类: " + instanceType);
        // 输出: ServiceImpl<M, T> (类型擦除后的)
        
        // 获取实际的泛型参数(需要特殊处理)
        if (userService.getClass().getGenericSuperclass() instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) userService.getClass().getGenericSuperclass();
            Type[] types = pt.getActualTypeArguments();
            // 这里获取到的是 M, T 类型变量,不是具体的 UserMapper, User
        }
    }
}

6. 如何正确获取实际类型

如果需要获取实际注入的类型,需要通过具体子类

JAVA
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
    // 这里可以获取到具体的泛型信息
    public void printActualTypes() {
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) genericSuperclass;
            Type[] types = pt.getActualTypeArguments();
            // 此时 types[0] = UserMapper, types[1] = User
        }
    }
}

总结:通过字段声明可以获取泛型信息,但通过运行时实例无法获取具体的泛型参数,因为类型擦除机制。

头像

什么情况可以获取泛型参数,什么时候不可以?不是说泛型被擦除了,怎么又可以获取????

头像

问得非常好!这正是很多人的困惑点。让我彻底澄清这个"泛型擦除"的误解:

1. 泛型擦除的真正含义

泛型擦除是指:运行时对象实例中不包含泛型信息

JAVA
// 编译时:有完整的类型信息
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 运行时:类型被擦除
// stringList 和 integerList 在运行时都是原始的 ArrayList
// JVM 不知道它们原本的泛型参数

2. 什么时候可以获取泛型参数?

在声明处(编译时保留的信息)可以获取:

情况1:类/接口的泛型声明

JAVA
// 声明一个具体类,继承泛型父类
public class UserService extends ServiceImpl<UserMapper, User> {
    // 这个类的字节码中会保留泛型信息
}

// 可以获取:
Class<UserService> clazz = UserService.class;
Type genericSuperclass = clazz.getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
    ParameterizedType pt = (ParameterizedType) genericSuperclass;
    Type[] actualTypes = pt.getActualTypeArguments();
    // actualTypes[0] = UserMapper, actualTypes[1] = User
}

情况2:字段/方法的泛型声明

JAVA
public class SomeClass {
    private List<String> stringList;  // 字段声明处的泛型信息保留
    
    public List<Integer> getNumbers() {  // 方法返回类型的泛型信息保留
        return new ArrayList<>();
    }
}

// 可以获取字段的泛型信息
Field field = SomeClass.class.getDeclaredField("stringList");
Type fieldType = field.getGenericType();  // 得到 List<String>

情况3:匿名内部类

JAVA
// 匿名类在创建时就确定了泛型类型
ServiceImpl<UserMapper, User> service = new ServiceImpl<UserMapper, User>() {};

// 可以获取泛型信息,因为匿名类在字节码中有具体类型
Type genericSuperclass = service.getClass().getGenericSuperclass();

3. 什么时候不能获取泛型参数?

在运行时对象实例中不能获取:

情况1:对象实例本身

JAVA
UserService userService = new UserService();

// 这个对象实例在堆内存中不包含泛型信息
// 只能知道它是 UserService 类型,不知道父类的泛型参数

情况2:原始类型的使用

JAVA
// 使用原始类型,丢失泛型信息
ServiceImpl rawService = new UserService();
// 无法获取具体的泛型参数

情况3:局部变量

JAVA
public void someMethod() {
    List<String> localList = new ArrayList<>();
    // 局部变量的泛型信息在运行时不可获取
}

4. 关键区别总结

场景 能否获取泛型 原因
类/接口声明 ✅ 可以 字节码的 Signature 属性保存了泛型信息
字段/方法声明 ✅ 可以 编译时保留了声明处的泛型信息
对象实例 ❌ 不可以 运行时类型擦除,堆内存中不包含泛型
局部变量 ❌ 不可以 编译后泛型信息丢失

5. 技术原理:Signature 属性

Java 编译器在编译时会在字节码中添加特殊属性:

JAVA
// 编译后的 .class 文件包含:
public class UserService extends ServiceImpl<UserMapper, User>
// ↓ 编译后 ↓
// Signature: Lcom/example/ServiceImpl<Lcom/example/UserMapper;Lcom/example/User;>;

// 这个 Signature 属性在运行时可以通过反射读取

6. 实际验证示例

JAVA
import java.lang.reflect.*;

public class GenericTest {
    
    static class BaseService<M, T> {}
    
    static class UserService extends BaseService<String, Integer> {}
    
    public static void main(String[] args) {
        UserService service = new UserService();
        
        // 情况1:通过类声明获取(可以)
        System.out.println("=== 通过类声明获取 ===");
        Type genericSuperclass = UserService.class.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) genericSuperclass;
            Type[] types = pt.getActualTypeArguments();
            System.out.println("泛型参数: " + Arrays.toString(types));
            // 输出: [class java.lang.String, class java.lang.Integer]
        }
        
        // 情况2:通过对象实例获取(不可以)
        System.out.println("=== 通过对象实例获取 ===");
        // 对象实例本身没有泛型信息,只能获取其类信息
        Class<?> runtimeClass = service.getClass();
        System.out.println("运行时类: " + runtimeClass);
        
        // 但可以通过类信息再次获取声明处的泛型
        Type superclass = runtimeClass.getGenericSuperclass();
        System.out.println("父类类型: " + superclass);
    }
}

7. Spring 依赖注入的原理

JAVA
// Spring 的工作方式:
// 1. 扫描到 UserService extends ServiceImpl<UserMapper, User>
// 2. 通过反射读取 UserService 类的泛型信息
// 3. 发现需要注入 UserMapper 类型的 Bean
// 4. 从容器中查找并注入 UserMapper

// 注意:Spring 是通过分析类声明,而不是分析对象实例!

总结

  • 声明处的泛型信息被保留在字节码的 Signature 属性中,可以通过反射获取
  • 运行时对象实例中的泛型信息被真正擦除,无法获取
  • Spring 依赖注入是利用了类声明处的泛型信息,而不是对象实例的信息

这就是为什么看似"泛型被擦除"了,但我们仍然能获取到泛型参数的原因!

全部评论: 0

    我有话说: