Spring系列3:bean实例化的方式知多少

 2023-02-18
原文作者:空虚碧海 原文地址:https://juejin.cn/post/7053454882799353869

本文内容

  1. 通过构造函数实例化bean
  2. 通过静态工厂方法实例化bean
  3. 通过实例工厂方法实例化bean

通过构造函数实例化bean

Spring中可以通过空构造函数或是默认构造函数来实例bean,直接上案例。

定义一个简单类

    package com.crab.spring.ioc.demo01;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 12:05
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class ConstructorBean {
    }

配置文件

    <?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">
    
        <!--默认构造函数实例化-->
        <bean class="com.crab.spring.ioc.demo01.ConstructorBean" id="constructorBean"/>
    </beans>

来个测试测试进行测试

    package com.crab.spring.ioc.demo01;
    import static org.junit.Assert.*;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 13:32
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class Test {
    
        @org.junit.Test
        public void test() {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo01/spring.xml");
            ConstructorBean constructorBean = context.getBean(ConstructorBean.class);
            System.out.println(constructorBean);
            context.close();
        }
    
    }

运行结果

    com.crab.spring.ioc.demo01.ConstructorBean@371a67ec

通过静态工厂方法实例化bean

在定义使用静态工厂方法创建的 bean 时,使用 class 属性来指定包含静态工厂方法的类,并使用名为 factory-method 的属性来指定工厂方法本身的名称。

定义一个类,包含一个静态工厂方法返回该类的实例

    package com.crab.spring.ioc.demo01;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 13:38
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class StaticBean {
    
        private static StaticBean instance = new StaticBean();
    
        private StaticBean() {
        }
    
        /**
         * 静态工厂方法返回实例
         *
         * @return
         */
        public static StaticBean factoryMethod() {
            return instance;
        }
    }

配置文件中配置静态工厂方法实例化bean

    <?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">
        <!--静态工厂方法-->
        <bean class="com.crab.spring.ioc.demo01.StaticBean" id="staticBean" factory-method="factoryMethod"/>
    
    </beans>

测试类

        @org.junit.Test
        public void test() {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo01/spring.xml");
            ConstructorBean constructorBean = context.getBean(ConstructorBean.class);
            System.out.println(constructorBean);
            System.out.println("测试静态工厂方法实例化类");
            StaticBean staticBean = context.getBean(StaticBean.class);
            System.out.println(staticBean); // 实例地址
            System.out.println(staticBean == StaticBean.factoryMethod()); // true
            context.close();
        }

通过实例工厂方法实例化bean

与通过静态工厂方法进行实例化类似,使用实例工厂方法进行实例化从容器中调用现有 bean 的 非静态方法 来创建新 bean。 要使用此机制, class 属性留空,并在 factory-bean 属性中指定当前(或父级或祖先)容器中包含要调用以创建对象的实例方法的 bean 的名称。 使用 factory-method 属性设置工厂方法本身的名称。

定义一个类及其工厂类

    package com.crab.spring.ioc.demo01;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 13:48
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class MyBean {
        private String name;
    
        public MyBean(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "MyBean{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    package com.crab.spring.ioc.demo01;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 13:48
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class MyBeanFactory {
    
        /**
         * 生成一个 MyBean
         * @return
         */
        public MyBean factoryMethod() {
            return new MyBean("xxx");
        }
    }

配置文件中进行bean定义

    <?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">
    
        <!--默认构造函数实例化-->
        <bean class="com.crab.spring.ioc.demo01.ConstructorBean" id="constructorBean"/>
    
        <!--静态工厂方法-->
        <bean class="com.crab.spring.ioc.demo01.StaticBean" id="staticBean" factory-method="factoryMethod"/>
    
        <!--实例工厂方法-->
        <!--工厂-->
        <bean class="com.crab.spring.ioc.demo01.MyBeanFactory" id="myBeanFactory"/>
        <!--    通过实例工厂方法创建的bean-->
        <bean id="myBean" factory-bean="myBeanFactory" factory-method="factoryMethod"/>
    </beans>

测试类输出通过实例工厂方法创建的bean

    package com.crab.spring.ioc.demo01;
    
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    import static org.junit.Assert.*;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 13:32
     * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
     */
    public class Test {
    
        @org.junit.Test
        public void test() {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo01/spring.xml");
            ConstructorBean constructorBean = context.getBean(ConstructorBean.class);
            System.out.println(constructorBean);
            System.out.println("测试静态工厂方法实例化类");
            StaticBean staticBean = context.getBean(StaticBean.class);
            System.out.println(staticBean);
            System.out.println(staticBean == StaticBean.factoryMethod());
            System.out.println("测试实例工厂方法实例化类");
            MyBean myBean = context.getBean(MyBean.class);
            System.out.println(myBean);
            context.close();
        }
    
    }

结果输出,输出了myBeanname 符合预期

    //省略
    测试实例工厂方法实例化类
    MyBean{name='xxx'}

扩展: 上面的工厂bean指的是在 Spring 容器中配置的 bean,它通过实例或静态工厂方法创建对象。Spring中还有一个强大的接口FactoryBean,同样可以产生bean,但2者不是同一个概念,感兴趣的可以了解下,后续有机会我们再详细讲。

总结

本篇主要说明了3中实例化bean的方式,可以按照实际需要进行选择使用。下一篇介绍bean中的依赖是如何注入的。

本篇源码地址: github.com/kongxubihai…

Spring系列4:依赖注入的2种方式

本文内容

  1. 基于构造器的依赖注入
  2. 基于setter的依赖注入

基于构造器的依赖注入

案例

定义2个简单的bean类,BeanOne 和 BeanTwo,前者依赖后者。

    package com.crab.spring.ioc.demo02;
    
    public class BeanTwo {
    }
    package com.crab.spring.ioc.demo02;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/12 16:59
     */
    public class BeanOne {
        private int age;
        private String name;
        private BeanTwo beanTwo;
       
        /**
         * 构造函数,用于依赖注入,定义3个依赖
         * @param age
         * @param name
         * @param beanTwo
         */
        public BeanOne(int age, String name, BeanTwo beanTwo) {
            this.age = age;
            this.name = name;
            this.beanTwo = beanTwo;
        }
    
        @Override
        public String toString() {
            return "BeanOne{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", beanTwo=" + beanTwo +
                    '}';
        }
    }

通过xml配置文件实现bean定义和依赖注入

    <?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">
    
        <bean id="bean2" class="com.crab.spring.ioc.demo02.BeanTwo"/>
    
        <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
            <constructor-arg name="age" index="0" type="int" value="20"/>
            <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
            <constructor-arg name="beanTwo" index="2" type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
        </bean>
    </beans>

来个测试类验证下注入

    package com.crab.spring.ioc.demo02;
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/12 17:09
     */
    public class demo02Test {
    
        @Test
        public void test_construct() {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo02/spring1.xml");
            BeanOne bean1 = context.getBean("bean1", BeanOne.class);
            System.out.println(bean1);
            context.close();
        }

输出如下

    BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@5204062d}

对照配置文件BeanOne的3个依赖都通过构造器的方式进行注入了,符合预期,很简单。

constructor-arg详解

标签constructor-arg支持的元素列表如下。

元素名 作用
name 参数名
index 参数索引,0开始
type 参数类型
value
ref bean引用

例如案例中的配置

    <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
        <constructor-arg name="age" index="0" type="int" value="20"/>
        <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
        <constructor-arg name="beanTwo" index="2" 		            		type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
    </bean>

注意: 在 没有引起歧义的 情况下,上面的部分元素并不是都必须配置的。如指定了index时可以定位参数位置,那么name是可以不配置的,又如通过ref引用依赖bean,type可以省略

    <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
        <constructor-arg index="0" type="int" value="20"/>
        <constructor-arg index="1" type="java.lang.String" value="xxx"/>
        <constructor-arg  index="2" ref="bean2"/>
    </bean>

注意事项

当在<constructor />使用 name元素指定构造函数中的参数名时,尤其要方法参数名编译后是否保留的情况。举个例子

    // 编译前的方法参数
    public BeanOne(int age, String name, BeanTwo beanTwo)
    // 编译后的方法参数
    public BeanOne(int var1, String var2, BeanTwo var3)

编译后方法参数被编译成var1这种形式会导致仅指定name的构造注入失效。

如何解决?提供2种方法

  1. Maven编译插件添加编译参数,保留参数名,pom文件下增加如下插件配置
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                            <encoding>UTF8</encoding>
                            <compilerArgs>
                                <arg>-parameters</arg>
                            </compilerArgs>
                        </configuration>
                    </plugin>
        
                </plugins>
            </build>
  1. 使用 @ConstructorProperties JDK 注释显式命名构造函数参数
            /**
             * 构造函数,用于依赖注入,定义3个依赖
             * @param age
             * @param name
             * @param beanTwo
             */
            @ConstructorProperties({"age", "name", "beanTwo"}) // 显式声明构造参数名称
            public BeanOne(int age, String name, BeanTwo beanTwo) {
                this.age = age;
                this.name = name;
                this.beanTwo = beanTwo;
            }

基于setter的依赖注入

基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。

来一个简单类BeanThree依赖BeanOneBeanTwo并提供了Setter方法来设置。

    package com.crab.spring.ioc.demo02;
    
    /**
     * @author zfd
     * @version v1.0
     * @date 2022/1/13 8:18
     */
    public class BeanThree {
        private BeanTwo beanTwo;
        private BeanOne beanOne;
    
        public void setBeanTwo(BeanTwo beanTwo) {
            this.beanTwo = beanTwo;
        }
    
        public void setBeanOne(BeanOne beanOne) {
            this.beanOne = beanOne;
        }
    }

对应的配置文件可以通过标签property中的refname来设置属性引用或是属性值

    <?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">
    
        <bean id="bean2" class="com.crab.spring.ioc.demo02.BeanTwo"/>
    
        <!--构造函数注入-->
        <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
            <constructor-arg name="age" index="0" type="int" value="20"/>
            <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
            <constructor-arg name="beanTwo" index="2" type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
        </bean>
    
        <!--setter注入-->
        <bean id="bean3" class="com.crab.spring.ioc.demo02.BeanThree">
            <!-- 1 ref元素-->
            <property name="beanOne" ref="bean1"></property>
            <!-- 2 ref标签-->
            <property name="beanTwo">
                <ref bean="bean2"></ref>
            </property>
             <property name="name" value="xxxx"/>
        </bean>
    </beans>

运行测试类和结果,可见依赖注入成功

    public class demo02Test {
    
        @Test
        public void test_construct() {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo02/spring1.xml");
            BeanOne bean1 = context.getBean("bean1", BeanOne.class);
            System.out.println(bean1);
    
            System.out.println("演示Setter注入");
            BeanThree beanThree = context.getBean(BeanThree.class);
            System.out.println(beanThree);
            context.close();
        }
    }
    BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24}
    演示Setter注入
    BeanThree{name='xxxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24, beanOne=BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24}}

依赖解决过程

  • ApplicationContext是用描述所有bean的配置元数据创建和初始化的。配置元数据可以通过XML、Java代码或注释指定。
  • 对于每个bean,它的依赖项都以属性、构造函数参数或静态工厂方法的参数的形式表示(如果使用静态工厂方法而不是普通构造函数的话)。当bean实际创建时,这些依赖项被提供给bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
  • 每个具有值的属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将字符串格式提供的值转换为所有内置类型,如int、long、string、boolean等。

什么是循环依赖,如何处理?

如果主要使用构造函数注入,则可能会创建一个不可解析的循环依赖场景。

例如:类A需要类B的一个实例通过构造函数注入,类B需要类A的一个实例通过构造函数注入。如果将类A和类B的bean配置为相互注入,Spring IoC容器会在运行时检测此循环引用,并抛出BeanCurrentlyInCreationException

一个可能的解决方案是编辑一些由setter而不是构造函数配置的类的源代码。或者,避免构造函数注入,只使用setter注入。换句话说,可以使用setter注入配置循环依赖项。

与典型情况(没有循环依赖关系)不同,bean A 和 bean B 之间的循环依赖关系强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。

萝卜青菜各有所爱

选择用构造器注入还是Setter注入?

Spring官方的推荐,构造函数用于强制依赖项,将 setter 方法或配置方法用于可选依赖项。请注意,在 setter 方法上使用 @Required 注释可用于使属性成为必需的依赖项;然而,带有参数的编程验证的构造函数注入是更可取的。

Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。作为旁注,大量的构造函数参数是一种不好的代码气味,这意味着该类可能有太多的职责,应该重构以更好地解决适当的关注点分离。 Setter 注入应该主要只用于可以在类中分配合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何地方执行非空检查。 setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。

有时,在处理没有源代码的第三方类时,如果第三方类没有公开任何 setter 方法,那么构造函数注入可能是 DI 的唯一可用形式。

总结

本文演示2种依赖注入的方式:构造函数注入和Setter方法注入,并对比如何选择这2种方式。下一篇继续深入依赖注入。

本篇源码地址: github.com/kongxubihai…

知识分享,转载请注明出处。学无先后,达者为先!