依赖注入(DI)可能是一个非常难掌握的主题,因为似乎有很多“魔术”正在发生。 通常情况下,它涉及到一堆注释,这些注释遍布整个地方,对象看起来似乎无处不在。 我当然知道我花了很长时间才真正理解了这个概念。 如果您发现很难理解Spring和Java EE在幕后所做的事情(以及原因!),请继续阅读!
在本教程中,我们将构建一个非常简单但功能齐全的依赖项注入容器从头开始在Java中。 以下是一些使事情易于管理的规则:
- 绝对没有图书馆允许,除了JDK本身。没有预先准备的代码转储。 我们将仔细研究所有内容并对此进行推理。No bells & whistles, just the basics.性能无关紧要,没有优化。可执行的主要()方法的每一步。
与其他文章相反,我将不预先解释什么是DI或为什么有用。 相反,我们将从一个简单的示例开始,让它“演变”。
DI Stage 0: Basic example
Find the source code of this section on github
我们从一个非常基本的例子开始。 我们要两个课,我们称它们为服务和服务B,和服务需要服务B做它的工作。 好吧,你可以用静态的 methods和be done with it! So here goes:
public class ServiceA {
public static String jobA(){
return "jobA(" + ServiceB.jobB() + ")";
}
}
public class ServiceB {
public static String jobB(){
return "jobB()";
}
}
public class Main {
public static void main(String[] args) {
System.out.println(ServiceA.jobA());
}
}
如果我们运行此代码,它将打印:
jobA(jobB())
凉! 那么,为什么还要为此烦恼呢? 好...
- 就OOP原则而言,该代码非常差。 ServiceS和ServiceS至少应对象。The code is tightly coupled and very hard to test in isolation。We have absolutely no chance of swapping neither ServiceA nor ServiceB with a different implementation。 Imagine one of them is doing credit card billings; you 别 want that to actually happen in your test suite。
DI Stage 1: Getting rid of static references
Find the source code of this section on github
我们在阶段0中确定的主要问题是只有静态方法,导致极其紧密的耦合。 我们希望我们的服务成为彼此交谈的对象,以便我们可以根据需要替换它们。 现在出现了一个问题:ServiceA如何知道哪一个服务交谈? 最基本的想法是将Service的实例简单地提供给ServiceA的构造函数:
public class ServiceA {
private ServiceB serviceB;
public ServiceA(ServiceB serviceB){
this.serviceB = serviceB;
}
public String jobA(){
return "jobA(" + this.serviceB.jobB() + ")";
}
}
服务B的变化不大:
public class ServiceB {
public String jobB() {
return "jobB()";
}
}
...和主要现在需要先组装对象,然后才能调用工作方法:
public static void main(String[] args) {
ServiceB serviceB = new ServiceB();
ServiceA serviceA = new ServiceA(serviceB);
System.out.println(serviceA.jobA());
}
这里没什么好看的,对吧? 我们当然改进了一些东西:
- 我们现在可以替换执行服务B由...使用服务通过提供另一个对象,甚至可能是另一个子类的对象。We can test both services in isolation with a proper test mock for 服务.
那么酷吗? 不完全的:
- 创建模拟游戏很困难,因为我们需要类。如果我们需要的话会更好介面 instead。Also, this would further reduce the coupling.
DI Stage 2: Using interfaces
Find the source code of this section on github
因此,我们为每个服务使用一个接口。 它们非常简单(我将实际的类重命名为ServiceAImpl和ServiceBImpl, 分别):
public interface ServiceA {
public String jobA();
}
public interface ServiceB {
public String jobB();
}
现在,在ServiceAImpl,我们可以实际使用该接口:
public class ServiceAImpl implements ServiceA {
private final ServiceB serviceB;
public ServiceAImpl(ServiceB serviceB){
this.serviceB = serviceB;
}
// jobA() is the same as before
}
这也使我们主要()方法好一点:
public static void main(String[] args) {
ServiceB serviceB = new ServiceBImpl();
ServiceA serviceA = new ServiceAImpl(serviceB);
System.out.println(serviceA.jobA());
}
从面向对象的角度来看,这对于简单的用例是完全可以接受的。 如果您一定可以解决这个问题,请在此处停止。 但是,随着您的项目变得越来越大...
- 它会变得越来越复杂建立服务网络在你里面主要()方法。你会遇到周期在您的服务依赖项中,如我们的示例所示,无法使用构造函数来解决。
DI Stage 3: Breaking the cycle with setters
Find the source code of this section on github
让我们假设不仅服务需要服务B,但反之亦然-我们有一个循环。 当然,我们仍然可以在* Impl类,像这样:
// constructor examples
public ServiceAImpl(ServiceB serviceB) { ... }
public ServiceBImpl(ServiceA serviceA) { ... }
...但这对我们没有好处:我们将无法创建实际的实例 of either of the two classes. To create an 实例 of 服务Impl, we would first require an 实例 of 服务B, and to create an 实例 of 服务BImpl we first require an 实例 of 服务。 我们陷入僵局。
旁注:实际上,使用代理。 但是,我们不会在这里走那条路线。
那么我们该怎么办呢? 好吧,由于我们正在处理循环依赖关系,因此我们需要具备以下能力:创建服务,然后线他们在一起。 我们通过setter来做到这一点:
public class ServiceAImpl implements ServiceA {
private ServiceB serviceB;
// no constructor anymore here!
@Override // <- added getter to ServiceA interface
public ServiceB getServiceB() { return serviceB; }
@Override // <- added setter to ServiceA interface
public void setServiceB(final ServiceB serviceB) { this.serviceB = serviceB; }
// jobA() same as before
}
我们的主要()方法看起来像这样:
public static void main(String[] args) {
// create instances
ServiceB serviceB = new ServiceBImpl();
ServiceA serviceA = new ServiceAImpl();
// wire them together
serviceA.setServiceB(serviceB);
serviceB.setServiceA(serviceA);
// call business logic
System.out.println(serviceA.jobA());
}
你看到图案了吗? 首先创建对象,然后将它们连接以形成服务图(即服务网络)。
那为什么不停在这里呢?
- 手动进行布线部分容易出错。 您可能会忘记打电话给二传手,然后空指针异常嘉豪您可能会不小心使用仍在构建中的服务实例,因此以某种方式封装网络构建将是有益的。
Up next
在下一篇博客文章中,我们将讨论如何使目前正在手动执行的布线自动化。