Spring 依赖注入

作者: 乐百川 | 来源:发表于2017-01-15 00:52 被阅读1639次

    依赖注入简介

    依赖注入是一个很常用的词。Java新手常会写出如下的代码,直接在写一个类的时候让它自己初始化自己。但是这不是一个好办法。一个类的状态应该由创建它的类决定,不能由自己决定自己。因此更好的办法是交给构造方法来初始化。

    public class User {
        private long id;
        private String username;
        private String password;
        private LocalDate birthday;
    
        public User() {
            id = 1;
            username = "yitian";
            password = "123456";
            birthday = LocalDate.now();
        }
    }
    

    也就是改成下面这样。这样一来,类不在由自己初始化自己,而是交给它的创造者处理,这就叫做控制反转,英文是(Inverse of Controll,简称IoC)。另外,由于数据由外界传入,所以这种方式又叫做依赖注入。这种使用构造方法注入的方式就叫做构造器注入。当然相应的还有使用Setters方法的依赖注入。这两种方式是最基本的,在此基础上例如Spring框架还提供了高级的基于注解的依赖注入等方式。

        public User(long id, String username, String password, LocalDate birthday) {
            this.id = id;
            this.username = username;
            this.password = password;
            this.birthday = birthday;
        }
    

    使用依赖注入的好处很明显。假如我们正在实现一个复杂的系统,需要将业务对象(比如上面的User对象)进行一些业务操作,然后用JDBC保存到数据库中。传统方式下,我们需要手动控制这些对象之间的关系。这样一来代码就耦合在一起,难以调试和维护。如果使用依赖注入方式,业务对象和数据库连接全部由IoC容器传入,我们要做的事情仅仅是处理业务逻辑。这样一来,数据的流入流出全部由依赖注入容器管理,我们编码不仅方便了,而且代码的可维护性也极大提高了。如果对此还有疑问的话,可以自己尝试不使用任何框架实现一个微型博客系统,然后在使用依赖注入重构一下。然后,你就会发现自己再也离不开依赖注入了。

    配置Spring环境

    添加依赖

    一开始我用的是Spring Boot,它自动为我们做了几乎所有的配置工作。这样虽然方便,但是对于初学者来说可能会隐藏一些重要的细节。因此这里用Gradle来说明一下Spring依赖注入的配置过程。Spring模块化做得非常好,如果我们想要使用某个功能,只需要导入对应的模块,也就是Jar包即可。要使用依赖注入和上下文管理,我们要导入spring-core.jarspring-context.jar这两个包。为了启用spring的单元测试支持,需要添加spring-test.jarjunit-4.12.jar。要运行最后面的Hibernate小例子,需要添加MySQL驱动和Hibernate核心包。在Gradle中,也就是简单地在配置文件中添加如下几行。springVersion是现在最新的稳定版Spring版本,值为'4.3.5.RELEASE'

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.12'
        compile group: 'org.springframework', name: 'spring-core', version: springVersion
        compile group: 'org.springframework', name: 'spring-context', version: springVersion
        compile group: 'org.springframework', name: 'spring-test', version: springVersion
        compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.6.Final'
        compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.40'
    
    }
    

    使用到的类

    我们来用几个类简单模拟一下教师上课的情景,Getter、Setter、构造器等方法均省略了。首先需要一个教师类:

    public class Teacher {
        private String name;
        private int age;
    }
    

    然后需要一个学生类:

    public class Student {
        private String name;
        private int age;
    }
    

    最后需要一个教室类:

    public class Classroom {
        private Teacher teacher;
        private List<Student> students;
    }
    

    为了能方便的创建学生,还需要一个工厂类:

    public class StudentFactory {
        public static Student getStudent(String name, int age) {
            return new Student(name, age);
        }
    
        public static List<Student> getStudents() {
            List<Student> students = new ArrayList<>();
            students.add(getStudent("男生甲", 16));
            students.add(getStudent("男生乙", 15));
            students.add(getStudent("女生甲", 15));
            return students;
        }
    }
    

    基于XML的配置

    使用最广泛和传统的方式就是XML文件配置了。Spring对于配置文件的名称没有固定要求。一个Spring 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">
    
    </beans>
    

    定义Bean

    Spring中Bean的意思就是我们需要进行依赖注入配置的类,比如JDBC连接、Hibernate的SessionFactory以及其它程序中会用到的类。要定义一个可以被注入到其他地方的Bean,只要在beans父节点下添加一个bean节点,指明要定义的Bean名称和类即可。

    <bean id="student"
          class="yitian.learn.ioc.Student"/>
    

    除了直接定义Bean之外,还可以由静态工厂方法生成Bean。这时候需要额外指定一个属性factory-method指明要使用的静态工厂方法。

    <bean id="someStudents"
          class="yitian.learn.ioc.StudentFactory"
          factory-method="getStudents"/>
    

    构造器注入

    构造器注入需要Bean有相应的构造器。构造器注入类似下面这样,定义一个bean节点,id属性指明该Bean的唯一标识符,class属性指定Bean对应的类名。然后在Bean节点中指明构造器的参数类型和值。对于基本类型和字符串等类型,直接将值写在双引号中即可。

    <bean id="maleStudent" class="yitian.learn.ioc.Student">
        <constructor-arg type="int" value="18"/>
        <constructor-arg type="java.lang.String" value="一个男生"/>
    </bean>
    

    上面是按照构造器参数类型来进行的依赖注入,如果构造器有相同的类型,上面的注入就无法进行了。这时候可以按照参数的索引来进行注入。

    <bean id="femaleStudent" class="yitian.learn.ioc.Student">
        <constructor-arg index="0" value="一个女生"/>
        <constructor-arg index="1" value="16"/>
    </bean>
    

    还可以使用c命名空间来简化构造器注入的编写。要使用c命名空间,需要在根节点beans上添加如下属性声明:

     xmlns:c="http://www.springframework.org/schema/c"
    

    然后构造器注入可以写为如下形式:

    <bean id="englishTeacher" class="yitian.learn.ioc.Teacher"
          c:name="英语老师"
          c:age="32"/>
    

    c命名空间也支持按照参数索引的方式注入,这时候的语法稍微有点奇怪,由于XML不支持数字开头的属性名,因此需要以下划线开头。

    <bean id="mathTeacer" class="yitian.learn.ioc.Teacher"
          c:_0="数学老师"
          c:_1="54"/>
    

    属性注入

    除了构造器注入之外,还有属性注入,也就是Setter注入。和构造器注入类似,只不过Bean里面使用property节点指定属性和值。

    <bean id="anotherMaleStudent" class="yitian.learn.ioc.Student">
        <property name="name" value="另一个男生"/>
        <property name="age" value="15"/>
    </bean>
    

    对于每一个属性都要编写一个property节点,稍嫌麻烦。Spring因此提供了p命名空间用来简化属性注入的编写。要使用p命名空间,需要在根节点beans上添加一行属性声明:

    xmlns:p="http://www.springframework.org/schema/p"
    

    使用p命名空间,Bean定义会简化不少。不过p命名空间也有缺点,p命名空间的类型需要在运行时动态读取,性能相比于上面的传统属性注入有所降低。所以如果对于性能要求很高,还是使用传统property节点最好。

    <bean id="anotherFemaleStudent" class="yitian.learn.ioc.Student"
          p:name="另一个女生"
          p:age="16"/>
    

    注入其他Bean

    上面几个例子都是在Bean中注入简单类型。当然,也可以在Bean中注入其他Bean,只需要使用ref属性指向已定义的一个Bean的id。

    <bean id="mathClassroom" class="yitian.learn.ioc.Classroom">
        <property name="teacher" ref="mathTeacer"/>
        <property name="students" ref="someStudents"/>
    </bean>
    

    前面几种注入方式自然也可以注入Bean,只需要将value属性改为ref属性即可。如果使用命名空间的话,就写为p:name-ref="someBean"这样的方式即可。

    使用集合

    在Bean中除了单个参数之外,还可以使用集合。支持的集合包括<list/><map/><set/>以及<props/>,对应于Java的集合ListMapSet以及Property

    <bean id="englishClassroom" class="yitian.learn.ioc.Classroom">
        <property name="teacher" ref="englishTeacher"/>
        <property name="students">
            <list>
                <ref bean="anotherFemaleStudent"/>
                <ref bean="anotherMaleStudent"/>
                <ref bean="femaleStudent"/>
                <ref bean="maleStudent"/>
            </list>
        </property>
    </bean>
    

    空值处理

    如果我们需要一个空字符串,XML可以写为如下这样:

    <bean class="ExampleBean">
        <property name="email" value=""/>
    </bean>
    

    如果我们需要一个空引用,可以写为如下这样:

    <bean class="ExampleBean">
        <property name="email">
            <null/>
        </property>
    </bean>
    

    延迟加载

    默认情况下,ApplicationContext会在初始化的时候创建和配置所有Bean。这样做的优点是如果Bean配置有错误,我们可以立即发现这些错误。不过有时候可能会需要延迟加载,将这些Bean的创建延迟到真正使用它的时候。要这么做很简单,只需要在bean上添加lazy-init="true"即可。

    <bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
    

    如果要让所有Bean都延迟加载,可以在配置文件的根节点beans上添加default-lazy-init="true"

    <beans default-lazy-init="true">
        <!-- no beans will be pre-instantiated... -->
    </beans>
    

    Bean作用域

    默认情况下Bean的作用域是单例,这就是说在整个应用程序中每次获得的Bean,完全就是同一个对象。这样一来,我们只要使用Spring依赖注入,就完全不需要实现单例模式了。Spring会帮我们把Bean设置成单例的。

    除了单例之外,还有一种常用的作用域——原型。原型作用域会在每次请求Bean的时候创建一个新对象。这种作用域用来定义有状态的Bean,比如用户会话。每次请求用户会话,都会返回一个新的会话,每个用户的会话因此不同。Spring IoC容器只负责创建和分配原型Bean,销毁工作需要由请求方进行。

    使用Bean

    前面说了这么多XML配置来定义Bean,下面来看看如何使用Bean。先来介绍一下ApplicationContext接口。

    Spring项目中,ApplicationContext可能是最重要的接口之一了。这个接口充当着应用程序环境的作用,我们在定义好了Bean之后,就可以通过ApplicationContext来获取相应的Bean。对于使用Java配置或者XML配置,以及其它环境例如Web应用程序等,都有相应的ApplicationContext。在真正使用Spring依赖注入的时候,我们一般情况下根本不需要关心ApplicationContext接口,它会由底层自动创建和使用。

    下面我们使用ApplicationContext来获取前面我们定义的Bean实例。这里用到了ClassPathXmlApplicationContext,一个接受类路径上的XML文件作为配置文件的ApplicationContext。要获取某个Bean,只需要调用ApplicationContext的getBean方法,这个方法接受Bean的id,以及Bean的类型。

    public void testXmlConfig() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        Student maleStudent = context.getBean("maleStudent", Student.class);
        System.out.println(maleStudent);
        Teacher mathTeacher = context.getBean("mathTeacher", Teacher.class);
        System.out.println(mathTeacher);
        Classroom mathClassroom = context.getBean("mathClassroom", Classroom.class);
        System.out.println(mathClassroom);
        Classroom englishClassroom = context.getBean("englishClassroom", Classroom.class);
        System.out.println(englishClassroom);
    }
    

    基于代码的配置

    Spring提供了多种方式来配置依赖注入,最常用和传统的方式就是使用XML文件进行配置。但是使用XML文件配置有一些弊端。如果看过一点Spring文档,就会发现几乎一半以上的篇幅都在介绍XML配置。为了灵活的实现各种依赖注入,Spring提供了一个强大的XML配置,但是这也同时使得XML配置变得复杂。所以现在基于代码的配置越来越流行,这种配置使用普通的Java方法和Spring提供的注解,让依赖注入配置变得非常方便。

    当然XML配置和代码配置相比,并不存在绝对的优劣问题。XML配置将配置和代码分离,让我们不需要重新编译就可以更改配置。而代码配置避免了繁复的XML节点编写,但是相应的配置类更难管理。具体使用哪种方式,还得取决于开发者的习惯和综合考虑。由于代码配置在XML配置之前,所以XML配置有可能会覆盖代码配置

    定义Bean

    定义Bean很简单,首先定义一个类,然后使用@Configuration注解,这样Spring就会将这个类识别为一个配置类,并从中寻找Bean定义。在一个@Configuration类中可以定义多个以@Bean注解的方法,在这些方法中我们可以通过普通的Java代码来初始化一个对象,然后返回这个对象。Spring用这些方法的名称作为返回的Bean的名称。当然还可以自定义Bean名称,这需要在@Bean注解中添加一个name参数,可以接受一组名称。还可以使用@Description注解为Bean添加一段描述信息。

    由于是Java代码来配置Bean,所以前面那些XML配置大部分都不需要了。我们只要像写普通代码那样来写这些Bean代码即可。如果需要引用其他Bean,使用注解将Bean注入到所在类即可。

    @Configuration
    public class SpringConfig {
        @Autowired
        private Teacher englishTeacher;
    
        @Autowired
        private List<Student> someStudents;
    
        @Bean(name = {"englishClassroom"})
        @Description("英语教室")
        public Classroom englishClassroom() {
            return new Classroom(englishTeacher, someStudents);
        }
        @Bean
        public List<Student> someStudents() {
            return StudentFactory.getStudents();
        }
        @Bean
        public Teacher englishTeacher() {
            return new Teacher("英语老师", 32);
        }
    }
    

    使用Bean

    我们同样使用ApplicationContext来获取Bean。这里使用AnnotationConfigApplicationContext,从一个标记了@Configuration的配置类中读取Bean对象。

    public void testJavaConfig() {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Classroom englishClassroom = context.getBean("englishClassroom", Classroom.class);
        System.out.println(englishClassroom);
    
    }
    

    使用注解获取Bean

    前面我们用了ApplicationContext来获取Bean。但是在实际应用中,完全不需要这么做。ApplicationContext是一个底层接口,Spring会将其封装起来。我们可以使用注解,透明地获取Bean。这也是我们以后使用Spring依赖注入的主要方式。最常用的注解是Autowired。

    Autowired注解

    定义好Bean之后,我们就可以通过使用自动装配来使用Bean了。Spring框架扫描到标记了Autowired注解的字段、构造器、Setter方法之后,就会从Bean定义中搜索对应的Bean来进行注入。Autowired首先会按照类型进行查找,如果发现同类型的多个Bean,就会按照名称进行匹配。如果既没有同类型的Bean也没有相同名称的Bean,Spring就会抛出异常。

    @ContextConfiguration(classes = {SpringConfig.class})
    @RunWith(SpringRunner.class)
    
    public class IoCTest {
    
        @Autowired
        private Classroom englishClassroom;
        @Test
        public void testTestContext() {
            assertNotNull(englishClassroom);
        }
    }
    

    @Autowired可以应用在字段、Setter方法和构造器上。Spring官方建议我们将其应用在Setter方法和构造器上,最好不要直接注入到字段中,除非是在单元测试这种情况下。

    Primary注解和Qualifier注解

    在自动装配中,如果想要同类型的某个Bean优先被使用,可以向其添加Primary注解。这样Spring在自动装配的时候,就会优先使用Primary注解的那个Bean,即使这个Bean名称不匹配。

    如果还想要具体的控制到底使用哪个Bean,还可以使用Qualifier注解。在定义Bean的时候,使用Qualifier注解为同类型的Bean提供一个名称:

    @Configuration
    public class BeanConfiguration {
        @Bean
        @Qualifier("mathTeacher")
        public Teacher mathTeacher() {
            return new Teacher("数学老师", 46);
        }
    
    }
    

    然后再使用Bean的时候,也用Qualifier注解,就可以指定注入哪个Bean了。

    @ContextConfiguration(classes = {SpringConfig.class})
    @RunWith(SpringRunner.class)
    public class AnnotationTest {
        @Autowired
        //如果同类型存在多个Bean,优先使用@Primary
        private Teacher teacher;
    
        @Autowired
        @Qualifier("mathTeacher")
        //注入相同Qualifier的Bean
        private Teacher qualifierTeacher;
    
        @Test
        public void testQualifier() {
            assertEquals("数学老师", qualifierTeacher.getName());
        }
    }
    

    例子

    说了这么多,我们来看一个Hibernate的例子。我们将Hibernate的SessionFactory注册成Spring Bean,这样就不需要我们使用单例模式或者静态类什么的了。这个例子很简单。首先我们添加一个Hibernate配置文件hibernate.cfg.xml

    <?xml version='1.0' encoding='utf-8'?>
    <!DOCTYPE hibernate-configuration PUBLIC
            "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
            "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    <hibernate-configuration>
    
        <session-factory>
            <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
            <property name="connection.url">jdbc:mysql://localhost:3306/test</property>
            <property name="connection.username">root</property>
            <property name="connection.password">12345678</property>
    
            <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
            <property name="show_sql">true</property>
            <property name="hbm2ddl.auto">create</property>
    
        </session-factory>
    
    </hibernate-configuration>
    

    然后,我们将SessionFactory注册为一个Spring Bean。

    @Configuration
    public class HibernateConfig {
        @Bean
        public SessionFactory sessionFactory() {
            final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
                    .configure()
                    .build();
            try {
                SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory();
                return sessionFactory;
            } catch (Exception e) {
                StandardServiceRegistryBuilder.destroy(registry);
                throw new RuntimeException(e);
            }
        }
    }
    

    最后,放到单元测试中来测一下。如果配置文件路径和配置都正确的话,就可以正确通过。这样,我们就将Hibernate的SessionFactory注册为了一个Spring Bean了。以后在项目中可以使用依赖注入直接注入SessionFactory了。

    @ContextConfiguration(classes = {HibernateConfig.class})
    @RunWith(SpringRunner.class)
    public class HibernateTest {
        @Autowired
        private SessionFactory sessionFactory;
    
        @Test
        public void testSessionFactory() {
            assertNotNull(sessionFactory);
        }
    }
    

    参考资料

    http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans

    项目代码我托管在Csdn Code上了,有兴趣的同学可以看看。

    相关文章

      网友评论

        本文标题:Spring 依赖注入

        本文链接:https://www.haomeiwen.com/subject/qfzebttx.html