二、IOC容器

2023年 9月 2日 108.3k 0

一、什么是IOC?

IOC(Inversion of Control)即控制反转,要理解什么叫控制反转,我们就需要知道这里的控制是指对什么的控制?反转又是如何进行反转的?

直接上结论,控制指的是对资源获取方式的控制,反转指的是从主动获取资源变为被动接收资源。 意思就是,控制反转指对资源的获取方式从主动获取变为被动接收。那么,为什么要做出这种改变?这种改变又是怎么实现的呢?

首先说说为什么要做出这种改变?传统模式中,资源都是new出来的,需要什么资源就自己创建(new)什么资源,这种方式就叫做主动获取。这种方式的缺点请参考下文:【我们到底为什么要使用IOC】

总结来说,传统模式主要有三大弊端:

  • 创建了许多重复对象,造成大量资源浪费;

  • 代码耦合度过高,更换实现类需要改动多个地方,不利于修改、维护和拓展;

  • 创建和配置组件工作繁杂,给组件调用方带来极大不便。

  • 在Spring之前,聪明的开发者尝试使用工厂模式来解决上述问题。具体什么是工厂模式请参考下文:【什么是工厂模式】

    了解了什么是工厂模式之后,我们会发现,虽然工厂模式可以把对象的创建和使用过程分开,以此来降低代码的耦合度,但是不管是普通工厂模式还是抽象工厂模式,多多少少都有不可避免的弊端。简单工厂模式容错性较差,抽象工厂模式开发复杂度较高。。。

    可能有人会说,那对象工厂和业务代码还是会耦合呀。emmm。。。是的,工厂模式只能降低代码耦合度,并不能去耦合。代码耦合度是必然存在的,我们要做的是通过某种方式尽可能的降低代码的耦合度,使代码更容易维护和拓展。Spring就很大程度解决了开发复杂和代码耦合度高的问题。

    Spring是如何做到的呢?Spring做的就是将资源的获取方式从主动式变为被动式(控制反转)。被动式意思就是资源不是由我们来创建,而是交给一个容器来创建和设置,容器管理所有的组件(有功能的类)。

    容器可以自动探查出哪些组件(类)需要用到另外的哪一些组件(类);这个组件运行的时候,需要另外一个组件,容器就通过反射的方式,将容器中准备好的对象注入(通过反射给属性赋值),这个过程就叫依赖注入(DI)。而且只要容器管理的组件,都可以使用容器强大的功能。

    举个例子,下方代码中,假设UserInfoService受容器管理,UserInfoDao也受容器管理;容器帮我们创建好了UserInfoService对象和UserInfoDao对象,并且容器也能知道UserInfoService运行的时候,需要用到UserInfoDao,当UserInfoService运行的时候,容器就把准备好的UserInfoDao对象赋值过去,我们不需要new UserInfoDao(),只需要接收容器给我们的UserInfoDao,就可以直接使用UserInfoDao中的insert方法。这就是主动的new资源变为了被动接收资源。

    public class UserInfoService{
        
        private UserInfoDao dao;
        
        public void add(){
            dao.insert();
        }
    }
    
    public class UserInfoDao{
        public void insert(){}
    }
    

    那么问题来了,Spring是怎么做到让我们不用new的呢?又是怎么自动探查出哪些组件需要用到另外的哪一些组件的呢?容器还有哪些强大的功能呢?

    二、IOC的底层原理

    1. IOC底层实现

    先说Spring是怎么做到让我们不用new,就可以直接获取对象实例的。

    Spring有一个xml配置文件,如果把Spring看做一个班级,这个配置文件就相当于一个花名册,花名册记载了这个班所有的学生信息。一个标签代表一个学生。把这种关系代入到代码中,就是一个标签代表一个类,Spring会去扫描这个配置文件,获取到所有在它这里注册了的类,然后给每个类都创建一个实例。

    
    
        
        
        
        
        
        
    
    
    

    所以如果想要让Spring来管理这个类,第一步就需要把这个类配置到Spring的配置文件当中,否则Spring不知道这个类是它班级的学生,他也就无权管理这个类。知道了这个类需要被管理,那Spring又是如何创建实例的呢?这里就用到了XML解析+工厂模式+反射,IOC思想基于IOC容器完成,IOC容器底层就是对象工厂。

    // 1. 创建一个工厂类
    public class UserFactory{
        
        public static ZhangSan getZhangSan(){
            
            // 2. 解析XML文件,得到class的属性值
            String value = "解析XML"; // com.ysy.learn.ZhangSan
            // 3. 通过反射的方式获取ZhangSan的字节码文件
            Class clazz = Class.forName(value);
            // 4. 创建ZhangSan
            return (ZhangSan)clazz.newInstance();
        }
        
    }
    

    容器已经帮我们创建好了对象,那我们如何从容器中获取出这个组件呢?要从容器中拿对象,当然需要先得到容器,ApplicationContext就是IOC容器的实现

    public class TestDemo{
        
        public void testZhangSan(){
            
            // 1. 根据Spring配置文件,得到IOC容器对象
            ApplicationContext context = new ClassPathXmlApplicationContext("user.xml");
            // 2. 获取对象实例
            // Zhangsan zhangsan1 = (HelloSpring)context.getBean("zhangsan");
            // Zhangsan zhangsan2 = context.getBean(Zhangsan.class);
            Zhangsan zhangsan3 = context.getBean("zhangsan",Zhangsan.class);
        }
    }
    

    2. IOC容器的实现方式

    Spring提供了两种IOC容器的实现方式,一种是BeanFactory接口,另一种是ApplicationContext接口。两种方式都可以加载配置文件,创建IOC容器对象。

    BeanFactory接口是IOC容器的基本实现,是Spring内部的使用接口,不提供给开发人员使用(Spring自己用的),使用BeanFactory接口在加载配置文件的时候不会创建对象,获取(使用)对象的时候才会创建对象

    BeanFactory需要关注的实现类有ConfigurableApplicationContext,它包含相关的扩展功能

    ApplicationContext接口是BeanFactory接口的子接口,提供更多强大的功能,一般由开发人员直接使用,使用ApplicationContext接口在加载配置文件的时候就会把在配置文件中配置了的对象创建

    ApplicationContext需要关注的实现类有FileSystemXmlApplicationContextClassPathXmlApplicationContextFileSystemXmlApplicationContext取的是配置文件在系统盘中的路径(E:MySelfDataUser.xml);ClassPathXmlApplicationContext取的是配置文件的类路径(src下)

    为什么推荐使用ApplicationContext而不是BeanFactory

    BeanFactory是在对象使用的时候才会创建对象,这样虽然可以节约资源,但是会降低程序运行效率。Spring一般和Web项目进行整合,需要tomcat启动,tomcat在启动的时候加载配置文件,使用ApplicationContext,就相当于tomcat启动的时候就会把对象创建完成,程序运行的时候,不需要再创建对象,可以提高程序的运行效率。把耗时耗资源的过程交给服务器,这样更合理,所以项目中使用ApplicationContext而不是BeanFactory

    三、Bean管理

    什么是Bean管理?Bean管理指的是两个操作:1.Spring创建对象;2.Spring注入属性。

    那么具体怎么用Spring创建对象和注入属性呢?有两种方式,一种是基于xml配置文件方式实现,另一种是基于注解方式实现。

    1. 基于xml配置文件方式

    1.1 创建对象

    
    
    id属性:唯一标识
    class属性:创建对象所在类的全路径(包+类名)
    name属性:作用和id属性一致,id中不能加特殊符号,name中可以使用特殊符号
    

    基于XML配置文件方式创建对象,就是在spring配置文件中,使用标签,标签里面添加对应属性,就完成了对象的注册。

    Spring默认执行无参构造方法完成对象的创建。如果在类中声明一个有参构造函数,系统将不会默认生成无参构造函数,此时配置文件会因为找不到无参构造函数而报错

    1.2 注入属性

    Spring提供了两种基于XML配置文件方式注入属性的方法,一种是属性的set()方法,一种是有参构造函数。

    1.使用属性的set()方法示例:

    是根据set方法来确定属性名的,如果将setName()改成setPname(),Spring默认得到的属性名就是pname,而不是name

    简化set()方法注入:P名称空间注入

    什么是名称空间?名称空间是用来防止标签重复的。

    
    
        西游记
        
            吴承恩  
        
    
    

    2.使用有参构造函数示例: 在使用有参构造注入属性的时候,Spring就不是默认使用无参构造函数创建对象了,而是使用有参构造函数创建对象

    
    
    value属性:属性值
    name属性:类里面的属性名称
    index属性:有参构造函数中参数的位置,0代表第一个参数
    
    
    
        
        
    
    
    
    
        
        
    
    
    
    
        
        
        
    
    
    
        
        
        
    
    
    
    
        
        
    
    
    
        
        
    
    

    以上为最基本的基于XML配置文件方式注入属性,此外还有其他为各种类型的属性赋值。

    1.往属性中设置空值

    如果不给属性赋值,属性默认就是null。所以标签的使用场景是如果属性原本有值,但是在操作中需要给这个属性赋值为null,才会用到这种写法。

    2.属性值中包含特殊符号

    3.注入属性—外部Bean

    ref是一个严格的引用,它引用了外部的bean。如果有如下代码和配置,从IOC容器获取Car car1 = context.getBean("car"),从Person中获取Car car2 = person.getCar(),此时car1和car2是同一个对象。如果先Car car = person.getCar(),之后修改car的属性,再获取一次car,获取到的对象属性是被修改过的属性。

    public class Person{
        
        private Car car;
        
        public void setCar(String car) {
            this.car = car;
        }
        
        public String getCar() {
            return car;
        }
       
    }
    
    public class Car{}
    
    
        
    
    
    
    

    4.注入属性—内部Bean

    内部Bean写id其实没用,只能内部使用,不能被全局使用。意思就是内部bean不能通过context.getBean("car")获取到,只能用person.getCar()

    5.注入属性—级联赋值

    什么是级联属性?属性的属性就是级联属性。内部Bean的配置方式也是一种级联赋值,此外,使用表达式方式的级联赋值需要在Emp类中生成属性dept的get()方法,因为需要先得到dept对象实例,才能通过对象实例点出对象的name属性

    6.注入属性—Properties类型

    7.注入属性—集合类型

    数组类型属性注入的时候,可以用标签,也可以用标签

    如何注入对象集合?

    如何给多个类重用同一个集合(抽取出作为公共部分,供多个对象使用)?

    1.3 通过继承实现Bean配置信息的重用

    1.4 通过abstract属性创建一个模板Bean

    1.5 Bean之间的依赖

    Bean的创建顺序是按照在配置文件中的顺序来创建的,可以用depends-on来标识类之间的依赖关系,改变类的创建顺序。只是改变类的创建顺序,并不是说没有book,person就不能用。若没有添加depends-on标识,类的创建顺序是Car、Person、Book,添加依赖之后,类的创建顺序是Car、Book、Person。另外在depends-on中写的顺序也会影响类的创建顺序。

    1.6 自动装配(自动赋值)

    自动装配就是自动为属性赋值,这个概念是基于自定义类型,基本类型不存在自动装配。如何实现自动装配呢?在注册Bean时,使用autowire属性就可以为当前Bean配置自动装配。

    
    

    autowire属性有以下几个值:

    default/no: 不进行自动装配

    byName: 以属性名作为id到容器中找到这个组件,为它赋值,如果找不到就装配null

    byType: 以属性类型作为查找依据去容器中找到这个组件,如果容器中有多个就报错,没有就装配null

    constructor: 按照构造器进行赋值;1. 先找该类型的有参构造器,没有的话,直接装配null;2. 存在有参构造器就按照有参构造器的参数类型进行装配,如果没有该参数类型的组件,就直接装配null;3. 如果该类型的组件存在多个,就用参数名作为id继续匹配,找到就装配,找不到就装配null。这种用构造器装配的方式不会报错。

    如果属性是一个Lsit,容器会把容器中所有的Book组件封装成list赋值给这个属性。

    1.7 外部属性文件

    我们一般习惯于将数据库连接信息抽取到配置文件中,然后直接获取配置文件中的值,便于修改和维护。假设我们有一个jdbc.properties文件,里面包含数据库连接信息

    jdbc.username=root
    jdbc.password=123456
    jdbc.jdbcUrl=jdbc:mysql://localhost:3306/test
    jdbc.driverClass=com.mysql.jdbc.Driver
    

    那么Spring怎么获取外部的属性文件呢?

    
    
    
        
        
            
            
            
            
        
    
        
        
        
        
            
            
            
            
            
        
    
    

    上面提到如果在配置文件中直接配置username,获取不到数据库的连接,因为username是Spring的key中的一个关键字,那么username在Spring中到底代表什么呢?

    personname属性赋值${username},打印发现,获取到的username是计算机用户名

    补充知识: 源码文件夹和普通文件夹

    源码文件夹中的内容在编译之后会被放在编译文件的类路径下,普通文件夹中的内容不会放在类路径下,所以放在源码文件夹中的内容可以 直接使用classpath:来取,但是放在普通文件夹中的内容使用这种方式是取不到的

    2. 工厂Bean

    有一个AirPlane类,我们分别用静态工厂、实例工厂和FactoryBean的方式创建AirPlane实例

    public class AirPlane {
        
        public AirPlane() {、
            System.out.println("AirPlane...的构造器");
        }
    
        private String fdj; // 发动机
        private Double airPlaneHeight; // 飞机重量
        private String jzName; // 机长名称
        private String fjName; // 副驾名称
        private Integer peopleNum; // 载人数
    }
    

    静态工厂: 工厂本身不用创建对象,通过静态方法调用,对象 = 工厂类.方法名()

    public class AirPlaneStaticFactory {
    
        public AirPlaneStaticFactory() {
            System.out.println("静态工厂。。。。的构造器。");
        }
    
        public static AirPlane getAirPlane(String jzName){
    
            AirPlane airPlane = new AirPlane();
    
            airPlane.setAirPlaneHeight(1500.00);
            airPlane.setFdj("发动机");
            airPlane.setFjName("张三");
            airPlane.setJzName(jzName);
            airPlane.setPeopleNum(200);
    
            return airPlane;
        }
    }
    
    
    
        
    
    

    最终控制台输出的结果如下图。虽然配置文件中的class配置的是AirPlaneStaticFactory,但是没有创建AirPlaneStaticFactory的实例,因为没有打印AirPlaneStaticFactory构造器中的语句,而是打印了AirPlane构造器中的语句,说明创建了一个AirPlane的实例,并且为AirPlane实例中机长名称赋值为传入的参数李四。

    实例工厂: 要先new一个工厂类的实例,再调用工厂类的方法

    public class AirPlaneInstanceFactory {
    
        public AirPlaneInstanceFactory() {
            System.out.println("AirPlaneInstanceFactory...的构造器");
        }
    
        public AirPlane getAirPlane(String jzName){
    
            AirPlane airPlane = new AirPlane();
    
            airPlane.setAirPlaneHeight(1500.00);
            airPlane.setFdj("发动机");
            airPlane.setFjName("张三");
            airPlane.setJzName(jzName);
            airPlane.setPeopleNum(200);
    
            return airPlane;
        }
    }
    
    
    
    
    
    
        
    
    

    最终控制台输出的结果如下图。先去创建了AirPlaneInstanceFactory实例,之后再创建AirPlane实例。虽然xml文件中配置了AirPlane标签,但是AirPlane不是容器通过反射的方式创建出来的,而是通过实例工厂的工厂方法创建出来的。

    FactoryBean:Spring规定的一个接口,只要是这个接口的实现类,Spring都认为是一个工厂

    public class MyAirPlaneFactoryBean implements FactoryBean {
    
        /**
         * 工厂方法,返回创建的对象
         * @return
         * @throws Exception
         */
        @Override
        public AirPlane getObject() throws Exception {
            AirPlane airPlane= new AirPlane();
            airPlane.setJzName("王五");
            return airPlane;
        }
    
        /**
         * 返回创建的对象是什么类型
         * Spring会自动调用方法来确认返回的对象是什么类型
         * @return
         */
        @Override
        public Class getObjectType() {
            return null;
        }
    
        /**
         * 是否是单例
         *  false:不是单例
         *  true:是单例
         * @return
         */
        @Override
        public boolean isSingleton() {
            return true;
        }
    }
    
    
    
    

    最终控制台输出结果如下图。我们可以发现,Spring先创建了MyAirPlaneFactoryBean的实例,然后再去创建了AirPlane的实例。那么MyAirPlaneFactoryBean的实例和AirPlane的实例分别是在什么时候创建的呢?

    isSingleton()方法return true:代表创建单实例对象,然后我们只创建容器,不获取对象。

    @Test
    public void testBean2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
        // AirPlane air1 = context.getBean("airPlane2", AirPlane.class);
        // System.out.println(air1);
    }
    

    输出结果如下。只打印了调用MyAirPlaneFactoryBean的构造器,说明只创建了MyAirPlaneFactoryBean的实例,没有创建AirPlane对象。

    isSingleton()方法return false:代表创建多实例对象,然后我们只创建容器,不获取对象。输出结果如下。也是只打印了调用MyAirPlaneFactoryBean的构造器。所以我们可以得出结论,我们通过FactoryBean创建的对象,不管是单实例还是多实例,都是在获取实例的时候创建的,IOC容器启动的时候不会创建。

    3. Bean的作用域

    作用域限定了Spring Bean的作用范围,可以通过在配置文件中配置scope属性配置Bean的作用域。

    目前Spring支持五种作用域:

    1.singleton

    singleton是默认的作用域,当定义Bean时,如果没有指定scope配置项,Bean的作用域被默认为singleton。singleton属于单例模式,在整个系统上下文环境中,仅有一个Bean实例。也就是说,在整个系统上下文环境中,你通过Spring IOC获取的都是同一个实例。

    singleton作用域   示例:

    在XML配置文件中未指定Bean的作用域(scope属性),之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址是一致的,说明两次获取的是同一个对象实例。

    2.prototype

    当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。这意味着每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。如果Bean的作用域配置为Prototype,那么在加载Spring配置文件的时候不会创建对象,而是在调用getBean()方法的时候创建多实例对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。

    prototype作用域示例:

    在XML配置文件中指定Bean的作用域为Prototype,之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址不一致,说明两次获取的不是同一个对象实例。

    3.request

    在web环境下,同一次请求创建一个Bean

    4.session

    在web环境下,同一次会话创建一个Bean

    5.global-session

    4. Bean的生命周期

    从对象创建到对象销毁的过程就是对象的生命周期。

    Bean的生命周期:

  • 通过构造器创建Bean实例(无参构造):单实例Bean是在容器启动的时候;多实例是在获取对象的时候
  • 为Bean的属性设置值和对其他Bean引用(调用set()方法)
  • 初始化之前执行的方法(后置处理器Before)
  • 调用Bean的初始化方法(需要在Spring配置文件中配置初始化方法)
  • 初始化之后执行的方法(后置处理器After)
  • 使用Bean对象
  • 容器关闭,Bean销毁(需要在Spring配置文件中配置销毁方法)

  • 自定义后置处理器,只需要实现BeanPostProcessor接口,之后在xml文件中配置自定义的类,Spring会自动识别这个后置处理器。postProcessBeforeInitialization是在初始化方法之前调用的方法,可以在Bean被初始化之前做操作,postProcessAfterInitialization是在初始化方法之后调用的方法,可以在Bean初始化之后,继续对Bean做其他操作。

    public class MyBeanPostProcessor implements BeanPostProcessor {
    
        /**
         * 初始化方法之前调用的方法
         * @param bean
         * @param beanName
         * @return
         * @throws BeansException
         */
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("postProcessBeforeInitialization" + bean + "beanName:" + beanName);
            return bean;
        }
    
        /**
         * 初始化方法之后调用的方法
         * @param bean
         * @param beanName
         * @return
         * @throws BeansException
         */
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("postProcessAfterInitialization" + bean + "beanName:" + beanName);
            return bean;
        }
    }
    
     
    

    Bean的生命周期示例:

    单实例对象定义后置处理器之前

    单实例对象定义后置处理器之后

    如果配置了后置处理器,就算Bean没有定义初始化方法,还是会执行后置处理器的方法

    多实例对象定义后置处理器之后

    多实例对象,Spring是不负责销毁的,就是说如果Bean的作用域配置的是prototype,那么容器关闭时,不会调用销毁方法。

    5. 基于注解方式

    什么是注解?注解是代码中特殊的标记,格式是@(属性名称=属性值,属性名称=属性值)

    注解可以在什么地方用?注解可以作用在类、方法、属性上

    为什么需要注解?为了让配置更加简洁,简化XML配置,用更优雅更简洁的方式实现XML配置

    5.1 创建对象

    Spring针对创建对象提供了4个注解,他们的功能是一样的,都可以快速的将组件加入到IOC容器的管理中。

    @Controller:推荐给控制器组件添加(Servlet)
    
    @Service:推荐给业务逻辑层组件添加(Service)
    
    @Repository:推荐给数据库层的组件添加(Dao)
    
    @Component:推荐给不属于已上几种的组件添加
    

    Spring底层不会去验证这个组件是否如添加的注解描述是一个Dao层还是一个Service层,推荐不同的层添加不同的注解,是为了让开发者更明确而已。

    所以具体如何使用注解创建一个对象呢?

    第一步,创建一个类,给类上添加上述4个之中任意一个注解;

    第二步,配置组件扫描,就是要告诉Spring,我要用注解创建对象,你帮我创建一下。这个过程就是去Spring的配置文件中,开启组件扫描。

    第三步,组件扫描开启之后,运行程序,会报错,所以需要注意,要支持注解开发,一定要导入AOP的jar包。导入jar包之后,Spring就会通过注解的方式帮我们创建对象。

    5.2 组件扫描配置

    上面提到开启组件扫描,那么如何开启组件扫描,需要注意些什么呢?

    
    
        
        
    
        
        
    
    

    5.3 注入属性

    使用@Autowired注解实现根据类型实现自动装配

    /**
     * 属性的自动注入
     *  @Autowired Spring会自动为这个属性赋值,去容器中找到这个属性对应的组件
     *             用注解不需要为属性生成get、set方法
     */
    @Controller
    public class BookController {
    
        // 自动装配,自动为这个属性赋值
        @Autowired
        private BookService bookService;
    
        public void insert(){
            bookService.insert();
        }
    }
    
    @Service
    public class BookService {
    
        @Autowired
        private BookDao bookDao;
    
        public void insert(){
            System.out.println("BookService insert....");
            bookDao.insert();
        }
    
    }
    
    @Repository
    public class BookDao {
    
        public void insert(){
            System.out.println("BookDao insert.....");
        }
    }
    

    1. @Autowired的原理:

    image.png

    2. 方法上有@Autowired

    1.  这个方法也会在Bean创建的时候自动运行
    2.  这个方法的每一个参数都会注入值
    3.  也可以在参数上加[@Qualifier]()
    

    @Autowired@Resource@Inject的区别:

    1. @Autowired和@Qualifier是一起使用的
    2. @Autowired是Spring自己的,功能更强大,但是离开了Spring就用不了了
    3. @Resource是JavaEE的,Java的标准,扩展性更强,如果切换成另外的框架,@Autowired就用不了了
    

    5.4 完全注解开发

    完全注解开发就是我们不需要Spring的XML文件,而是利用一个类来实现Spring的XML文件的功能。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论