一个简单的编译时依赖注入框架


Java 中最流行的依赖注入框架——Spring 和 Guice——是使用反射构建的。这种方法存在一些问题,我相信在大多数情况下编译时解决方案更好。已经有编译时实现(例如 Dagger),但我想编写 自己的基本实现。它不是为在实际项目中使用而设计的,而是作为此类实现如何工作的说明性示例。

我看到使用反射进行依赖注入的三个缺陷:

  1. 如果某些东西没有按照您的预期连接在一起,则很难调试。单步执行严重依赖反射的代码通常是可怕的。春天是美好的,直到你看到香肠是如何制作的。
  2. 在实际运行应用程序之前,您不会发现问题。发现问题的反馈循环比需要的要慢。
  3. 有启动成本。在很多情况下,这可能可以忽略不计,但您每次启动应用程序时都会进行一些计算,而这通常只需要进行一次。尤其是在基于云的应用程序中,优化启动时间可能很重要。

基于反射的方法也有其自身的好处。这意味着您可以在运行时发现 bean,我已经使用它来创建类似插件的体系结构。不过,大多数应用程序不需要它。

反射的替代方法是在编译时生成一些执行连接的代码。生成的代码可能如下所示:

public final class SimpleDIContext {
    private final Map<String, Object> nameToBean = new HashMap<>();

    public SimpleDIContext() {
        PassengerSeat passengerSeat = new PassengerSeat();
        Driver driver = new Driver();
        DriversSeat driversSeat = new DriversSeat();
        Turbocharger turbocharger = new Turbocharger();
        Engine engine = new Engine(turbocharger);
        Car car = new Car(engine, List.of(driversSeat, passengerSeat));
        car.addDriver(driver);
        car.addSeats(new Seat[] {passengerSeat, driversSeat});
        nameToBean.put("passengerSeat", passengerSeat);
        nameToBean.put(
"driver", driver);
        nameToBean.put(
"driversSeat", driversSeat);
        nameToBean.put(
"turbocharger", turbocharger);
        nameToBean.put(
"engine", engine);
        nameToBean.put(
"car", car);
    }

    public Object getBeanByName(String name) {
        return nameToBean.get(name);
    }
}

在实践中,这涉及到编写一个构建插件,在编译阶段之前钩住。我使用了谷歌的AutoService,这意味着用户需要做的就是在他们的项目中添加一个提供范围的依赖,类似于Lombok。我使用JavaPoet来生成Java源代码,而不用担心繁琐和容易出错的String操作。

为了将类标记为Bean,我选择了javax.inject,因为它已经定义了所有我们需要的基本东西。通过提供AbstractProcessor的实现,我们可以发现并处理每一个用@Singleton注释的类。

在找到所有被注解的类之后,我们需要弄清楚它们应该如何连接在一起,以及它们应该以何种顺序被创建。你不能创建没有发动机的汽车,你也不能创建没有涡轮增压器的发动机,但你如何以编程方式表达呢?

我注意到,只要你按照依赖关系的总数(包括直接依赖和交叉依赖)来构建所有的东西,那么你构建东西的准确顺序就不重要了。如果你首先构建的是零依赖关系的东西,然后是有1个依赖关系的东西,然后是2个,以此类推,那么准确的排列组合就不重要了。两个依赖关系总数相同的豆子不可能相互依赖。

我们需要的是一个方法,找到每个Bean的依赖总数,然后我们就可以按这个数字排序。这是一个简单的递归问题。对于每个Bean来说,总的依赖数量是直接依赖的数量,加上每个Bean的总依赖数量。一旦我们为一个给定的Bean获得了这个数字,我们就可以把它存储在一个map中以备忘:

private long getNumDependencies(Map<String, Long> fqnToNumDependents, Bean bean) {
    final Long SENTINEL = -123L;

    Long prevNumDeps = fqnToNumDependents.get(bean.getFqn());
    if (SENTINEL.equals(prevNumDeps)) throw new RuntimeException("Circular dependency!");
    if (prevNumDeps != null) return prevNumDeps;

    fqnToNumDependents.put(bean.getFqn(), SENTINEL);
    long numDependencies = 0;
    for (Bean dependency : bean.dependencies()) {
        numDependencies += (1 + getNumDependencies(fqnToNumDependents, dependency));
    }
    fqnToNumDependents.put(bean.getFqn(), numDependencies);
    return numDependencies;
}

在循环依赖的情况下,标记的插入和检查是一种防止无限递归的简单保护。当我们开始计算一个 bean 的编号时,我们会插入哨兵,如果我们再次遇到它,那么就意味着我们在兜圈子。哨兵是负数,因为某些东西不可能有负数的依赖关系。更好的实现会跟踪完整路径,以便用户可以看到导致循环的原因。

对于依赖集合和Providers的Bean来说,还有一点复杂,我在上面跳过了这一点。方法注入很容易;它可以在所有结构注入之后进行。

我选择不实施字段注入,无论如何, Spring 团队 推荐构造函数注入 ,如果一个字段是私有的,那么连接它的唯一方法是使用反射。

完整的项目托管在 GitHub 上。自述文件解释了如何构建和运行它。包含一个示例模块,可帮助您快速启动和运行。