Spring 依赖注入

依赖注入简介

依赖注入是一个很常用的词。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上了,有兴趣的同学可以看看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容