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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/p_3er/article/details/50680914

1、前言

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

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

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

2、IoC和依赖注入的概念

Inverse of Control,控制反转。

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

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

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

最常用的IoC和DI框架没有之一:Spring。
Spring IoC的介绍和使用: http://blog.csdn.net/jrainbow/article/details/9091577

3、源码结构

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

这里写图片描述
我要看大图:https://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构架源码,不包含使用源码。

猜你喜欢

转载自blog.csdn.net/p_3er/article/details/50680914