美文网首页Java 之旅
中级17 - Spring IoC容器原理与手写简单实现

中级17 - Spring IoC容器原理与手写简单实现

作者: 晓风残月1994 | 来源:发表于2020-06-02 17:41 被阅读0次

    一切开始之前,先了解下 JavaBean 是什么,它是一种标准和约定。
    一个 JavaBean:

    • 所有的属性都是 private(通过 getter/setter 进行读写)
    • 有一个 public 无参数的构造器
    • 实现了 Serializable(序列化,注意 Serializable 接口没有方法也没有字段,更多的是一种规范和约定)

    1. Spring 是什么

    Srping 是自动化管理 Java 对象和其中依赖关系的容器。

    • Java世界中Web容器的事实标准
    • Spring 容器 - 一个 IoC 容器
      • Spring MVC - 基于 Spring 和 Servlet 的 Web 应用框架
        • Spring Boot - 集成度和自动化程度更高(内嵌了 Servlet 容器)

    -let 词根,“小”的意思,servlet 小服务器(service + let)。
    Servlet 将网络请求封装成对象,交给上层 WebApp(spring容器、mvc、boot)。
    Servlet <-> HttpServletRequest/HttpServletResponse <-> WebApp。
    常见的 servlet 有:

    • tomcat
    • jetty

    没有 Spring 怎么办?
    **

    • 一个 main 程序走天下:轻量简单,但是规模大了后难以维护。
    • 拆分并且手动管理:优点方便测试、共同开发、维护,但缺点还是规模更大后,依赖关系太复杂。

    Spring 的出现解放了我们的双手。

    2. Spring 最简单的用法

    通过配置一种“上古”的 xml 来使用。
    xml 配置文件中声明了两个 Bean, 而这两个 Bean 之间存在依赖关系,因此为 OrderService 中的成员属性 OrderDao 添加了注解,用于对 OrderDao 自动装配。

    在这里,容器就是 BeanFactory。有了容器之后,向容器索要 Bean。

    // maven 依赖
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    
    package spring.demo;
    
    public class OrderDao {
        public void select() {
            System.out.println("select!");
        }
    }
    
    package spring.demo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    public class OrderService {
    
        @Autowired // 加了这个注解才会自动装配 OrderDao,否则 OrderService 对象中的 orderDao 是 null
        private OrderDao orderDao;
    
        public void doSomething() {
            orderDao.select();
        }
    }
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:annotation-config /> <!-- 这句不加的话,即使写了上面的注解也不生效 -->
        <!-- bean definitions here -->
        <bean id="orderDao" class="spring.demo.OrderDao"/>
        <bean id="orderService" class="spring.demo.OrderService"/>
    </beans>
    
    package spring.demo;
    
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class SpringMain {
        public static void main(String[] args) {
            BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:spring/config.xml");
    
            OrderService orderService = (OrderService)beanFactory.getBean("orderService");
            OrderDao orderDao = (OrderDao) beanFactory.getBean("orderDao");
    
            System.out.println(orderService);
            orderService.doSomething();
            System.out.println(orderDao);
        }
    }
    
    

    现在没有写 new,却拿到了 OrderService 对象(一个Bean),默认是单例模式,也就是 OrderService 对象中的 OrderDao 和再次通过 getBean 得到的 OrderDao 对象是同一个对象。


    image.pngimage.png

    假如没有使用 Spring,那么可能要自己手动创建,涉及到的各种对象的各种 new。

    3. Spring 容器核心概念

    • Bean

    容器中的最小工作单元,通常为一个 Java 对象

    • BeanFactory/ApplicationContext

    容器本身对应的 Java 对象

    • 依赖注入(DI)

    容器负责注入所有的依赖

    • 控制反转(IoC)

    用户将控制权交给了 Spring 容器来进行自动装配

    4. 手写一个简单的 IoC 容器

    • 定义 Bean
    • 加载 Bean 的定义
    • 实例化 Bean
    • 查找依赖,实现依赖注入
    • 要什么 Bean 就设置成什么 Bean

    目录结构:


    image.pngimage.png

    使用方法是在字段是声明注解(部分代码不再罗列,这里仅供举例):

    import org.springframework.beans.factory.annotation.Autowired;
    
    public class OrderService {
        @Autowired private OrderDao orderDao;
        @Autowired private UserService userService;
    
        public void createOrder() {
            orderDao.createOrder(userService.getCurrentLoginUser());
        }
    }
    

    具体实现:

    Java 从初代就支持了 .properties 格式的配置文件,该文件用来存储简单的基于 key-value pairs 的参数。该配置文件处于被编译的代码之外。

    1. 先写一个简单的 beans.properties 配置文件,定义了 Bean 的名字和对应的实现类:
    # Bean 名字 和 Bean 的全限定类名
    orderDao=com.github.hcsp.ioc.OrderDao
    userDao=com.github.hcsp.ioc.UserDao
    userService=com.github.hcsp.ioc.UserService
    orderService=com.github.hcsp.ioc.OrderService
    
    1. MyIoCContainer 容器:
    import org.springframework.beans.factory.annotation.Autowired;
    
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Properties;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    public class MyIoCContainer {
        // 实现一个简单的IoC容器,使得:
        // 1. 从beans.properties里加载bean定义
        // 2. 自动扫描bean中的@Autowired注解并完成依赖注入
    
        // 定义一个容器! 存放 bean 的名字到 bean 实例对象的映射
        private Map<String, Object> beans = new HashMap<>();
    
        /**
         * 依赖注入
         *
         * @param beanInstance bean 的 实例
         */
        private void dependencyInject(Object beanInstance) {
            // 拿到带有 @AutoWired 注解的 fields
            List<Field> fieldsToBeAutoWired = Stream.of(beanInstance.getClass().getDeclaredFields())
                    .filter(field -> field.getAnnotation(Autowired.class) != null)
                    .collect(Collectors.toList());
            // 为当前 bean 对象的需要依赖的字段注入依赖(设置字段值)
            fieldsToBeAutoWired.forEach(field -> {
                String fieldName = field.getName(); // 加了 @AutoWired 的字段名即是所要依赖的 bean 的名字
                Object dependencyBeanInstance = beans.get(fieldName); // 所依赖的 bean 实例
                try {
                    field.setAccessible(true); // 设置为 true 用来压制针对被反射对象的访问检查
                    field.set(beanInstance, dependencyBeanInstance); // 从而可以在这里设置当前 bean 的私有字段
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    
        /**
         * 启动该容器
         */
        public void start() {
            // bean 的初始化
    
            Properties properties = new Properties();
    
            // 从 InputStream 中读取属性列表(键值对)
            try {
                properties.load(MyIoCContainer.class.getResourceAsStream("/beans.properties"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            System.out.println(properties);
    
            properties.forEach((beanName, beanClassName) -> {
                try {
                    // 通过反射拿到 bean 的实例并放入容器中
                    Class<?> klass = Class.forName((String) beanClassName);
                    Object beanInstance = klass.getConstructor().newInstance();
                    beans.put((String) beanName, beanInstance);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
    
            // 使用反射,处理依赖关系,注入依赖
            beans.forEach((beanName, beanInstance) -> dependencyInject(beanInstance));
        }
    
        /**
         * 从容器中获取一个bean
         *
         * @param beanName bean 的名字
         * @return 返回 bean 的实例
         */
        public Object getBean(String beanName) {
            return beans.get(beanName);
        }
    
        public static void main(String[] args) {
    
            MyIoCContainer container = new MyIoCContainer();
            container.start();
            OrderService orderService = (OrderService) container.getBean("orderService");
            orderService.createOrder();
        }
    }
    

    即使是两个依赖字段存在循环依赖也没关系,因为在创造期间,会先各自创建出实例对象。相关依赖字段此时是 null,不影响 bean 的创建。而创建完之后,再回头对字段注入所依赖的 bean。

    当然,以上只是个简单的实现,实际的 Spring 中还可以在构造器上使用 @Autowired 注解,而不推荐在私有字段上使用。这样即使不是用 Spring,比如写测试代码的时候,还是可以方便的 new 一个实例,否则,私有字段还要通过反射一顿操作才能创建实例进行测试。

    另外,实际应用中 @Autowired 也不推荐使用了,更推荐 @Inject 注解,这样除了 Spring 之外,还可能受到其他类似框架的识别。

    所谓的 Spring 只不过是在以上基础上扩充了无穷无尽的功能,比如支持 xml 中配置依赖,支持构造器注入(调用哪个构造器,传递哪些参数)等等,从一个很简单的思想不断扩充成庞大的体系。

    5. Spring 启动过程浅析

    • 在 xml 里中定义 Bean
    • BeanDefinition 的加载和解析
    • Bean 的实例化和依赖注入
    • 对外提供服务

    建议 debug Spring 源码的时候,要带着目前来进行,不要在无关细节里浪费太多时间。


    image.pngimage.png
    image.pngimage.png
    image.pngimage.png

    6. 参考

    相关文章

      网友评论

        本文标题:中级17 - Spring IoC容器原理与手写简单实现

        本文链接:https://www.haomeiwen.com/subject/ggcpzhtx.html