注:本文大部分内容出自《Spring实战(第4版)》
概况
Spring是一个开源框架,是为了解决企业级应用开发的复杂性而创建的。Spring不仅仅局限于服务器端开发, 任何Java应用都能在简单性、 可测试性和松耦合等方面从Spring中获益。
Spring发布于2003年,目前已经到5.x版本。
为了降低Java开发的复杂性, Spring采取了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板式代码。
Bean,控制反转(IoC)含义的解释:https://www.awaimai.com/2596.html
bean
Spring有很多概念,其中最基本的一个就是bean,那么spring bean到底是什么?
Spring官方文档对bean的解释
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
翻译:在 Spring 中,构成应用程序主干并由Spring IoC容器管理的对象称为bean。bean是一个由Spring IoC容器实例化、组装和管理的对象。
概括一下就是:
-
bean是Java对象,一个或者多个不限定
-
bean由Spring中一个叫IoC的东西管理
-
我们的应用程序由一个个bean构成
那么什么是IoC呢?
控制反转(IoC:Inversion of Control)
控制反转通过依赖注入(DI)方式实现对象之间的松耦合关系。
程序运行时,依赖对象由【辅助程序】动态生成并注入到被依赖对象中,动态绑定两者的使用关系。
Spring IoC容器就是这样的【辅助程序】,它负责对象的生成和依赖的注入,然后再由我们使用。
简而言之,IoC就是一个对象定义其依赖关系而不创建它们。
1、使用私有属性保存依赖对象,并且只能通过构造函数参数传入
假设我们有一个Computer类:
public class Computer {
private String cpu; // CPU型号
private int ram; // RAM大小,单位GB
public Computer(String cpu, int ram) {
this.cpu = cpu;
this.ram = ram;
}
}
我们有另一个Person类依赖于Computer类,符合IoC的做法是这样:
public class Person {
private Computer computer;
public Person(Computer computer) {
this.computer = computer;
}
}
不符合IoC的做法如下:
// 直接在Person里实例化Computer类
public class Person {
private Computer computer = new Computer("AMD", 3);
}
// 通过【非构造函数】传入依赖
public class Person {
private Computer computer;
public void init(Computer computer) {
this.computer = computer;
}
}
2、让Spring控制类构建过程
在Spring中,我们基本不需要 new 一个类,这些都是让 Spring 去做的。
Spring 启动时会把所需的类实例化成对象,如果需要依赖,则先实例化依赖,然后实例化当前类。
依赖通过构建函数传入当前类对象,这一步就是依赖注入(DI)
在 Spring 中,类的实例化、依赖的实例化、依赖的传入都交由 Spring Bean 容器控制,
而不是用new
方式实例化对象、通过非构造函数方法传入依赖等常规方式。
实质的控制权由人工方式转为Spring Bean容器自动管理,故而称为控制反转
bean规范如下:
- 所有属性为private
- 提供默认构造方法
- 提供getter和setter
- 实现serializable接口
1、激发POJO的潜能
很多框架通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。
Spring竭力避免因自身的API而弄乱你的应用代码。 Spring不会强迫你实现Spring规范的接口或继承Spring规范的类。
在基于Spring构建的应用中, 它的类通常没有任何痕迹表明你使用了Spring。 即使一个类使用Spring注解, 但它依旧是POJO。
Spring不会在HelloWorldBean上有任何不合理的要求
package com.habuma.spring
public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}
可以看到, 这是一个简单普通的Java类,没有任何地方表明它是一个Spring组件。 Spring的非侵入编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。Spring赋予POJO魔力的方式之一就是通过DI来装配它们。
2、依赖注入
依赖注入是如何实现的
任何一个有实际意义的应用都会由两个或者更多的类组成, 这些类相互之间进行协作来完成特定的业务逻辑。
按照传统的做法, 每个对象负责管理与自己相互协作的对象(即它所依赖的对象) 的引用, 这将会导致高度耦合和难以测试的代码,牵一发而动全身。
DamselRescuingKnight只能执行RescueDamselQuest探险任务
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
quest = new RescueDamselQuest();// 与RescueDamselQuest紧耦合
}
@Override
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
DamselRescuingKnight和RescueDamselQuest紧密的耦合在一起,极大地限制了这个骑士执行探险的能力。
如果需要救援少女, 这个骑士能够召之即来。 但是如果一条恶龙需要杀掉, 或者有别的任务, 那么这个骑士就爱莫能助了。
耦合具有两面性:
一方面, 紧密耦合的代码难以测试、 难以复用、 难以理解, 并且修复一个bug, 将会出现一个或者更多新的bug(打地鼠式) 。
另一方面, 一定程度的耦合又是必须的——为了完成有实际意义的功能, 不同的类必须以适当的方式进行交互,完全没有耦合的代码什么也做不了。
通过依赖注入的方式来完成对象之间的依赖关系,对象不再需要自行管理它们的依赖关系,而是通过依赖注入自动地注入到对象中去。
BraveKnight足够灵活可以接受任何赋予他的探险任务
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;// quest被注入到对象中
}
@Override
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
我们可以看到,不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一, 即构造器注入(constructor injection)。
更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。 所以,BraveKnight能够响应RescueDamselQuest、SlayDragonQuest、MakeRound TableRounderQuest等任意的Quest实现。这正是多态的体现。
这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说, 被要求挑战的探险任务只要实现了Quest接口, 那么具体是哪种类型的探险就无关紧要了。 这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程) 来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
将Quest注入到Knight中
假设, 希望BraveKnight所要进行探险任务是杀死一只怪龙,代码如下:
SlayDragonQuest是要注入到BraveKnight中的Quest实现
public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
@Override
public void embark() {
stream.println("Embarking on quest to slay the dragon!!");
}
}
SlayDragonQuest实现类Quest接口,这样它就适合注入到BraveKnight中了,与其他Java入门样例不同的是,SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。那么如何将屠龙任务SlayDragonQuest交给BraveKnight呢?
创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,如XML方式和Java配置方式。
Java配置方式将SlayDragonQuest注入到BraveKnight中
import com.springinaction.knights.BraveKnight;
import com.springinaction.knights.Knight;
import com.springinaction.knights.Quest;
import com.springinaction.knights.SlayDragonQuest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲, 它在构造时传入了对SlayDragonQuest bean的引用, 将其作为构造器参数。 同时, SlayDragonQuest bean的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream) 传入到了SlayDragonQuest的构造器中。
同时Spring还支持XML方式描述配置,它的功能和上面是相同的。
XML方式配置依赖注入
<?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="knight" class="guo.knights.BraveKnight">
<constructor-arg ref="quest"/> <!--注入Quest bean-->
</bean>
<bean id="quest" class="guo.knights.SlayDragonQuest"> <!--创建SlayDragonQuest-->
<constructor-arg value="#{T(System).out}"/>
</bean>
</beans>
不管你使用的是基于XML的配置还是基于Java的配置, 依赖注入所带来的收益都是相同的。尽管BraveKnight依赖于Quest, 但是它并不知道传递给它的是什么类型的Quest, 也不知道这个Quest来自哪里。 与之类似, SlayDragonQuest依赖于PrintStream, 但是在编码时它并不需要知道这个PrintStream是什么样子的。 只有Spring通过它的配置, 能够了解这些组成部分是如何装配起来的。 这样的话, 就可以在不改变所依赖的类的情况下, 修改依赖关系。
观察它如何工作
Spring通过应用上下文(Application Context) 装载bean的定义并把它们组装起来。 Spring应用上下文全权负责对象的创建和组装。 Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。
调用ClassPathXmlApplicationContext加载knights.xml,并获得Knight对象的引用。
public class KnightMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring/knights.xml"); //加载Sprinig应用上下文
//加载Java配置
//AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(KnightConfig.class);
Knight knight = context.getBean(Knight.class); //获取knight bean
knight.embarkOnQuest(); //使用knight调用方法
context.close(); //关闭应用上下文
}
}
这里的main()方法基于knights.xml文件创建了Spring应用上下文。 随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后, 只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。
注意这个类完全不知道我们的英雄骑士接受哪种探险任务, 而且完全没有意识到这是由BraveKnight来执行的。 只有knights.xml文件知道哪个骑士执行哪种探险任务。
通过示例我们对依赖注入进行了一个快速介绍。接下来再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。
3、应用切面
依赖注入能够让相互协作的软件组件保持松散耦合, 而面向切面编程(aspect-oriented programming, AOP) 允许你把遍布应用各处的功能分离出来形成可重用的组件。
系统由许多不同的组件组成, 每一个组件各负责一块特定功能。 除了实现自身核心的功能之外, 这些组件还经常承担着额外的职责。 诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。
如果将这些关注点分散到多个组件中去, 你的代码将会带来双重的复杂性。
- 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。
- 组件会因为那些与自身核心业务无关的代码而变得混乱。
下图展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还要亲自执行这些服务。
AOP能够使这些服务模块化, 并以声明的方式将它们应用到它们需要影响的组件中去。
如下图所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。 应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。 这些层以声明的方式灵活地应用到系统中, 你的核心应用甚至根本不知道它们的存在。 这是一个非常强大的理念, 可以将安全、事务和日志关注点与核心业务逻辑相分离。
为了示范在Spring中如何应用切面, 让我们重新回到骑士的例子, 并为它添加一个切面。
AOP应用
每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。
吟游诗人是中世纪的音乐记录器
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() {
stream.println("Fa la la ,the Knight is so brabe"); //探险之前调用
}
public void singAfterQuest() {
stream.println("Tee hee hhe,the brave knight " + "did embark on a quest"); //探险之后调用
}
}
Minstrel是只有两个方法的简单类。在骑士执行每一个探险任务之前,singBeforeQuest()方法会被调用;在骑士完成探险任务之后,singAfterQuest()方法会被调用。在这两种情况下,Minstrel都会通过一个PrintStream类来歌颂骑士的事迹,这个类是通过构造器注入进来的。
把吟游诗人的方法加入骑士的类中很简单,如下:
BraveKnight必须要调用Minstrel的方法
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}
@Override
public void embarkOnQuest() {
minstrel.singBeforeQuest(); //Knight应该管理Minstrel吗?
quest.embark();
minstrel.singAfterQuest(); //同上的问题
}
}
这应该可以达到预期效果,只需到Spring配置中声明骑士的依赖注入。但是管理他的吟游诗人真的是骑士职责范围内的工作吗?
吟游诗人应该做他份内的事, 根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?
此外,因为必须把吟游诗人注入到BarveKnight类中,这不仅使BraveKnight的代码复杂化了,而且还让人疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrel为null会发生什么呢? 我是否应该引入一个空值校验逻辑来覆盖该场景?
简单的BraveKnight类开始变得复杂,但利用AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问Minstrel的方法。
要将Minstrel抽象为一个切面, 你所需要做的事情就是在一个Spring配置文件中声明它。下属代码是更新后的knights.xml文件。
将Minstrel声明为一个切面
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"></constructor-arg>
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest"></bean>
<!-- 声明诗人Minstrel,待切入的对象(刀) -->
<bean id="minstrel" class="com.springinaction.knights.Minstrel"></bean>
<aop:config>
<aop:aspect ref="minstrel">
<!-- 定义切面,即定义从哪里切入 -->
<aop:pointcut expression="execution(* *.embarkOnQuest(..))"
id="embark" />
<!-- 声明前置通知,在切入点之前执行的方法 -->
<aop:before method="singBeforeQuest" pointcut-ref="embark" />
<!-- 声明后置通知,在切入点之后执行的方法 -->
<aop:after method="singAfterQuest" pointcut-ref="embark" />
</aop:aspect>
</aop:config>
</beans>
首先,需要把Minstrel声明为一个bean,然后在<aop:aspect>元素中引用该bean。
为了进一步定义切面, 声明(使用<aop:before>) 在embarkOnQuest()方法执行前调用Minstrel的singBeforeQuest()方法。这种方式被称为前置通知(before advice)。同时声明(使用<aop:after>)在embarkOnQuest()方法执行后调用singAfter Quest()方法。这种方式被称为后置通知(after advice)。
pointcut-ref属性都引用了名字为embank的切入点。该切入点是在前边的<pointcut>元素中定义的,并配置expression属性来选择所应用的通知。表达式的语法采用的是AspectJ的切点表达式语言。
首先,Minstrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。当我们按照上面那样进行配置后,在Spring的上下文中,Minstrel实际上已经变成一个切面了。
其次,也是最重要的,Minstrel可以被应用到BraveKnight中, 而BraveKnight不需要显式地调用它。实际上,BraveKnight完全不知道Minstrel的存在。
4、使用模板消除样板式代码
使用Spring模版可以消除很多样板式代码,比如JDBC、JMS、JNDI、REST等。
比如JDBC查询,首先你需要创建一个数据库连接,然后再创建一个语句对象,最后你才能进行查询,同时还要捕捉SQLException。之后还要关闭数据库连接、 语句和结果集,同时依然要捕捉SQLException。
所有JDBC代码基本都是上面相同的步骤。Spring旨在通过模板封装来消除样板式代码,Spring中的JdbcTemplate使得执行数据库操作时, 避免传统的JDBC样板代码成为了可能。
容纳Bean
Spring的容器实现
-
Bean工厂(org.springframework.beans.factory.BeanFactory):最简单的容器,提供基本的DI支持;
-
应用上下文(org.springframework.context.ApplicationContext):基于BeanFactory之上构建,提供面向应用的服务。
常用的几种应用上下文
-
AnnotationConfigApplicationContext: 从一个或多个基于Java的配置类中加载Spring应用上下文。
- AnnotationConfigWebApplicationContext: 从一个或多个基于Java的配置类中加载Spring Web应用上下文。
-
ClassPathXmlApplicationContext:从类路径中的XML配置文件加载上下文,会在所有的类路径(包括jar文件)下查找;
-
FileSystemXmlApplicationContext:从文件系统中读取XML配置文件并加载上下文,在指定的文件系统路径下查找;
-
XmlWebApplicationContext:读取Web应用下的XML配置文件并加载上下文;
加载应用上下文的格式如下:
//加载Java配置类
ApplicationContext context = new AnnotationConfigApplicationContext(
com.springinaction.knights.config.KnightConfig.class);
//加载XML配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");
应用上下文准备就绪之后,我们就可以调用上下文的getBean()方法从Spring容器中获取bean。
俯瞰Spring
通过DI、 AOP和消除样板式代码来简化企业级Java开发只是Spring功能的一部分。
Spring模块
在Spring 4.0中,Spring框架的发布版本包括了20个不同的模块,每个模块会有3个JAR文件。
如下图所示,这些模块依据其所属的功能可以划分为6类不同的功能。
这些模块为开发企业级应用提供了所需的一切。 但是你也不必将应用建立在整个Spring框架之上, 你可以自由地选择适合自身应用需求的Spring模块;当Spring不能满足需求时,完全可以考虑其他选择。