分类
Java

自己实现一个 Mini IoC 容器

前言

IoC(Inversion of Control,控制反转)将对象的生成、设置等行为交给容器来完成,而不是由开发者自己编写 new, setXxx 等语句控制。其中最常见的实现方式是 Spring 的依赖注入(Dependency Injection, DI),如有个类 A, 其中一个字段是 B(带有注解 @Resource 或 @AutoWired ),那么容器会自动先查找一个 B 再注入到 A 的这个字段中(查找过程中可能会发现 B 也需要注入一个 C, 这些容器都会帮你完成)。
通常控制反转和依赖注入可以等同起来,我这里用 IoC 这个名称是因为它看起来比较好看——以上是我随便说的,说错了请指正……

于是我们可以自己实现一个 IoC 容器吗?虽然 Spring 已经做得足够好了,但我就是闲得蛋疼(并不闲,每天加班到九点)特别想自己试试怎么实现可以吗?——当然可以,毕竟我还是一个爱学习的好孩子。

接口定义

首先,我们定义一个上下文 Context —— 这个词真是不好理解,哪里都有上下文的概念,比如 安卓开发,比如…… 总之就是很常见,这里可以理解为就是我们要的那个容器。
学了这么多年 Java, 直觉告诉我,它应该要是一个接口,于是我们的第一行代码出来了:

public interface Context{}

万事开头难,开了头就好办了。
容器应该有什么功能呢?
读者:你傻啊,当然是刚刚说的自动注入啊!
No, No, No, 这个是容器自己内部的功能,并不是暴露给开发者用的功能,因此不应该出现在接口中。我们要用到的其实就是从容器中获取 Bean 对象就行了:

<T> T getBean(Class<T> clazz);

使用场景

下面我们先写一下我们希望怎么使用这个 IoC 容器吧:

public interface IUserDao{
    //...
}

@Bean //means this class should register to the IoC container
public class UserDao implements IUserDao{
    //implement
}

@Bean
public class UserService{
    @Bean //means this field should injected
    private IUserDao userDao;
    //...service method
}

public class App{
    public static void main(String[] args){
        Context context = new ClasspathContext(); //will auto scan 
        UserService userService = context.getBean(UserService.class);
        //...
    }
}

毕竟都是用 Spring 用习惯了,我们希望先写一个带注解的 Dao,表示它可以注册到容器中;再写一个 Service, 其中一个字段是 Dao 也带一个注解,表示需要自动注入;主程序中从容器中获取 Service,然后干需要干的事。

嗯,这里我们反复提到了注册到容器,看来 Context 还需要一个方法

void register(Object bean);

OK, 为了达到这个目的,我们需要扫描所有带注解的类,如果它需要注册到容器中,那么我们就生成它的一个实例然后加入到容器中;如果它包含需要注入的字段,那么就从容器中寻找一个合适的 Bean 注入进去。
思路就是这样,听起来是不是特别简单~~
聪明的你一定会想到这肯定需要用到反射吧~~~
答对咯,我们一步步来:

注解

注解定义

先定义一下注解吧,最初我想的是,可以用两个注解,一个叫做 @Out 写在类上的,表示这个类可以给其他类使用(需要注册到容器中);另一个叫做 @In, 写在字段上,表示需要从容器中取,注入到字段上。不过,后来我们其实用一个注解就行,干脆就叫做 @Bean 可以写在类上或字段上。不过最后我又发现,其实我们可以使用 Java 自带那个 @Resource 啊——额,好吧,那就 @Resource 吧,然而我都写完了 @Bean 了,干脆两个都支持吧~~

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)  //必须是 RUNTIME 这样我们才能在代码中获取到
public @interface Bean {
    String value() default "";
}

注解处理器

因为要扫描带注解的类嘛,那么还需要定义一个注解处理器,其中有一个 autoScan 的方法:

public interface IAnnotationProcessor{
  void autoScan(Context context, String... basePackage);
}

然后我们实现一个 Context 在构造时调用这个处理器的 autoScan 方法,那就万事大吉啦!

public class ClasspathContext implements Context{
    private Map<class ,Object> clazzBeanMap = new ConcurrentHashMap<>();
    public ClasspathContext(String... scanPackages){
        new SimpleAnnotationProcessor().autoScan(this, scanPackages);
    }
    public <T> T getBean(Class<T> clazz){
        return clazzBeanMap.get(clazz);
    }
    public void register(Object bean){
        clazzBeanMap.put(bean.getClass(),bean);
    }
}

看起来工作都集中在注解处理器中了呢:

  • – 第一步,怎样扫描类
  • – 第二步,怎样把需要的 Bean 注册到容器中
  • – 第三步,怎样把需要自动注入的字段填补上

扫描类

就是获取所有的类,最好只获取带我们需要的注解的那些。很遗憾,貌似无法一步到位,一下子拿到那些带注解的类的 Class 对象,不过我们可以曲线救国。
先使用 ClassLoader 获取类路径中所有包含 class 文件的资源,使用 ClassLoader.getResources 可以返回一个 URL 集合,然后遍历所有 class 资源,获取类路径下所有的类名。
有了类名,我们即可以通过 Class.forName("xxx.xxx.Xxx") 得到 class 对象,得到 class 对象后,才能使用反射获取我们关心的信息。

注册 Bean

有了 class 对象,通过反射,可以知道它是否包含注解 @Bean/@Resource 如果包含,那就通过 `Class.newInstance` 生成一个实例,注册到容器中——等一下!它的字段怎么办?——别急,下一步我们再注入字段,反正现在容器还没就绪,字段暂时是空也不会有事的。

自动注入

嘿嘿嘿,虽然刚开始是这样描述的:

在查找 A 时发现它有个字段 B 需要注入,于是我们再去查找 B, 在构造 B 时发现还依赖一个 C, 于是我们去构造 C.

但其实我们换个思路,一下子把所有 Bean 都实例出来加入到容器中(第二步)(虽然现在这些 bean 可能是“半成品”),然后再注入每个 Bean 的每个字段。那就简单了,遍历每个 Bean 的所有字段,要是包含注解 @Bean/@Resouece 注解,就从容器里取一个填上就 OK 啦,这样下来容器里所有 Bean 都是完整可用的“成品”了。

public void autoScan(Context context, String... scanPackages) {
        Set<string> classNames = new HashSet<>();
        classNames.addAll(AnnotationUtil.getClassNames(scanPackages));

        // 构造 Bean
        for (String className : classNames) {
            registerClass(context, className);
        }

        // 注入需要的字段
         for (Map.Entry<class , Object> entry : context.getClazzBeanMap().entrySet()) {
            Object obj = entry.getValue();
            injectFiled(context, obj);
            injectMethod(context, obj);
        }
    
        protected void registerClass(Context context, String className){
          try{
            Class<?> aClass = Class.forName(className);
            Object bean = aClass.newInstance();  //具体处理请看下方链接中完整代码
            context.register(bean);
          }catch (Exception ignore){}
        }
    
        protected void injectFiled(Context context, Object bean){
          try{
            Class<?> objClass = object.getClass();
            for(Field field: objClass.getDeclaredFields()){
                 Class<?> type = field.getType();
                 Object fieldValue = context.getBean(type);
                 field.set(bean, filedValue);           
            }
          }catch (Exception ignore){}
        }

        protected void injectMethod(Context context, Object object) {
          //...
        }
}

以上仅是粗略的表述,还有许多细节由于篇幅原因没有完全展开,如果你有兴趣,可以在 https://github.com/YouthLin/mini-framework/tree/master/mini-ioc 查看完整代码~

局限性

由于我们只是一个 Mini Ioc 容器,所以现在的 Bean 对象必须要有无参构造器、并且暂时只支持字段注入,其实要支持 setter 注入也挺简单的,获取方法参数类型,然后从容器中找出参数,调用方法就行啦。同时没有 xml 配置文件,只有注解。

示例

如果你有兴趣看看到底能不能跑起来,可以通过 Maven 坐标引用:



    com.youthlin
    mini-ioc
    1.0.1

或者在 https://github.com/YouthLin/examples/tree/master/example-my-ioc 能看到示例代码~

鸣谢

本项目参考了厦大学长的项目: https://github.com/Vino007/mini-ioc

下期预告

写完发现这个容器根本用不上啊,现在大都是写 Web 项目,该怎么集成到 Java Web 中呢?
Spring MVC 是一个很赞的 MVC 框架,不需要写原生的 Servlet+JSP 了,直接在 Controller 中处理页面传过来的参数,调用 Service 层方法,往页面上添加属性,返回页面名称就能渲染出结果页面。那么我们的 Mini IoC 能不能也支持 Web 项目呢?

——请看下集:自己实现一个 Mini MVC 框架


“自己实现一个 Mini IoC 容器”上的2条回复

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

[/鼓掌] [/难过] [/调皮] [/白眼] [/疑问] [/流泪] [/流汗] [/撇嘴] [/抠鼻] [/惊讶] [/微笑] [/得意] [/大兵] [/坏笑] [/呲牙] [/吓到] [/可爱] [/发怒] [/发呆] [/偷笑] [/亲亲]