IOC

[IOC]

1. ioc 简介

1.1 ioc 定义

ioc (Inversion of Control, 控制反转)把创建对象的操作交给框架,亦被称为 DI(Dependency Injection, 依赖注入)。

为什么叫做 “控制反转” 呢?之前,我们想要一个对象都是 new 出来的,天天需要 new 对象是不是感觉有点麻烦。有人就想到了,把这些简单重复的工作也交给框架做。本来需要我们向框架 “射入” 对象,现在框架自己能产生对象了,这不正是 控制反转 吗?于是,就有了这个响亮的名字。

1.2 ioc 目标

其实定义中已经把 ioc 的目标说清楚了。这里,再强调一下:把创建对象的操作交给框架,“解放程序员的生产力”。

下面代码是 ioc 的使用用例,它可实现 customerService 变量的自动注入。本文的目标是实现 ioc ,也就是使得下面代码可用。

    // 使用了基于注解实现的 ioc 代码
    @Controller
    public class CustomerController {

        // 这里可以自动注入 CustomerService 对象
        @InJect
        private CustomerService customerService;

        ...

    }

ioc 的实现主要有两种形式:1)声明式;2)注解。其中,声明式是基于 xml 配置文件来做的,这种方式对 jdk 版本无要求,兼容性强,但相注解方式较冗杂;而注解方式,需要 jdk1.5 以上(因为从 jdk1.5 才有了注解),但它更灵活、更简洁。鉴于现在很少有人在使用 jdk1.5 之前的版本,本文将基于注解实现 ioc

2. ioc 实现

2.1 实现关键

  1. 如何标识出类中哪些变量需要注入;
  2. 怎样生成可用于注入的对象实例以及如何存储它们;
  3. 何时注入;

2.2 详细过程

1) 创建注解 InJect 用它标识类中需要注入的属性;
    // InJect.java
    package top.inotwant.annocation;

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

    @Target(ElementType.FIELD)  // 元注解,表示 InJect 作用于属性之上
    @Retention(RetentionPolicy.RUNTIME) // 元注解,用于描述 InJect 生存周期
    public @interface InJect {
    }
2) 创建 Controller Service 注解,它们都作用于类上(比如,1.2 节中 @Controller 作用于 CustomerController 之上)。被它们修饰的类将要求产生对象实例,这个过程将会用到 反射,下面再详细描述该过程。(使用过 Spring 框架的是不是对这两个注解很熟悉)
    // Controller.java
    package top.inotwant.annocation;

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

    @Target(ElementType.TYPE)   // 元注解,表示 Controller 作用于类之上
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Controller {

    }

    // Service.java
    package top.inotwant.annocation;

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

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Service {

    }
3) 自定义类加载器加载包下的所有 java 文件,并从中获取被上面两个注解之一修饰的类的元类 Class<?> 有了这些元类,我们就可以使用反射产生对应的对象实例了。

ClassUtil 工具类中的 loadClassSet(String packageName) 静态方法实现了该逻辑。它将返回 packageName 指定的包下所有类的元类。

ClassHelper 帮助类实现了对上面两个注解之一修饰类的元类的抽取。

    // ClassUtil.java
    package top.inotwant.utils;

    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;

    public final class ClassUtil {

        /**
         * 获取类加载器
         */
        public static ClassLoader getClassLoader() {
            return Thread.currentThread().getContextClassLoader();
        }

        /**
         * 加载指定类
         */
        public static Class<?> loadClass(String className) {
            try {
                // TODO 注意 class.forName 与 ClassLoader.loadClass 的区别(关于 static 是否静态初始化)
                // 注意第二个参数,一定要设置为 true 。否则,在加载类时不会运行 static 块,这将影响一部分类的初始化。
                return Class.forName(className, true, getClassLoader());
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 加载包下所有类
         * @param packageName 为包名,比如 “top.inotwant.sample”
         */
        public static Set<Class<?>> loadClassSet(String packageName) {
            String packagePath = packageName.replace(".", "/");
            try {
                Set<Class<?>> result = new HashSet<>();
                // getResources 的使用请看 API
                Enumeration<URL> resources = getClassLoader().getResources(packagePath);
                while (resources.hasMoreElements()) {
                    URL currentURL = resources.nextElement();
                    if ("file".equals(currentURL.getProtocol())) {
                        String path = currentURL.getPath().replaceAll("%20", " ");
                        addClass(path, packageName, result);
                    } else if ("jar".equals(currentURL.getProtocol())) {    // 此处为了加载文件夹下 `jar` 中所包含的所有类文件,若不理解可忽略此处,不影响总体
                        JarURLConnection conn = (JarURLConnection) currentURL.openConnection();
                        JarFile jarFile = conn.getJarFile();
                        Enumeration<JarEntry> entries = jarFile.entries();
                        while (entries.hasMoreElements()) {
                            JarEntry jarEntry = entries.nextElement();
                            String jarEntryName = jarEntry.getName();
                            if (jarEntryName.endsWith(".class")) {
                                String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                                result.add(loadClass(className));
                            }
                        }
                    }
                }
                return result;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        private static void addClass(String path, String packageName, Set<Class<?>> result) {
            File[] files = new File(path).listFiles(new FileFilter() {
                @Override
                public boolean accept(File file) {
                    return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
                }
            });
            if (files != null) {
                for (File file : files) {
                    if (file.isFile()) {
                        String subFileName = file.getName();
                        String className = subFileName.substring(0, subFileName.lastIndexOf("."));
                        result.add(loadClass(packageName + "." + className));
                    } else if (file.isDirectory()) {
                        // 递归
                        addClass(file.getAbsolutePath(), packageName + "." + file.getName(), result);
                    }
                }
            }
        }
    }


    // ClassHelper.java
    package top.inotwant.helper;

    import top.inotwant.annocation.Controller;
    import top.inotwant.annocation.Service;
    import top.inotwant.utils.ClassUtil;

    import java.util.HashSet;
    import java.util.Set;

    /**
     * Class 帮助类
     */
    public final class ClassHelper {

        private static Set<Class<?>> CLASS_SET;

        static {
            String basePackage = "指定包";
            CLASS_SET = ClassUtil.loadClassSet(basePackage);
        }

        /**
         * 加载 base package 下的所有类
         */
        public static Set<Class<?>> getClassSet() {
            return CLASS_SET;
        }

        /**
         * 获取加载的由 service 标注的所有类
         */
        public static Set<Class<?>> getServiceClassSet() {
            Set<Class<?>> result = new HashSet<>();
            for (Class<?> clazz : CLASS_SET) {
                if (clazz.isAnnotationPresent(Service.class))
                    result.add(clazz);
            }
            return result;
        }

        /**
         * 获取加载的由 controller 标注的所有类
         */
        public static Set<Class<?>> getControllerClassSet() {
            Set<Class<?>> result = new HashSet<>();
            for (Class<?> clazz : CLASS_SET) {
                if (clazz.isAnnotationPresent(Controller.class))
                    result.add(clazz);
            }
            return result;
        }

        /**
         * 获取框架管理的 bean (包含, service controller)
         */
        public static Set<Class<?>> getBeanClassSet() {
            Set<Class<?>> result = new HashSet<>();
            result.addAll(getServiceClassSet());
            result.addAll(getControllerClassSet());
            return result;
        }
    }
4) 利用上面的元类,使用反射生成它们对应的对象实例。

BeanHelper 中的静态块中实现这个逻辑。这些对象实例中包含了可用于注入的(与 @Service 对应的),它们被存储在 map 中,以 <Class,Object> 键值对的形式,是不是很容易理解。

    // BeanHelper.java
    package top.inotwant.helper;

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

    /**
     * bean 帮助类
     */
    public final class BeanHelper {

        private final static Map<Class<?>, Object> BEAN_MAP = new HashMap<>();

        static {
            Set<Class<?>> beanClassSet = ClassHelper.getBeanClassSet();
            for (Class<?> clazz : beanClassSet) {
                BEAN_MAP.put(clazz, getInstance(clazz));
            }
        }

        /**
         * 获取类对应的实例
         */
        @SuppressWarnings("unchecked")
        private static <T> T getInstance(Class<?> clazz) {
            try {
                return (T) clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 获取 bean 映射
         */
        public static Map<Class<?>, Object> getBeanMap() {
            return BEAN_MAP;
        }

        /**
         * 获取 bean
         */
        @SuppressWarnings("unchecked")
        public static <T> T getBean(Class<T> clazz) {
            Object obj = BEAN_MAP.get(clazz);
            if (obj != null)
                return (T) obj;
            else {
                throw new RuntimeException("can't get bean by class: " + clazz.getSimpleName());
            }
        }
    }
5) 注入

IocHelper 帮助类的 static 块中完成最后一步 注入。其中的逻辑很简单,我们把所有的 bean 遍历一遍,每一次迭代查看该 bean 的各个属性是否标注 InJect 。如果标注了,就从 beanMap 中获取相应的对象实例进行注入。

    package top.inotwant.helper;

    import top.inotwant.annocation.InJect;
    import top.inotwant.utils.ArrayUtil;
    import top.inotwant.utils.CollectionUtil;
    import top.inotwant.utils.ReflectUtil;

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

    /**
     * ioc 实现类
     */
    public final class IocHelper {

        static {
            Map<Class<?>, Object> beanMap = BeanHelper.getBeanMap();
            if (CollectionUtil.isNotEmpty(beanMap)) {   // 判断集合是否为空
                for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
                    Class<?> clazz = entry.getKey();
                    Object obj = entry.getValue();
                    // TODO 切勿调成 getFields() ,它不能获取私有属性
                    Field[] fields = clazz.getDeclaredFields();
                    if (ArrayUtil.isNotEmpty(fields)) { // 判断数组是否
                        for (Field field : fields) {
                            if (field.isAnnotationPresent(InJect.class)) {
                                Object newValue = BeanHelper.getBean(field.getType());
                                ReflectUtil.modifyField(obj, field, newValue);  // 注入
                            }
                        }
                    }
                }
            }
        }

    }
6) 初始化 ioc

前面 5 步已经实现了 ioc,并且可通过系统初始化时加载 IocHelper.class 让它起到作用(想一想为什么只需要加载一个类就可以?)。但是,我建议再创建如下一个类,让加载更集中、初始化的逻辑更清晰。

    package top.inotwant;

    import top.inotwant.helper.*;
    import top.inotwant.utils.ClassUtil;

    /**
     * 加载相关的 helper 类
     */
    public final class HelperLoader {

        public static void init() {
            Class<?>[] classes = {
                    ClassHelper.class,
                    BeanHelper.class,
                    IocHelper.class
            };
            for (Class<?> clazz : classes) {
                ClassUtil.loadClass(clazz.getName());
            }
        }
    }

至此,1.2 节中的代码已经可以使用。

3. 参考

架构探险(张勇)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值