(一)Spring的核心

Spring之旅

  • Spring是一个轻量级的开源Java框架

  • Spring的优势就是分层架构

  • Spring的核心就是控制反转(IOC)和面向切面(AOP)

  • JavaEE开发分为三层结构:

    • Web层 –>SpringMVC

    • 业务层 –>Bean管理(IOC)

    • 持久层 –>Spring的JDBC模板、ORM模板用于整合其他持久层框架

首先我们要引入Spring提供的jar包:

Spring核心之装配Bean

Spring通过装配Bean对象来完成各个应用之间的协同合作,这也是依赖注入的本质。
而依赖注入即是我们前文提到的IOC控制反转的思想–>通过将应用对象装配进Spring Bean中,即由Spring管理对象的依赖关系。

创建Spring的配置

Spring是一个基于容器的框架,我们需要通过配置告诉Spring去加载哪些Bean和如果装配这些Bean。

  • 配置Spring的方式有两种:
    1. 在XML文件中声明Bean
    2. 通过注解配置Spring

首先以下是一个基本的Spring XML配置:

1
2
3
4
5
6
<?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>

<beans>元素内,可以配置所有的Spring的配置信息。而beans不是唯一的Spring命名空间,Spring常用的命名空间有:
* aop: 为声明切面以及将@AspectJ注解的类代理为Spring切面提供了配置元素
* beans: 支持声明bean和装配bean,是Spring最基本的命名空间
* context: 为配置Spring应用上下文气功配置元素,包括自动检测和自动装配Bean、注入非Spring直接管理的对象
* jms: 为声明小气驱动的POJO提供配置元素
* mvc: 启用SpringMVC,例如面向注解的控制器、视图解析器和拦截器
* tx:提供声明式事务配置

声明一个简单的Bean

  1. 创建一个User.java接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package demo1;
    public class User {
    private int age;
    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    }
  2. 创建Spring的配置文件spring.xml ,并将User.java交给Spring管理

1
2
3
4
5
6
7
8
<?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="user" class="demo1.User"/>
</beans>

结果:

  1. 解释:

    分析以上案例:我们首先创建一个JavaBean对象User.java,然后将这个JavaBean对象交给Spring管理—>即在Spring配置文件中注入。那么,Spring是怎么实例化这个名字叫user的Bean的呢?

  2. 改进上述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo1;
public class User {

public User(){
System.out.println("这是无参构造...");
}
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

​ 即我们手动创建一个无参构造函数,结果如下:

解释:

​ 可以看到,当我们手动创建一个无参构造函数时,定义在无参构造函数中的语句就会打印出来。原因是Spring实例化Bean采用了User user = new User()的方式,并且会使用默认的无参构造函数进行实例化,而在Java中无参构造函数会被系统自动创建(在没有其他构造函数的情况下)。

通过构造器注入

通过以上案例,我们发现,Spring默认会使用JavaBean的无参构造函数进行注入,其实Spring还提供了一种注入方式:构造器注入

改变Spring配置文件spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">

<!-- 注入user(默认使用无参构造) -->
<bean id="user" class="demo1.User"/>

<!-- 使用Spring的构造方法 -->
<bean id="user2" class="demo1.User">
<constructor-arg value="12"/>
</bean>
</beans>

如上所示:在<bean>中使用<constructor-arg>元素来告诉Spring额外的信息,但是我们直接这样写是会报错的:

通过IDEA的报错提示我们发现解决办法是要在User.java中创建一个带参构造函数,所以我们创建如下带参构造函数:

为了更直观的展示,我们改进上述代码:

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo1;
public class User {
public User() {
System.out.println("这是无参构造...");
}

public User(int age) {
this.age = age;
System.out.println("打印age的值:" + age);
System.out.println("这是一个带参构造函数");
}

private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">

<!-- 注入user(默认使用无参构造) -->
<bean id="user" class="demo1.User"/>

<!-- 使用Spring的构造方法 -->
<bean id="user2" class="demo1.User">
<constructor-arg value="12"/>
</bean>
</beans>

打印结果:

综上

我们发现Spring会通过JavaBean的默认无参构造来实例化对象,并提供一种特殊的构造方法:构造器来实例化Bean对象(但其实是使用了Bean对象的带参构造函数),构造器的特点就是按需实例化Spring支持实例化对象时提供额外的信息来覆盖本身定义的数据。

构造器另一种用法—>注入对象引用

spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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">
<!-- 注入user(默认使用无参构造) -->
<bean id="user" class="demo1.User"/>

<!-- 使用Spring的构造方法 -->
<bean id="user2" class="demo1.User">
<constructor-arg value="12"/>
</bean>

<bean id="user3" class="demo1.User">
<constructor-arg value="joke"/>
<constructor-arg ref="user2"/>
</bean>
</beans>

User.java添加:

1
2
3
public User(String name, User user){
System.out.println("打印name的值和age的值:" + name + "," + user.getAge());
}

打印结果:

总结

同上上面的构造器注入方式我们发现,使用构造器注入,其实是使用JavaBean中的构造方法,而我们在构造器<constructor-arg>中注入什么值,那么在Bean对象带参构造函数中的参数列表中就应该用什么值接收,比如我们,传入的是一个引用对象,那么就应该用对象最后接收参数。

通过工厂方法创建Bean

一般来说,单例类的实例只能通过静态工厂方法来创建。Spring支持通过<bean>元素的factory-method属性来装配工厂创建的Bean。

以下是一个典型的单例类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package demo1;
public class Case {
private Case() {
}

//延迟加载实例
private static class CaseSingleHolder{
static Case instance = new Case();
}

//返回实例
public static Case getInstance(){
return CaseSingleHolder.instance;
}
}

以上使用了单例模式中一种懒加载单例模式,即在调用的时候才创建实例(故称为懒加载)。

以上懒加载特点就是: 1.构造函数私有 2. 方法静态

Java中单例模式具有以下特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。 —> 加载实例
  • 单例类必须给所有其他对象提供这一实例。 —> 返回实例对象

spring.xml中添加如下:

1
<bean id="case" class="demo1.Case" factory-method="getInstance"/>

注:这样应用的场景是我们不想在加载spring.xml时就实例化Bean,而是调用factory-method所指定的方法时,才开始真正的实例化Bean。

使用要静态工厂创建Bean要注意:这里的class属性并不是指定Bean实例的实现类,而是静态工厂类。因为Spring需要知道是用哪个工厂承诺噶来创建Bean的实例。其次factory-method指定的是静态工厂方法名(必须是静态的)。

如下:

运行结果(注意:此时我们测试的是run2()方法):

发现:此时我们没有调用run1()方法,但是两次调用run2()方法都已经实例化了名字是user的Bean,但是,没有调用getBean()实例化名字是case的Bean,Spring就不会实例化该Bean。这样正印证了<factory-method>的特点:只有在调用<factory-method>指定的方法时才开始实例化Bean,而不是加载spring.xml时就实例化。

Bean的作用域

所有的Spring默认都是单例,当容器实例化一个Bean时,无论是通过装配(<constructor-arg>),还是通过getBean() (调用默认的无参构造),都会返回Bean的同一个实例。那么如何覆盖Spring的默认单例配置呢?

通过将scope="property"即可,如下:

1
<bean id="user" class="demo1.User" scope="prototype"/>

首先我们仍用上面的例子,但让其实例化两次Bean对象,如下:

spring.xml

1
<bean id="user" class="demo1.User"/>

Test测试类

1
2
3
4
5
6
7
8
9
@Test
public void run() {
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
User user = (User) ac.getBean("user");
User user2 = (User) ac.getBean("user");
System.out.println(user);
System.out.println(user2);
}

打印结果发现:

两次打印的结果都相同,那么就证实了Spring默认实例化Bean都是采用的单例模式。阻止了默认单例配置后的效果如下:

spring.xml

1
<bean id="user" class="demo1.User" scope="prototype"/>

Test测试类

1
2
3
4
5
6
7
8
9
@Test
public void run() {
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
User user = (User) ac.getBean("user");
User user2 = (User) ac.getBean("user");
System.out.println(user);
System.out.println(user2);
}

打印结果:

明显发现两次打印的地址值不同。

除了以上prototype作用域,Spring Bean还有其他几类作用域:

  • singleton: 在每一个Spring容器中,一个Bean定义只有一个对象实例(默认)。
    • prototype: 允许Bean的定义可以被实例化任意次(每次调用都创建一个实例)。
    • request: 在一次HTTP请求中,每个Bean定义对应一个实例。该作用域尽在基于Web的Spring上下文中有效。
    • session: 在一次HTTP Session请求中,每个Bean定义对应一个实例。该作用域仅在Portlet上下文中有效。

初始化和销毁Bean

Spring提供了Bean声明周期的钩子方法,用来在Bean初始化和销毁前执行。

  • 初始化:init-method —>初始化后调用
    • 销毁: destory-method —>销毁前调用

举例:

spring.xml

1
2
3
4
5
6
7
8
9
<?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">

<!-- 注入user(默认使用无参构造) -->
<bean id="user" class="demo1.User" init-method="turnOn" destroy-method="turnOff"/>
</beans>

User.java

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
package demo1;
public class User {
public User() {
}
public void turnOn(){
System.out.println("初始化Bean...");
}
public void turnOff(){
System.out.println("销毁Bean...");
}

private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Test测试类

1
2
3
4
5
6
7
8
9
@Test
public void run() {
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
User user = (User) ac.getBean("user");
System.out.println(user);
//手动销毁Bean
((ClassPathXmlApplicationContext) ac).close();
}

输出结果:

拓展: 上面的案例都是基于Bean是单例模式的基础上,那么我们使用多例模式的情况会是怎样呢?

首先修改spring.xml

1
<bean id="user" class="demo1.User" scope="prototype" init-method="turnOn" destroy-method="turnOff"/>

然后我们观察运行结果:

源码分析

Spring已经调用了销毁Bean的方法,但是此时并没有执行我们定义的销毁方法turnOff(),这是为什么呢?

那么我们看一下Spring中scope属性的源码:

可以看到上面注解的含义大概就是说这个destory-method方法只是对于Spring默认的单例(singletons)而言的,而当我们定义为多例模式(prototype)时,此时该Bean的声明周期(lifycycle)将不再进行此方法。也就是说:当我们将Bean定义为多例模式时,当此Bean被实例化之后,Spring的IOC容器将不再对此Bean的声明周期进行管理了,也就不会再执行销毁方法。此时Spring对该Bean的管理仅是在执行new对象的操作。

2.3.1 默认的init-method和destory-method

如果Spring的上下文中出现较多的Bean需要同一个初始化、销毁方法。那么我们可以定义一个全局的方法:

<beans>中定义:

default-init-method="turnOn"

default-destory-method="turnOff"

注入Bean属性

通常JavaBean的属性是私有的,同时拥有一组存取器方法,以setXxx()getXxx()形式存在,而Spring就可以借助setXxx()方法里配置属性的值,以实现setter方法注入,请看下面。

spring.xml

1
2
3
4
5
6
7
8
9
10
11
<?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">

<!-- 注入user(默认使用无参构造) -->
<bean id="user" class="demo1.User">
<property name="age" value="18"/>
</bean>
</beans>

User.java

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
package demo1;
public class User {

public User() {
}
public User(int age) {
this.age = age;
System.out.println("打印age的值:" + age);
}
public User(String name, User user){
System.out.println("打印name的值和age的值:" + name + "," + user.getAge());
}

private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
System.out.println("执行setAge()...");
this.age = age;
}

private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Test测试方法

1
2
3
4
5
6
7
@Test
public void run() {
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
User user = (User) ac.getBean("user");
System.out.println(user.getAge());
}

打印结果

可以看到这里打印了age的值,且是通过setAge()方法进行设置值。

注意:Spring是通过setter方法注入简单值的,而且这值的类型并不做区分,即你注入int类型值和注入String类型值是一样的,Spring会根据setter方法将你注入的值类型转换成指定的数据类型。

区分:Spring的<poperty><constuctor-arg>元素在很多地方是相似的。只不过前者是通过setter方法注入值,厚泽是通过构造器注入值的。

引用其他Bean

Spring的<constructor-arg>可以注入对象引用,<property>同样支持:

创建Child.java对象

1
2
3
4
5
6
7
8
9
10
package demo1;
public class Child {
private User user;
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}

spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="user" class="demo1.User">
<property name="age" value="18"/>
</bean>

<bean id="child" class="demo1.Child">
<property name="user" ref="user"/>
</bean>
</beans>

Test测试类

1
2
3
4
5
6
7
@Test
public void run3(){
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Child child = (Child) ac.getBean("child");
System.out.println(child.getUser().getAge());
}

打印结果:

注入内部Bean

改进上述代码:

spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="user" class="demo1.User">
<property name="age" value="18"/>
</bean>
<bean id="child" class="demo1.Child">
<property name="user">
<bean class="demo1.User"/>
</property>
</bean>
</beans>

可以看到我们在<property>元素内右嵌套了一个<bean>,这中技术称为注入内部Bean,我们观察打印结果:

我们发现,这里却没有打印我们名字是user的Bean中注入的数据,这就体现注入内部Bean的特点就是被注入的Bean对象中的数据不会被影响。这也体现了内部Bean的最大一个缺点就是:不能被复用。只适合一次注入,而且不能被其他Bean所引用(即我们给内部Bean配置id属性是毫无意义的)。

3.3 使用Spring的p命名空间

Spring提供了一个命名空间p用来作为<bean>元素所有属性的前缀来装配Bean的属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- <bean id="user" class="demo1.User">
<property name="age" value="18"/>
</bean>-->
<bean id="user" class="demo1.User" p:age="19"/>
</beans>

装配集合

以上我们仅了解了Spring配置简单的属性值(使用value或ref属性),但valueref都只是在配置单个值的情况下可以,那么对于集合类型,该怎么处理呢?

Spring提供了一些相应的集合配置元素:

  • : 装配list类型的值,允许重复
    • : 装配set类型的值,不允许重复
    • : 装配map类型的值,名称和值可以是任意类型的
    • : 装配properties类型的值,名称和值必须都是String类型

当装配类型是java.util.Collection任意实现的属性时,<list><set>几乎可以互用,所以装配的类型和选择的元素没有任何关系。<map><props>这两个元素分别对应java.util.Mapjava.util.Properties。当我们需要由键-值组成的集合时,常用这两个元素。

举例:我们改变Child.java的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
package demo1;
import java.util.Map;
public class Child {
private Map<String, String> childMap;
public void setChildMap(Map<String, String> childMap) {
this.childMap = childMap;
}
public Map<String, String> getChildMap() {
return childMap;
}
}

spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="child" class="demo1.Child">
<property name="childMap">
<map>
<entry key="1" value="TyCoding"/>
<entry key="2" value="涂陌"/>
</map>
</property>
</bean>
</beans>

Test测试类

1
2
3
4
5
6
7
8
9
@Test
public void run3(){
//加载Spring上下文
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Child child = (Child) ac.getBean("child");
for (Object obj : child.getChildMap().values()){
System.out.println(obj);
}
}

打印结果:

SpEL表达式

Spring的SpEL表达式具有以下特性:

  • 使用Bean的ID来引用Bean;
    • 调用方法和访问对象的属性;
    • 对值进行算术、关系和逻辑运算;
    • 集合操作;

语法:

1. 用 `#{}`标记的内容是SpEL表达式
   2. 代替`ref`将一个bean装配到另一个bean中:`#{bean}`即可
   3. 通过bean的引用来获取bean的属性:`#{bean.value}`
   4. ...

总结之—Bean的生命周期

首先我们从源码开始分析:

以上是Spring Bean从初始化到销毁所经历的方法,那么下面我们来画一个具体的流程图:

综上:再强调几点:

  1. Spring实例化一个Bean,通常就是我们所说的new操作。

  2. Spring上下文对实例化Bean进行配置,也就是IOC注入。

  3. 对于多例模式而言,在Spring对该Bean进行了初始化之后,就不会再对此Bean的后续生命周期管理,从上面的destory-method方法可以验证得到。

  4. 容器是Spring的核心,但是并不存在单一的容器。Spring自带几种容器实现,可以归纳为两种不同的类型。

    • Bean工厂:org.springframework.beans.factory.BeanFactory接口定义,是最简单的容器。
    • 应用上下文:org.springframework.context.ApplicationContext接口定义,基于BeanFactory之上构建,并提供面向应用的服务。
    1. 以上流程并不是每个Spring Bean一定都会经历的,只有实现了对应的接口,才会实现对应的功能。

交流

如果大家有兴趣,欢迎大家加入我的Java交流群:671017003 ,一起交流学习Java技术。博主目前一直在自学JAVA中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法,当然群里的大佬都会积极给新手答疑的。所以,别犹豫,快来加入我们吧!


联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果你觉得这篇文章帮助到了你,你可以帮作者买一杯果汁表示鼓励