用简单代码实现IOC容器

本文作者:加耀 投稿 手写IOC容器了解一下!

相信每一个java程序员在面试经历中,都被面试官问到过AOP和IOC,用官方的话语来回答AOP和IOC,那就是切面编程和控制反转及依赖注入。

具体什么是IOC呢,IOC(inversion of control)其含义是控制反转,即我们平时通过NEW出来的对象交由IOC来管理,当我们在代码中通过注入注解进行对象标记时,IOC容器会将对应的对象进行属性注入,这样就省去了我们自己NEW的过程,消除了大良耦合代码;

在这里,将对象实例交给IOC容器管理的过程可以称为控制反转,而对被IOC管理的对象中有特殊标识的需要进行注入的属性对象进行连续关联称为依赖注入DI(depend injection 依赖注入);控制反转和依赖注入是相辅相成的,统称为IOC;

在日常编码使用Spring框架时,我们通常会使用注解@Autowried或者是@Resource来标记当前类所需要注入的对象,省去了我们直接NEW的过程,从而减少代码耦合。但如果仅仅是帮我们NEW我们需要的对象,直接称之为注入即可,何来的依赖注入呢。

重点就在这个”依赖”二字上;举个代码中的简单的例子,比如我们在访问控制层注入了服务层的类或者是接口,我们如果是通过new的方式来获取到服务层的类的实例,这样访问控制层中注入的持久层的对象则为null;直接调用则会报错空指针异常;

而依赖注入则是我们在访问控制层注入业务服务层代码时,会将服务层中所注入的接口或者类依次注入,这里面存在一个依赖关系,将依赖的需要注入的类递归注入。这就是依赖注入名称的来源;

就像我们吃饭的时候,当我们需要米饭时,我们可以自己去盛米饭,这个过程是我们主动的;但是如果结合IOC,那么我们只需要告诉服务员,需要米饭,然后服务员就会盛一碗米饭过来,这个动作就交给服务员去做,而省去了自己去盛的这个过程。并且,服务员在上米饭的时候,还将饭碗和筷子一块送过来了,米饭需要用碗来盛,需要用筷子吃,这就是依赖注入的体现;

对于IOC和AOP我也写过相关的文章,如果还不了解的同学不妨先去阅读一下

在手写IOC容器之前,我们需要掌握一些java基础的知识点,分别有:注解、反射、IO流等知识点;我们先来看一下IOC容器的整体流程

image-20190325092707843

首先,我们先创建一个Maven项目,然后在项目的resources目录下添加一个配置文件application.properties,在配置文件中指定需要扫描的包路径

image-20190325092746167

然后我们定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解,代码如下:

依赖注入注解@MyAutowired

/**
 * 类 名: MyAutowired
 * 描 述: 注入注解--将需要交给IOC容器管理的类放置 -- 定义在属性上的
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface MyAutowired {

    String value() default "";

}

访问控制层注解@MyController

业务服务层注解@MyService

/**
 * 类 名: MyService
 * 描 述: 自定义注解 -- 服务层 -- 定义在类、接口、枚举上的
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyService {

    String value() default "";

}

数据持久层注解@MyMapping:

/**
 * 类 名: MyService
 * 描 述: 自定义注解 -- 数据持久层 -- 定义在类、接口、枚举上的
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyMapping {

    String value() default "";

}

配置文件获取注解@Value:

/* 类 名: Value
 * 描 述: 获取配置文件中的键值对
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface Value {

    String value() default "";

}

定义完注解后,我们可以开始编写代码了。根据上面的流程图,此时应该先获取读取配置文件,从配置文件中获取需要扫描的包路径。

我们先写一个配置文件工具类ConfigurationUtils,代码如下:

/**
 * 类 名: ConfigurationUtils
 * 描 述:
 */
public class ConfigurationUtils {

    /**
     * 项目配置文件信息
     */
    public static Properties properties;

    public ConfigurationUtils(String propertiesPath) {
        properties = this.getBeanScanPath(propertiesPath);
    }

    /**
     * @author: JiaYao
     * @demand: 读取配置文件
     */
    private Properties getBeanScanPath(String propertiesPath) {
        if (StringUtils.isEmpty(propertiesPath)) {
            propertiesPath = "/application.properties";
        }
        Properties properties = new Properties();
        // 通过类的加载器获取具有给定名称的资源
        InputStream in = ConfigurationUtils.class.getResourceAsStream(propertiesPath);
        try {
            System.out.println("正在加载配置文件application.properties");
            properties.load(in);
            return properties;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return properties;
    }

    /**
     * @author: JiaYao
     * @demand: 根据配置文件的key获取value的值
     */
    public static Object getPropertiesByKey(String propertiesKey) {
        if (properties.size() > 0) {
            return properties.get(propertiesKey);
        }
        return null;
    }

}

上述代码中,我们通过读取配置文件获取到配置文件信息的key-value键值对;然后我们再根据配置文件中指定的扫描包路径进行包扫描

拿到包扫描路径后,我们就可以获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储。代码如下:

/**
 * @author: JiaYao
 * @demand: 类加载器
 */
private void classLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    // 加载配置文件所有配置信息
    new ConfigurationUtils(null);
    // 获取扫描包路径
    String classScanPath = (String) ConfigurationUtils.properties.get("ioc.scan.path");
    if (StringUtils.isNotEmpty(classScanPath)) {
        classScanPath = classScanPath.replace(".", "/");
    } else {
        throw new RuntimeException("请配置项目包扫描路径 ioc.scan.path");
    }
    // 扫描项目根目录中所有的class文件
    getPackageClassFile(classScanPath);
    for (String className : classSet) {
        addServiceToIoc(Class.forName(className));
    }
    // 获取带有MyService注解类的所有的带MyAutowired注解的属性并对其进行实例化
    Set<String> beanKeySet = iocBeanMap.keySet();
    for (String beanName : beanKeySet) {
        addAutowiredToField(iocBeanMap.get(beanName));
    }
}

我们需要扫描项目路径中所有以.class结尾的文件,将其添加到一个全局的Set集合中,代码如下:

/**
 * 类集合--存放所有的全限制类名
 */
private Set<String> classSet = new HashSet();

/**
 * @author: JiaYao
 * @demand: 扫描项目根目录中所有的class文件
 */
private void getPackageClassFile(String packageName) {
    URL url = this.getClass().getClassLoader().getResource(packageName);
    File file = new File(url.getFile());
    if (file.exists() && file.isDirectory()) {
        File[] files = file.listFiles();
        for (File fileSon : files) {
            if (fileSon.isDirectory()) {
                // 递归扫描
                getPackageClassFile(packageName + "/" + fileSon.getName());
            } else {
                // 是文件并且是以 .class结尾
                if (fileSon.getName().endsWith(".class")) {
                    System.out.println("正在加载: " + packageName.replace("/", ".") + "." + fileSon.getName());
                    classSet.add(packageName.replace("/", ".") + "." + fileSon.getName().replace(".class", ""));
                }
            }
        }
    } else {
        throw new RuntimeException("没有找到需要扫描的文件目录");
    }
}

我们将所有的类的字节码对象都存储到一个全局的Set集合中之后,遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器;我们先定义一个安全的Map用来存储这些对象

/**
 * IOC容器 如: String(loginController) --> Object(loginController实例)
 */
private Map<String, Object> iocBeanMap = new ConcurrentHashMap(32);

/**
 * @author: JiaYao
 * @demand: 控制反转
 */
private void addServiceToIoc(Class classZ) throws IllegalAccessException, InstantiationException {
    // 预留位置,之后优化
    if (classZ.getAnnotation(MyController.class) != null) {
        iocBeanMap.put(toLowercaseIndex(classZ.getSimpleName()), classZ.newInstance());
        System.out.println("控制反转访问控制层:" + toLowercaseIndex(classZ.getSimpleName()));
    } else if (classZ.getAnnotation(MyService.class) != null) {
        // 将当前类交由IOC管理
        MyService myService = (MyService) classZ.getAnnotation(MyService.class);
        iocBeanMap.put(StringUtils.isEmpty(myService.value()) ? toLowercaseIndex(classZ.getSimpleName()) : toLowercaseIndex(myService.value()), classZ.newInstance());
        System.out.println("控制反转服务层:" + toLowercaseIndex(classZ.getSimpleName()));
    } else if (classZ.getAnnotation(MyMapping.class) != null) {
        MyMapping myMapping = (MyMapping) classZ.getAnnotation(MyMapping.class);
        iocBeanMap.put(StringUtils.isEmpty(myMapping.value()) ? toLowercaseIndex(classZ.getSimpleName()) : toLowercaseIndex(myMapping.value()), classZ.newInstance());
        System.out.println("控制反转持久层:" + toLowercaseIndex(classZ.getSimpleName()));
    }
}

/**
 * @author: JiaYao
 * @demand: 类名首字母转小写
 */
public static String toLowercaseIndex(String name) {
    if (StringUtils.isNotEmpty(name)) {
        return name.substring(0, 1).toLowerCase() + name.substring(1, name.length());
    }
    return name;
}

然后我们再遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,即依赖注入,代码如下:

     /**
     * @author: JiaYao
     * @demand: 依赖注入
     */
    private void addAutowiredToField(Object obj) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.getAnnotation(MyAutowired.class) != null) {
                field.setAccessible(true);
                MyAutowired myAutowired = field.getAnnotation(MyAutowired.class);
                Class<?> fieldClass = field.getType();
                // 接口不能被实例化,需要对接口进行特殊处理获取其子类,获取所有实现类
                if (fieldClass.isInterface()) {
                    // 如果有指定获取子类名
                    if (StringUtils.isNotEmpty(myAutowired.value())) {
                        field.set(obj, iocBeanMap.get(myAutowired.value()));
                    } else {
                        List<Object> list = findSuperInterfaceByIoc(field.getType());
                        if (list != null && list.size() > 0) {
                            if (list.size() > 1) {
                                throw new RuntimeException(obj.getClass() + "  注入接口 " + field.getType() + "   失败,请在注解中指定需要注入的具体实现类");
                            } else {
                                field.set(obj, list.get(0));
                                // 递归依赖注入
                                addAutowiredToField(field.getType());
                            }
                        } else {
                            throw new RuntimeException("当前类" + obj.getClass() + "  不能注入接口 " + field.getType().getClass() + "  , 接口没有实现类不能被实例化");
                        }
                    }
                } else {
                    String beanName = StringUtils.isEmpty(myAutowired.value()) ? toLowercaseIndex(field.getName()) : toLowercaseIndex(myAutowired.value());
                    Object beanObj = iocBeanMap.get(beanName);
                    field.set(obj, beanObj == null ? field.getType().newInstance() : beanObj);
                    System.out.println("依赖注入" + field.getName());
//                递归依赖注入
                }
                addAutowiredToField(field.getType());
            }
            if (field.getAnnotation(Value.class) != null) {
                field.setAccessible(true);
                Value value = field.getAnnotation(Value.class);
                field.set(obj, StringUtils.isNotEmpty(value.value()) ? getPropertiesByKey(value.value()) : null);
                System.out.println("注入配置文件  " + obj.getClass() + " 加载配置属性" + value.value());
            }
        }
    }

上述代码中,我们通过判断带指定注解的类里面是否有注入其它的类,然后进行递归注入;但是有一个问题,接口和抽象类不能被实例化,所以在处理接口时,就出现了一个难题。通常我们习惯注入接口,但是接口不能被实例化,我们需要对接口赋值它的子类,如何获取到接口的实现类呢?

翻遍了JDK1.8的API,没有找到能够提供这样的方法。于是这里做了写了一个循环,遍历IOC容器中的每一个类是否有实现接口,如果是相同的接口则记录,但是这样做会非常消耗性能的,其代码如下:

/**
 * @author: JiaYao
 * @demand: 判断需要注入的接口所有的实现类
 */
private List<Object> findSuperInterfaceByIoc(Class classz) {
    Set<String> beanNameList = iocBeanMap.keySet();
    ArrayList<Object> objectArrayList = new ArrayList<>();
    for (String beanName : beanNameList) {
        Object obj = iocBeanMap.get(beanName);
        Class<?>[] interfaces = obj.getClass().getInterfaces();
        if (useArrayUtils(interfaces, classz)) {
            objectArrayList.add(obj);
        }
    }
    return objectArrayList;
}

对于接口的注入,暂时还没有想到有什么好的方式可以优化,也不知道Spring是怎么做的。当一个接口有多个实现类时,需要用过自定义名称进行交给IOC管理和注入注解进行获取。

截至到这里,我们就完成了整个IOC容器的创建以及依赖注入功能了。我们可以写一个简单的测试类来试一下我们写的这个IOC容器;

测试代码:访问控制层

@MyController
public class LoginController {

    @Value(value = "ioc.scan.pathTest")
    private String test;

    @MyAutowired(value = "test")
    private LoginService loginService;

    public String login() {
        return loginService.login();
    }

}

测试代码:业务服务接口层:

public interface LoginService {

    String login();
}

测试代码:具体服务层(这里尝试了写两个实现类,多态情况下)

@MyService(value = "test")
public class LoginServiceImpl implements LoginService {

    @MyAutowired
    private LoginMapping loginMapping;

    @Override
    public String login() {
        return loginMapping.login();
    }
}

@MyService
public class TestLoginServiceImpl implements LoginService {

    @Override
    public String login() {
        return "测试多态情况下依赖注入";
    }
}

测试代码:数据持久层接口层

public interface LoginMapping {

    String login();
}

测试代码:数据持久层具体持久层

@MyMapping
public class LoginMappingImpl implements LoginMapping {

    @Override
    public String login() {
        return "项目启动成功";
    }
}

然后我们写一个启动类PlatformApplication

/**
 * 类 名: PlatformApplication
 *
 * @author: jiaYao
 */
public class PlatformApplication {

    public static void main(String[] args) throws Exception {
        // 从容器中获取对象(自动首字母小写)
        MyApplicationContext applicationContext = new MyApplicationContext();
        LoginController loginController = (LoginController) applicationContext.getIocBean("LoginController");
        String login = loginController.login();
        System.out.println(login);
    }

}

启动程序后我们通过断点观察一下代码情况

image-20190325093910145

控制台输出日志信息如下:

正在加载配置文件application.properties
正在加载: cn.jiayao.platform.common.MyApplicationContext.class
正在加载: cn.jiayao.platform.config.ConfigurationUtils.class
正在加载: cn.jiayao.platform.core.annotation.MyAutowired.class
正在加载: cn.jiayao.platform.core.annotation.MyController.class
正在加载: cn.jiayao.platform.core.annotation.MyMapping.class
正在加载: cn.jiayao.platform.core.annotation.MyService.class
正在加载: cn.jiayao.platform.core.annotation.Value.class
正在加载: cn.jiayao.platform.modular.controller.LoginController.class
正在加载: cn.jiayao.platform.modular.dao.impl.LoginMappingImpl.class
正在加载: cn.jiayao.platform.modular.dao.LoginMapping.class
正在加载: cn.jiayao.platform.modular.service.impl.LoginServiceImpl.class
正在加载: cn.jiayao.platform.modular.service.impl.TestLoginServiceImpl.class
正在加载: cn.jiayao.platform.modular.service.LoginService.class
正在加载: cn.jiayao.platform.PlatformApplication.class
正在加载: cn.jiayao.platform.utils.MyArrayUtils.class
控制反转访问控制层:loginController
控制反转持久层:loginMappingImpl
控制反转服务层:loginServiceImpl
控制反转服务层:testLoginServiceImpl
注入配置文件  class cn.jiayao.platform.modular.controller.LoginController 加载配置属性ioc.scan.pathTest
项目启动成功

这样,一个简单的IOC容器就创建完成了;

但是此处还有几点需要再次优化一下,目前对于Maven中引用的jar包无法进行注入,因为没有将jar包中的对象交给IOC容器管理,然后就是对接口的注入,由于接口的特殊性,不能被实例化,如何高效的获取到接口的实现类这个问题还待优化;

Gitlab地址:

  • https://gitlab.com/qingsongxi/ioc

原文发布于微信公众号 - Java3y(java3y)

原文发表时间:2019-03-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券