简单模拟实现Spring-IoC

        Spring框架的核心内容中,主要有两个功能:IoC和AOP。IoC又称为:控制反转,或者叫“自动注入”,而AOP则称为“面向切面编程”方案,或者称为:非侵入式逻辑更改/扩展。IoC的控制反转
中所谓的反转是指:非反转情况下,类的对象应该由代码自身实现实例化,而控制反转意味着,类的对象不是由代码本身完成实例化的,而是由某个“外在”系统实现实例化。下面我们来看一个非反转经典代码:

class Some {
    private UserClass obj;
    // ...

    某个方法() {
        this.obj = new UserClass();
    }
}

这是由代码自身实现对象实例化。那么,IoC是使用“配置、自动注入”这两个配套手段实现对对象的实例化过程,即DI。代码中不会出现实例化过程。这样做的好处至少可以使得用户代码更加容易替换。

在学习了SpringIoC的相关配置以及一些简单的实现原理后,开始试着模拟实现一个简单的容器来模拟IoC的自动装配以及依赖注入。

模拟实现的几个部分如下所示:

        1、可以实现对于需要管理的bean对象的包进行扫描,得到需要管理的bean对象。

        2、对于bean对象的获取,采用了注解+包扫描的方式,注解借鉴了spring的注解命名,但是我们也可以使用XML的方式来配置。

        3、采用“懒汉模式”来获取bean对象,即在调用getBean时才进行DI,最终返回一个完成依赖注入的bean对象。

        4、对于依赖注入采用两种不同的方式来进行,一种是通过属性,另一种是通过属性的setting方法来进行注入。

        5、对于循环依赖进行了简单的处理,解决了循环依赖的问题

        6、对于无法操作的jar包中的对象,采用Bean注解的方式,调用get方法来返回这个bean对象,对于无参与参数中存在依赖的情况进行了简单处理

下面对这几点进行简单的介绍:

1、容器对bean对象的管理以及注解的配置

        采用工厂模式,使用BeanFactory作为容器来产生并且管理bean对象,容器的真正实现在与BeanFactory中的beanPool成员,采用HashMap的方式以bean对象的Class名为键,BeanDefinition为值来存储bean对象。

        而BeanDefinition的作用在于存储bean的类以及反射得到的实例化对象,以及是否完成了依赖注入,对于这个成员在后面的解决循环依赖的部分中介绍。

public class BeanDefinition {
    private Class<?> klass;
    private Object object;
    private boolean isInject;

    public BeanDefinition() {
        this.isInject = false;

BeanFactory中定义了一系列的对bean对象的管理以及获取操作,它的主体代码如下:

private static Map<String, BeanDefinition> beanPool;
    static {
        beanPool = new HashMap<>();
    }

    public BeanFactory() {
    }

    public static void scanBeanByAnnotation(String packageName) throws ClassNotFoundException, IOException, URISyntaxException {
        new PackageScanner() {
            @Override
            public void dealClass(Class<?> klass) {
                //对带有Component注解的类进行bean管理
                if (klass.isAnnotationPresent(Component.class)) {
                    try {
                        BeanDefinition beanDefinition = new BeanDefinition();
                        beanDefinition.setKlass(klass);
                        Object object = klass.newInstance();
                        beanDefinition.setObject(object);

                        String beanId = klass.getName();
                        beanPool.put(beanId, beanDefinition);

                        //处理Component的类中带Bean注解的方法,即把带有Bean注解的方法也加入到容器中
                        dealBeanMethod(beanDefinition);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

        }.scanPackage(packageName);
    }

上面的代码就是对bean对象获取后管理在容器中,里面使用到了一个包扫描类,PackageScanner类是我自己定义的用来得到一个包下的所有文件的抽象工具类,且不局限于在此模块中使用。这个点在下面会展示出来。我们通过扫描之后得到包下的所有文件,且通过PackageScanner工具预留的dealClass抽象方法,得到带有@Component注解的类,即想管理的bean对象的类。

        对于注解的配置我参考Spring的命名方式,@Component注解只能用来修饰类,标识需要管理的bean对象;@Autowired注解可以修饰方法和成员,标识采用何种依赖注入式来注入依赖;@Bean注解可以修饰方法,它的作用是标识那些无法直接设置注解但是我们又需要管理的类,通过get方法获得bean对象来进一步管理。

2、bean对象的获取-包扫描

       bean对象的获取是基于包扫描抽象类的来得以实现的,这里主要讲述包扫描的具体实现。主体代码如下所示:

//提供给使用此工具的类来使用,处理扫描到的类
    public abstract void dealClass(Class<?> klass);

    public void scanPackage(String packageName) throws URISyntaxException, ClassNotFoundException, IOException {
        //得到包路径
        String pathName = packageName.replace(".", "/");
        //由包路径得到Uniform Resource Locator - 统一资源定位符
        URL url = Thread.currentThread().getContextClassLoader().getResource(pathName);
        //得到资源的协议,这里主要用来区分jar包与普通包
        String protocol = url.getProtocol();

        if ("jar".equals(protocol)) {
            dealJar(packageName, url);
        } else {
            //通过URL得到文件根目录,即文件的路径
            File root = new File(url.toURI());
            dealDir(packageName, root);
        }
    }

包扫描是通过对传入的包名进行解析与分割,分为jar包与非jar包来处理,对于非jar包采取下面的方式来处理:

private void dealDir(String packageName, File root) throws ClassNotFoundException {
        //得到此根目录下的所有文件即文件夹
        File[] files = root.listFiles();

        for(File file : files) {
            if (file.isDirectory()) {
                //递归此文件夹的所有文件
                dealDir(packageName + file.getName(), file);
            } else {
                //找出所有的.class
                if (file.getName().endsWith(".class")) {
                    String className = file.getName().replace(".class", "");
                    //得到class文件全类名
                    className = packageName + "." + className;
                    Class<?> klass = Class.forName(className);
                    dealClass(klass);
                }
            }
        }
    }

        采用递归的方式得到这个包路径下的所有文件,自动调用dealClass这个抽象方法来处理得到的class文件。

而对于jar包我的处理方式大致类似与处理非jar包,主要调用了jdk中的util包下的jar包下的一些方法来得到具体文件,如下所示:

//对jar包的处理
    private void dealJar(String packageName, URL url) throws IOException, ClassNotFoundException {
        //得到jar包的URLConnection,之后得到jar文件
        JarURLConnection connection = (JarURLConnection) url.openConnection();
        JarFile jarFile = connection.getJarFile();
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String entryName = entry.getName();
            if (!entryName.endsWith(".class")) {
                continue;
            }
            entryName = entryName.replace(".class", "");
            String className = entryName.replace("/", ".");
            if (!className.startsWith(packageName)) {
                continue;
            }
            Class<?> klass = Class.forName(className);
            dealClass(klass);
        }
    }

3、bean对象的获取

对于bean对象的获取是采用getBean方法,以想得到的class名为键,来得到完成依赖注入的bean对象结果。在返回bean对象之前完成依赖注入:

/**
     * 完成两个任务
     * 1、给需要得到的bean对象进行依赖注入
     * 2、返回需要的bean的对象
     * @param beanId
     * @param <T>
     * @return
     */
    public static <T> T getBean(String beanId) throws Exception {
        BeanDefinition beanDefinition = beanPool.get(beanId);
        if (beanDefinition == null) {
            throw new RuntimeException("Bean:" + beanId + "不存在");
        }

        T bean = (T) beanDefinition.getObject();
        if (!beanDefinition.isInject()) {
            //依赖注入
            beanDefinition.setInject(true);
            inject(beanDefinition);
        }
        return bean;
    }

    //注入依赖
    private static void inject(BeanDefinition beanDefinition) throws Exception {
        Class<?> klass = beanDefinition.getKlass();
        Object object = beanDefinition.getObject();
        //通过成员注入
        injectByField(klass, object);
        //通过方法注入
        injectByMethod(klass, object);
    }

还提供了一个使用类来得到bean对象,返回执行方法的返回值,即使用类.getName()转化为beanId来得到bean对象。

 public static <T> T getBean(Class<?> klass) throws Exception {
        return getBean(klass.getName());
    }

4、依赖注入以及简单解决循环依赖问题

        对于依赖注入采用两种方式来注入,即使用@Autowired标识成员变量,或者标识set方法来注入依赖。但是二者最终的实现方式均是采用执行set方法进行依赖的注入,实现代码及注释如下:

//注入依赖
    private static void inject(BeanDefinition beanDefinition) throws Exception {
        Class<?> klass = beanDefinition.getKlass();
        Object object = beanDefinition.getObject();
        //通过成员注入
        injectByField(klass, object);
        //通过方法注入
        injectByMethod(klass, object);
    }

    private static void injectByField(Class<?> klass, Object object) throws Exception {
        Field[] fields = klass.getFields();
        for (Field field : fields) {
            if(!field.isAnnotationPresent(Autowired.class)) {
                continue;
            }
            String fieldName = field.getName();
            Class<?> fieldType = field.getType();
            String fieldMethodName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
            Method fieldMethod = klass.getDeclaredMethod(fieldMethodName,fieldType);
            //注入的属性,即注入的依赖bean的类型

            setterMethodInvoke(fieldMethod, object, fieldType);

        }
    }

    private static void injectByMethod(Class<?> klass, Object object) throws Exception {
        Method[] methods = klass.getDeclaredMethods();
        for (Method method : methods) {
            String methodName = method.getName();
            if(!methodName.startsWith("set") || !method.isAnnotationPresent(Autowired.class) || method.getParameterCount() != 1) {
                continue;
            }
            //得到set方法的参数类型,传递方法、对象以及参数类型给doSetterMethod
            Class<?> parameterType = method.getParameterTypes()[0];
            setterMethodInvoke(method, object, parameterType);
        }
    }

    private static void setterMethodInvoke(Method fieldMethod, Object object, Class<?> fieldType) throws Exception {
        Object parameter = getBean(fieldType);
        if(parameter == null) {
            throw new Exception("需要注入的类【" + parameter.getClass().getName() + "】");
        }
        //执行setter方法注入了依赖
        fieldMethod.invoke(object, parameter);
    }

这些方法都是私有方法,换句话说,不需要使用者进行其他操作,由容器自动装配以及依赖注入,对容器进行了封装,简化了容器的使用。

对于循环依赖的解释及处理如下:

 @Component
public class ClassOne {
    @Autowired
    private ClassTwo two;
    ...
}
@Component
public class ClassTwo {
    @Autowired
    private ClassThird third;
    ...
}
public class ClassThird {
    @Autowired
    private ClassOne one;
    ...
}

        明显,上述三个类的各个成员之间形成了循环依赖关系,则,对三个类中的任何一个对象进行“注入”操作时,都会产生递归!
首先看一种简单的循环依赖关系:
A -> B    表示,A类中存在B类的成员,且,需要注入;
B -> A    表示,B类中存在A类的成员,且,需要注入。
假设getBean(A.class);
则,对A的对象a进行注入操作,即,需要对a.b进行注入;但是,B的对象是通过getBean(B.class)获取的;而getBean()方法,又会
继续实现对b对象的注入;即,需要对b.a进行注入;发现,b.a的注入过程已经出现循环递归了!
但是,如果先:
            beanDefinition.setInject(true);
            inject(beanDefinition);
这个过程详细变量跟踪一次:
getBean(A.class);在这个过程中,需要完成对a的注入,也就是对a.b实现注入;但是,先设置A已经完成注入了!a.b的获取是通过getBean(B.class)获得的,这个过程需要实现对b.a进行注入;但是,先设置B已经完成注入了!再次对A的对象进行注入时,发现A类的注入操作已经完成了,就不需要再对A进行注入了,这就避免了无限递归!

        简单的对设置依赖已经注入提前到真正注入依赖之前,便可解决循环递归依赖注入问题。

6、对于jar包内的bean对象的获取以及依赖注入问题

包内类处理:
        对于Jar包内的类,采取由用户书写一个函数来实例化,但是由IoC调用的方法。即在本身包中新建一个类用于配置,其中书写的方法由新增的Bean注解标记,扫描到该方法,由IoC调用,即可将Jar包内的类实例出来。对该方法应该如下理解:
1.方法的返回值是一个BeanDefinition,方便与放入pool
2.在方法中实例化相关类,这是主要目的
3.允许方法的参数是其他Bean,这点是处理难点,当所需的Bean未生成甚至该类在其他包内需要额外处理

        此时重点问题是依赖存在于参数,那么如何判断依赖是否满足及当满足时运行该方法。这种主要的应用场景是需要的Bean存在于其他Jar包内,无法获取该参数,即依赖此时无法完成,应利用Map先存储。额外新建一个定义类BeanMethod,用于被Bean标记得方法,对应类的实例化对象,需要注入数目(即参数类型个数)

总共生成两个Map,一个List
        第一个Map:用于存储方法BeanMethod和该类所需参数类型(使用Map存储,虽然值没有用处,但是借助Map键只能出现一次的特性可以解决参数重复出现的情况)的对应关系。
        List:用于存储依赖关系以满足(即BeanMethod中的注入数目为0)的方法
        第二个Map:该Map主要是为了效率考虑,我们每生成一个Bean,会生成其自己所需的依赖关系,那么依赖于这个Bean的方法,我们应该进行相应的处理,利用存储了该Bean及依赖于该Bean的BeanMethodDefinition列表,可以简化寻找的过程,按键找到对应的值,进行注入数目减一
包内循环依赖:
jar包内的循环依赖是无解的,因为循环依赖的存在(即相关方法的参数发生了循环关系),所设置的方法参数不全,无法调用,即无法实例化出对象。此时解决方法是抛出一个异常。
故如何查找到某个对象依赖不完全并识别出来是重点:

static BeanMethodDefinition getBeanMethodDefinitionByClassName(String className) {
        if (beanMethodDepandent.isEmpty()) {
            return null;
        }
        for (BeanMethodDefinition beanMethodDefinition : beanMethodDepandent.keySet()) {
            Class<?> klass = beanMethodDefinition.getKlass();
            if (klass.getName().equals(className)) {
                return beanMethodDefinition;
            }
        }
        return null;
    }

在beanMethodDepandent图中,存储了该Bean方法定义及其依赖的参数对应关系,当在pool内查找不到该类的对象,说明该类没有生成,那么就需要获得该类,已知类名称,就可以在图中依据方法定义查找该类,并返回。

        上面就是我对于spring中的IoC的简单模拟实现,对于加深sping的理解与使用有了更大一部分的体会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值