Spring之IOC容器从入门到手写源码

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 Spring之IOC容器从入门到手写源码,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

Spring Core(核心容器)

在这里插入图片描述
spring core提供了IOC,DI,Bean配置装载创建的核心实现
核心概念: Beans、BeanFactory、BeanDefinitions、ApplicationContext

  • spring-core :IOC和DI的基本实现
  • spring-beans:BeanFactory和Bean的装配管理(BeanFactory)
  • spring-context:Spring context上下文,即IOC容器(AppliactionContext)
  • spring-expression:spring表达式语言

一、IOC容器

1、控制反转(ioc)

  • 控制反转是一种思想
  • 控制反转是为了降低程序耦合度,提高程序扩展力
  • 控制反转,反转的是什么?
    • 对象的创建权利交出去,交给第三方容器负责
    • 对象和对象之间关系的维护权交出去,交给第三方容器负责
  • 控制反转这种思想如何实现呢?
    • DI(Dependency Injection):依赖注入

2、依赖注入

DI(Dependency Injection):依赖注入,依赖注入实现了控制反转的思想

  • 指Spring创建对象的过程中,将对象依赖属性通过配置进行注入
  • 依赖注入常见的实现方式包括两种:
    • set注入
    • 构造注入
  • Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)

所以结论是:IOC 就是一种控制反转的思想, 而 DI 是对IoC的一种具体实现

3、IoC容器在Spring的实现

  • Spring 的 IoC 容器就是 IoC思想的一个落地的产品实现
  • IoC容器中管理的组件也叫做 bean
  • 在创建 bean 之前,首先需要创建IoC 容器
  • Spring 提供了IoC 容器的两种实现方式:
    • BeanFactory
      • 这是 IoC 容器的基本实现
      • 是 Spring 内部使用的接口
      • 面向 Spring 本身,不提供给开发人员使用
    • ApplicationContext
      • BeanFactory 的子接口,提供了更多高级特性
      • 面向 Spring 的使用者
      • 几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory

ApplicationContext的主要实现类

在这里插入图片描述

类型名 简介
ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContext ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力
WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中

二、基于XML管理Bean

搭建项目

  • 添加依赖
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.3</version>
</dependency>
  • 引入java类
public class User {
}
  • beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--1 获取bean演示,user对象创建-->
    <bean id="user" class="com.xc.spring6.iocxml.bean.User"></bean>
</beans>

1、获取bean

方式一、根据id获取

public class TestUser {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        // 根据id获取bean
        User user = (User) context.getBean("user");
    }
}

方式二、根据类型获取

public class TestUser {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        User user = context.getBean(User.class);
    }
}

方式三、根据id和类型获取bean

public class TestUser {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        User user = context.getBean("user",User.class);
    }
}

注意

  • 当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个
  • 当IOC容器中一共配置了两个
<bean id="user" class="com.xc.spring6.iocxml.bean.User"></bean>
<bean id="user1" class="com.xc.spring6.iocxml.bean.User"></bean>

根据类型获取时会抛出异常:NoUniqueBeanDefinitionException(没有唯一Bean定义异常)

扩展

  • 如果组件类实现了接口,根据接口类型可以获取 bean 吗?(可以,前提是bean唯一)
  • 如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?(不行,因为bean不唯一)

2、依赖注入之setter注入

  • 创建学生类Student
@Data
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
}
  • 配置bean时为属性赋值
<bean id="studentOne" class="com.xc.spring6.bean.Student">
    <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
    <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
    <!-- value属性:指定属性值 -->
    <property name="id" value="1001"></property>
    <property name="name" value="张三"></property>
    <property name="age" value="23"></property>
    <property name="sex" value=""></property>
</bean>
  • 测试
@Test
public void testDIBySet(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring-di.xml");
    Student studentOne = ac.getBean("studentOne", Student.class);
    System.out.println(studentOne);
}

3、依赖注入之构造器注入

  • 在Student类中添加有参构造
public Student(Integer id, String name, Integer age, String sex) {
    this.id = id;
    this.name = name;
    this.age = age;
    this.sex = sex;
}
  • 配置bean
<bean id="studentTwo" class="com.xc.spring6.bean.Student">
    <constructor-arg value="1002"></constructor-arg>
    <constructor-arg value="李四"></constructor-arg>
    <constructor-arg value="33"></constructor-arg>
    <constructor-arg value="女"></constructor-arg>
</bean>

注意:
constructor-arg标签还有两个属性可以进一步描述构造器参数

  • index属性:指定参数所在位置的索引(从0开始)
  • name属性:指定参数名
  • 测试
@Test
public void testDIByConstructor(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring-di.xml");
    Student studentOne = ac.getBean("studentTwo", Student.class);
    System.out.println(studentOne);
}

4、特殊值处理

字面量赋值

<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="name" value="张三"/>

null值

<property name="name">
    <null />
</property>

xml实体

<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a &lt; b"/>

CDATA节

<property name="expression">
    <!-- 解决方案二:使用CDATA节 -->
    <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
    <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
    <!-- 所以CDATA节中写什么符号都随意 -->
    <value><![CDATA[a < b]]></value>
</property>

5、为对象类型属性赋值

  • 员工类和部门类
// 部门累
@Data
public class Dept {
    private String dname;
}
// 员工类
@Data
public class Emp {
    //对象类型属性:员工属于某个部门
    private Dept dept;
    //员工名称
    private String ename;
    //员工年龄
    private Integer age;
}

方式一:引用外部bean

<bean id="dept" class="com.xc.spring6.iocxml.ditest.Dept">
    <property name="dname" value="安保部"></property>
</bean>
  • 使用ref属性给Emp类的部门属性赋值对象
<bean id="emp" class="com.xc.spring6.iocxml.ditest.Emp">
    <!--注入对象类型属性
        private Dept dept;
    -->
    <property name="dept" ref="dept"></property>
    <!--普通属性注入-->
    <property name="ename" value="lucy"></property>
    <property name="age" value="50"></property>
</bean>

方式二:内部bean

<bean id="emp2" class="com.xc.spring6.iocxml.ditest.Emp">
    <!--普通属性注入-->
    <property name="ename" value="mary"></property>
    <property name="age" value="20"></property>
    <!--内部bean-->
    <property name="dept">
        <bean id="dept2" class="com.xc.spring6.iocxml.ditest.Dept">
            <property name="dname" value="财务部"></property>
        </bean>
    </property>
</bean>

方式三:级联属性赋值

  • name属性 = 对象.属性名
<bean id="dept3" class="com.xc.spring6.iocxml.ditest.Dept">
    <property name="dname" value="技术研发部"></property>
</bean>

<bean id="emp3" class="com.xc.spring6.iocxml.ditest.Emp">
    <property name="ename" value="tom"></property>
    <property name="age" value="30"></property>
    
    <property name="dept" ref="dept3"></property>
    <property name="dept.dname" value="测试部"></property>
</bean>

6、为数组类型属性赋值

  • Emp员工类添加爱好属性数组
//爱好
private String[] loves;
<bean id="emp" class="com.xc.spring6.iocxml.ditest.Emp">
    <!--普通属性-->
    <property name="ename" value="lucy"></property>
    <property name="age" value="20"></property>
    <!--对象类型属性-->
    <property name="dept" ref="dept"></property>
    <!--数组类型属性-->
    <property name="loves">
        <array>
            <value>吃饭</value>
            <value>睡觉</value>
            <value>敲代码</value>
        </array>
    </property>
</bean>

7、为集合类型属性赋值

  • Dept部门类添加员工集合
//一个部门有很多员工
private List<Emp> empList;
<bean id="empone" class="com.xc.spring6.iocxml.ditest.Emp">
    <property name="ename" value="lucy"></property>
    <property name="age" value="20"></property>
</bean>
<bean id="emptwo" class="com.xc.spring6.iocxml.ditest.Emp">
    <property name="ename" value="mary"></property>
    <property name="age" value="30"></property>
</bean>

<bean id="dept" class="com.xc.spring6.iocxml.ditest.Dept">
  <property name="dname" value="技术部"></property>
  <property name="empList">
        <list>
            <ref bean="empone"></ref>
            <ref bean="emptwo"></ref>
        </list>
  </property>
</bean>

若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可

8、基于xml自动装配

  • 根据指定的策略,在IOC容器中匹配某一个bean
  • 自动为指定的bean中所依赖的类类型或接口类型属性赋值
  • 基于XML自动装配,底层使用set注入

bean类

public class UserServiceImpl  implements UserService{

    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void addUserService() {
        userDao.addUserDao();
    }
}

配置bean.xml

  • 自动装配方式:byType
  • byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
    • 若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值null
    • 若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
<bean id="userService" class="com.xc.spring6.autowire.service.impl.UserServiceImpl" 
autowire="byType"></bean>

<bean id="userDao" class="com.xc.spring6.autowire.dao.impl.UserDaoImpl"></bean>
  • 自动装配方式:byName
  • byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值

三、基于注解管理Bean(

1、配置类替换bean.xml

@Configuration
//@ComponentScan({"com.xc.spring6.controller", "com.xc.spring6.service","com.xc.spring6.dao"})
@ComponentScan("com.xc.spring6")
public class Spring6Config {
}

加载配置类:

 ApplicationContext context1 = 
 new AnnotationConfigApplicationContext(Spring6Config.class);

2、@Autowired注入

  • 单独使用@Autowired注解,默认根据类型装配
  • 查看注解源码
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, 
ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}
  • 注解可以标注的位置
    • 构造方法上
    • 方法上
    • 形参上
    • 属性上
    • 注解上
  • 该注解有一个required属性
    • 默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错
    • 如果required属性设置为false,表示注入的Bean存在或者不存在都没关系

方式一:属性注入(最常用

  • 基于注解自动装配,底层使用反射注入,故不需要set方法
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
}

方式二、set注入

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

方式三、构造方法注入

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}

方式四、形参注入

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserServiceImpl(@Autowired UserDao userDao) {
        this.userDao = userDao;
    }
}

方式五、只有一个构造函数,通过构造器注入不需要注解

  • 当有参数的构造方法只有一个时,@Autowired注解可以省略
@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}

方式六、@Autowired注解和@Qualifier注解联合

  • 如果UserDao接口有两个实现类
  • 通过@Autowired注入会抛异常(expected single matching bean but found 2:xx1,xx2)
  • 此时可以通过@Qualifier注解指定具体bean的名称
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    @Qualifier("userDaoImpl") // 指定bean的名字
    private UserDao userDao;

}

3、@Resource注入

@Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?

  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分
    • 所以该注解是标准注解,更加具有通用性
    • JSR-250标准中制定的注解类型。JSR是Java规范提案
  • @Autowired注解是Spring框架自己的
  • @Resource注解默认根据名称装配byName,通过name找不到的话会自动启动通过类型byType装配
  • @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用
  • @Resource注解用在属性上、setter方法上
  • @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上

@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖
如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

方式一、根据name注入

@Service
public class UserServiceImpl implements UserService {

    @Resource(name = "userDao")
    private UserDao myUserDao;

}

方式二、name未知注入

  • 当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserDao userDao;
}

方式三、name找不到的情况

  • 显然当通过name找不到的时候,自然会启动byType进行注入
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserDao userDaoNotFound ;

}

四、原理-手写IoC

定义标记bean的@Bean注解和依赖注入的@Di注解

  • @Bean相当于@Component
  • @Di相当于@Autowired
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
}

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

定义bean容器接口

public interface ApplicationContext {
    Object getBean(Class<?> clazz);
}

注解bean容器接口和依赖注入的实现

public class AnnotationApplicationContext implements ApplicationContext {
    //创建map集合,放bean对象
    private final Map<Class<?>, Object> beanFactory = new HashMap<>();
    private static String rootPath;

    //返回对象
    @Override
    public Object getBean(Class<?> clazz) {
        return beanFactory.get(clazz);
    }

    //创建有参数构造,传递包路径,设置包扫描规则
    //当前包及其子包,哪个类有@Bean注解,把这个类通过反射实例化
    public AnnotationApplicationContext(String basePackage) {
        // com.xc
        try {
            //1 把.替换成\
            String packagePath = basePackage.replaceAll("\\.", "/");
            //2 获取包绝对路径
            Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
                //获取包前面路径部分,字符串截取
                rootPath = filePath.substring(0, filePath.length() - packagePath.length());
                //包扫描
                loadBean(new File(filePath));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //属性注入
        loadDi();
    }

    //包扫描过程,实例化
    private void loadBean(File file) throws Exception {
        //1 判断当前是否文件夹
        if (file.isDirectory()) {
            //2 获取文件夹里面所有内容
            File[] childrenFiles = file.listFiles();
            //3 判断文件夹里面为空,直接返回
            if (childrenFiles == null || childrenFiles.length == 0) {
                return;
            }
            //4 如果文件夹里面不为空,遍历文件夹所有内容
            for (File child : childrenFiles) {
                //4.1 遍历得到每个File对象,继续判断,如果还是文件夹,递归
                if (child.isDirectory()) {
                    //递归
                    loadBean(child);
                } else {
                    //4.2 遍历得到File对象不是文件夹,是文件,
                    //4.3 得到包路径+类名称部分-字符串截取
                    String pathWithClass = child.getAbsolutePath().substring(rootPath.length());
                    //4.4 判断当前文件类型是否.class
                    if (pathWithClass.contains(".class")) {
                        //4.5 如果是.class类型,把路径\替换成.  把.class去掉
                        // com.xc.service.UserServiceImpl
                        String allName = pathWithClass.replaceAll("/", ".").replace(".class", "");
                        //4.6 判断类上面是否有注解 @Bean,如果有实例化过程
                        //4.6.1 获取类的Class
                        Class<?> clazz = Class.forName(allName);
                        //4.6.2 判断不是接口
                        if (!clazz.isInterface()) {
                            //4.6.3 判断类上面是否有注解 @Bean
                            if (clazz.isAnnotationPresent(Bean.class)) {
                                //4.6.4 实例化
                                Object instance = clazz.getConstructor().newInstance();
                                //4.7 把对象实例化之后,放到map集合beanFactory
                                //4.7.1 判断当前类如果有接口,让接口class作为map的key
                                if (clazz.getInterfaces().length > 0) {
                                    beanFactory.put(clazz.getInterfaces()[0], instance);
                                } else {
                                    beanFactory.put(clazz, instance);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    //属性注入
    private void loadDi() {
        //实例化对象在beanFactory的map集合里面
        //1 遍历beanFactory的map集合
        Set<Map.Entry<Class<?>, Object>> entries = beanFactory.entrySet();
        for (Map.Entry<Class<?>, Object> entry : entries) {
            //2 获取map集合每个对象(value),每个对象属性获取到
            Object obj = entry.getValue();
            //获取对象Class
            Class<?> clazz = obj.getClass();
            //获取每个对象属性获取到
            Field[] declaredFields = clazz.getDeclaredFields();
            //3 遍历得到每个对象属性数组,得到每个属性
            for (Field field : declaredFields) {
                //4 判断属性上面是否有@Di注解
                if (field.isAnnotationPresent(Di.class)) {
                    //如果私有属性,设置可以设置值
                    field.setAccessible(true);
                    //5 如果有@Di注解,把对象进行设置(注入)
                    try {
                        field.set(obj, beanFactory.get(field.getType()));
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

测试类

@Bean
public class UserDaoImpl  implements UserDao {
    @Override
    public void add() {
        System.out.println("dao.......");
    }
}
@Bean
public class UserServiceImpl implements UserService {

    @Di
    private UserDao userDao;

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}
public class SpringIocTest {
    @Test
    public void testIoc() {
        ApplicationContext applicationContext = new AnnotationApplicationContext("com.xc");
        UserService userService = (UserService)applicationContext.getBean(UserService.class);
        userService.add();
        System.out.println("run success");
    }
}

输出结果

service.......
dao.......
run success

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/148526.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!