通过编写DI容器来了解依赖注入-从头开始! (第2部分)

这是我的“ DI从零开始”系列的第二部分。 在上一篇文章中,我们讨论了我们的基本示例以及“手动”方法存在的问题。 现在,我们要使服务网络的布线自动化。

DI Stage 4: Automating the wiring

Find the source code of this section on github

让我们专注于主要()方法,并尝试找到一种更自动的创建服务网络的方式。 当然,在这里忘记调用setter是一个真正的威胁,编译器甚至无法警告您。 但是我们的服务在结构上有所不同,并且没有制服访问它们的方式...还是在那里? Java反射来营救!

我们本质上想要提供的设置就是我们的服务类列表。 从那里,安装程序应该构造并连线服务网络。 特别是,我们的主要()方法只真正感兴趣服务,甚至不需要服务B。 让我们这样重写它:

    public static void main(String[] args) throws Exception {
        Set<Class<?>> serviceClasses = new HashSet<>();
        serviceClasses.add(ServiceAImpl.class);
        serviceClasses.add(ServiceBImpl.class);

        ServiceA serviceA = createServiceA(serviceClasses);

        // call business logic
        System.out.println(serviceA.jobA());
    }

但是我们如何实现“魔术”createServiceA方法? 原来不是那硬...

    private static ServiceA createServiceA(Set<Class<?>> serviceClasses) throws Exception{
        // step 1: create an instance of each service class
        Set<Object> serviceInstances = new HashSet<>();
        for(Class<?> serviceClass : serviceClasses){
            Constructor<?> constructor = serviceClass.getConstructor();
            constructor.setAccessible(true);
            serviceInstances.add(constructor.newInstance());
        }
        // step 2: wire them together
        for(Object serviceInstance : serviceInstances){
            for(Field field : serviceInstance.getClass().getDeclaredFields()){
                Class<?> fieldType = field.getType();
                field.setAccessible(true);
                // find a suitable matching service instance
                for(Object matchPartner : serviceInstances){
                    if(fieldType.isInstance(matchPartner)){
                        field.set(serviceInstance, matchPartner);
                    }
                }
            }
        }
        // step 3: from all our service instances, find ServiceA
        for(Object serviceInstance : serviceInstances){
            if(serviceInstance instanceof ServiceA){
                return (ServiceA)serviceInstance;
            }
        }
        // we didn't find the requested service instance
        return null;
    }

让我们分解一下。 在第1步我们遍历我们的课程,对于每个课程,我们尝试获取默认构造函数(即无参数的构造函数)。 由于两者都不ServiceAImpl也不ServiceBImpl specifies any constructor (we deleted them when introducing the getters/setters), the Java compiler provides a public 默认构造函数 - so that will work fine. Then, we make this constructor 无障碍。 这只是防御性编程,以确保私有构造函数也能正常工作。 最后,我们打电话newInstance()在构造函数上创建类的实例,并且加它到我们的实例集。

在第2步我们希望将各个服务实例连接在一起。 为此,我们逐一查看每个服务对象。 我们通过以下方式检索它的Java类getClass(),并要求该班所有声明字段(宣告意思是私人的字段也将返回)。 就像构造函数一样,我们确保该字段可访问,然后检查类型领域的。 这将为我们提供我们需要投入该领域的服务类别。 剩下要做的就是找到合适的matchParter,是字段指定类型的对象。 找到一个,我们就打电话给field.set(...)并将匹配伙伴分配给该字段。 请注意第一个参数的field.set(...)方法是将更改其字段值的对象。

在第三步,网络已经完成; 剩下要做的就是找到的实例服务。 我们可以简单地扫描或实例,并通过使用来检查是否找到了正确的实例instanceof 服务。

这可能有点令人生畏,所以请尝试再次阅读。 另外,如果您对Java反射基础知识感到陌生,则可能需要重新学习。

那么我们获得了什么呢?

  • 我们的服务会自动连接在一起。我们再也不会忘记打电话给二传手(事实上,我们不再需要它们了)。我们的申请将失败启动时如果接线失败,则不在业务逻辑期间。

接下来需要处理的主要痛苦是,我们不想在每次想要获得服务时都重复整个过程。 我们希望能够访问每一个网络中的服务,而不仅仅是一项。

DI Stage 5: Encapsulating the Context

Find the source code of this section on github

负责维护服务网络的对象称为依赖注入容器,或(按春季计算)应用环境。 我将使用“上下文”术语,但是这些术语实际上是同义词。 上下文的主要工作是提供一个getServiceInstance(...)接受服务类作为参数,并返回(完成和有线)服务实例的方法。 所以我们开始:

public class DIContext {

    private final Set<Object> serviceInstances = new HashSet<>();

    public DIContext(Collection<Class<?>> serviceClasses) throws Exception {
        // create an instance of each service class
        for(Class<?> serviceClass : serviceClasses){
            Constructor<?> constructor = serviceClass.getConstructor();
            constructor.setAccessible(true);
            Object serviceInstance = constructor.newInstance();
            this.serviceInstances.add(serviceInstance);
        }
        // wire them together
        for(Object serviceInstance : this.serviceInstances){
            for(Field field : serviceInstance.getClass().getDeclaredFields()){
                Class<?> fieldType = field.getType();
                field.setAccessible(true);
                // find a suitable matching service instance
                for(Object matchPartner : this.serviceInstances){
                    if(fieldType.isInstance(matchPartner)){
                        field.set(serviceInstance, matchPartner);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T> T getServiceInstance(Class<T> serviceClass){
        for(Object serviceInstance : this.serviceInstances){
            if(serviceClass.isInstance(serviceInstance)){
                return (T)serviceInstance;
            }
        }
        return null;
    }

}

如您所见,代码与上一步没有太大变化,只是我们现在有了一个对象来封装上下文(DIContext)。 在内部,它管理一组serviceInstances就像以前从服务类集合中创建的一样。 的第三步从上面移到了自己的getServiceInstance方法,该方法接受要检索的类作为参数。 由于我们不能使用实例不再(它需要一个硬编码的类,而不是一个动态变量值),我们必须回退到serviceClass.isInstance(...)做同样的事情。

我们可以在新版本中使用此类主要():

    public static void main(String[] args) throws Exception {
        DIContext context = createContext();
        doBusinessLogic(context);
    }

    private static DIContext createContext() throws Exception {
        Set<Class<?>> serviceClasses = new HashSet<>();
        serviceClasses.add(ServiceAImpl.class);
        serviceClasses.add(ServiceBImpl.class);
        return new DIContext(serviceClasses);
    }

    private static void doBusinessLogic(DIContext context){
        ServiceA serviceA = context.getServiceInstance(ServiceA.class);
        ServiceB serviceB = context.getServiceInstance(ServiceB.class);
        System.out.println(serviceA.jobA());
        System.out.println(serviceB.jobB());
    }

如您所见,我们现在可以通过调用轻松地从上下文中提取完整的服务实例getServiceInstance根据需要,使用不同的输入类。 还要注意,服务本身可以简单地通过声明适当类型的字段来相互访问-它们甚至不必了解DIContext宾语。

但是仍然存在一些问题。 例如,如果我们想在我们的服务中有一个字段不 refer to a不her service (say, an 整型领域)? 我们需要一种方法告诉我们的算法我们希望它设置哪些字段-以及哪些字段不保留。

DI Stage 6: Annotating fields

Find the source code of this section on github

那么我们如何才能告诉我们的算法需要分配哪些字段呢? 我们可以引入一些奇特的命名方案并解析field.getName(),但这是一个容易出错的解决方案。 相反,我们将使用注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

}

@目标告诉编译器我们可以在哪些元素上使用此注释-我们希望它适用于字段。 用@保留我们指示编译器将该注释保留到运行时,并且不要在编译期间将其丢弃。

让我们注释一下我们的字段:

public class ServiceAImpl implements ServiceA {

    @Inject
    private ServiceB serviceB;

    // rest is the same as before

}
public class ServiceBImpl implements ServiceB {

    @Inject
    private ServiceA serviceA;

    // rest is the same as before

}

本身及其内部的注释没有。 我们需要积极读注释。 因此,让我们在我们的构造函数中进行操作DIContext:

       // wire them together
       for(Object serviceInstance : this.serviceInstances){
           for(Field field : serviceInstance.getClass().getDeclaredFields()){
               // check that the field is annotated
               if(!field.isAnnotationPresent(Inject.class)){
                   // this field is none of our business
                   continue;
               }
               // rest is the same as before

跑过主要()再次方法 它应该像以前一样工作。 但是,现在您可以随意向服务中添加更多字段,并且接线算法不会中断。

Closing words

到目前为止,我们已经创建了一个非常基本但实用的DI容器。 它依靠我们为其提供服务类的集合。 在下一部分中,我们将讨论如何实际发现我们的服务等级。

from: https://dev.to//martinhaeusler/understanding-dependency-injection-by-writing-a-di-container-from-scratch-part-2-2np6

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值