spring 第一篇(1-1):让java开发变得更简单(上)

时间:2022-01-04 07:51:35

1.释放POJOS能量

  传统开发中是如何束缚POJOS呢,如果你开发过java很长时间,那你一定有接触过EJB的开发。那时候开发一个小小的功能都要扩展框架的类或者实现其接口。所以你很容易在早期的Struts,WebWork,Taperstry等框架中看到侵入到你应用中的框架代码。

  spring尽可能避免在你的应用中充满它的API.spring从来都不强迫你实现具体的spring接口或者扩展一个具体的spring类。替代的是,在基于spring开发的应用中经常没有指示说明你正在使用spring。最糟的情况是,类中使用spring注解,不过它还是一个POJO。下面是一个POJO实例

public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}

  如你所见,这是一个简单的,普通的java类 ---- 一个POJO。没有任何指示可以看出它是spring的一个组件。spring的非侵略性编程模型意味着这个类在spring应用和非spring应用中表现出的功能是一样的。

  尽管它们的表现形式很简单,但POJO却是很强大的。spring使它们变得强大的一种方式是通过DI来组装。下面我们来看下DI如何让应用中的对象彼此间保持解耦的。

2.依赖注入

  依赖注入这个短语听起来可能很吓人,好像一个复杂编程技术或者设计模式概念那样神秘。但是事实正好相反,DI不像它听起来的那么复杂。通过在你的项目中应用DI,你会发现你的代码会变得非常的简单,容易理解和测试。

  DI如何工作?

  任何一个重要的应用(比起hello world复杂的多)都是由两个或者更多的类进行协作来执行一系列业务逻辑。传统上,每个对象都有责任去获取它所依赖的对象。这样就会导致高度耦合并且很难去进行代码测试。下面是一个例子

public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}

  如你所见,在DamselRescuingKnight构造器中创建了quest,一个RescueDamselQuest,这导致了DamselRescuingKnight和RescueDamselQuest紧紧的耦合在一起并且限制了quest-embarking的操作。更糟糕的是,你很难去为这个类写单元测试。在这样的一个测试中,你希望能够在embarkOnQuest()方法被调用的时候能够评估quest's embark()方法被调用。显然这里没有明显的方法来实现那个。很不幸,DamselRescuingKnight会变得不可测试。

  耦合是一个双头怪物。一方面,紧耦合代码很难去测试,很难去复用和理解,它是打鼹鼠游戏的典型展现(修复一个会导致一个或者更多的bug出现)。另一方面,一定量的耦合也是必要的。完全解耦的代码什么也做不了。为了能够实现有用的功能,类必须以某种方法知道其他类的一些情况。耦合是必须的但是应当小心管理。

  使用DI,对象的依赖的创建将会由第三方来协调处理。对象将不再期待创建或者获取它们的依赖,如图1.1所示,依赖将会被注入到需要的对象上。

spring 第一篇(1-1):让java开发变得更简单(上)

  下面我们来看一下BraveKnight例子

public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}

  如你所见,BraveKnight,不像DamselResucingKnight那样,没有创建它自己的quest。取而代之,将quest作为构成参数传进来。这种类型的DI我们称为构造器注入。

  而且,quest是作为一个接口,而不是一个具体的实现。这样所有实现quest的接口都可以用在这里。BraveKnight关键点是不和任何Quest具体实现耦合。它不关心哪个类型的quest的embark被调用。只要它实现Quest接口就行。这就是DI最重要的好处 ---- 解耦。

  在测试期间最常见的做法是将依赖包装成一个mock实现。你可能由于紧耦合不能充分测试DamselResucingKnight,但是却能很容易通过一个mock实现来测试BraveKnight。如下所示

public class BraveKnightTest {
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class);
BraveKnight knight = new BraveKnight(mockQuest);
knight.embarkOnQuest();
verify(mockQuest, times(1)).embark();
}
}

  这里使用Mockito框架来mock对象,伴随着mock对象,你创建一个新的BraveKnight实例,通过构造器来注入到mock Quest。随后调用embarkOnQuest()方法,询问Mockito去验证mock Quest's embark()方法是否调用一次。下面是一个具体Quest实现

public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}

  如你所见,SlayDragonQuest是Quest具体实现,但是你可能注意到在Quest的构造器中还有个PrintStream对象,你可能会有疑问,如何将SlayDragonQuest给BraveKnight?还有PrintStream如何给SlayDragonQuest?

  创建相关应用组件的行为通常被叫做装配(wiring)。在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="com.springinaction.knights.BraveKnight">
  <constructor-arg ref="quest" />
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
  <constructor-arg value="#{T(System).out}" />
</bean>
</beans>

  在这里,BraveKnight和SlayDragonQuest被声明为Spring的bean。在BraveKnight bean的例子中,将一个SlayDragonQuest bean的引用作为构造器参数传到BraveKnight构造器中。同时,SlayDragonQuest bean声明使用Spring表达式语言来传递System.out到SlayDragonQuest’s构造器中。

  如果XML配置不合适你的胃口,你可能会喜欢使用java来达到相同的目的

  

@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}

  这段代码和上面的XML配置等价。这个例子简单的展示了spring中如何装配bean的。你现在不用太担心这些细节,在以后的章节中将会深入来了解这些配置。现在一切都准备好了,只差如何运行这个应用。

  在Spring应用中,application context主要用来加载bean定义并且将它们装配在一起。Spring  application context全部的责任就是用来创建和装配对象,并以此来构造应用。Spring自带有几种application context实现,主要不同在于如何加载它的配置。

  当使用XML文件来声明beans时候,一个恰当的application context选择可能是ClassPathXmlApplicationContext。这个Spring context实现从应用的类路径中一个或者多个XML文件来加载Spring context。下面是个具体例子

public class KnightMain {

    public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext(
"META-INF/spring/knight.xml");
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}

  在这个main()方法中通过knight.xml中来加载Spring application context。然后将application context当作一个工厂来获取ID为knight的bean。最后从一个引用为Knight的对象来调用embarkOnQuest方法。注意到这个类根本不知道Quest的具体实现,只有knights.xml文件才知道具体实现。