• Spring学习第4篇:Spring 的依赖注入


           大家家好,我是一名网络怪咖,北漂五年。相信大家和我一样,都有一个大厂梦,作为一名资深Java选手,深知Spring重要性,现在普遍都使用SpringBoot来开发,面试的时候SpringBoot原理也是经常会问到,SpringBoot是为了简化Spring开发,但是底层仍然是Spring。如果不了解Spring源码,那就更别提SpringBoot源码了,接下来我准备用两个月时间,从基础到源码彻彻底底的学习一遍Spring。

           学习框架一定要踏踏实实一步一个脚印的学,很多人都比较喜欢急功近利,选择跳着学,包括我有时候也是,最终会发现你不但节约不了时间,反而会更耗时,而且学着学着很容易放弃。

    一、依赖注入的概念

    依赖注入:Dependency Injection。它是 spring 框架核心 ioc 的具体实现。我们的程序在编写时,通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。例如:我们的controller层仍会调用service层的方法。那这种controllerservice层的依赖关系,在使用 spring 之后,就让 spring 来维护了。简单的说,就是坐等框架把service层对象传入controller层,而不用我们自己去获取。

    public class TestController {
    
        private TestService testService;
    
        public void method1(){
            testService.method1();
        }
    
    	public void method2(){
            testService.method2();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    TestController当中需要通过TestService来完成某些操作,这就叫TestController依赖于TestService

    上面的TestController其实是存在一个问题的,testService并没有赋值,我们要给成员变量赋值只有两个途径,构造器赋值set方法赋值。那也就意味着创建TestController对象之前,需要先将被依赖对象通过new的方式创建好,然后通过两种方式当中的任意一种将其传递给TestController,这些工作都是TestController的使用者自己去做的,所有对象的创建都是由使用者自己去控制的。

    上一篇我们刚刚接触了IOC容器,也就是TestController我们肯定是要通过容器来创建的,那么他依赖的对象当然也要交给spring来给我们注入。所谓的注入其实就是给依赖的成员变量赋值

    二、三种注入方式

    spring中依赖注入主要分为手动注入自动注入,本文我们主要说一下手动注入,手动注入需要我们明确配置需要注入的对象。

    刚才上面我们回顾了,将被依赖方注入到依赖方,通常有2种方式:构造函数的方式和set属性的方式,spring中也是通过这两种方式实现注入的,下面详解2种方式。

    2.1.构造器注入的4种方式

    顾名思义,就是使用类中的构造函数,给成员变量赋值。注意,赋值的操作不是我们自己做的,而是通过配置的方式,让 spring 框架来为我们注入。

    构造器的参数就是被依赖的对象,构造器注入又分为4种注入方式:

    • 根据构造器参数索引注入
    • 根据构造器参数类型注入
    • 根据构造器参数名称注入
    • 根据bean的名称注入

    如下类是我们要测试的类:

    public class TestController {
    
        private String name;
        private Integer age;
    
        public TestController(String name,Integer age) {
            this.name = name;
            this.age = age;
            System.out.println("TestController初始化了");
        }
    
        @Override
        public String toString() {
            return "TestController{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    之前无参构造的话是:

    bean标签是单标签,现在因为要使用标签当中的子标签来注入构造器参数,所以使用了双标签。

    使用构造函数的方式,给 service 中的属性传值,要求:类中需要提供一个对应参数列表的构造函数。 可以存在多个构造器,不影响的。

    涉及的标签:constructor-arg
    属性:

    • index: 指定参数在构造函数参数列表的索引位置
    • type: 指定参数在构造函数中的数据类型
    • name: 指定参数在构造函数中的名称
    • value: 它能赋的值是基本数据类型和 String 类型
    • ref: ref 是 reference 的缩写形式,翻译为:涉及,参考的意思,用于引用其他Bean的id。

    依赖注入的数据类型有如下三种:

    • 普通数据类型,例如:String、int、boolean等,通过value属性指定。
    • 引用数据类型,例如:UserDaoImpl、DataSource等,通过ref属性指定。
    • 集合数据类型,例如:List、Map、Properties等。
    2.1.1.根据构造器参数索引注入
    
    <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="testController" class="com.gzl.cn.controller.TestController">
            <constructor-arg index="0" value="张三">constructor-arg>
            <constructor-arg index="1" value="11">constructor-arg>
        bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    public class Client {
        public static void main(String[] args) {
        	// 默认就是从项目根路径寻找bean.xml, classpath:bean.xml 和 bean.xml 是一样的效果
            ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");
            // 根据bean的名称来获取bean
            TestController testController = (TestController)classPathXmlApplicationContext.getBean("testController");
            System.out.println(testController);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果:

    在这里插入图片描述

    优缺点:

    • 参数位置的注入对参数顺序有很强的依赖性,若构造函数参数位置被人调整过,会导致注入出错。
    • 不过通常情况下,不建议去在代码中修改构造函数,如果需要新增参数的,可以新增一个构造函数来实现,这算是一种扩展,不会影响目前已有的功能。
    2.1.2.根据构造器参数类型注入
    <bean id="testController" class="com.gzl.cn.controller.TestController">
        <constructor-arg type="java.lang.String" value="张三">constructor-arg>
        <constructor-arg type="java.lang.Integer" value="11">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    假如有多个String类型,那么按照类型他还能注入吗?

    其实是不影响的,只要我们写的时候,参数顺序和构造器当中参数的顺序对应上即可。

    优缺点:

    实际上按照参数位置或者按照参数的类型注入,都有一个问题,很难通过bean的配置文件,知道这个参数是对应UserModel中的那个属性的,代码的可读性不好,比如我想知道这每个参数对应UserModel中的那个属性,必须要去看UserModel的源码,下面要介绍按照参数名称注入的方式比上面这2种更优秀一些。

    2.1.3.根据构造器参数名称注入
    <bean id="testController" class="com.gzl.cn.controller.TestController">
         <constructor-arg name="name" value="张三">constructor-arg>
         <constructor-arg name="age" value="11">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    关于方法参数名称的问题

    java通过反射的方式可以获取到方法的参数名称,不过源码中的参数通过编译之后会变成class对象,通常情况下源码变成class文件之后,参数的真实名称会丢失,关于这一点我们可以通过javac 类名.class命令来讲java编译成class文件,参数的名称会变成paramString,paramInteger或者arg1 arg2再或者var1 var2等等,总之和实际参数名称不一样了,如下:

    在这里插入图片描述

    如果需要将源码中的参数名称保留在编译之后的class文件中,编译的时候需要用下面的命令: 主要是加一个parameters

    javac -parameters java源码.class
    
    • 1

    在这里插入图片描述

    如果使用ider的话,我们正常编译后的class会到target目录当中,可能是编译器处理了这个问题,并没有做任何配置,然后测试了一下方法名称是保留下来的。

    参数名称可能不稳定的问题,spring提供了解决方案,通过ConstructorProperties注解来定义参数的名称,将这个注解加在构造方法上面,注意只能在构造器上使用,如下:

    @ConstructorProperties({"第一个参数名称", "第二个参数的名称",..."第n个参数的名称"})
    public 类名(String p1, String p2...,参数n) {
    }
    
    • 1
    • 2
    • 3
    public class TestController {
    
        private String name;
        private Integer age;
    
        @ConstructorProperties({"name", "age"})
        public TestController(String p1, Integer p2) {
            this.name = p1;
            this.age = p2;
            System.out.println("TestController初始化了");
        }
    
        @Override
        public String toString() {
            return "TestController{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    xml当中可能会报红,因为他找不到对应的构造器参数,实际我们通过@ConstructorProperties注解来指定了,所以报红并不影响正常运行!

    <bean id="testController" class="com.gzl.cn.controller.TestController">
         <constructor-arg name="name" value="张三">constructor-arg>
         <constructor-arg name="age" value="11">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    运行结果:

    在这里插入图片描述

    说直白点其实就是类似于一个参数别名一样,他并不会改变参数名称的编译结果,但是spring会通过这个注解所指定的名称,找到当前参数。

    2.1.4.根据bean的名称注入
    public class TestController {
    
        private TestService testService;
    
        public TestController(TestService testService) {
            this.testService = testService;
        }
    
        @Override
        public String toString() {
            return "TestController{" +
                    "testService=" + testService +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    <bean id="testService" class="com.gzl.cn.service.TestService">bean>
    
    <bean id="testController" class="com.gzl.cn.controller.TestController">
        <constructor-arg name="testService" ref="testService">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行结果:

    在这里插入图片描述

    2.2.setter注入

    通常情况下,我们的类都是标准的javabean,javabean类的特点:

    • 属性都是private访问级别的
    • 属性通常情况下通过一组setter(修改器)和getter(访问器)方法来访问
    • setter方法,以set开头,后跟首字母大写的属性名,如:setUserName,简单属性一般只有一个方法参数,方法返回值通常为void;
    • getter方法,一般属性以get开头,对于boolean类型一般以is开头,后跟首字母大写的属性名,如:getUserName,isOk;

    所谓的setter注入顾名思义就是通过构造器创建好对象之后,通过指定属性的set方法,将值赋值给成员变量。

    涉及的标签:property,set注入同构造器注入一样,都是可以注入普通类型和bean类型
    属性:
            name:找的是类中 set 方法后面的部分
            ref:指定bean的名称
            value:给属性赋值是基本数据类型和 string 类型的

    public class TestController {
    
        private String name;
        private Integer age;
        private TestService testService;
    
        public TestController() {
            System.out.println("无参构造执行了");
        }
    
        public void setName(String name) {
            System.out.println("name的set方法执行了");
            this.name = name;
        }
    
        public void setAge(Integer age) {
            System.out.println("age的set方法执行了");
            this.age = age;
        }
    
        public void setTestService(TestService testService) {
            System.out.println("testService的set方法执行了");
            this.testService = testService;
        }
    
        @Override
        public String toString() {
            return "TestController{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", testService=" + testService +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    public class TestService {
    
        public TestService() {
            System.out.println("TestService初始化了");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    <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="testService" class="com.gzl.cn.service.TestService">bean>
    
        <bean id="testController" class="com.gzl.cn.controller.TestController">
            <property name="age" value="11"/>
            <property name="name" value="张三"/>
            <property name="testService" ref="testService">property>
        bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    在这里插入图片描述

    2.3.使用 p 名称空间注入数据

    此种方式是通过在 xml 中导入 p 名称空间,使用 p:propertyName 来注入数据,它的本质仍然是调用类中的set 方法实现注入功能。

    使用p标签得需要引入:xmlns:p="http://www.springframework.org/schema/p" 否则他无法识别p标签!

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:p="http://www.springframework.org/schema/p"
           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="testService" class="com.gzl.cn.service.TestService">bean>
    
        <bean id="testController" class="com.gzl.cn.controller.TestController" p:age="11" p:name="李四"
              p:testService-ref="testService"/>
    
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    在这里插入图片描述

    三、注入容器中的bean

    注入容器中的bean有两种写法:

    • ref属性方式
    • 内置bean的方式

    3.1.ref属性方式

    将上面介绍的constructor-arg或者property元素的value属性名称替换为ref,ref属性的值为容器中其他bean的名称,如:构造器方式,将value替换为ref:

    <constructor-arg ref="需要注入的bean的名称"/>
    
    • 1

    setter方式,将value替换为ref:

    <property name="属性名称" ref="需要注入的bean的名称" />
    
    • 1

    3.2.内置bean的方式

    构造器的方式:

    <constructor-arg>
        <bean class=""/>
    constructor-arg>
    
    • 1
    • 2
    • 3

    setter方式:

    <property name="属性名称">
        <bean class=""/>
    property>
    
    • 1
    • 2
    • 3

    四、注入集合属性

    注入集合属性同样是可以用set方式注入,也可以用构造器方式注入,只不过集合得需要使用集合的标签。

    public class TestController {
    
        private String[] myStrs;
        private List<String> myList;
        private Set<String> mySet;
        private Map<String,String> myMap;
        private Properties myProps;
    
        public TestController(String[] myStrs, List<String> myList, Set<String> mySet, Map<String, String> myMap, Properties myProps) {
            this.myStrs = myStrs;
            this.myList = myList;
            this.mySet = mySet;
            this.myMap = myMap;
            this.myProps = myProps;
        }
    
        @Override
        public String toString() {
            return "TestController{" +
                    "myStrs=" + Arrays.toString(myStrs) +
                    ", myList=" + myList +
                    ", mySet=" + mySet +
                    ", myMap=" + myMap +
                    ", myProps=" + myProps +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    其中有set、array、list、props、map等标签,可以用在constructor-arg标签下,或者property标签下!

    <bean id="testController" class="com.gzl.cn.controller.TestController">
    	
    	
        <constructor-arg name="myStrs">
            <set>
                <value>AAAvalue>
                <value>BBBvalue>
                <value>CCCvalue>
            set>
        constructor-arg>
        <constructor-arg name="myList">
            <array>
                <value>AAAvalue>
                <value>BBBvalue>
                <value>CCCvalue>
            array>
        constructor-arg>
        <constructor-arg name="mySet">
            <list>
                <value>AAAvalue>
                <value>BBBvalue>
                <value>CCCvalue>
            list>
        constructor-arg>
        <constructor-arg name="myMap">
            <props>
                <prop key="testA">aaaprop>
                <prop key="testB">bbbprop>
            props>
        constructor-arg>
        <constructor-arg name="myProps">
            <map>
                <entry key="testA" value="aaa">entry>
                <entry key="testB">
                <value>bbbvalue>
                entry>
            map>
        constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    运行结果:

    在这里插入图片描述

  • 相关阅读:
    使用python进行页面开发——Django模型层
    Linux-centos
    设计模式: 建造者模式
    P02014030 陈子俊
    并发容器线程安全应对之道-ConcurrentHashMap
    Fortran捕捉浮点数非法运算
    mindspore训练retinanet时报错无法计算loss,停止训练
    代码,写的复杂点还是简单点?
    神经网络反向传播的数学原理
    百分点大数据技术团队:解读ToB产品架构设计的挑战及应对方案
  • 原文地址:https://blog.csdn.net/weixin_43888891/article/details/127675128