对控制反转理解不深?带你手写一个基于注解的IOC容器 加深对spring底层代码的理解

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 对控制反转理解不深?带你手写一个基于注解的IOC容器 加深对spring底层代码的理解

Spring 手撕IOC

提到spring 不可避免的就是两个核心组件 IOC 和 AOP 这里我们主要实现的是 IOC


那接触到这个实现ioc的文章 适合哪些人群一起学习呢


了解反射

至少 用过 spring 或多或少能感受到IOC给我们带来的好处


IOC (inverse of control)

简单介绍

IOC就是将创建对象的权限,从Java 程序员 交给 IOC容器来创建,就是将对象的创造全 给到了框架,


我们称为 控制反转


创建一个Maven项目

我们用maven 项目来演示和编写spring 的ioc 容器代码


1、配置依赖

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>

2、编写三层架构


模拟数据 DAO


public class hellloDaoImpl implements helloDao {
    @Override
    public List<String> findAll() {
        return Arrays.asList("1", "2", "3");
    }
}


连接视图和数据层: service

public class helloServiceImpl implements helloservice {
    private helloDao helloDao = new hellloDaoImpl();
    @Override
    public List<String> findAll() {
        return helloDao.findAll();
    }
}


控制层 : servlet/controller

@WebServlet("/hello")
public class helloservlet extends HttpServlet {
    helloservice helloservice = new helloServiceImpl();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("spring" + helloservice.findAll());
    }
}

3、配置Tomcat 部署

2.png



需要解决的问题

不同实现需要重写接口代码

DAO 操作的是数据源, 数据源(数据库) 如果此时项目变更,比如数据库变成 Oracle 那么我们的DAO代码可能就需要推翻重写


如何解决呢?


设计模式


使用静态工厂来创建特定的类, 不再把代码写死在service 中,通过工厂的方式来实现

public class beanfactory {
    public static helloDao getDao() {
        return new hellloDaoImpl();
    }
}

service 代码


 private helloDao helloDao = beanfactory.getDao();


上述方法提供的思路 还是不能解决我们的问题,需求发生改变的时候 , 仍然需要改变代码 ,这个时候我们就要用到反射 如果不修改java 代码 如何实现类的切换呢?


使用 : 外部配置文件的方式 将具体的类写到配置文件中, Java 程序只需要读取配置文件即可


编写外部文件


properties , yml , xml 等配置文件的方式来读取需要根绝什么数据源来使用什么工厂、这里我们使用 properties,


1、定义外部配置文件


hellDao=com.hyc.Dao.Impl.hellloDaoImpl


2、Java程序读取这个配置文件

    private static Properties properties;
    static {
        properties = new Properties();
        try {
            properties.load(beanfactory.class.getClassLoader().getResourceAsStream("factory.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }  
public static Object getDao() {
        String helloDao = properties.getProperty("helloDao");
        //利用反射机制创建对象
        try {
            Class clazz = Class.forName(helloDao);
            Object object = clazz.getConstructor(null).newInstance();
            return object;
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

3、修改service


 private helloDao helloDao = (helloDao) beanfactory.getDao();


测试


正常来说 我们访问 hello 会返回 123 (再dao中模拟的数据),这个时候我们需要切换数据源只需要修改外部配置文件即可

2.png

此时我们修改外部配置文件


helloDao=com.hyc.Dao.Impl.hellloDaoImpl2


hellloDaoImpl2 中模拟的数据是 7,8,9 ,重启项目

2.png

到这里我们就使用外部配置+工厂的方法解决了代码不能切换的问题,


创建出来的对象并非单例

这个问题会出现什么问题呢 重复使用对象的话会出现相同的对象创造很多个无用实例的问题


这里我们就提出用缓存的思路来保证单例,


用Map 来存放创建的对象,避免重复创建

双重判断保证线程安全

修改工厂生成对象的代码

    public static Object getDao(String beanName) {
        if (!cache.containsKey(beanName)) {
            synchronized (beanfactory.class) {
                if (!cache.containsKey(beanName)) {
                    //利用反射机制创建对象
                    //将对象放入我们的缓存里(map)
                    try {
                        String helloDao = properties.getProperty("helloDao");
                        Class clazz = Class.forName(helloDao);
                        Object object = clazz.getConstructor(null).newInstance();
                        cache.put(beanName, object);
                    } catch (ClassNotFoundException | NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return cache.get(beanName);
    }

测试

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            helloDao helloDao = (helloDao) beanfactory.getDao("helloDao");
            System.out.println(helloDao);
        }
    }

2.png

这样我们就实现了对工厂生成的实例复用,而并非每次都重新创建一个


两种方式的区别


private helloDao hellodao = new hellodaoimpl();


private helloDao hellodao = (helloDao) BeanFactory.getDao("helloDao");


强依赖/紧耦合 编译之后无法修改 没有拓展性

弱依赖/松耦合 编译之后可以通过修改配置文件来变动,让程序具有更好的拓展性

我们自己放弃了创造对象的权限,将创建对象的权限交给了BeanFactory 这种将控制权交给别人或者是对象的思想


就是 : IOC 控制反转


IOC基于注解的执行原理和实现

2.png


手写基于注解的Ioc思路

自定义 MyAnnotationConfigApplicationContext ,构造器中传入要扫描的包

获取包下所有类

遍历这些类找到添加了 @Component 注解的类,获取它的class 和对应的beanName 封装成beanDefinition 存入Set集合 这个机会就是IOC自动装载的原材料

遍历Set集合 通过反射机制创建对象 同时检测属性有没有添加 @ Value 注解 如果有给属性赋值,再将这些动态创建的对象以K-V的形式 放入缓存区

提供 getBean 方法 通过 beanName 取出对应的bean即可

根据上述的思路我们实现代码


注解

自动装配注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}


组件注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}

指定Name注入注解


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifer {
    String value();
}

赋值注解


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value {
    String value();
}

核心上下文

扫描包中组件类

public class MyTools {
    public static Set<Class<?>> getClasses(String pack) {
        // 第一个class类的集合
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        // 是否循环迭代
        boolean recursive = true;
        // 获取包的名字 并进行替换
        String packageName = pack;
        String packageDirName = packageName.replace('.', '/');
        // 定义一个枚举的集合 并进行循环来处理这个目录下的things
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            // 循环迭代下去
            while (dirs.hasMoreElements()) {
                // 获取下一个元素
                URL url = dirs.nextElement();
                // 得到协议的名称
                String protocol = url.getProtocol();
                // 如果是以文件的形式保存在服务器上
                if ("file".equals(protocol)) {
                    // 获取包的物理路径
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    // 以文件的方式扫描整个包下的文件 并添加到集合中
                    findClassesInPackageByFile(packageName, filePath, recursive, classes);
                } else if ("jar".equals(protocol)) {
                    // 如果是jar包文件
                    // 定义一个JarFile
                    System.out.println("jar类型的扫描");
                    JarFile jar;
                    try {
                        // 获取jar
                        jar = ((JarURLConnection) url.openConnection()).getJarFile();
                        // 从此jar包 得到一个枚举类
                        Enumeration<JarEntry> entries = jar.entries();
                        findClassesInPackageByJar(packageName, entries, packageDirName, recursive, classes);
                    } catch (IOException e) {
                        // log.error("在扫描用户定义视图时从jar包获取文件出错");
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return classes;
    }
    private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, final boolean recursive, Set<Class<?>> classes) {
        // 同样的进行循环迭代
        while (entries.hasMoreElements()) {
            // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
            JarEntry entry = entries.nextElement();
            String name = entry.getName();
            // 如果是以/开头的
            if (name.charAt(0) == '/') {
                // 获取后面的字符串
                name = name.substring(1);
            }
            // 如果前半部分和定义的包名相同
            if (name.startsWith(packageDirName)) {
                int idx = name.lastIndexOf('/');
                // 如果以"/"结尾 是一个包
                if (idx != -1) {
                    // 获取包名 把"/"替换成"."
                    packageName = name.substring(0, idx).replace('/', '.');
                }
                // 如果可以迭代下去 并且是一个包
                if ((idx != -1) || recursive) {
                    // 如果是一个.class文件 而且不是目录
                    if (name.endsWith(".class") && !entry.isDirectory()) {
                        // 去掉后面的".class" 获取真正的类名
                        String className = name.substring(packageName.length() + 1, name.length() - 6);
                        try {
                            // 添加到classes
                            classes.add(Class.forName(packageName + '.' + className));
                        } catch (ClassNotFoundException e) {
                            // .error("添加用户自定义视图类错误 找不到此类的.class文件");
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    private static void findClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) {
        // 获取此包的目录 建立一个File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            // log.warn("用户定义包名 " + packageName + " 下没有任何文件");
            return;
        }
        // 如果存在 就获取包下的所有文件 包括目录
        File[] dirfiles = dir.listFiles(new FileFilter() {
            // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
            @Override
            public boolean accept(File file) {
                return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
            }
        });
        // 循环所有文件
        for (File file : dirfiles) {
            // 如果是目录 则继续扫描
            if (file.isDirectory()) {
                findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);
            } else {
                // 如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    // classes.add(Class.forName(packageName + '.' +
                    // className));
                    // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }
}

容器上下文代码

findBeanDefinitions 扫描包中符合条件的类

createObject 根据上个方法获取到的类的元数据信息创造对象,加入缓存

autowireObject 是否有需要自动注入的对象,判断注解存在决定ByName或者ByType

/**
 * @projectName: SpringIOC
 * @package: com.hyc.MySpring
 * @className: MyAnnotationConfigApplicationContext
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/24 18:53
 * @version: 1.0
 */
public class MyAnnotationConfigApplicationContext {
    private Map<String, Object> ioc = new HashMap<>();
    public MyAnnotationConfigApplicationContext(String pack) {
        //扫描包中的目标类
        Set<BeanDefinition> beanDefinitions = findBeanDefinitions(pack);
        //根据原材料 创建bean
        createObject(beanDefinitions);
        //    自动装载
        autowireObject(beanDefinitions);
    }
    public void autowireObject(Set<BeanDefinition> beanDefinitions) {
        //获取注入的集合
        Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
        while (iterator.hasNext()) {
            //拿到bean 信息
            BeanDefinition beanDefinition = iterator.next();
            //获取当前需要自动装配的对象
            Class clazz = beanDefinition.getBeanClass();
            //拿到全部属性
            Field[] declaredFields = clazz.getDeclaredFields();
            for (Field field : declaredFields) {
                Autowired annotation = field.getAnnotation(Autowired.class);
                if (annotation != null) {
                    Qualifer qualifer = field.getAnnotation(Qualifer.class);
                    if (qualifer != null) {
                        try {
                            //byName
                            String beanName = qualifer.value();
                            Object bean = getBean(beanName);
                            String fieldName = field.getName();
                            String mothedName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
                            Method method = clazz.getMethod(mothedName, field.getType());
                            Object object = getBean(beanDefinition.getBeanName());
                            System.out.println(bean);
                            method.invoke(object, bean);
                        } catch (NoSuchMethodException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //byType
                    }
                }
            }
        }
    }
    public Object getBean(String beanName) {
        return ioc.get(beanName);
    }
    public void createObject(Set<BeanDefinition> beanDefinitions) {
        Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
        while (iterator.hasNext()) {
            BeanDefinition beanDefinition = iterator.next();
            Class clazz = beanDefinition.getBeanClass();
            String beanName = beanDefinition.getBeanName();
            try {
                //创造对象
                Object objects = clazz.getConstructor().newInstance();
                //完成属性的赋值
                Field[] declaredFields = clazz.getDeclaredFields();
                for (Field declaredField : declaredFields) {
                    Value valueAnnotation = declaredField.getAnnotation(Value.class);
                    if (valueAnnotation != null) {
                        String value = valueAnnotation.value();
                        String fieldName = declaredField.getName();
                        String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
                        Method method = clazz.getMethod(methodName, declaredField.getType());
                        //数据类型转换
                        Object val = null;
                        //可能会有很多类型 篇幅过长
                        switch (declaredField.getType().getName()) {
                            case "java.lang.Integer":
                                val = Integer.parseInt(value);
                                break;
                            case "java.lang.String":
                                val = value;
                                break;
                            case "java.lang.Float":
                                val = Float.parseFloat(value);
                                break;
                        }
                        method.invoke(objects, val);
                    }
                }
                ioc.put(beanName, objects);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }
    public Set<BeanDefinition> findBeanDefinitions(String pack) {
        //    1 获取包下的所有类
        Set<Class<?>> classes = MyTools.getClasses(pack);
        Iterator<Class<?>> iterator = classes.iterator();
        Set<BeanDefinition> BeanDefinitions = new HashSet<>();
        while (iterator.hasNext()) {
            //    2,遍历这些类 找到添加了注解的类
            Class<?> clazz = iterator.next();
            Component annotation = clazz.getAnnotation(Component.class);
            if (annotation != null) {
                //    获取Compoent的值
                String beanName = annotation.value();
                if ("".equals(beanName)) {
                    //   获取类名首字母小写、
                    String className = clazz.getSimpleName();
                    beanName = className.substring(0, 1).toLowerCase() + className.substring(1, className.length());
                }
                BeanDefinition beanDefinition = new BeanDefinition(beanName, clazz);
                BeanDefinitions.add(beanDefinition);
            }
        }
        //    3. 将这些类封装成BeanDefinition 装载到集合中
        return BeanDefinitions;
    }
}

元数据

@Data
@AllArgsConstructor
public class BeanDefinition {
    private String BeanName;
    private Class BeanClass;
}

测试对象

Account

@Component()
@Data
public class Account {
    public Account() {
    }
    @Value("1")
    private Integer id;
    @Value("张三")
    private String name;
    @Value("12")
    private Integer age;
    @Autowired
    @Qualifer("myOrder")
    private Order myorder;
}

Orderj

@Component("myOrder")
@Data
public class Order {
    @Value("xxx123")
    private String orderId;
    @Value("1000.5")
    private Float price;
}

总结

手动实现基于注解Ioc 这篇文章,对于理解spring的装配过程 ,Bean的声明周期 有着非常好的启发作用

手动实现过之后会才会知道为什么要解决这个问题,怎么解决这个问题

得到的成果

一个自己编写的IOC容器

反射技术的实战

spring原理的加深理解


相关文章
|
29天前
|
缓存 Java 数据库连接
Spring Boot奇迹时刻:@PostConstruct注解如何成为应用初始化的关键先生?
【8月更文挑战第29天】作为一名Java开发工程师,我一直对Spring Boot的便捷性和灵活性着迷。本文将深入探讨@PostConstruct注解在Spring Boot中的应用场景,展示其在资源加载、数据初始化及第三方库初始化等方面的作用。
49 0
|
12天前
|
XML Java 数据格式
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
Spring 第二节内容补充 关于Bean配置的更多内容和细节 万字详解!
87 18
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
|
22小时前
|
XML Java 开发者
经典面试---spring IOC容器的核心实现原理
作为一名拥有十年研发经验的工程师,对Spring框架尤其是其IOC(Inversion of Control,控制反转)容器的核心实现原理有着深入的理解。
12 3
|
13天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
13天前
|
Java 数据库连接 Maven
Spring基础1——Spring(配置开发版),IOC和DI
spring介绍、入门案例、控制反转IOC、IOC容器、Bean、依赖注入DI
Spring基础1——Spring(配置开发版),IOC和DI
|
26天前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
36 0
|
26天前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
35 0
|
29天前
|
监控 安全 Java
【开发者必备】Spring Boot中自定义注解与处理器的神奇魔力:一键解锁代码新高度!
【8月更文挑战第29天】本文介绍如何在Spring Boot中利用自定义注解与处理器增强应用功能。通过定义如`@CustomProcessor`注解并结合`BeanPostProcessor`实现特定逻辑处理,如业务逻辑封装、配置管理及元数据分析等,从而提升代码整洁度与可维护性。文章详细展示了从注解定义、处理器编写到实际应用的具体步骤,并提供了实战案例,帮助开发者更好地理解和运用这一强大特性,以实现代码的高效组织与优化。
48 0
|
29天前
|
Java 开发者 Spring
Spring Boot大法好:解耦、隔离、异步,让代码‘活’起来,性能飙升的秘密武器!
【8月更文挑战第29天】解耦、隔离与异步是Spring Boot中的关键设计原则,能大幅提升软件的可维护性、扩展性和性能。本文通过示例代码详细探讨了这些原则的应用:依赖注入和面向接口编程实现解耦;模块化设计与配置文件实现隔离;`@Async`注解和`CompletableFuture`实现异步处理。综合运用这些原则,可以显著提升软件质量和性能,使系统更加健壮、灵活和高效。
22 0
|
XML Java 数据格式
Spring.Net——理解控制反转和依赖倒置
一,控制反转(Inversion of Control,缩写IoC)和依赖注入(Dependency Injection,简称DI)        1,控制反转                         IoC(Inversion of Control),这是spring的核心,贯穿始终。
1186 0