Spring从入门到精通—IOC详解

1、什么是Spring IOC/DI

  • 控制反转(Inversion of Control,IoC)
  • 所谓控制反转就是应用本身不负责依赖对象的创建及维护,依赖对象的创建及维护是由外部容器负责的。这样控制权就由应用转移到了外部容器,控制权的转移就是所谓反转。
  • 依赖注入(Dependency Injection,DI)
  • 在运行期,由外部容器动态地将依赖对象注入到组件中。换句话说,就是在运行时能 Bean对象设置属性值

    2、bean标签

    一个bean标签代表一个spring容器中的java对象,可以在bean中经常使用的属性如下:
    1. id 属性 :起名称 不能包含特殊符号 根据id 获得配置对象
    2. class属性:创建对象所在全路径
    3. name属性:功能和id一样 ,id不能包含特殊符号,name可以(基本不用,为了满足struts1遗留问题)
    4. scope属性:Bean的作用范围,scope常用的值有:-singleton-prototype,分别表示单例多例,如果没写默认就是单例

    3、Bean的3种实例化方式

    • 1.直接使用bean标签来实例化pojo,这中方法Spring默认调用的是这个pojo的无参构造器来实例化bean对象的

    首先创建一个EmailDaoImpl.java

    package com.xzy.dao.Impl;
    
    import com.xzy.dao.EmailDao;
    public class EmailDaoImpl implements EmailDao {
    
        @Override
        public void sent() {
            System.out.println("发送email>>>>>>>>>>>>");
        }
    }
    
    

    在ApplicationContext.xml文件中使用<bean id=“" class="" />标签配置bean:

     <!--1.直接使用bean class来实例化-->
    <bean id="email" class="com.xzy.dao.Impl.EmailDaoImpl"/>
    

    经过这两步就配置好了一个bean,测试代码简单,就是调用了一下sent方法,下面是执行的结果:

    • 2.使用静态工厂实例化pojo
      首先新建一个静态工厂DaoFactory.java
    package com.xzy.dao;
    
    import com.xzy.dao.Impl.EmailDaoImpl;
    public class DaoStaticFactory {
    
        /**
         * 静态工厂实例化bean
         * @return
         */
        public static EmailDao createInstance(){
            return new EmailDaoImpl();
        }
    }
    

    接着在xml中配置如下:

    <bean id="email1" class="com.xzy.dao.DaoStaticFactory" factory-method="createInstance"/>
    
    • 3.使用实例化工厂实例化pojo
      首先新建一个实例化工厂:
    package com.xzy.dao;
    
    import com.xzy.dao.Impl.EmailDaoImpl;
    /**
     * 实例工厂实例化bean
     */
    public class DaoInstanceFactory {
    
        public EmailDao createInstance(){
            return new EmailDaoImpl();
        }
    }
    

    在xml文件中配置如下:

    <!--3.通过实例工厂实例化bean-->
    <bean id="factory" class="com.xzy.dao.DaoInstanceFactory"/>
    <bean id="email2" factory-bean="factory" factory-method="createInstance"/>
    

    4、Spring依赖注入的2种常用方式

    1. 构造方法注入
    2. setter方法注入

    首先,新建学生实体类Student:

    package com.xzy.bean;
    
    import org.springframework.stereotype.Component;
    import java.util.Properties;
    
    public class Student {
        private int age;
        private String name;
        private Teacher tea;
        private Properties info=null;
    
    
        public Student(){
            System.out.println("默认调用无参构造方法实例化bean");
        }
    
        public Student(int age, String name) {
            this.age = age;
            this.name = name;
        }
    
        public Student(String name, Teacher tea) {
            this.name = name;
            this.tea = tea;
        }
    
       //省略getter,setter......
    

    1. 使用构造方法注入在xml中配置如下,正常情况下只用指定参数的名字和参数的值:
    <constructor-arg name="" value="" />,name就是构造方法中的参数名,value即为这个参数的值下面是一个简单的配置示例:

        <!--1.构造方法注入-->
     <bean id="student" class="com.xzy.bean.Student">
          <constructor-arg name="age" value="20" />
          <constructor-arg name="name" value="小明" />
     </bean>
    

    程序运行的结果:

    当构造方法出现命名冲突的时候,可以使用type属性指定参数的数据类型:

       <!--当构造方法中出现命名冲突的时候,还可以使用type属性指定参数的类型-->
    <bean id="student" class="com.xzy.bean.Student">
        <constructor-arg  value="20" type="int"></constructor-arg>
        <constructor-arg  value="小红" type="java.lang.String"></constructor-arg>
    </bean>
    

    程序运行的结果:

    2. setter方法注入,这种方法和实例化bean相同,都是用了property属性

    <bean id="student" class="com.xzy.bean.Student">
            <property name="age" value="#{25*7-20*7}"></property>
            <property name="name" value="#{'aaaaaaa'.toUpperCase()}"></property>
            <property name="tea" value="#{teacher}"></property>
            <property name="info">
                <value>
                    jdbc.driver=com.mysql.jdbc.Driver
                    jdbc.url=jdbc:mysql://localhost:3306/mydb
                </value>
            </property>
        </bean>
    

    程寻运行的结果:


    5、SpEL(Spring表达式)

            SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。为什么要总结SpEL,因为它可以在运行时查询和操作数据,尤其是数组列表型数据,因此可以缩减代码量,优化代码结构。个人认为很有用。
            SpEL有三种用法,一种是在注解@Value中;一种是用于XML配置;最后一种是在代码块中使用Expression。下面说一下它最基础最重要的一种用法:xml配置法。(使用注解的方式的语法和xml方式的语法一样的,只是使用注解会更方便)

    5.1 SpEL的基本语法
      语法格式``
    • #{123}、#{'字符串'} :数字、字符串
    • #{beanId}:对另一个bean的引用,类似ref属性
    • #{beanId.propName}::操作数据
    • #{beanId.toString()}:执行方法
    • #{T(类).字段|方法}:静态方法或字段
      • Spring表达式支持大多数的数学操作符、逻辑操作符、关系操作符。
        1.关系操作符
        包括:等于 (==, eq),不等于 (!=, ne),小于 (<, lt),,小于等于(<= ,
        le),大于(>, gt),大于等于 (>=, ge)
        2.逻辑操作符
        包括:and,or,and not(!)
        3.数学操作符
        包括:加 (+),减 (-),乘 (*),除 (/),取模 (%),幂指数 (^)。

        新建一个实体Customer

        package com.xzy.bean;
        
        import java.util.Arrays;
        import java.util.List;
        import java.util.Map;
        import java.util.Set;
        
        
        public class Customer {
        
            private String name;
            private String sex="男";
            private double pi;
        
        @Override
            public String toString() {
                return "Customer{" +
                        "name='" + name + '\'' +
                        ", sex='" + sex + '\'' +
                        ", pi=" + pi 
                        '}';
            }
        

        在配置文件中如下配置:

           <bean id="customer" class="com.xzy.bean.Customer">
                <!--操作字段:#{ref.Field}-->
                <property name="name" value="#{student.name}"></property>
                <!--静态字段:#{T.(TYPE).staticField}-->
                <property name="pi" value="#{T(Math).PI}"></property>
                <property name="sex" value="#{'女'}"></property>
            </bean>
        

        程序的执行结果如下:

        剩下的各种运算就不在这里试了,有兴趣的话可以自己尝试。

        6、Spring集合类型注入

            官方的一句话:In the <list/>, <set/>, <map/>, and <props/> elements, you set the properties and arguments of the Java Collection types List, Set, Map, and Properties, respectively.就是说,你可以用<list>setmapprops来配置对应的Java集合类型:List、Set、Map、以及Array(数组),以及Properties也可以配置。
        举个栗子:
        在上例Customer实体类的基础上修改,分别增加List属性、Set属性、Map属性和数组属性一个,具体代码如下:

        package com.xzy.bean;
        
        import java.util.Arrays;
        import java.util.List;
        import java.util.Map;
        import java.util.Set;
        public class Customer {
        
            private String name;
            private String sex="男";
            private List<String> shopCar;    //购物车
            private Set<String> price;       //价格
            private Map<String,Double> goods;   //物品
            private String[] address;        //地址
        
             //省略getter、setter....
        
            @Override
            public String toString() {
                return "Customer{" +
                        "name='" + name + '\'' +
                        ", sex='" + sex + '\'' +
                        ", shopCar=" + shopCar +
                        ", price=" + price +
                        ", goods=" + goods +
                        ", address=" + Arrays.toString(address) +
                        '}';
            }
        }
        
        • 使用<list>标签给List类型注入初始值:
        <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
                <!--List注入使用<list>元素 -->
                <property name="shopCar">
                    <list>
                        <value>书</value>
                        <value>手机</value>
                        <value>衣服</value>
                        <value>电脑</value>
                    </list>
                </property>
           </bean>
        
        • 使用<set>标签给Set类型注入初始值:
        <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
        
                <!--Set注入使用<set>元素 -->
                <property name="price">
                    <set>
                        <value>"#{3.5*6}"</value>
                        <value>"#{2000*2}"</value>
                        <value>"#{100*6}"</value>
                        <value>"#{5000*1}"</value>
                    </set>
                </property>
           </bean>
        
        • 使用<map>标签给Map类型注入初始值:
        <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
                <!--Map输注入
                  使用<map>元素,特别注意,使用<entry>来指定一条数据的key和value
                -->
                <property name="goods">
                    <map>
                        <entry key="p1" value="23"></entry>
                        <entry key="p2" value="24"></entry>
                        <entry key="p3" value="25"></entry>
                        <entry key="p4" value="26"></entry>
                    </map>
                </property>
           </bean>
        
        • 使用<list>标签给List类型注入初始值:
        <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
                <!--List注入使用<list>元素 -->
                <property name="shopCar">
                    <list>
                        <value>书</value>
                        <value>手机</value>
                        <value>衣服</value>
                        <value>电脑</value>
                    </list>
                </property>
           </bean>
        
        • 使用<list>标签给List类型注入初始值:
        <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
               <!--数组注入使用<array>元素 -->
                <property name="address">
                    <array>
                        <value>西安</value>
                        <value>北京</value>
                        <value>南京</value>
                        <value>广州</value>
                    </array>
                </property>
           </bean>
        

        最后,对于customer的DI配置如下:

         <!--bean的集合注入-->
            <bean id="customer" class="com.xzy.bean.Customer">
                <property name="name" value="#{student.name}"></property>
                <property name="sex" value="#{'女'}"></property>
                <!--List注入
                    使用<list>元素
                -->
                <property name="shopCar">
                    <list>
                        <value>书</value>
                        <value>手机</value>
                        <value>衣服</value>
                        <value>电脑</value>
                    </list>
                </property>
                <!--Set注入
                   使用<set>元素
                -->
                <property name="price">
                    <set>
                        <value>"#{3.5*6}"</value>
                        <value>"#{2000*2}"</value>
                        <value>"#{100*6}"</value>
                        <value>"#{5000*1}"</value>
                    </set>
                </property>
                <!--Map输注入
                  使用<map>元素,特别注意,使用<entry>来指定一条数据的key和value
                -->
                <property name="goods">
                    <map>
                        <entry key="p1" value="23"></entry>
                        <entry key="p2" value="24"></entry>
                        <entry key="p3" value="25"></entry>
                        <entry key="p4" value="26"></entry>
                    </map>
                </property>
                <!--数组注入
                    使用<array>元素
                -->
                <property name="address">
                    <array>
                        <value>西安</value>
                        <value>北京</value>
                        <value>南京</value>
                        <value>广州</value>
                    </array>
                </property>
            </bean>
        

        程序执行结果:

        7、使用Annotation自动装配扫描

        要使用Anntation配置spring容器,首先需要在ApplicationContext.xml文件中配置如下信息:

        <!--开启注解-->
        <context:annotation-config/>
        <!--告诉Spring去扫描哪里-->
        <context:component-scan base-package="com.xzy"></context:component-scan>
        

        设置组件与bean命名

      • 1.@Repository, @Service, and @Controller,@Component 这 四 个 Annotation 功 能 相 同都是声明一个bean组件,不同的是 @Repository 声明 Dao层 @Service 声明 Service层 @controller 声明 控制器层, @Component 就是一个普通的组件,例如对pojo实体可以使用他 都是用在类上的 Annotation,说明让Spring 实例化此类的对像,并放入 spring 容器中
      • 2.@componet(“id”)其中 id 声明 bean 对像的名字
      • 举个栗子:
        新建Student.java

        package com.xzy.bean;
        
        import org.springframework.stereotype.Component;
        
        @Component    //这一句就是告诉Spring这是个普通的组件
        public class Student {
        
            private int age;
            private String name;
        
        
            public Student() {
                System.out.println("1.实例化了bean。。。。。");
            }
        
            //省略getter、setter
            
            @Override
            public String toString() {
                return "Student{" +
                        "age=" + age +
                        ", name='" + name + '\'' +
                        '}';
            }
        }
        

        测试代码:

        package com.xzy;
        
        import com.xzy.bean.Student;
        import org.apache.log4j.Logger;
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.test.context.ContextConfiguration;
        import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
        
        
        /**
         * Unit test for simple App.
         */
        @RunWith(SpringJUnit4ClassRunner.class)
        @ContextConfiguration(value={"/ApplicationContext.xml"})
        public class AppTest {
        
            private static Logger log=Logger.getLogger(AppTest.class);
        
            @Autowired  //自动注入,Spring会为我们自动从容器中找到student的对象然后注入这里的变量
            private Student student;
        
            @Test
            public void test01(){
                System.out.println(student);
            }
        }
        

        程序运行结果:

        @Repository, @Service, and @Controller
        新建StudentDao以及StudentDaoImpl

        StudentDao
        package com.xzy.Dao;
        
        import com.xzy.bean.Student;
        public interface StudentDao {
        
            /**
             * 增加一个学生
             * @param stu
             * @return
             */
            public void addStudent(Student stu);
        
        }
        

        写一个StudentDaoImpi,模拟DAO层,并使用@Respositiry告诉Spring这是DAO层的组件

        StudentDaoImpl
        package com.xzy.Dao.DaoImpi;
        
        import com.xzy.Dao.StudentDao;
        import com.xzy.bean.Student;
        import org.springframework.stereotype.Repository;
        
        @Repository
        public class StudentDaoImpi implements StudentDao {
        
            public StudentDaoImpl() {
                System.out.println("Repository层实例化");
        
            }
        
            @Override
            public void addStudent(Student stu) {
                System.out.print("3.Dao层处理数据:");
        
                System.out.println("向数据库发一条insert语句添加一个学生:"+stu.getName());
        
            }
        }
        

        新建StudentService.java,模拟Service层,使用@Service注解告诉Spring这是一个Service的组件

        StudentService.java
        package com.xzy.Service;
        
        import com.xzy.Dao.StudentDao;
        import com.xzy.bean.Student;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Service;
        
        @Service
        public class StudentService {
        
            public StudentService() {
                System.out.println("Service层实例化");
            }
        
            @Autowired
            private StudentDao stuImp;
        
            public void add(Student stu){
                System.out.println("2.service层收到控制层的数据后发给Dao层");
                 stuImp.addStudent(stu);
            }
        }
        

        新建StudentServlet.java,模拟控制器层,并使用@Controller注解告诉Spring这是控制器。

        StudentServlet.java
        package com.xzy.Servlet;
        
        import com.xzy.Service.StudentService;
        import com.xzy.bean.Student;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Controller;
        
        @Controller
        public class StudentServlet {
        
            public StudentServlet() {
                System.out.println("Controller层实例化");
            }
        
            @Autowired
            private StudentService stuService;
            @Autowired
            private Student stu;
        
            public void addAction() {
                System.out.println("1.控制层发数据给sevice层");
                stu.setAge(23);
                stu.setName("狗子");
                stuService.add(stu);
            }
        }
        

        最终的执行结果:

        可以看到这三个注解是由其存在的意义的,@Repository是告诉spring这是一个DAO层的类应该最先实例化(确实也应该如此), @Service层接着DAO层实例化完成后实例化, and @Controller层最后实例化,只用这样才可以确保不会空指针。

        设置组件扫描的base-packages

        @Configuration
        @ComponentScan(“基包名”)
        Public class AppConfig{}

        @Configuration
        @ComponentScan(basepackages=“基包名”)
        Public class AppConfig{}

        @Configuration
        @ComponentScan(basepackages={“基包名”,”…”})
        Public class AppConfig{}

        @Configuration
        @ComponentScan(basePackageClasses={App1Config.class,App2Config.class})
        Public class AppConfig{}
        以上 App1Config 与 App2Config 所在的包作为组件扫描的基础包

        8.3 Annotation 自动装配

        1.@Autowired 自动装配和 JSR 330’s @Inject 对应,可用在构造方法、属性 setter 方法,有属性@Autowired(required=false)
        @Primary 用于声明 bean 的首先,用在多个 bean,无法选择装配谁的情况可以指明使用哪个

        2.@Required 声明依赖必须提供 用在 setter 方法
        @Required
        public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
        }

        3.@Qualifiers 注明要装配 bean 的标识,用于多个 bean 无法确定装配哪个的情况

        8.4 处理自动装配的歧义
        Spring提供的自动装配是非常好用,可是用这么个问题:比如,一个接口有三个实现类,当要将接口类型自动装配置时,就出现不唯一的问题,Spring 会抛出 NoUniqueBeanDefinitionException。正如下面这种情况:
        写一个接口:

        package com.xzy.utils;
        
        import org.springframework.stereotype.Component;
        @Component
        public interface ReadData {
        
            public void read();
        
        }
        
        

        接口的三个实现类:

        package com.xzy.utils;
        
        import org.springframework.stereotype.Component;
        
        @Component
        public class USBRead implements ReadData {
            @Override
            public void read() {
               System.out.println("USB读取数据.....");
            }
        }
        
        
        package com.xzy.utils;
        
        import org.springframework.stereotype.Component;
        
        @Component
        public class SSDRead implements ReadData {
            @Override
            public void read() {
                System.out.println("SSD读取数据......");
            }
        }
        
        
        package com.xzy.utils;
        
        import org.springframework.stereotype.Component;
        
        @Component
        public class BlueRead implements ReadData {
            @Override
            public void read() {
                System.out.println("蓝牙读取数据.......");
            }
        }
        

        这时如果让Spring给我们自动装配,他都懵逼了,因为这个接口有3个实现类,都可以装配,他不知道装配那个,如下图所示:

        此时如果直接运行就会发生如下异常:

        2019-08-01 21:49:51 [ERROR]-[org.springframework.test.context.TestContextManager] Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@685cb137] to prepare test instance [com.xzy.AppTest@50a638b5]
          org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.xzy.AppTest': Unsatisfied dependency expressed through field 'read'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.xzy.utils.ReadData' available: expected single matching bean but found 3: blueRead,SSDRead,USBRead
        

        解决办法
        解决方法1:在实现类的头上使用@Primary注解告诉Spring首选哪个装配,比如在USBRead类的头上加上@Primary:

        解决方法2:使用@Qualifier 注解限定自动装配的 Bean

    留言区

    还能输入500个字符