手把手带你实现一个简单的依靠名称注入的IOC容器

一、IOC容器简介


        IOC(Inversion of Control,控制反转)是一种设计原则,在传统的编程中,对象之间的依赖关系是由对象自己管理和创建的,而在IOC容器中,控制权被反转,对象的创建和管理由容器来负责,简单来说,IOC容器负责各个对象之间的依赖管理,开发人员无需再去手动为对象注入依赖。

二、实现IOC容器的基本思路


        1.我们需要明确哪些类需要ioc容器去管理,这里我们使用注解去标识

        2.再由ioc管理的类中,哪些属性需要自动注入,这里我们同样使用注解来标识

        3.需要一个扫描器来扫描包中的所有类,将需要管理的类加入ioc容器管理

        4.获取需要管理的类,利用java反射机制为其创建对象以及进行依赖注入

 三、IOC容器的代码实现


1.标识注解的实现

        为了方便管理,创建一个名为Annotation的包用来存储标识注解,这个包下面有两个注解:InjectByName和Component,第一个注解用于标识类中属性,告诉ioc这个属性需要自动注入,第二个注解标识类,表明此类需要由ioc管理

 

Component

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
    String value() default ""; //value用于指定该类在ioc容器中存储的名称
}                              //如果没有指定则由类名作为名称                      

InjectByName

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectByName {
    String value(); //用来指定需要注入的对象的类名称,即在ioc容器中存储的对象的名称
}                    //(名称由Component注解指定,或默认为类的名称)

2.包扫描器的实现

设计思路如下,传入包名字符串,即为需要扫描的包,然后将拥有Component注解的类都挑选出来,由于包下面可能会还有包,所以我们需要递归的去寻找,直到把改包下以.class文件结尾且拥有Component注解的文件都挑选出来

 首先创建一个名叫PackageScanner的类,用于包扫描

import java.io.File;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

public class PackageScanner {
    //传入需要扫描的包名(形如"com.myPackage.*"的形式)
    public static void getAllBeans(String packageName) throws Exception {
        if (packageName == null) throw new IllegalArgumentException("the package name is null."); //包名为null则抛出异常
        String path = packageName.replace(".", "/"); //将包名的"."替换为"/"
        ClassLoader classLoader = PackageScanner.class.getClassLoader();
        URL pathUrl = classLoader.getResource(path); //获取包的URL
        if (pathUrl != null) {
            File dir = new File(pathUrl.getFile());
            File[] files = dir.listFiles();  //获取该目录下的所有文件,包括目录
            assert files != null;
            for (File file : files) {
                if (file.isFile() && file.getName().endsWith(".class")) { //满足要求,对该类进行处理
                      //找到满足要求的类后的逻辑我们待会去实现
                        /* ...........*/
                    
                } else if (file.isDirectory()) { //如果是目录则递归的去遍历该目录下的文件
                    getAllBeans(packageName + "." + file.getName());
                }
            }
        }
    }
}

3.二级缓冲以及BeanDefinition的实现

使用过spring的同学应该都听说过三级缓冲这个东西,笔者先带大家回顾一下为什么spring的IOC容器需要三重缓冲。

扫描二维码关注公众号,回复: 17278723 查看本文章

1.首先就是要解决对象循环依赖的问题,即A依赖B,B又依赖C,C又依赖A,如果没有缓冲池,那么就会出现问题,所以我们需要把创建对象所需的"原材料"放进一种特殊的类中,在spring中这个特殊的类叫BeanDefinition,这个类的对象可以理解为一个"还没创建完全的对象",把这些还未创建完全的对象(BeanDefinition)放进缓存池里,然后再用缓存池里的BeanDefinition里的“原材料”来创建完全的,我们需要的对象。

2.事实上,解决循环依赖只需要使用两个缓存,也就是说二级缓冲就足够了,spring还要多使用一个缓冲池的原因是因为spring还需要对对象进行AOP增强,由于本篇文章只实现IOC中的依赖注入,并没有Aop的功能,所以这里使用两级缓冲即可

了解了BeanDefinition(用于存储创建对象的原材料的类)的作用后,我们对该类做出实现

首先我们通过反射创建对象,且需要反射来为其进行依赖注入,所以原材料就包括该类的class对象,然后又因为我们是通过类名称来获取该对象的名称,所以原材料还包括类的名称

BeanDefinition

public class BeanDefinition {
    private Class clazz;  //目标类的class对象
    private String beanName;  //目标类的对象名称(用于标识ioc管理的对象)
    // 有参无参构造,属性的getter,setter用idea快捷键生成一下
    BeanDefinition (Class clazz, String beanName) {
        this.clazz = clazz;
        this.beanName = beanName;
    }
    BeanDefinition () {
        this.clazz = null;
        this.beanName = null;
    }
    public Class getClazz() {
        return clazz;
    }

    public void setClazz(Class clazz) {
        this.clazz = clazz;
    }

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
    @Override
    public String toString() {
        return "{" + this.beanName + "   " + this.clazz + "}";
    }
}

 然后是二级缓冲中的第二级缓冲池,即用来存储类的BeanDefinition对象的缓冲池,因为是单例模式,所以这里使用Set集合,为了方便,我们把它放进PackageScanner这个类里

private static final Set<BeanDefinition> beanDefinitions = new HashSet<>();
//获取beanDefinitions的方法
public static Set<BeanDefinition> getBeanDefinitionSet() {
        return beanDefinitions;
}

 最后我们就要处理,在包扫描过程中,满足条件(以.class结尾,且拥有Component注解的类文件)的文件,将其封装为BeanDifinition对象并存进beanDefinitions中(二级缓存池)

//获取该类的完整限定名
String clazzName = packageName + "." + file.getName().substring(0, file.getName().length() - 6);
//使用Class.forName来获取Class对象,并获取类的名称,将其放进beanHandler方法中处理
beanHandler(Class.forName(clazzName), file.getName().substring(0, file.getName().length() - 6));

所以 getAllBeans的完整写法如下

public static void getAllBeans(String packageName) throws Exception {
        if (packageName == null) throw new IllegalArgumentException("the package name is null.");
        String path = packageName.replace(".", "/");
        ClassLoader classLoader = PackageScanner.class.getClassLoader();
        URL pathUrl = classLoader.getResource(path);
        if (pathUrl != null) {
            File dir = new File(pathUrl.getFile());
            File[] files = dir.listFiles();
            assert files != null;
            for (File file : files) {
                if (file.isFile() && file.getName().endsWith(".class")) {
                    //处理正确文件的代码放下面
                    String clazzName = packageName + "." + file.getName().substring(0, file.getName().length() - 6);
                    //这个方法下一步实现
                    beanHandler(Class.forName(clazzName), file.getName().substring(0, file.getName().length() - 6));
                } else if (file.isDirectory()) {
                    getAllBeans(packageName + "." + file.getName());
                }
            }
        }
    }

接下来就是创建BeanDefinition的方法beanHandler

传进Class对象以及类名,然后判断Component注解是否指明了对象名,如果有则用指定的对象名标识该对象,如果没有则使用类名作为默认名

private static void beanHandler(Class<?> clazz, String typeName) {
        //判断是否存在Component注解
        if (clazz.isAnnotationPresent(Component.class)) {
            String beanName = clazz.getAnnotation(Component.class).value();
            //判断Component注解是否指明了对象名,没有则使用类名作为默认对象名
            beanDefinitions.add(new BeanDefinition(clazz, beanName.isEmpty() ? typeName : beanName));
        }
 }

最终,PackageScanner类的完整代码如下:


import java.io.File;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

public class PackageScanner {
    private static final Set<BeanDefinition> beanDefinitions = new HashSet<>();
    private static void beanHandler(Class<?> clazz, String typeName) {
        if (clazz.isAnnotationPresent(Component.class)) {
            String beanName = clazz.getAnnotation(Component.class).value();
            beanDefinitions.add(new BeanDefinition(clazz, beanName.isEmpty() ? typeName : beanName));
        }
    }
    public static Set<BeanDefinition> getBeanDefinitionSet() {
        return beanDefinitions;
    }
    public static void getAllBeans(String packageName) throws Exception {
        if (packageName == null) throw new IllegalArgumentException("the package name is null.");
        String path = packageName.replace(".", "/");
        ClassLoader classLoader = PackageScanner.class.getClassLoader();
        URL pathUrl = classLoader.getResource(path);
        if (pathUrl != null) {
            File dir = new File(pathUrl.getFile());
            File[] files = dir.listFiles();
            assert files != null;
            for (File file : files) {
                if (file.isFile() && file.getName().endsWith(".class")) {
                    String clazzName = packageName + "." + file.getName().substring(0, file.getName().length() - 6);
                    beanHandler(Class.forName(clazzName), file.getName().substring(0, file.getName().length() - 6));
                } else if (file.isDirectory()) {
                    getAllBeans(packageName + "." + file.getName());
                }
            }
        }
    }
}

至此,我们已经完成了包扫描功能以及第二级缓存池的创建

4.将BeanDefinition转换为bean对象,并存入一级缓存

创建名为BeanCreator的对象,一级缓存使用Map实现,利用BeanDefinition中的Class对象创建对象放入Map中作为值,将beanName即对象名放入Map作为键

    //二级缓存
    private final Set<BeanDefinition> beanDefinitionSet;
    //一级缓存
    private final Map<String, Object> beanMap = new HashMap<>();
    //注入二级缓存
    public BeanCreator(Set<BeanDefinition> beanDefinitions) {
        this.beanDefinitionSet = beanDefinitions;
    }
    private void getBeanDefinitions() throws Exception {
        //用每一个BeanDefinition对象中的Class属性来创建对象,并以beanName为值存入beanMap中
        for (BeanDefinition beanDefinition : this.beanDefinitionSet) {
            this.beanMap.put(beanDefinition.getBeanName(), beanDefinition.getClazz().getConstructor(null).newInstance());
        }
    }

然后就是最重要的依赖注入了,我们需要把对象中标记了InjectByName注解的属性利用属性进行注入,创造完整的bean对象

public void createBean() throws Exception {
        //初始化以及缓存(beanMap)
        this.getBeanDefinitions();

        //遍历beanMap中的每一个bean
        for (Map.Entry<String, Object> beanDefinition : this.beanMap.entrySet()) {
            Object obj = beanDefinition.getValue();
            Class<?> clazz = obj.getClass();

            //反射获取bean的所有属性
            Field[] fields = clazz.getDeclaredFields();

            //遍历所有属性
            for (Field field : fields) {
                field.setAccessible(true);

                   //判断该属性是否存在InjectByName注解
                if (field.isAnnotationPresent(InjectByName.class)) {

                    //获取需要注入的对象的对象名
                    String beanName = field.getAnnotation(InjectByName.class).value();
                    
                    //判断beanMap中是否存在该类,不存在则抛出异常
                    if (beanMap.containsKey(beanName)) {
                        //存在则注入该对象
                        field.set(obj, beanMap.get(beanName));
                    } else throw new ClassNotFoundException("the bean that required is not exist.");
                }
            }
        }
   }

顺带提供一个接口,通过对象名来返回beanMap中的对象

public Object getBeanByName(String name) throws ClassNotFoundException {
        if (this.beanMap.containsKey(name)) return this.beanMap.get(name);
        else throw new ClassNotFoundException("the bean that required is not exist.");
}

最终,BeanCreator的完整代码如下

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class BeanCreator {
    private final Set<BeanDefinition> beanDefinitionSet;
    private final Map<String, Object> beanMap = new HashMap<>();
    public BeanCreator(Set<BeanDefinition> beanDefinitions) {
        this.beanDefinitionSet = beanDefinitions;
    }
    private void getBeanDefinitions() throws Exception {
        for (BeanDefinition beanDefinition : this.beanDefinitionSet) {
            this.beanMap.put(beanDefinition.getBeanName(), beanDefinition.getClazz().getConstructor(null).newInstance());
        }
    }
    public void createBean() throws Exception {
        this.getBeanDefinitions();
        for (Map.Entry<String, Object> beanDefinition : this.beanMap.entrySet()) {
            Object obj = beanDefinition.getValue();
            Class<?> clazz = obj.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                if (field.isAnnotationPresent(InjectByName.class)) {
                    String beanName = field.getAnnotation(InjectByName.class).value();
                    if (beanMap.containsKey(beanName)) {
                        field.set(obj, beanMap.get(beanName));
                    } else throw new ClassNotFoundException("the bean that required is not exist.");
                }
            }
        }
    }
    public Object getBeanByName(String name) throws ClassNotFoundException {
        if (this.beanMap.containsKey(name)) return this.beanMap.get(name);
        else throw new ClassNotFoundException("the bean that required is not exist.");
    }
}

5.将之前的所有功能用IocApplication类进行整合

此部分比较简单,直接上代码吧

public class IocApplication {
    private final BeanCreator beanCreator;
    
    //构造方法传入需要扫描的包名
    public IocApplication(String scannerPath) {
        try {
            PackageScanner.getAllBeans(scannerPath);
            this.beanCreator = new BeanCreator(PackageScanner.getBeanDefinitionSet());
            this.beanCreator.createBean();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    //通过beanName来获取对象
    public Object getBeanByName(String beanName) {
        try {
            return this.beanCreator.getBeanByName(beanName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

至此本篇文章中简易ioc容器的代码以全部完成

 四、测试

我们创建三个类Test01,Test02,Test03,其中Test03依赖Test01和Test02三个类中都有print方法,具体内容如下

@Component("test01")
public class Test01 {
    public void print() {
        System.out.println("test01");
    }
}


@Component("test02")
public class Test02 {
    public void print() {
        System.out.println("test02");
    }
}



@Component("test03")
public class Test03 {
     //表明需要注入属性需要的对象的beanName
    @InjectByName("test01")
    private Test01 test01;

    @InjectByName("test02")
    private Test02 test02;

    public void print() {
        test01.print();
        test02.print();
    }
}

main方法如下

public class Main {
    public static void main(String[] args) {
        IocApplication application = new IocApplication("com.example");

        //通过bean name获取对象
        Test03 test03 = (Test03) application.getBeanByName("test03");
        test03.print();
    }
}

运行结果:

打印成功,没有出现空指针异常,说明依赖注入成功

五、总结

        本文做出了对ioc容器的简单实现,然而事实上spring的ioc容器由于需要扩展Aop增强,还有各种初始化处理以及各种beanFactory以及要考虑可扩展性,所以实际上要复杂的多,总而言之本文只是提供了关于spring ioc的一个基本思路,如果想要了解更多关于spring ioc的内容,建议阅读spring源码

猜你喜欢

转载自blog.csdn.net/greatming666/article/details/134794559