加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

我要造轮子之IoC和依赖注入

发布时间:2020-12-13 21:58:26 所属栏目:百科 来源:网络整理
导读:1、前言 因为这是我设想要写的一系列文章的第一篇。所以我先说明一下我为什么要重复造轮子。 在这里造轮子的目的不是为了造出比前人更出色的轮子来,而是通过造轮子,学习轮子内部的结构及相关原理。甚至去模仿前人轮子上的优点,吸收这些优点。 这一系列文

1、前言

因为这是我设想要写的一系列文章的第一篇。所以我先说明一下我为什么要重复造轮子。

在这里造轮子的目的不是为了造出比前人更出色的轮子来,而是通过造轮子,学习轮子内部的结构及相关原理。甚至去模仿前人轮子上的优点,吸收这些优点。

这一系列文章初步估计应该包括:IoC和依赖注入、AOP、ORM、Servlet容器(tomcat)等。

2、IoC和依赖注入的概念

Inverse of Control,控制反转。

IoC主要功能是依赖关系的转移。应用的本身不负责依赖对象的创建和维护,而是由第三方容器负责。控制权就由应用转移到了外部容器。

IoC的主要功能由控制反转来解释并不是很好理解。所以提出了新的概念Dependency Injection.

DI依赖注入,调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以转移对某一接口实现类的依赖。也就是在运行期,由外部容器动态地将所依赖的对象注入到组件中去。

最常用的IoC和DI框架没有之一:Spring。
Spring IoC的介绍和使用: http://www.52php.cn/article/p-hdyilwng-ny.html

3、源码结构

本文中使用的是通过注解(annotation)的方式来对需要进行IoC和DI的类进行管理。


我要看大图:http://img.blog.csdn.net/20160218111901359

4、实现过程

4.1 工具类

首先我们需要两个工具类:类加载器ClassLoader 和反射工厂类:

  • 类加载器
    此类加载器通过getClassSet(String packageName)方法获取到packageName路径下及Jar包中的类的集合
package xyz.letus.framework.ioc;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** * 类加载器 * @ClassName: ClassLoader * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月14日 * */
public class ClassLoader {
    public static final Logger LOGGER = LoggerFactory.getLogger(ClassLoader.class);

    /** * 获取线程上的类加载器 * @Title: getClassLoader * @Description: TODO * @param @return * @return java.lang.ClassLoader * @throws */
    public static java.lang.ClassLoader getClassLoader(){
        return Thread.currentThread().getContextClassLoader();
    }
    /** * 加载类 * @Title: loadClass * @Description: TODO * @param @param className * @param @param isInitialized * @param @return * @return Class<?> * @throws */
    public static Class<?> loadClass(String className,boolean isInitialized){
        Class<?> clazz = null;
        try {
            clazz = Class.forName(className,isInitialized,getClassLoader());
        } catch (ClassNotFoundException e) {
            LOGGER.error("load class failure",e);
            throw new RuntimeException(e);
        }

        return clazz;
    }
    /** * 获取包下所有的类 * @Title: getClassSet * @Description: TODO * @param @param packageName * @param @return * @return Set<Class<?>> * @throws */
    public static Set<Class<?>> getClassSet(String packageName){
        Set<Class<?>> classes = new HashSet<Class<?>>();

        try {
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".","/"));

            while(urls.hasMoreElements()){
                URL url = urls.nextElement();
                if(url != null){
                    String protocol = url.getProtocol();
                    if(protocol.equals("file")){
                        String packagePath = url.getPath().replace("%20"," ");
                        addCommonClass(classes,packagePath,packageName);
                    }else if(protocol.equals("jar")){
                        addJarClasses(classes,url);
                    }
                }
            }

        } catch (IOException e) {
            LOGGER.error("get class set failure",e);
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        return classes;
    }
    /** * 把jar包中所有的类加入集合 * @Title: addJarClasses * @Description: TODO * @param @param classes * @param @param url * @param @throws IOException * @return void * @throws */
    private static void addJarClasses(Set<Class<?>> classes,URL url)
            throws IOException {
        JarURLConnection connetion = (JarURLConnection) url.openConnection();
        if(connetion != null){
            JarFile jar = connetion.getJarFile();
            if(jar != null){
                Enumeration<JarEntry> enties = jar.entries();
                while(enties.hasMoreElements()){
                    JarEntry entry = enties.nextElement();
                    String entryName = entry.getName();
                    if(entryName.endsWith(".class")){
                        String className = entryName.substring(0,entryName.lastIndexOf('.')).replaceAll("/",".");
                        doAddClass(classes,className);
                    }
                }
            }
        }
    }

    /** * 把文件夹中的所有类加入集合 * @Title: addCommonClass * @Description: TODO * @param @param classes * @param @param packagePath * @param @param packageName * @return void * @throws */
    private static void addCommonClass(Set<Class<?>> classes,String packagePath,String packageName){
        File[] files = new File(packagePath).listFiles(new FileFilter() {

            public boolean accept(File file) {
                // TODO Auto-generated method stub
                return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
            }
        });

        for(File file : files){
            String fileName = file.getName();
            if(file.isFile()){
                String className = packageName + '.' +  fileName.substring(0,fileName.lastIndexOf('.'));
                doAddClass(classes,className);
            }else{
                String subPackagetPath = packagePath + "/" + fileName;
                String subPackagetName = packageName + "/" + fileName;
                addCommonClass(classes,subPackagetPath,subPackagetName);
            }
        }
    }
    /** * 加入类到集合 * @Title: doAddClass * @Description: TODO * @param @param classes * @param @param className * @return void * @throws */
    private static void doAddClass(Set<Class<?>> classes,String className){
        classes.add(loadClass(className,false));
    }
}
  • 反射工厂

    此工厂的功能是:
    1、在运行期创建类对象
    2、运行期为对象的属性赋值
    3、运行期调用对象的方法

package xyz.letus.framework.ioc;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** * 反射工厂 * @ClassName: ReflectionFactory * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
public class ReflectionFactory {
    public static final Logger LOGGER = LoggerFactory.getLogger(ReflectionFactory.class);
    /** * 创建实例 * @Title: newInstance * @Description: TODO * @param @param clazz * @param @return * @return Object * @throws */
    public static Object newInstance(Class<?> clazz){
        Object instance = null;
        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            LOGGER.error("new instatnce failure",e);
            throw new RuntimeException(e);
        } 

        return instance;
    }

    /** * 调用方法 * @Title: invokeMethod * @Description: TODO * @param @param obj * @param @param method * @param @param args * @param @return * @return Object * @throws */
    public static Object invokeMethod(Object obj,Method method,Object...args){
        Object result = null;
        try {
            method.setAccessible(true);
            result = method.invoke(obj,args);
        } catch (Exception e) {
            LOGGER.error("invoke method failure",e);
            throw new RuntimeException(e);
        } 
        return result;
    }
    /** * 设置成员变量值 * @Title: setField * @Description: TODO * @param @param obj * @param @param field * @param @param value * @return void * @throws */
    public static void setField(Object obj,Field field,Object value){
        try {
            field.setAccessible(true);
            field.set(obj,value);
        } catch (Exception e) {
            LOGGER.error("set field failure",e);
            throw new RuntimeException(e);
        } 
    }
}

4.2 注解类

声明两个注解类:Component和Inject

  • Component
    这是一个类注解,加上这样注解的类会在运行期交由IoC容器扫描并自动创建实例。
package xyz.letus.framework.ioc.annotation;

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

/** * 组件描述注解 * @ClassName: Component * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}
  • Inject
    这是一个属性注解,加上此注解的属性,会在运行期由IoC容器进行DI操作,为属性赋值。
package xyz.letus.framework.ioc.annotation;

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

/** * 依赖注入注解 * @ClassName: Controller * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    String value() default "";
}

4.3 扫描托管的类

ClassFactory类查找所有basePackage包下的类,并判断这些类是否有标注@Component注解。带此注解的类是需要我们的IoC容器管理的类。

这里我们获取到的类集合是个一Map。如果用户在使用@Component注解的时候,有指定value值,我们后面在DI的时候会根据此值去查询并赋值。如果没指定value,则会使用缺省的类名。

package xyz.letus.framework.ioc;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import xyz.letus.framework.ioc.annotation.Component;

/** * 类操作助手 * @ClassName: ClassHelper * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
public class ClassFactory {

    /** * 获取交由容器管理的类 * @Title: getBeanClasses * @Description: TODO * @param @return * @return Map<String,Class<?>> * @throws */
    public static  Map<String,Class<?>> getBeanClasses(String basePackage){
        Map<String,Class<?>> annotationClasses = new HashMap<String,Class<?>>();
        Set<Class<?>> classes = ClassLoader.getClassSet(basePackage);
        for(Class<?> clazz : classes){
            if(clazz.isAnnotationPresent(Component.class)){
                Component component = clazz.getAnnotation(Component.class);
                String name = clazz.getSimpleName();
                String value = component.value();
                if(value.length() > 0){
                    name = value;
                }
                classes.add(clazz);
                annotationClasses.put(name,clazz);
            }
        }
        return annotationClasses;
    }
}

4.4 创建实例及属性赋值

我们通过ClassFactory获取到所有托管的类后,我们可以ReflectionFactory来创建所有类的实例。

Object obj = ReflectionFactory.newInstance(entry.getValue());
package xyz.letus.framework.ioc;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


/** * Bean助手类 * @ClassName: BeanHelper * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
public class BeanFactory {
    private static final Map<String,Object> BEAN_MAP = new HashMap<String,Object>();

    /** * 创建所有托管的实例 * @Title: createInstance * @Description: TODO * @param @param packages * @return void * @throws */
    public static void createInstance(List<String> packages){
        for (String packagePath : packages) {
            Map<String,Class<?>> beanClasses = ClassFactory
                    .getBeanClasses(packagePath);
            for (Entry<String,Class<?>> entry : beanClasses.entrySet()) {
                Object obj = ReflectionFactory.newInstance(entry.getValue());

                BEAN_MAP.put(entry.getKey(),obj);
            }
        }

        IocHelper.inject(BEAN_MAP);
    }


    /** * 获取Bean实例 * @Title: getBean * @Description: TODO * @param @param name * @param @return * @return T * @throws */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name){
        if(!BEAN_MAP.containsKey(name)){
            throw new RuntimeException("can not get bean by className:"+name);
        }

        return (T) BEAN_MAP.get(name);
    }
}

实例化所有类后,由IocHelper类来判断这些对象中是否有带@Inject注解的属性,然后同样通过ReflectionFactory来为这些属性进行依赖注入(DI赋值)。

package xyz.letus.framework.ioc;

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

import xyz.letus.framework.ioc.annotation.Inject;

/** * 依赖注入助手类 * @ClassName: IocHelper * @Description: TODO * @author 潘广伟(笨笨) * @date 2015年9月19日 * */
public class IocHelper {
    /** * 注入属性 * @Title: inject * @Description: TODO * @param * @return void * @throws */
    public static void inject(Map<String,Object> beanMap){
        /** * 为类中的属性注入值 */
        for(Map.Entry<String,Object> entry : beanMap.entrySet()){
            String name = entry.getKey();
            Object beanInstance = entry.getValue();

            Field[] beanFields = beanInstance.getClass().getDeclaredFields();

            for(Field field : beanFields){
                if(field.isAnnotationPresent(Inject.class)){
                    Inject inject = field.getAnnotation(Inject.class);
                    if(inject.value().length() > 0){
                        name = inject.value();
                    }
                    Class<?> fieldClazz = field.getType();
                    Object fieldInstance = beanMap.get(name);
                    if(fieldInstance != null){
                        ReflectionFactory.setField(beanInstance,field,fieldInstance);
                    }
                }
            }
        }
    }
}

4.4 资源管理器

虽然我们使用的是annotation的方式来进行管理配置信息,但像Spring一样,简单的资源文件可以使用框架更便捷与快速地工作。

我们这里使用的是Java资源文件来作为配置文件,当然如果我们有层次比较分明的配置信息时,我们也可以像Spring框架那样使用XML文件。

ResourceFactory对资源文件解析比较简单,我们现在仅仅解析scanPackage资源。这个资源告诉我们的框架需要托管的类放在哪个包路径下。当然,如果没有这个说明,我们也可以扫描整个项目下的类文件,这样效率明显比较低。

package xyz.letus.framework.context;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

/** * 资源管理器 * @ClassName: ResourceFactory * @Description: TODO * @author 潘广伟(笨笨) * @date 2016年2月17日 * */
public class ResourceFactory {
    private static final List<String> SCAN_PACKAGES = new ArrayList<String>();

    /** * 解析资源文件(配置文件) * @Title: parse * @Description: TODO * @param @param fileName * @param @throws FileNotFoundException * @param @throws IOException * @return void * @throws */
    public static void parse(String fileName) throws FileNotFoundException,IOException {
        InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);  
        Properties properties = new Properties();
        properties.load(stream);
        Enumeration<?> e = properties.propertyNames();// 得到配置文件的名字
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            String value = properties.getProperty(key);

            if("scanPackage".equals(key)){
                SCAN_PACKAGES.add(value);
            }
        }
    }
    /** * 获取自动描述的包路径 * @Title: getPackages * @Description: TODO * @param @return * @return List<String> * @throws */
    public static List<String> getPackages(){
        return SCAN_PACKAGES;
    }

}

4.5 应用容器

ApplicationContext 通过资源管理器ResourceFactory 获取到所以需要托管类的包路径。然后交由BeanFactory创建所有的类实例,并为其属性赋值。

ApplicationContext 提供了getBean(String name),此方法用户可以通过之前定义的名称为默认的名称来获取类实例。

package xyz.letus.framework.context;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;

import xyz.letus.framework.ioc.BeanFactory;

/** * 应用容器 * @ClassName: IocContext * @Description: TODO * @author 潘广伟(笨笨) * @date 2016年2月17日 * */
public class ApplicationContext {
    private String path;

    private ApplicationContext(String path){
        this.path = path;
        init();
    }

    public static ApplicationContext getContext(String path){
        return new ApplicationContext(path);
    }
    /** * 初始化 * @Title: init * @Description: TODO * @param * @return void * @throws */
    public void init(){
        try {
            ResourceFactory.parse(path);
            List<String> packages = ResourceFactory.getPackages();
            BeanFactory.createInstance(packages);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /** * 根据别名获取一个对象 * @Title: getBean * @Description: TODO * @param @param name * @param @return * @return T * @throws */
    public <T> T getBean(String name)  {
        return BeanFactory.getBean(name);
    }
}

5、框架使用

现在我们使用本文编写的IoC框架进行编程。

  • 首先我们需要一个Dao类及一个Service类。
    Dao类比较简单,一个say()方法,并加上@Component将由IoC容器管理。
    Service和Dao不同的是,它有一个未初始化的dao属性,并由@Inject告诉IoC容器,需要在运行时,为此属性初始化。
package xyz.letus.demo;

import xyz.letus.framework.ioc.annotation.Component;

@Component
public class Dao {
    public void say(){
        System.out.println("Dao say something.");
    }
}
package xyz.letus.demo;

import xyz.letus.framework.ioc.annotation.Component;
import xyz.letus.framework.ioc.annotation.Inject;

@Component
public class Service {
    @Inject
    private Dao dao;

    public void say(){
        dao.say();
        System.out.println("Service say something.");
    }
}
  • 资源文件context.properties
scanPackage=xyz.letus.demo
  • 启动IoC容器,并获取service实例
ApplicationContext context = ApplicationContext.getContext("context.properties");
Service service = context.getBean("Service");
service.say();
  • 输出结果
Dao say something.
Service say something.

当然我们上面使用的注解都是缺省模式的。我们也可以为托管的对象加上别名:

@Component("a")
public class Dao {
@Inject("a")
private Dao dao;
@Component("b")
public class Service {
Service service = context.getBean("b");

得到的结果是一样的。

6、我们还要继续

就这样,我们完成了最简单的IoC及DI框架。当然说是简单,因为它离Spring等框架的IoC功能还很远很远,包括为接口注入实现的实例、单例模式及多例模式的实现等等。

我们还要继续造轮子。

7、源码下载

https://github.com/benben-ren/wheel/tree/d389e62b1f1380b45bb38b098c5ed0ce8123c9dd

注:源码中只有ioc构架源码,不包含使用源码。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读