关于依赖注入

Scroll Down

概述

依赖注入对于经常使用Spring我们在熟悉不过了,今天想写一篇关于依赖注入的文章,把依赖注入的细节讲清楚。

依赖注入的细节

首先依赖注入就是我们理解的字面意思,将一个对象的依赖进行注入。这里我举一个我们平时开发的例子来看看如果没有依赖注入是怎样的?

就拿我们数据库存储数据来模拟吧。

我们先定义一个入参Request代表一次请求的内容:

static class Request {
    // 用户名
    private String username;
    // token
    private String token;
    // 请求内容
    private String content;
 }

接下来是我们处理一次请求的流程:

/**
 * 处理请求主流程
 * @param request 请求
 */
public void requestHandler(Request request) {
    // 1、权限验证
    this.checkAcl(request);
    // 2、将请求解析成对应的SQL
    this.convertSql(request);
    // 3、成本分析
    this.costCalculate(request);
    // 4、调用存储引擎
    this.invokeDbEngine(request);
}

假设我们一次完整请求的步骤总共是4步。
1、在第一步中,我们有三种权限验证的方式。AclVerify1,AclVerify2,AclVerify3。
2、在第二步中,我们将请求转换成SQL。SqlConvertor。
3、在第三步中,我们有两种成本分析策略。CostCalProcessor1,CostCalProcessor2。
4、在第四步中,我们有4种存储引擎,DbEngine1,DbEngine2,DbEngine4,DbEngine5。

我们通常实现多种ACL校验时是这样的:

private void checkAcl(Request request) {
    if (AclType.ACL_VERIFY_ONE.equals(request.getType())) {
        // 执行ACL1验证处理
        // ...
    } else if (AclType.ACL_VERIFY_TWO.equals(request.getType())) {
        // 执行ACL2验证处理
        // ...
    } else {
        // 执行ACL3验证处理
        // ...
    }
}

但是这种实现有一个问题就是这个checkAcl方法承担了一些不属于它的职责,比如区分aclType。通常情况下,上层接口在调用过程中应该关心的是调用的结果而非调用过程,那么这时我们就会想到定义一个AclCheck的接口,这个接口的职责就是抽象校验能力。然后checkAcl这个方法只负责调用AclCheck这个接口,不关心具体实现。

interface AclCheck {
    /**
     * 权限校验
     * @param request 请求
     */
    void checkAcl(Request request);
}

static class AclCheck1 implements AclCheck{
    @Override
    public void checkAcl(Request request) {
        // ACL1的校验逻辑
    }
}

static class AclCheck2 implements AclCheck{
    @Override
    public void checkAcl(Request request) {
        // ACL2的校验逻辑
    }
}

static class AclCheck3 implements AclCheck{
    @Override
    public void checkAcl(Request request) {
        // ACL3的校验逻辑
    }
}

那么这时候我们就可以把checkAcl改造成如下:


private static AckCheckRegistry ACL_CHECK_REGISTRY = new AckCheckRegistry();

static {
    ACL_CHECK_REGISTRY.registryAclCheck(AclType.ACL_VERIFY_ONE, new AclCheck1());
    ACL_CHECK_REGISTRY.registryAclCheck(AclType.ACL_VERIFY_TWO, new AclCheck2());
    ACL_CHECK_REGISTRY.registryAclCheck(AclType.ACL_VERIFY_THREE, new AclCheck3());
}

// 默认使用AclType.ONE的方式具体的值可以通过类型或者配置读取的方式来确定
private AclCheck aclCheck = ACL_CHECK_REGISTRY.getAclCheck(AclType.ACL_VERIFY_ONE);

static class AckCheckRegistry {

    Map<AclType, AclCheck> aclCheckMap = new HashMap<>();

    AclCheck getAclCheck(AclType aclType) {
        return aclCheckMap.get(aclType);
    }

    void registryAclCheck(AclType aclType, AclCheck aclCheck) {
        if (!aclCheckMap.containsKey(aclType)) {
            aclCheckMap.put(aclType, aclCheck);
        }
    }
}

private void checkAcl(Request request) {
    aclCheck.checkAcl(request);
}

这样改造后,我们的主流程不再需要关注AclCheck的具体实现,由一个管理器来负责实现具体的逻辑。
上面的CostCalProcessor和DbEngine都是如此。
design-pattern.drawio

图中的每个依赖都可以理解为可以拆开替换的模块。通过上图我们可以看到完成一次请求处理,需要Checker、Convertor、CostCalProcessor、DBEngine、store这些组件。如果我们在写代码的时候只需要关注我们实现这个功能需要哪些组件,然后将对应的组件定义好,然后框架自动帮我们注入,那么我们就会省掉很多的代码。

其实这就是Spring所做的事情。Spring通过一个Map来维护所有注册的容器中的对象,然后在解析创建对象的时候会对每个依赖进行自动注入,并且Spring做的非常成熟,主要是以下几点:
1、对象注册的方式:XML、注解、编程。主要分为自动扫描和手动注册。这里有个设计原则就是可配置一定可编程,并保持友好的 CoC 约定。
2、自动注入不同的方式,ByType和ByName。
3、对象生命周期扩展回调。说的是BeanPostProcessor等等。重要的阶段需要留出供使用者改变行为的能力。
4、AOP、也就是自动创建代理。通过在运行期间将某个类的一些功能进行增强。
5、容器运行期间发布事件。重要节点需要发出事件。

Spring中的依赖注入的核心目的其实是想让我们尽可能的找出扩展点,然后核心流程是需要依赖这些扩展点来完成的,这个扩展点对于业务来说就是针对于某一功能的不同业务场景下的不同实现,对于框架来说就是某一组件在不同使用场景下可能会有不同的实现,这就要求我们能够在开发过程中识别出扩展点并对扩展点进行抽象。这就是我们通常讲的开闭原则(对扩展开放,对修改关闭)。

Dubbo中的依赖注入

Dubbo中也有类似Spring相关的功能就是SPI。可以说Dubbo中的SPI是微型的Spring内核,它涵盖了所有Spring中核心的功能(就是如果想让你在框架中实现一个最基本的Spring容器功能,你可以参照Dubbo的SPI就可以了)。
比如:

  • 1、对象的注册。这是最基本的功能,目的是通过各种方式来获取对象,并注册。
  • 2、对象的获取。不多说了,容器需要获取对象
  • 3、对象的自动依赖。通过Setter的方式来自动注入依赖。
  • 4、切面AOP。将原对象进行代理。注意这里有两种实现方式一种是装饰模式,另一种是动态代理。两种方式都可以实现这个功能。

Dubbo的SPI不仅仅支持上面这些功能。

  • 1、对象的获取(getExtension)
  • 2、根据Activate注解获取实例(getActivateExtension)
  • 3、自适应对象获取(getAdaptiveExtension)
  • 4、对象注入(injectExtension)对象注入功能是通过ObjectFactory来实现的。
  • 5、AOP(Wrapper)