Q:Spring 的目标是什么?
致力于全方位的简化 Java 开发。
Q:为了降低 Java 开发的复杂性,Spring 采取了哪些关键策略?
- 基于 POJO 的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板式代码;
几乎 Spring 所做的任何事情都可以追溯到上述的一条或者多条策略。
1.1 激发 POJO 的潜能
很多框架通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。
而 Spring 就不会,它会竭力避免因自身的 API 而弄乱你的应用代码。
比如:在基于 Spring 构建的应用中,它的类通常没有任何痕迹表明你使用了 Spring 。 最坏的场景就是,一个类或许会使用 Spring 注解,但它依旧是 POJO。
// Spring 不会在 HelloWorldBean 上有任何不合理的要求,这是一个简单普通的 Java 类 —— POJO。没有任何地方表明它是一个 Spring 组件。
public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}
Spring 的非侵入编程模型意味着这个类在 Spring 应用中和非 Spring 应用中都可以发挥同样的作用。
Spring 通过 DI(依赖注入)来装配它们。
1.2 依赖注入
Q:DI(依赖注入)功能是如何实现的?
说明问题:
通常,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。
/**
* 营救少女的骑士(难以测试的代码)
*/
public class DamselRescuingKnight {
// 营救少女的任务
private RescueDamselQuest quest;
// 构造函数中,自行创建了 RescueDamselQuest,并与 RescueDamselQuest 紧耦合,因此该骑士只能执行营救少女的探险,不能执行其他的的探险
public DamselRescuingKnight(RescueDamselQuest quest) {
this.quest = new RescueDamselQuest();
}
// 在单元测试中,必须保证该方法被调用时,探险的 embark() 方法也被调用。此时只能执行营救少女的探险,无法执行其他的探险,难以测试。
public void embarkOnQuest() {
quest.embark();
}
}
总而言之,耦合是必须的,但应当小心谨慎的管理它。
解决方法:
通过 DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系。
也就是说,依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖。
/**
* 勇敢的骑士
* BraveKnight 没有与任何特定的 Quest 实现发生耦合。
*/
public class BraveKnight implements Knight{
private Quest quest;
// 没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。
// 这是依赖注入的方式之一:构造器注入。
// 传入的探险类型是 Quest,也就是说所有的探险任务都必须实现一个 Quest 接口。所以,他能响应更多的探险任务。
// 这就是 DI 所带来的最大收益——松耦合
public BraveKnight(Quest quest) {
this.quest = quest; //Quest 被注入进来
}
public void embarkOnQuest() {
quest.embark();
}
}
BraveKnight 类可以接受你传递给它的任意一种 Quest 的实现。
Q:如何把特定的 Query 实现传给它呢?
import java.io.PrintStream;
/**
* 特定的Quest:杀死一只怪龙
* SlayDragonQuest 是要注入到 BraveKnight 中的 Quest 实现
*/
public class SlayDragonQuest implements Quest {
private PrintStream stream;
// 需要将 PrintStream 注入到 SlayDragonQuest
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}
这样注入:
方式一:使用 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="com.springinaction.knights.BraveKnight">
<!-- 注入 Quest bean -->
<constructor-arg ref="quest"/>
</bean>
<!-- 创建 SlayDragonQuest -->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<!-- 使用了 Spring 表达式语言(Spring Expression Language),将 System.out(这是一个 PrintStream) 传入到 SlayDragonQuest 的构造器中 -->
<constructor-arg value="#{T(System).out}"/>
</bean>
</beans>
方式二:使用 Java 来描述配置(记得导包)
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
这个样例展现了在 Spring 中装配 bean 的一种简单方法。
现在已经声明了关系,接下来只需要装载 XML 配置文件,并把应用启动起来。
Spring 通过应用上下文(Application Context)装载 bean 的定义并把它们组装起来。Spring 应用上下文全权负责对象的创建和组装。Spring 自带了多种应用上下文的实现,它们之间主要的区别在于如何加载配置文件。
Q:如何加载配置文件?
因为 knight.xml 中的 bean 是使用 XML 文件进行配置的,所以选择 ClassPathXmlApplicationContext 作为应用上下文相对是比较合适的。该类加载位于应用程序类路径下的一个或多个 XML 配置文件。
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 注意:该类完全不知道我们的英雄骑士接受的哪种探险任务,而且完全不知道这是由 BraveKnight 来执行的。
* 只有 knight.xml 文件知道哪个骑士执行哪种探险任务。
*/
public class KnightMain {
public static void main(String[] args) {
// 加载 Spinrg 上下文
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("WEB-INF/spring/knight.xml");
// 获取 knight bean
Knight knight = context.getBean(Knight.class);
// 使用 knight
knight.embarkOnQuest();
context.close();
}
}
1.3 应用切面
DI 能够让相互协作的软件组织保持松散耦合,而面向切面编程(AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
在这整个系统内,关注点(如:日志和安全)的调用经常散布到各个模块中,而这些关注点并不是模块的核心业务。
Q:面对这样的复杂性, AOP 能做什么?
A:AOP 能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。使得这些组件具有更高的内聚性并且会更加关注自身的业务,总之,AOP 能够确保 POJO 的简单性。
Q:简化的理念是什么?
A:我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。这样就可以将安全、事务和日志关注点与核心业务逻辑相分离。
简化之后的系统
利用AOP,系统范围内的关注点覆盖在它们所影响组件之上
Q: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 brave!");
}
// 探险之后调用
public void singAfterQuest() {
stream.println("Tee hee hee,the brave knight did embark on a quest!");
}
}
/**
* 勇敢的骑士(错误的展示)
* 骑士不需要管理游吟诗人,所以此代码 ×
*/
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
/**
* 不能直接声明 Minstrel bean 并将其注入到 BraveKnight 的构造器中。
* 因为,管理他的游吟诗人并不是骑士的工作,但是骑士需要知道游吟诗人,所以就必须把游吟诗人注入到 BraveKnight 类中。
* 问题来了:如果一个骑士不需要游吟诗人?即 Minstrel 为 null 时会发生什么?这样,BraveKnight 类就变得复杂。
* 但利用 AOP,你可以声明游吟诗人必须歌颂骑士的探险事迹,而骑士本身不用直接访问 Minstrel 的方法。
*/
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}
public void embarkOnQuest() {
// Knight 应该管理它的 Minstrel 吗?不应该!!!
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}
Q:如何将 Minstrel 抽象为一个切面?
A:在一个 Spring 配置文件中声明它。
<?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"/>
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}"/>
</bean>
<!-- 声明 Minstrel bean -->
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}"/>
</bean>
<aop:config>
<!-- 引用 Minstrel bean -->
<aop:aspect ref="minstrel">
<!-- 定义切点,并配置 expression 属性来选择所应用的通知。表达式的语法采用的是 AspectJ 的切点表示语言(此处无需了解细节) -->
<aop:pointcut id="embark" expression="execution(* *.embarkOnQuest{..})"/>
<!-- 声明前置通知 -->
<aop:before pointcut-ref="embark" method="singBeforeQuest"/>
<!-- 声明后置通知 -->
<aop:after pointcut-ref="embark" method="singAfterQuest"/>
</aop:aspect>
</aop:config>
</beans>
1.4 使用模板消除样板式代码
问题阐述:许多 Java API , 例如 JDBC,会涉及编写大量的样板式代码,如下列代码。
上述代码中,只有少量的代码与查询员工逻辑有关系,其他的代码都是 JDBC 样板代码。
Q:Spring 是怎么解决这一问题的?
Spring 通过模板封装来消除样板式代码。Spring 的 JdbcTemplate 使得执行数据库操作时,避免传统的 JDBC 样板代码。
总结:
以上已经展示了 Spring 通过面向 POJO编程、DI、切面和模板技术来简化 Java 开发中的复杂性。
在这个过程中,展示了 基于 XML 的配置文件中如何配置 bean 和切面,但这些文件是如何加载的?它们被加载到哪里去了?
接下来,我们了解下 Spring 容器,这是应用中的所有 bean 所驻留的地方。