7.3.1 理解依赖注入

虽然Spring并不是依赖注入的首创者,但Rod Johnson是第一个高度重视以配置文件来管理Java实例的协作关系的人,他给这种方式起了一个名字:控制反转( Inversion of control,loC)。在后来的日子里, Martine fowler为这种方式起了另一个名称:依赖注入( Dependency Injection)
因此,不管是依赖注入,还是控制反转,其含义完全相同。当某个Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常有如下两种做法。

  • 原始做法:调用者主动创建被依赖对象,然后再调用被依赖对象的方法。
  • 简单工厂模式:调用者先找到被依赖对象的工厂,然后主动通过工厂去获取被依赖对象,最后再调用被依赖对象的方法。

对于第一种方式,由于调用者需要通过形如"new"关键字调用被依赖对象的构造器来创建对象,因此必然导致调用者与被依赖对象实现类的硬编码耦合,非常不利于项目升级的维护。
对于简单工厂的方式,大致需要把握三点:

  1. 调用者面向被依赖对象的接口编程;
  2. 将被依赖对象的创建交给工厂完成;
  3. 调用者通过工厂来获得被依赖组件。

通过这三点改造,可以保证调用者只需与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合。这种方式唯一的缺点是,调用组件需要主动通过工厂去获取被依赖对象,这就会带来调用组件与被依赖对象工厂的耦合。
使用Spring框架之后,调用者无须主动获取被依赖对象,调用者只要被动接受Spring容器为调用者的成员变量赋值即可(只要配置一个<property>子元素, Spring就会执行对应的setter方法为调用者的成员变量赋值)。由此可见,使用Spring框架之后,调用者获取被依赖对象的方式由原来的主动获取,变成了被动接受—于是Rod Johnson将这种方式称为控制反转
正因为Spring将被依赖对象注入给了调用者,所以调用者无须主动获取被依赖对象,只要被动等待Spring容器注入即可。由此可见,控制反转和依赖注入其实是同一个行为的两种表达,只是描述的角度不同而已

举例说明

为了更好地理解依赖注入,可以参考人类社会的发展来看以下问题在各种社会形态里如何解决:一个人(Java实例,调用者)需要一把斧头(Java实例,被依赖对象)。

  1. 在原始社会里,几乎没有社会分工。需要斧头的人(调用者)只能自己去磨一把斧头(被依赖对象)。对应的情形为:Java程序里的调用者自己创建被依赖对象,通常采用new关键字调用构造器创建一个被依赖对。
  2. 进入工业社会,工厂出现了,斧头不再由普通人完成,而在工厂里被生产出来,此时需要斧头的人(调用者)找到工厂,购买斧头,无须关心斧头的制造过程。对应简单工厂设计模式,调用者只需要定位工厂,无须理会被依赖对象的具体实现过程。
  3. 进入”共产主义”社会,需要斧头的人甚至无须定位工厂,“坐等”社会提供即可。调用者无须关心被依赖对象的实现,无须理会工厂,等待Spring依赖注入。

三种情况的对比

在第一种情况下,Java实例的调用者创建被调用的Java实例,调用者直接使用new关键字创建被依赖对象,程序高度耦合,效率低下。真实应用极少使用这种方式。
这种模式有如下两个坏处:

  1. 可扩展性差。由于”人”组件与”斧头”组件的实现类高度耦合,当程序试图扩展斧头组件时,”人”组件的代码也要随之改变。
  2. 各组件职责不清。对于”人”组件而言,它只需要调用”斧头”组件的方法即可,并不关心”斧头”组件的创建过程。但在这种模式下,”人”组件却需要主动创建”斧头”组件,因此职责混乱。

在第二种情况下,调用者无须关心被依赖对象的具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被依赖对象的实现类解耦,这也是工厂模式大量使用的原因。但调用者依然需要主动定位工厂,调用者与工厂耦合在一起。
第三种情况是最理想的情况:程序完全无须理会被依赖对象的实现,也无须主动定位工厂,这是种优秀的解耦方式。实例之间的依赖关系由IoC容器负责管理
图7.9显示了依赖注入的示意图
这里有一张图片
由此可见,使用Spring框架之后的两个主要改变是:

  1. 程序无须使用new调用构造器去创建对象。所有的Java对象都可交给Spring容器去创建。
  2. 当调用者需要调用被依赖对象的方法时,调用者无须主动获取被依赖对象,只要等待Spring容器注入即可。
  3. 设值注入:IoC容器使用成员变量的setter方法来注入被依赖对象
  4. 构造注入:IoC容器使用构造器来注入被依赖对象。

7.3 Spring的核心机制 依赖注入

正如在前面代码中所看到的,程序代码并没有主动为Person对象的axe成员变量设置值,但执行Person对象的usxAxe()方法时, useAxe()方法完全可以正常访问到Axe对象,并可以调用Axe对象的chop()方法。
由此可见, Person对象的axe成员变量并不是程序主动设置的,而是由Spring容器负责设置的:

  • 开发者主要为axe成员变量提供一个setter方法,
  • 并通过<property>元素驱动Spring容器调用该setter方法为Person对象的axe成员变量设置值。

什么是依赖

纵观所有的Java应用(从基于Applet的小应用到多层结构的企业级应用),这些应用中大量存在A对象需要调用B对象方法的情形,这种情形被Spring称为依赖,即A对象依赖B对象。对于Java应用而言,它们总是由一些互相调用的对象构成的, Spring把这种互相调用的关系称为依赖关系。假如A组件调用了B组件的方法,即可称A组件依赖B组件
Spring框架的核心功能有两个。

  1. Spring容器作为超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean
  2. Spring容器管理容器中Bean之间的依赖关系, Spring使用一种被称为”依赖注入“的方式来管理Bean之间的依赖关系。

使用依赖注入,不仅可以为Bean注入普通的属性值,还可以注入其他Bean的引用。通过这种依赖注入, Java EE应用中的各种组件不需要以硬编码方式耦合在一起,甚至无须使用工厂模式。当某个Java实例需要其他Java实例时,系统自动提供所需要的实例,无须程序显式获取。

依赖注入是一种优秀的解耦方式。依赖注入让SpringBean以配置文件组织在一起,而不是以硬编码的方式耦合在一起。

7.2.3 在Eclipse中使用Spring

下面以开发一个简单的Java应用为例,介绍在Eclipse工具中开发Spring应用。

  1. 启动Eclipse工具,通过单击Eclipse工具栏上的新建图标,选择新建一个Java项目,为该项目命名为MySpring
  2. 单击"Next"按钮,再单击随后出现的对话框中的"Finish"按钮,建立项目成功一旦新建项目成功,即可在Eclipse的左边见到项目的结构树。
  3. 接下来应该为该项目增加Spring支持。右键单击"MySpring"节点,在出现的快捷菜单中单击" Build Path"->" Add Libraries"菜单项。
  4. 出现Add Library,选择"User Library"项,表明此处打算添加用户库.
  5. 单击"Next"按钮,进入选择并添加用户库对话框.
  6. 在弹出的Preferences(Filtered)对话框中,选择new按钮,则表示要创建一个用户库,在弹出的New User Library对话框中,写上用户库的名称:Spring5.0
  7. 然后选中新建的Spring5.0这个用户库,接着点击右边的Add External JARs,进入下载好的Spring目录中的libs目录.选择所有以.RELEASE.jar结尾的jar包导入即可.

刚才所添加的Eclipse用户库只能保证在Eclipse下编译、运行该程序可找到相应的类库;如果需要发布该应用,则还要将刚刚添加的用户库所引用的JAR文件随应用一起发布。对于一个Web应用,由于Eclipse部署Web应用时不会将用户库的JAR文件复制到web应用的WEB-INF/lib路径下,所以还需要主动将用户库所引用的JAR文件复制到Web应用的WEB-INF/lib路径下。

7.2.2 使用Spring管理Bean

Spring核心容器的理论很简单:Spring核心容器就是一个超级大工厂,所有的对象(包括数据源、HibernateSession Factory等基础性资源)都会被当成Spring核心容器管理的对象—— Spring容器中的一切对象统称为Bean
Spring容器中的Bean,与以前听过的Java Bean是不同的。不像Java Bean,必须遵守一些特定的规范,而SpringBean没有任何要求。只要是一个Java类, Spring就可以管理该Java类,并将它当成Bean处理
对于Spring框架而言,一切Java对象都是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
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\springQs
├─lib\
│ ├─spring-aop-5.0.1.RELEASE.jar
│ ├─spring-aspects-5.0.1.RELEASE.jar
│ ├─spring-beans-5.0.1.RELEASE.jar
│ ├─spring-context-5.0.1.RELEASE.jar
│ ├─spring-context-indexer-5.0.1.RELEASE.jar
│ ├─spring-context-support-5.0.1.RELEASE.jar
│ ├─spring-core-5.0.1.RELEASE.jar
│ ├─spring-expression-5.0.1.RELEASE.jar
│ ├─spring-instrument-5.0.1.RELEASE.jar
│ ├─spring-jcl-5.0.1.RELEASE.jar
│ ├─spring-jdbc-5.0.1.RELEASE.jar
│ ├─spring-jms-5.0.1.RELEASE.jar
│ ├─spring-messaging-5.0.1.RELEASE.jar
│ ├─spring-orm-5.0.1.RELEASE.jar
│ ├─spring-oxm-5.0.1.RELEASE.jar
│ ├─spring-test-5.0.1.RELEASE.jar
│ ├─spring-tx-5.0.1.RELEASE.jar
│ ├─spring-web-5.0.1.RELEASE.jar
│ ├─spring-webflux-5.0.1.RELEASE.jar
│ ├─spring-webmvc-5.0.1.RELEASE.jar
│ └─spring-websocket-5.0.1.RELEASE.jar
└─src\
├─beans.xml
├─lee\
│ └─BeanTest.java
└─org\
└─crazyit\
└─app\
└─service\
├─Axe.java
└─Person.java

Axe.java

下面程序先定义一个简单的类。

1
2
3
4
5
6
7
8
package org.crazyit.app.service;
public class Axe
{
public String chop()
{
return "使用斧头砍柴";
}
}

从上面代码可以看出,该Axe类只是一个最普通的Java类,简单到令人难以置信——但这也是前面所介绍的, SpringBean类没有任何要求,只要它是个Java类即可

什么是依赖

下面再定义一个简单的Person类,该Person类的useAxe()方法需要调用Axe对象的chop()方法,**这种A对象需要调用B对象方法的情形,被称为依赖**。下面是依赖Axe对象的Person类的源代码。

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.crazyit.app.service;

public class Person
{
private Axe axe;
// 设值注入所需的setter方法
public void setAxe(Axe axe)
{
this.axe = axe;
}

public void useAxe()
{
System.out.println("我打算去砍点柴火!");
// 调用axe的chop()方法,
// 表明Person对象依赖于axe对象
System.out.println(axe.chop());
}
}

使用Spring框架之后, Spring核心容器是整个应用中的超级大工厂,所有的Java对象都交给Spring容器管理,这些由Spring容器管理的Java对象被统称为Spring容器中的Bean.

Spring如何管理Bean

现在的问题是:Spring容器怎么知道管理哪些Bean呢?答案是XML配置文件(也可用注解,后面会介绍), **Spring使用XML配置文件来管理容器中的Bean**。因此,接下来为该项目增加XML配置文件, SpringXML配置文件的文件名没有任何要求,读者可以随意指定。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="GBK"?>
<!-- Spring配置文件的根元素,使用spring-beans.xsd语义约束 -->
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置名为person的Bean,其实现类是org.crazyit.app.service.Person类 -->
<bean id="person" class="org.crazyit.app.service.Person">
<!-- 控制调用setAxe()方法,将容器中axe Bean作为传入参数 -->
<property name="axe" ref="axe" />
</bean>
<!-- 配置名为axe的Bean,其实现类是org.crazyit.app.service.Axe类 -->
<bean id="axe" class="org.crazyit.app.service.Axe" />
<!-- 配置名为win的Bean,其实现类是javax.swing.JFrame类 -->
<bean id="win" class="javax.swing.JFrame" />
<!-- 配置名为date的Bean,其实现类是java.util.Date类 -->
<bean id="date" class="java.util.Date" />
</beans>

上面的配置文件很简单,该配置文件的根元素是<beans>,根元素主要就是包括多个<bean>元素,每个<bean>元素定义一个Bean。上面配置文件中一共定义了4个Bean,其中前两个Bean是本示例提供的AxePerson类;而后两个Bean则直接使用了JDK提供的java.util.Datejavax.swing.JFrame.
再次强调:Spring可以把”一切Java对象”当成容器中的Bean,因此不管该Java类是JDK提的,还是第三方框架提供的,抑或是开发者自己实现的类都可以使用Spring管理。只要是个Java类,并将它配置在XML配置文件中, Spring容器就可以管理它

元素说明

实际上,配置文件中的<bean>元素默认以反射方式来调用该类无参数的构造器,以如下元素为例:

1
2
3
4
<bean id="person" class="org.crazyit.app.service.Person">
<!-- 控制调用setAxe()方法,将容器中axe Bean作为传入参数 -->
<property name="axe" ref="axe" />
</bean>

Spring框架解析该<bean>元素后将可以得到两个字符串:

  • 解析<bean>元素的id属性得到的idStr的值为"person"
  • 解析<bean>元素的class属性得到classStr的值为"org.crazyit.app.service Person"

也就是说, Spring底层会执行形如以下格式的代码

1
2
3
4
5
6
String idStr=...;//解析`<bean>`元素的`id`属性得到的`idStr`的值为`"person"`
Sting classStr=...;//解析`<bean>`元素的`class`属性得到`classStr`的值为`"org.crazyit.app.service Person"`
Class clazz=Class.forName(classStr);
Object object=clazz.newInstance();
//container代表Spring容器
container.put(idStr,object);

上面代码就是最基本的反射代码(实际上Spring底层代码会更完善一些), Spring框架通过反射根据<bean>元素的class属性指定的类名创建了一个Java对象,并以<bean>元素的id属性的值为key,将该对象放入Spring容器中—这个Java对象就成为了Spring容器中的Bean.
每个<bean>元素默认驱动Spring调用该类无参数的构造器来创建实例,并将该实例作为Spring容器中的Bean

通过上面的反射代码还可以得到一个结论:在Spring配置文件中配置Bean时,class属性的值必须是Bean实现类的完整类名(必须带包名),不能是接口,不能是抽象类(除非有特殊配置),否则Spring无法使用反射创建该类的实例.

元素说明

上面配置文件中还包括一个<property>子元素,<property>子元素通常用于作为<bean>元素的子元素,它驱动Spring在底层以反射执行一次setter方法。其中:

  • <property>name属性值决定执行哪setter方法,
  • <property>valueref属性决定执行setter方法的传入参数:
    • 如果传入参数是基本类型及其包装类String等类型,则使用value属性指定传入参数
    • 如果以容器中其他Bean作为传入参数,则使用ref属性指定传入参数。

Spring框架只要看到<property>子元素, Spring框架就会在底层以反射方式执行一次setter方法。何时执行这个setter方法呢?该Bean一旦创建处理, Spring会立即根据<property>子元素来执行setter方法。也就是说,<bean>元素驱动Spring调用构造器创建对象; <property>子元素驱动Spring执行setter方法,这两步是先后执行的,中间几乎没有任何间隔。

以上面配置文件中的如下配置为例:

1
2
3
4
<bean id="person" class="org.crazyit.app.service.Person">
<!-- 控制调用setAxe()方法,将容器中axe Bean作为传入参数 -->
<property name="axe" ref="axe" />
</bean>

上面配置中<property>元素的

  • name属性值为axe,该元素将驱动Spring以反射方式执行person这个BeansetAxe()方法;
  • ref属性值为axe,该属性值指定以容器中名为axeBean作为执行setter方法的传入参数.

也就是说, Spring底层会执行形如以下格式的代码

1
2
3
4
5
6
7
8
9
10
11
12
//解析`<property>`元素的name属性得到该字符串的值为"axe"
String nameStr=...;
//解析`<property>`元素的ref属性得到该字符串的值为"axe"
String refStr=...;
// 生成要调用的setter方法名
String setterName="set"+nameStr.substring(0,1).toUpperCase()+nameStr.substring(1);
// 获取Spring容器中名为refStr的Bean,该Bean将会作为传入参数.
Object paramBean=container.get(refStr);
// 此处的clazz是前一段反射代码通过<bean>元素的class属性得到的Class对象
Method setter=clazz.getMethod(setterName,paramBean.getClass());
// 此处的object是前一段反射代码为<bean>元素创建的对象
setter.invoke(object,paramBean);

上面代码就是最基本的反射代码(实际上Spring底层代码会更完善一些),
Spring框架通过反射根据<property>元素的name属性决定调用哪个setter方法,并根据valueref决定调用setter方法的传入参数。
如果不想真正理解Spring框架的底层机制,则只要记住:每个property>元素默认驱动Spring调用一次setter方法.
理解了Spring配置文件中<bean>元素的作用:默认驱动Spring在底层调用无参数的构造器创建对象.
就能猜到上面配置中4个<bean>元素产生的效果:Spring会依次创建org.crazyit.app.service.Personorg.crazyit.app.service.Axejavax.Swing.JFramejava.uti.Date这4个类的对象,并把它们当成容器中的Bean

其中idperson<bean>元素还包括一个<property>子元素,因此Spring会在创建完personBean之后,立即以容器中idaxeBean作为参数来调用personBeansettAxe方法—这样会导致容器中idaxeBean被赋值给person对象的axe实例变量。

使用ApplicationContext接口创建Spring容器

接下来程序就可通过Spring容器来访问容器中的Bean, ApplicationContextSpring容器最常用的接口,该接口有如下两个实现类。

  1. ClassPathXmlApplicationContext:从类加载路径下搜索配置文件,并根据配置文件来创建Spring容器
  2. FileSystemXmlApplicationContext:从文件系统的相对路径或绝对路径下去搜索配置文件,并根据配置文件来创建Spring容器

对于Java项目而言,类加载路径总是稳定的,因此通常总是使用ClassPathXmlApplicationContext创建Spring容器。

下面是本示例的主程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lee;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.crazyit.app.service.*;

public class BeanTest
{
public static void main(String[] args) throws Exception
{
// 创建Spring容器
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// 获取id为person的Bean
Person p = ctx.getBean("person", Person.class);
// 调用useAxe()方法
p.useAxe();
}
}

上面程序中main方法里的
第1行代码创建了Spring容器,
第2行代码通过Spring容器获取idpersonBean(Spring容器中的Bean,就是Java对象。)

使用getBean方法从Spring容器获取Bean

Spring容器获取Bean对象主要有如下两个方法。

方法 描述
Object getBean(String id) 根据容器中Beanid来获取指定Bean,获取Bean之后需要进行强制类型转换.
T getBean(String name, Class<T> requiredType) 根据容器中Beanid来获取指定Bean,但该方法带一个泛型参数,因此获取Bean之后无须进行强制类型转换

上面程序使用的是带泛型参数的getBean()方法,所以通过该方法获取Bean之后无须进行强制类型转换获得Bean(即Java对象)之后,即可通过该对象来调用方法访问实例变量(如果访问权限允许)总之,**原来怎么使用Java对象,现在还怎么使用这个获取到的Bean**。
编译、运行该程序,即可看到如下输出:

1
2
我打算去砍点柴火!
使用斧头砍柴

使用Spring不用自己创建对象

从上面的运行结果可以看出,使用Spring框架之后最大的改变之一是:程序不再使用new调用构造器创建Jaa对象,所有的Java对象都由Spring容器负责创建。

7.2 Spring入门

本书成书之时, Spring的最新稳定版本是5.0.2,本书的代码都基于该版本的Spring.

7.2.1 Spring下载和安装

Spring是一个独立的框架,它不需要依赖于任何Web服务器或容器,它既可在独立的Java SE项目中使用,当然也可在Java Web项目中使用。下面先介绍如何为Java项目和Java Web项目添加Spring支持。
下载和安装Spring框架请按如下步骤进行。

下载Spring

登录Spring站点,该页面显示一个目录列表,读者沿着org.springframeworkspring路径进入,即可看到Spring框架各版本的压缩包的下载链接。下载Spring的最新稳定版:5.0.2。

Spring目录说明

下载完成,得到一个spring-framework-5.0.2.RELEASE-dist.zip压缩文件,解压该压缩文件得到个名为spring-framework-5.0.2 RELEASE的文件夹,该文件夹下有如下几个子文件夹。
docs:该文件夹下存放Spring的相关文档,包含开发指南、API参考文档。
libs:该目录下的JAR包分为三类:
1. Spring框架class文件的JAR包;
1. Spring框架源文件的压缩包,文件名以-sources结尾;
1. Spring框架API文档的压缩包,文件名以javadoc结尾.

整个Spring框架由21个模块组成,该目录下将看到Spring为每个模块都提供了三个压缩包。
schemas:该目录下包含了Spring各种配置文件的XML Schema文档

复制Spring的class文件的jar包到项目的类加载路径

libs目录下所需要模块的class文件的JAR包复制添加到项目的类加载路径中—既可通过添加环境变量的方式来添加,也可使用Ant或IDE工具来管理应用程序的类加载路径。如果需要发布该应用,则将这些JAR包一同发布即可。如果没有太多要求,建议将libs目录下所有模块的class文件的JAR包(一共21个JAR包)添加进去。

经过上面三个步骤,接下来即可在Java应用中使用Spring框架了。

7.1 Spring简介和Spring5.0的变化

Spring框架由Rod Johnson开发,2004年发布了Spring框架的第一个版本。经过十多年的发展,Spring已经发展成Java EE开发中最重要的框架之一。对于一个Java开发者来说, Spring已经成为必须掌握的技能。
不仅如此,围绕Spring,以Spring为核心还衍生出了一系列框架,如Spring Web FlowSpring SecuritySpring DataSpring bootSpring Cloud等,具体请登录Spring官方网站, Spring越来越强大,带给开发者越来越多的便捷。本书所介绍的是Spring框架本身.

7.1.1 Spring简介

Spring是一个从实际开发中抽取出来的框架,因此它完成了大量开发中的通用步骤,留给开发者的仅仅是与特定应用相关的部分,从而大大提高了企业应用的开发效率。
Spring为企业应用的开发提供了一个轻量级的解决方案。该解决方案包括:

  • 基于依赖注入的核心机制、
  • 基于AOP的声明式事务管理、
  • 与多种持久层技术的整合,
  • 以及优秀的Web MVC框架等。

Spring致力于Java EE应用各层的解决方案,而不是仅仅专注于某一层的方案。可以说: Spring是企业应用开发的”一站式”选择, Spring贯穿表现层业务层持久层。然而, Spring并不想取代那些已有的框架而是以高度的开放性与它们无缝整合。
总结起来, Spring具有如下优点

  1. 低侵入式设计,代码的污染极低。
  2. 独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write once, Run Anywhere的承诺。
  3. SpringIoC容器降低了业务对象替换的复杂性,提高了组件之间的解耦。
  4. SpringAOP支持允许将一些通用任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用。
  5. SpringORMDAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。
  6. Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部。

这里有一张图片
图7.1显示了Spring框架的组成结构图。正如从图7.1所见到的,当使用Spring框架时,必须使用Spring Core Container(即Spring容器),它代表了Spring框架的核心机制, Spring Core Container主要由:

  1. org.springframework.core
  2. org.springframework.beans
  3. org.springframework.context
  4. org.springframework.expression

四个包及其子包组成,主要提供Spring loc容器支持。其中org.springframework.expression及其子包是Spring3.0新增的,它提供了Spring Expression Language支持。

7.1.2 Spring5.0的变化

与之前的Spring版本相比, Spring5.0发生了一些变化,这些变化包括:

  1. Spring5.0整个框架已经全面基于Java8,因此**Spring5.0JDK的最低要求就是Java8**,Spring5.0可以在运行时支持Java9
  2. 因为Java8的反射增强,因此Spring5.0框架可以对方法的参数进行更高效的访问
  3. Spring5.0核心接口已加入了Java8接口支持的默认方法
  4. Spring5.0框架已经自带了通用的日志封装,因此不再需要额外的common-logging日志包。当然,新版的日志封装也会对Log4j 2.xSLF4JJUL( java util.logging)进行自动检测。
  5. 引入@Nullable@notNull注解来修饰可空的参数以及返回值,避免运行时导致NPE异常。
  6. Spring5.0框架支持使用组件索引来扫描目标组件,使用组件索引扫描比使用类路径扫描更髙效。
  7. Spring5.0框架支持JetBrains Kotlin语言。
  8. Spring5.0Web支持已经升级为支持Servlet3.1以及更高版本的规范。

从上面介绍可以看出, Spring5.0的升级主要就是全面基于Java8,并在运行时支持Java9Servlet3.1规范,也为核心IoC容器增强了一些注解,并通过组件索引扫描来提升运行效率。本书所介绍的是Spring的最新发布版:Spring5.0.2,后面会介绍Spring5.0为核心IoC容器引入的注解.

第7章 Spring的基本用法

本章要点

  • Spring的起源和背景
  • 如何在项目中使用Spring框架
  • Eclipse中使用Spring
  • 理解依赖注入
  • Spring容器理解
  • Spring容器中的Bean管理
  • 容器中的Bean及其依赖注入
  • 自动装配
  • 使用Java类进行配置管理
  • 使用静态工厂、实例工厂创建Bean实例
  • 抽象Bean与子Bean
  • 容器中的工厂Bean管理
  • Bean的生命周期
  • 几种特殊的依赖注入
  • Spring的简化配置
  • SpEL的功能和用法

13.6 本章小结

本章详细介绍了一个完整的Java EE项目:人事管理系统,在此基础上可以扩展出企业的HRM系统、OA系统等。因为企业平台本身的复杂性,所以本项目涉及的表 达到6个,而且各个模块的业务逻辑也比较复杂,这些对初学者可能有一定难度,但只要读者先认真阅读本书前面章节所介绍的知识,并结合本章的讲解,再配合资源文件中的案例代码,则一定可以掌握本章所介绍的内容。
本章介绍的Java EE应用综合使用了前面介绍的两个框架: Spring MVC+ MyBatis3,因此本章内容既是对前面知识点的回顾和复习,也是将理论知识应用到实际开发的典范。一旦读者掌握了本章案例的开发方法,就会对实际Java EE企业应用的开发产生一种豁然开朗的感觉。

13.5.7 下载中心

处理下载中心的DocumentController代码如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package org.fkit.hrm.controller;

import java.io.File;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.FileUtils;
import org.fkit.hrm.domain.Document;
import org.fkit.hrm.domain.User;
import org.fkit.hrm.service.HrmService;
import org.fkit.hrm.util.common.HrmConstants;
import org.fkit.hrm.util.tag.PageModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/**
* @Description: 处理上传下载文件请求控制器
*/

@Controller
public class DocumentController
{
/**
* 自动注入UserService
*/
@Autowired
@Qualifier("hrmService")
private HrmService hrmService;

/**
* 处理/login请求
*/
@RequestMapping(value = "/document/selectDocument")
public String selectDocument(Model model, Integer pageIndex,
@ModelAttribute Document document)
{
PageModel pageModel = new PageModel();
if (pageIndex != null)
{
pageModel.setPageIndex(pageIndex);
}
/** 查询用户信息 */
List<Document> documents = hrmService.findDocument(document, pageModel);
model.addAttribute("documents", documents);
model.addAttribute("pageModel", pageModel);
return "document/document";

}

/**
* 处理添加请求
* @param String flag 标记, 1表示跳转到上传页面,2表示执行上传操作
* @param Notice notice 要添加的公告对象
* @param ModelAndView mv
*/
@RequestMapping(value = "/document/addDocument")
public ModelAndView addDocument(String flag,
@ModelAttribute Document document, ModelAndView mv,
HttpSession session) throws Exception
{
if (flag.equals("1"))
{
mv.setViewName("document/showAddDocument");
} else
{
// 上传文件路径
String path = session.getServletContext().getRealPath("/upload/");
System.out.println(path);
// 上传文件名
String fileName = document.getFile().getOriginalFilename();
// 将上传文件保存到一个目标文件当中
document.getFile()
.transferTo(new File(path + File.separator + fileName));

// 插入数据库
// 设置fileName
document.setFileName(fileName);
// 设置关联的User对象
User user = (User) session.getAttribute(HrmConstants.USER_SESSION);
document.setUser(user);
// 插入数据库
hrmService.addDocument(document);
// 返回
mv.setViewName("redirect:/document/selectDocument");
}
// 返回
return mv;
}

/**
* 处理删除文档请求
* @param String ids 需要删除的id字符串
* @param ModelAndView mv
*/
@RequestMapping(value = "/document/removeDocument")
public ModelAndView removeDocument(String ids, ModelAndView mv)
{
// 分解id字符串
String[] idArray = ids.split(",");
for (String id : idArray)
{
// 根据id删除文档
hrmService.removeDocumentById(Integer.parseInt(id));
}
// 设置客户端跳转到查询请求
mv.setViewName("redirect:/document/selectDocument");
// 返回ModelAndView
return mv;
}

/**
* 处理修改文档请求
* @param String flag 标记, 1表示跳转到修改页面,2表示执行修改操作
* @param Document document 要修改文档的对象
* @param ModelAndView mv
*/
@RequestMapping(value = "/document/updateDocument")
public ModelAndView updateDocument(String flag,
@ModelAttribute Document document, ModelAndView mv)
{
if (flag.equals("1"))
{
// 根据id查询文档
Document target = hrmService.findDocumentById(document.getId());
// 设置Model数据
mv.addObject("document", target);
// 设置跳转到修改页面
mv.setViewName("document/showUpdateDocument");
} else
{
// 执行修改操作
hrmService.modifyDocument(document);
// 设置客户端跳转到查询请求
mv.setViewName("redirect:/document/selectDocument");
}
// 返回
return mv;
}

/**
* 处理文档下载请求
* @param String flag 标记, 1表示跳转到修改页面,2表示执行修改操作
* @param Document document 要修改文档的对象
* @param ModelAndView mv
*/
@RequestMapping(value = "/document/downLoad")
public ResponseEntity<byte[]> downLoad(Integer id, HttpSession session)
throws Exception
{
// 根据id查询文档
Document target = hrmService.findDocumentById(id);
String fileName = target.getFileName();
// 上传文件路径
String path = session.getServletContext().getRealPath("/upload/");
// 获得要下载文件的File对象
File file = new File(path + File.separator + fileName);
// 创建springframework的HttpHeaders对象
HttpHeaders headers = new HttpHeaders();
// 下载显示的文件名,解决中文名称乱码问题
String downloadFielName = new String(fileName.getBytes("UTF-8"),
"iso-8859-1");
// 通知浏览器以attachment(下载方式)打开图片
headers.setContentDispositionFormData("attachment", downloadFielName);
// application/octet-stream : 二进制流数据(最常见的文件下载)。
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 201 HttpStatus.CREATED
return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),
headers, HttpStatus.CREATED);
}
}

单击左侧菜单下载中心下面的上传文档命令,跳转到上传文档界面。
输入需要上传的文档标题和文档概述,选择要上传的文档,单击上传按钮,若上传成功则跳转到文档查询界面,显示所有文档信息。
选择每一行最后的下载按钮,会弹出下载页面,选择下载文档保存的路径,即可以下载文档。

输入文档标题,单击搜索按钮可以完成模糊查询功能
选择每一行的操作按钮,可以进入修改页面,对选中的文档进行修改操作
选择每一行第一列的复选框,单击删除按钮,则可以对选中的文档进行删除操作

13.5.6 公告管理

处理公告的NoticeController代码如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package org.fkit.hrm.controller;

import java.util.List;
import javax.servlet.http.HttpSession;
import org.fkit.hrm.domain.Notice;
import org.fkit.hrm.domain.User;
import org.fkit.hrm.service.HrmService;
import org.fkit.hrm.util.common.HrmConstants;
import org.fkit.hrm.util.tag.PageModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/**
* @Description: 处理公告请求控制器
*/

@Controller
public class NoticeController
{
/**
* 自动注入UserService
*/
@Autowired
@Qualifier("hrmService")
private HrmService hrmService;

/**
* 处理/login请求
*/
@RequestMapping(value = "/notice/selectNotice")
public String selectNotice(Model model, Integer pageIndex,
@ModelAttribute Notice notice)
{
PageModel pageModel = new PageModel();
if (pageIndex != null)
{
pageModel.setPageIndex(pageIndex);
}
/** 查询用户信息 */
List<Notice> notices = hrmService.findNotice(notice, pageModel);
model.addAttribute("notices", notices);
model.addAttribute("pageModel", pageModel);
return "notice/notice";

}

/**
* 处理添加请求
* @param Integer id 要显示的公告id
* @param Model model
*/
@RequestMapping(value = "/notice/previewNotice")
public String previewNotice(Integer id, Model model)
{

Notice notice = hrmService.findNoticeById(id);

model.addAttribute("notice", notice);
// 返回
return "notice/previewNotice";
}

/**
* 处理删除公告请求
* @param String ids 需要删除的id字符串
* @param ModelAndView mv
*/
@RequestMapping(value = "/notice/removeNotice")
public ModelAndView removeNotice(String ids, ModelAndView mv)
{
// 分解id字符串
String[] idArray = ids.split(",");
for (String id : idArray)
{
// 根据id删除公告
hrmService.removeNoticeById(Integer.parseInt(id));
}
// 设置客户端跳转到查询请求
mv.setViewName("redirect:/notice/selectNotice");
// 返回ModelAndView
return mv;
}

/**
* 处理添加请求
* @param String flag 标记, 1表示跳转到添加页面,2表示执行添加操作
* @param Notice notice 要添加的公告对象
* @param ModelAndView mv
*/
@RequestMapping(value = "/notice/addNotice")
public ModelAndView addNotice(String flag, @ModelAttribute Notice notice,
ModelAndView mv, HttpSession session)
{
if (flag.equals("1"))
{
mv.setViewName("notice/showAddNotice");
} else
{
User user = (User) session.getAttribute(HrmConstants.USER_SESSION);
notice.setUser(user);
hrmService.addNotice(notice);
mv.setViewName("redirect:/notice/selectNotice");
}
// 返回
return mv;
}

/**
* 处理添加请求
* @param String flag 标记, 1表示跳转到修改页面,2表示执行修改操作
* @param Notice notice 要添加的公告对象
* @param ModelAndView mv
*/
@RequestMapping(value = "/notice/updateNotice")
public ModelAndView updateNotice(String flag, @ModelAttribute Notice notice,
ModelAndView mv, HttpSession session)
{
if (flag.equals("1"))
{
Notice target = hrmService.findNoticeById(notice.getId());
mv.addObject("notice", target);
mv.setViewName("notice/showUpdateNotice");
} else
{
hrmService.modifyNotice(notice);
mv.setViewName("redirect:/notice/selectNotice");
}
// 返回
return mv;
}
}

单击左侧菜单公告管理下面的添加公告命令,跳转到添加公告界面。
输入需要添加的公告标题和公告内容,单击添加按钮,若添加成功则跳转到公告查询界面,显示所有公告信息。
选择每一行公告最后一列的预览按钮,则可以预览公告内容。
输入公告名称和公告内容,单击搜索按钮可以完成模糊査询功能
选择每一行的”操作”按钮,可以进入修改页面,对选中的公告进行修改操作
选择每一行第一列的复选框,单击删除按钮,则可以对选中的公告进行删除操作