为了降低Java开发的复杂性,Spring采取了以下四种关键策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过依赖注入(DI)和面向接口实现松耦合;
- 基于切面和惯例进行切面式编程;
- 通过切面和模板减少样本式代码;
下面对这几种策略进行通俗的讲解,以便能够快速入门。
1.基于POJO的轻量级和最小侵入性编程
最小侵入性编程体现在,Spring竭力避免因自身的API而弄乱你的应用代码。Spring不会强迫你使用Spring的接口或是继承Spring的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。最坏的场景是,一个类可能会使用Spring注解,但它依旧是POJO(Plain Old Java Object, 即普通Java对象)。
Spring赋予POJO魔力的方式之一就是通过DI装配它们,DI是如何帮助应用对象之间保持松散耦合的?此时依赖注入(DI)登场了。
2. 通过依赖注入(DI)和面向接口实现松耦合
2.1DI功能如何实现的?
任何有实际意义的应用都会由两个或者更多的类组成,它们之间相互协作来完成特定的业务逻辑。传统的做法是每个对象管理与自己交互协作的对象(即它所以来的对象)的引用,这样会导致高度耦合和难以测试的代码,首先看一个例子:
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest(); // 与RescueDamselQuest紧耦合
}
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
这里的DamselRescuingKnight在构造函数中自行创建了RescueDamselQuest,使得两者高度耦合,这样极大限制了骑士执行探险的能力:只能救援少女,若要杀掉恶龙就爱莫能助了。而且,为DamselRescuingKnight编写单元测试极其困难。
通过DI,对象的依赖关系将由系统中负责协调对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们之间的依赖关系。下面的代码是一个勇敢骑士类,可以挑战任何探险:
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) { // Quest被注入到对象中
this.quest = quest;
}
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
与之前不同的是,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。(这是依赖注入的方式之一:构造器注入)更重要的是,传入的探险类型是Quest接口,BraveKnight并没有与任何特定的Quest发生耦合。因此骑士挑战的探险任务只要实现Quest接口就可以了,具体是什么类型的探险任务就无关紧要了,实现了骑士与探险任务之间的松耦合。
这就是DI带来的最大利益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化的过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。那如何将特定的Quest实现类SlayDragonQuest传给BraveKnight类呢?
SlayDragonQuest实现如下:
package com.springinaction.knights;
import java.io.PrintStream;
public class SlayDragonQuest implements Quest { //实现Quest接口
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}
创建了这个类之后,剩下的就是将这个类交给BraveKnight。这就是创建应用组件之间的写作的行为,被称为装配(wiring)。Spring有多种装配Bean的方式,其中最常用的就是通过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">
<constructor-arg ref="quest"/> <!--注入Quest bean-->
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest"><!--创建SlayDragonQuest-->
<constructor-arg value="#{T(System).out}" />
</bean>
</beans>
以上配置代码实现了:
- 将BraveKnight和SlayDragonQuest声明为Spring中的bean。
- 在构造BraveKnight bean的时候传入SlayDragonQuest bean的应用,作为构造器的参数。
- 在构造SlayDragonQuest bean的时候将System.out传入到构造器中
2.2我们如何使用?
Spring通过应用上下文(ApplicationContext)来装载Bean,ApplicationContext全权负责对象的创建和组装。
Spring自带了多种ApplicationContext来加载配置,比如,Spring可以使用ClassPathXmlApplicationContext来装载XML文件中的Bean对象。
package com.springinaction.knights;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("knights.xml");// 加载Spring上下文
Knight knight = (Knight) context.getBean("knight");// 获取knight bean
knight.embarhOnQuest();// 使用knight
}
}
这个示例代码中,Spring上下文加载了knights.xml文件,随后获取了一个ID为knight的Bean的实例,得到该对象实例后,就可以进行正常的使用了。需要注意的是,这个类中完全不知道是由哪个Knight来执行何种Quest任务,只有knights.xml文件知道。
3. 基于切面和惯例进行切面式编程
通常情况下,系统由许多不同组件组成,其中的每一个组件分别负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责,诸如日志、事务管理和安全等,此类的系统服务经常融入到有自身核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们总是跨越系统的多个组件。如下图:
AOP可以使得这些服务模块化,如下图。借助AOP,可以使用各种功能层去包裹核心业务层,并以声明的方式将它们应用到相应的组件中去,核心应用根本不知道他们的存在,只需关注自身业务,完全不需要了解系统服务的复杂性。
我们把AOP应用在上面的例子中,人们大多是通过说书人描写叙述的骑士的事迹来了解骑士的。如果你想使用说书人这个服务来记录BraveKnight的一些事迹。以下的代码清单列出了你可能会用到的说书人(Minstrel)类。
package com.springinaction.knights;
import java.io.PrintStream;
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!");
}
}
如代码中所示,诗人会在骑士每次执行探险前和结束时被调用,完成骑士事迹的歌颂。骑士必须调用诗人的方法完成歌颂:
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest) {
this.quest = quest;// quest被注入到对象中
}
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;// quest被注入到对象中
this.minstrel = minstrel;
}
public void embarhOnQuest() throws QuestException {
minstrel.singAfterQuest();
quest.embark();
minstrel.singAfterQuest();
}
}
但是,感觉是骑士在路边抓了一个诗人为自己「歌功颂德」,而不是诗人主动地为其传扬事迹,简单的BraveKnight类开始变得复杂。但是有了AOP,骑士就不再需要自己调用诗人的方法为自己服务了,这就需要把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>
<!-- 将Minstrel类设置为切面 -->
<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>
通过运行结果可以发现,在没有改动BraveKnight的代码的情况下,就完成了Minstrel对其的歌颂,而且BraveKnight并不知道Minstrel的存在。
4. 通过切面和模板减少样本式代码
使用Spring模版可以消除很多样板式代码,比如JDBC、JMS、JNDI、REST等。