Spring是什么啊 O_O
春天?emm…我现在所说的Spring是指一个开源框架
PS!!!从现在开始,一定要很熟悉的知道下面这些英文单词首字母组成的简称,后面不再做解释
EJB:Enterprise JavaBean ——企业级JavaBean
JDO:Java Data Object ——Java数据对象
POJO:Plain Old java Object ——简单老式Java对象
DI:Dependency Injection ——依赖注入
AOP:Aspect-Oriented Programming ——面向切面编程
Spring到底是什么?它用来干嘛呢?
Spring是为了解决企业级应用开发的复杂性而创建 的,使用Spring可以让简单的JavaBean来实现之前只有EJB和其他企业级Java规范才能完成的事情。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型,它增强了POJO的功能。
但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从Spring中获益。
Spring的目标是全方位简化Java开发,它采取了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模版减少样板式代码
Spring如何达到它的目的(简化Java开发)?
激发POJO的潜能
在基于Spring构建的应用中,它的类通常没有任何痕迹表明我们使用Spring。也就是说,Spring会竭力避免因自身的API而弄乱我们的应用代码,不会强迫我们实现Spring规范的接口或继承Spring规范的类。
尽管我们可能只是写了一个形式非常简单的POJO,Spring也会发挥它的作用让POJO一样可以具有魔力——通过DI来装配它们。也就是说,Spring可以激发POJO的潜能。
可能类里面使用了Spring的注解,但是去掉注解,它仍然是一个普通的Java类。
依赖注入(DI)
依赖注入已经演变成一项复杂的编程技巧或设计模式理念,可以帮助应用对象彼此之间保持松散耦合。应用DI可以让我们的代码变得异常简单并且更容易理解和测试。
耦合涉及到什么?
耦合具有两面性,一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且bug也是一个接一个;另一方面,一定程度的耦合又是必须的,完全没有耦合的代码什么也做不了。所以耦合性高的代码将会给开发者的测试和维护带来巨大的麻烦,因为它们往往牵一发而动全身。但为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。
Spring就解决了这个关键的问题,它将对象之间的依赖关系转而用配置文件来管理。也就是它的依赖注入机制。
依赖注入机制就是面向接口编程,对象通过接口来表明依赖关系。这就是依赖注入机制带来的最大的收益——松耦合。
如果一个对象至通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
接下来我们以经典的骑士例子来说明依赖注入机制.
接下来我们来看一个骑士的例子:
编写一个类来实现“勇敢的骑士要去拯救被绑架的少女”。
1.1 一个骑士的接口
public interface Knight {
public void embarkOnQuest();
//embarkOnQuest()方法为骑士开始执行任务。
}
这个骑士现在要去拯救少女啦~
所以我们要实现拯救少女这个动作的类
1.2 拯救少女的动作
public class RescueDamselQuest {
public void embark() {
System.out.println("骑士去拯救少女啦!");
}
}
再实现拯救少女的骑士
1.3 : 骑士去拯救少女
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
我们可以从代码中看到,DamselRescuingKnight在它的构造函数里自行创建了RescueDamselQuest。这使得DamselRescuingKnight和RescueDamselQuest紧密耦合到了一起,也就是说如果一个少女需要救援,这个骑士能够召之而来,但是如果去做其他的活动,这个骑士就无能为力了,极大地限制了骑士执行探险的能力。
骑士应该是无所不能的,接着我们对上面的代码进行改动。
首先我们把骑士要执行的任务抽象为一个接口。
1.4 : 骑士要执行的任务接口Quest
public interface Quest {
public void embark();
//embark()方法代表开始执行任务
}
然后我们让拯救少女这个任务继承Quest接口
1.5:可以执行各种任务的勇敢骑士
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) { //Quest被注入进来
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}
这个勇敢的骑士不像上一个骑士,没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是DI的方式之一,即构造器注入。
为了验证程序1.5中Quest是否成功注入,我们使用使用mock测试来测试一下。
mock:在测试过程中,对于某些不容易获取的对象,用一个虚拟的对象来创建以便测试的方法。
1.6 为了测试BraveKnight,需要注入一个mock Quest
import static org.mockito.Mockito.*;
import org.junit.Test;
public class BraveKnightTest{
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class);
//创建mock Quest
BraveKnight knight = new BraveKnight(mockQuest);
//注入mock Quest
Knight.embarkQuest();
//times(1): 在上述条件下, 验证embark()是否只被调用了一次
}
}
这个勇敢的骑士现在不止可以拯救少女,还可以斩杀恶龙。
1.7 斩杀恶龙的任务
public class SlayDragonQuest implements Quest {
private PrintStream stream;
//这里并没有直接指定输出的格式, 输出格式由用户在构造方法中决定
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("骑士在斩杀恶龙!");
}
}
现在SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。
可是,我们要怎样将SlayDragonQuest交给BraveKnight呢?
这就使用到了依赖注入机制的装配(wiring)。
创建应用组件之间协作的行为通常称为装配。Spring有多种装配bean的方式,比如使用XML,或者基于Java的配置。接下来我们来装配SlayDragonQuest。
1.8 knight.xml 将SlayDragonQuest、BraveKnight和PrintStream装配在一起
<?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">
<!--创建SlayDragonQuest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
<!--注入Quest bean-->
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
</beans>
PS!!!XML配置文件一定要放在resources目录下
BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了对SlayDragonQuest bean的引用,将其作为构造器参数。
SlayDragonQuest bean的声明中使用了Spring表达式语言(SpEL),将System.out传入到了SlayDragonQuest的构造器中。
- 在SpEL中,使用T()运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math类,我们可以像下面的示例这样使用T()运算符:
T(java.lang.Math)
T()运算符的结果会返回一个java.lang.Math类对象。
Spring还支持使用Java来描述配置。
1.9 Spring提供了基于Java的配置,可作为XML的替代方案
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Queue quest() {
return new SlayDragonQuest(System.out);
}
}
不管使用XML还是Java配置,DI所带来的收益都是相同的。这样我们就可以在不改变所依赖的类的情况下,修改依赖关系。
现在已经声明了BraveKnight和SlayDragonQuest的关系,接下来只需要装载XML配置文件,并把应用启动起来。
Spring通过应用上下文(Application Context)装载Bean的定义并把它们组装起来,Spring应用上下文全权负责对象的创建和组装。
Spring提供了多种Application Context,可列举如下:
- AnnotationConfigApplicationContext——从Java配置文件中加载应用上下文
- AnnotationConfigWebApplicationContext——从Java配置文件中加载Spring web应用上下文
- ClassPathXmlApplicationContext——从classpath(resources目录)下加载XML格式的应用上下文定义文件
- FileSystemXmlApplicationContext——从指定文件系统目录下加载XML格式的应用上下文定义文件
- XmlWebApplicationContext——从classpath(resources目录)下加载XML格式的Spring web应用上下文
现在假设我们加载的为XML配置文件,那么应该使用ClassPathXmlApplicationContext来加载knight.xml。
1.10 : KnightMain.java加载包含Knight的Spring上下文
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath*:knight.xml");
Knight knight = (Knight) context.getBean("knight");
//获取Knight Bean
knight.embarkOnQuest();
//使用knight
context.close();
}
}
这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。
通过这个例子,我们大概了解了什么是DI,以及它的作用。现在我们再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。
应用切面(AOP)
DI能让相互协作的软件组件保持松散耦合,而AOP允许我们把遍布应用各处的功能分离出来形成可重用的组件。
系统由许多不同的组件组成,每一个组件各负责一块特定功能。而AOP往往被定义为促使软件系统实现关注点分离的一项技术。
可以说AOP是OOP的补充和完善。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,核心应用甚至根本不知道它们的存在。
为了示范Spring中如何应用切面,让我们重新回到骑士的例子,并为它添加一个切面。
我们现在能够知道骑士所做的事情,是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。所以我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。
1.11 吟游诗人类
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() { //探险前调用
stream.println("啦啦啦~骑士真勇敢啊!");
}
public void singAfterQuest() { //探险后调用
stream.println("哈哈哈~勇敢的骑士执行任务回来啦!");
}
}
接下来让我们将BraveKnight和Minstrel进行结合,让诗人传唱骑士的事迹。
1.12 修改BarveKnight,让它调用Minstrel方法
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrell;
public BraveKnight(Quest quest, Minstrel minstrel) { //Quest被注入进来
this.quest = quest;
this.minstrell = minstrel;
}
public void embarkOnQuest() {
minstrell.singBeforeQuest();
quest.embark();
minstrell.singAfterQuest();
}
}
好像完成了?
但是再仔细想想,管理吟游诗人是骑士应该做的事情吗?并不是,传唱事迹是吟游诗人的职责。简单的代码开始变得复杂…
但利用AOP,将Minstrel抽象为一个切面,在Spring配置文件中声明它,就可以解决这个问题。
1.13 修改knight.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" 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">
<!--创建SlayDragonQuest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
<!--注入Quest bean-->
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean>
<aop:config>
<aop:aspect ref="minstrel">
<!--定义切点, 即定义从哪里切入-->
<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>
通过XML配置,就把Minstrel声明为一个Spring切面了。但Minstrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。
使用模版消除样板式代码
我们通常为了实现通用的和简单的任务,会写一些样板式的代码。样板式代码的一个常见范例就是使用JDBC访问数据库查询数据。
1.14 查询数据库获得员工姓名和薪水
public Employee getEmployeeById(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.preparedStatement(
"select id, firstname, lastname, salary from employee where id=?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
Employee employee = null;
if (rs.next()) {
employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
employee.setSalary(rs.getBigDecimal("salary"));
}
return employee;
} catch (SQLException e) {
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
}
}
return null;
}
可以看到,只有少量的代码与查询员工逻辑有关系,其他的代码都是JDBC的样板代码。
Spring旨在通过模版封装来消除样板式代码,Spring的JdbcTemplate使得执行数据库操作时,可以避免传统的JDBC样板式代码。
1.15 : 使用Spring模板消除样板式代码
public Employee getEmployeeById(long id) {
//SQL查询
return jdbcTemplate.queryForObject(
"select id, firstname, lastname, salary from employee where id=?",
new RowMapper<Employee>() {
//将结果匹配为对象
public Employee mapRow(ResultSet rs, int int rowNum) throws SQLException {
Employee employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
employee.setSalary(rs.getBigDecimal("salary"));
employee.setName(resultSet.getString("name"));
return employee;
}
}, id); //指定查询参数
}
这样使用起来就简单多了。
以上就是Spring通过面向POJO编程、DI、AOP和模板技术来简化Java开发的简单介绍。