Java依赖注入:如何通过Provider获取子类或合并多个Provider
你现在的场景是:抽象类Foo有三个子类FooA、FooB、FooC,都通过相同构造函数实现依赖注入,目前用javax.inject.Provider分别获取这三个子类的实例,想把这些Provider合并成一个Provider<Foo>,或者找到其他方式消除多个Provider的冗余。
刚好之前碰到过类似的场景,给你几个可行的方案参考:
方案1:用限定符+工厂类封装(最通用的方式)
直接用Provider<Foo>的话,DI容器没法知道该返回哪个子类实例,所以我们需要给每个子类打上限定标记,再用一个工厂类来统一管理这些Provider,这样代码里只需要注入工厂类就行。
步骤1:定义限定符注解
先写一个自定义的限定符,用来标记不同的Foo子类:
@Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface FooType { String value(); // 用来区分不同子类,比如"A"、"B"、"C" }
步骤2:给子类添加限定符
给每个Foo子类加上这个注解,明确标记类型:
@FooType("A") public class FooA extends Foo { public FooA(Dependency requiredDep) { // 假设构造函数需要注入Dependency super(requiredDep); } } @FooType("B") public class FooB extends Foo { public FooB(Dependency requiredDep) { super(requiredDep); } } // FooC同理,加上@FooType("C")
步骤3:实现Foo工厂类
这个工厂类会自动收集所有带@FooType标记的Foo子类Provider,然后提供统一的获取方法:
public class FooFactory { private final Map<String, Provider<Foo>> fooProviderMap; // 注入所有Foo类型的实例,通过DI容器的Instance API收集带限定符的Provider @Inject public FooFactory(@Any Instance<Foo> fooInstance) { this.fooProviderMap = fooInstance.select(new FooTypeLiteral()) .stream() .collect(Collectors.toMap( instance -> instance.getAnnotation(FooType.class).value(), Instance::provider )); } // 自定义Literal类,用来匹配@FooType注解类型 private static class FooTypeLiteral extends AnnotationLiteral<FooType> implements FooType { @Override public String value() { return ""; // 这里只需要匹配注解类型,实际值由子类的注解提供 } } // 对外提供获取指定子类实例的方法 public Foo getFoo(String type) { Provider<Foo> provider = fooProviderMap.get(type); if (provider == null) { throw new IllegalArgumentException("找不到对应的Foo类型:" + type); } return provider.get(); } }
之后你只需要在业务代码里注入FooFactory,通过fooFactory.getFoo("A")就能拿到FooA的实例,不用再维护多个Provider变量了。
方案2:直接注入Provider Map(依赖DI容器支持)
如果用的是Guice、CDI这类支持集合注入的容器,你可以直接注入一个Map来管理所有Provider,不用自己写工厂类:
以Guice为例:
先在模块里绑定子类并指定命名:
public class FooModule extends AbstractModule { @Override protected void configure() { bind(Foo.class).annotatedWith(Names.named("A")).to(FooA.class); bind(Foo.class).annotatedWith(Names.named("B")).to(FooB.class); bind(Foo.class).annotatedWith(Names.named("C")).to(FooC.class); } }
然后直接注入Map<Named, Provider<Foo>>:
public class YourService { private final Map<Named, Provider<Foo>> fooProviders; @Inject public YourService(Map<Named, Provider<Foo>> fooProviders) { this.fooProviders = fooProviders; } public void doSomething() { // 获取FooA的实例 Foo fooA = fooProviders.get(Names.named("A")).get(); // 使用fooA... } }
以CDI为例:
用@Named注解标记子类后,直接注入Map<String, Provider<Foo>>,key就是@Named的值:
@Named("A") public class FooA extends Foo { /* ... */ } // 注入时 @Inject private Map<String, Provider<Foo>> fooProviders; // 使用时 Foo fooA = fooProviders.get("A").get();
方案3:如果不需要动态获取,直接注入子类
如果你的业务代码里是固定需要某个子类,其实可以直接注入FooA、FooB本身,而不是用Provider——除非你需要延迟加载或者解决循环依赖问题。但如果确实需要Provider,那前面两种方案更合适。
内容的提问来源于stack exchange,提问作者Absent

