一.传统Java开发弊端
在传统的开发之中,任何一个有实际意义的应用都会由两个或更多的类所组成,这些类之间相互协调来完成特定的业务逻辑,按照传统的做法,每个对象负责管理与自己相互协作的对象(即他所依赖的对象)的引用,这将会导致高耦合度和难以测试的代码.
--给出一个示例代码:分派一个拯救公主的骑士去完成拯救公主的任务:
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class RescueDamselQuest { //拯救公主的任务
8 public void embark(){
9 System.out.println("骑士执行拯救公主的任务");
10 }
11 }
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class DamselRescuingKnight implements Knight { //拯救骑士的公主
8 private RescueDamselQuest quest;
9
10 public DamselRescuingKnight() {
11 this.quest = new ResuceDamselQuest();
12 }
13 public void embarkOnQuest(){
14 quest.embark();
15 }
16 }
--在DamselRescuingKnight的构造方法之中自行创建了一个RescueDamselQuest的实例,这样一来使得DamselRescuingKnight和RescueDamselQuest类就紧密的耦合到了一起,因此极大的限制了这个骑士探险的能力.如果骑士需要去拯救这个公主,那么尚且可以完成,如果需要这个骑士去完成杀掉恶龙等等的任务,就显得有些困难了.同时对于DamselRescuingKnight这个类,编写单元测试也会十分的困难.
--我们首先需要知道耦合具有两面性:
1.紧密耦合的代码难以测试,难以复用,难以理解.并且在修改BUG的时候很容易造成修改一个BUG后出现多个BUG的情况(俗称打地鼠式的BUG特性).
2.在另一方面,一定程度的耦合又是必须的,完全没有耦合的代码什么也做不了.为了完成有实际意义的功能,不同的类必须以适当的方式进行交互.总而言之,耦合是必须的,但应当被小心谨慎的管理.
二.Spring开源框架简介
Spring是一个开源框架,最早由Rod Johnson 创建.Spring是为了解决企业级应用开发的复杂性而建立的,但Spring不仅限于服务器端的开发.为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
基于POJO的轻量级和最小入侵性编程;
通过依赖注入(DI)和面向接口编程实现松耦合;
基于切面和惯例进行声明式编程;
通过切面(AOP)和模板减少样板式代码;
--修改上述范例代码,使得BraveKnight可以灵活接收任何形式的探险任务:
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public interface Quest {
8 /**
9 * 执行任务
10 */
11 public void embark();
12 }
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class RescueDamselQuest implements Quest{ //拯救公主的任务
8 @Override
9 public void embark(){
10 System.out.println("骑士执行拯救公主的任务");
11 }
12 }
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class BraveKnight implements Knight {
8 private Quest quest;
9
10 public BraveKnight(Quest quest) {
11 this.quest = quest;
12 }
13 public void embarkOnQuest(){
14 quest.embark();
15 }
16 }
--DamselRescuingKnight在他的构造函数中并没有自行的创建一个RescueDamselQuest的实例,而是将Quest接口作为了参入的参数类型,在进行代码开发的时候,所有的任务RescueDamselQuest(拯救公主),SlayDragonQuest(杀死恶龙)都应当是实现Quest接口来进行的.这样一来BraveKnight并没有与任何特地的Quest实例发生耦合.那么此时,如果SlayDragonQuest的实现是这样情况:
1 package knights;
2
3 import java.io.PrintStream;
4
5 /**
6 * @author : S K Y
7 * @version :0.0.1
8 */
9 public class SlayDragonQuest implements Quest {
10 private PrintStream stream;
11
12 public SlayDragonQuest(PrintStream stream) {
13 this.stream = stream;
14 }
15
16 @Override
17 public void embark() {
18 stream.println("骑士接收到任务去击败恶龙");
19 }
20 }
--那么此时我们如何将SlayDragonQuest交给BraveKnight,又如何将PringStream交给SlayDragonQuest呢:创建组件之间协作的行为通常称为装配(wiring).在Spring之中有多种的装配bean的方式,在此,演示采用xml进行装配的过程(在后文会介绍进行相关Spring配置所需要导入的jar包),创建Spring配置文件knights.xml:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
5 <bean id="knight" class="knights.BraveKnight">
6 <!--将id为quest的bean注入到knight中-->
7 <constructor-arg ref="quest"/>
8 </bean>
9
10 <bean id="quest" class="knights.SlayDragonQuest">
11 <constructor-arg value="#{T(System).out}"/>
12 </bean>
13 </beans>
--在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean.就BraveKnight bean来讲,他在构造方法之中注入对SlayDragonQuest bean的引用,将其作为构造参数.而SlayDragonQuest bean中使用Spring表达式语言(Spring Expression language),将System.out传入到了SlayDragonQuest的构造器之中,当然也可以根据Java来配置:
1 package knights;
2
3 import org.springframework.context.annotation.Bean;
4 import org.springframework.context.annotation.Configuration;
5
6 /**
7 * @author : S K Y
8 * @version :0.0.1
9 */
10 @Configuration
11 public class KnightConfig {
12 @Bean
13 public Knight knight() {
14 return new BraveKnight(quest());
15 }
16
17 @Bean
18 public Quest quest() {
19 return new SlayDragonQuest(System.out);
20 }
21 }
--范例:通过XML配置文件装载配置:
1 package knights;
2
3 import org.springframework.context.support.ClassPathXmlApplicationContext;
4
5 /**
6 * @author : S K Y
7 * @version :0.0.1
8 */
9 public class ApplicationContext {
10 public static void main(String[] args) {
11 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("knights.xml");
12 Knight knight = context.getBean("knight", Knight.class); //通过bean的id来获取Bean装配对象
13 knight.embarkOnQuest();
14 context.close();
15 }
16 }
--运行结果
九月 06, 2019 8:15:03 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@179d3b25: startup date [Fri Sep 06 20:15:03 CST 2019]; root of context hierarchy
九月 06, 2019 8:15:03 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [knights.xml]
骑士接收到任务去击败恶龙
九月 06, 2019 8:15:03 下午 org.springframework.context.support.ClassPathXmlApplicationContext doClose
信息: Closing org.springframework.context.support.ClassPathXmlApplicationContext@179d3b25: startup date [Fri Sep 06 20:15:03 CST 2019]; root of context hierarchy
Process finished with exit code 0
--红色的文字为日志输出打印信息,可以看到在我们的代码之中,完全不知道这个Questr任务是由BraveKnight执行的,也不知道执行的是哪个任务,这些信息只有knights.xml才能明确.
三.应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件.例如下图所示:左边的业务对象和系统级服务结合的过于紧密,每个对象不但要知道他需要记录日志,进行安全控制和参与事务,还要亲自执行这些服务:
--而使用AOP编程则可以使这些服务模块化,并以声明的方式将他们应用到他们需要影响的组件中去.这样一来这些组件完全不需要了解设计系统服务所带来的复杂性(高内聚,低耦合).可以把切面想象成覆盖在很多组件之上的一个外壳.应用是由那些实现各自业务功能的模块组成:
--以上述骑士的例子举例.,每一个人都熟知骑士所做的任何事情,是因为吟游诗人用诗词记载了骑士的事迹并将其进行传唱.假设我们需要使用吟游诗人这个服务来记载骑士的所有事迹,我们给出一个吟游诗人的类:
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class Minstrel {
8 public void singBeforeQuest(){ //探险之前调用
9 System.out.println("这个骑士是那么的勇敢,");
10 }
11 public void singAfterQuest(){ //探险之后调用
12 System.out.println(",他的功绩值得我们赞赏");
13 }
14 }
--按照传统的流程,我们将这样实现吟游诗人的相关方法调用
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class BraveKnight implements Knight {
8 private Quest quest;
9 private Minstrel minstrel;
10
11 public BraveKnight(Quest quest) {
12 this.quest = quest;
13 }
14
15 public BraveKnight(Quest quest, Minstrel minstrel) {
16 this.quest = quest;
17 this.minstrel = minstrel;
18 }
19
20 @Override
21 public void embarkOnQuest() {
22 minstrel.singBeforeQuest(); //吟游诗人传唱
23 quest.embark();
24 minstrel.singAfterQuest(); //吟游诗人传唱
25 }
26 }
1 package knights;
2
3
4
5 /**
6 * @author : S K Y
7 * @version :0.0.1
8 */
9 public class ApplicationContext {
10 public static void main(String[] args) {
11 Quest quest = new SlayDragonQuest(System.out);
12 Minstrel minstrel = new Minstrel();
13 Knight knight = new BraveKnight(quest,minstrel);
14 knight.embarkOnQuest();
15
16 }
17 }
--当然,此时调用单参数的构造方法会造成空异常的触发,所以需要进行对空判断,运行结果:
这个骑士是那么的勇敢,
骑士接收到任务去击败恶龙
,他的功绩值得我们赞赏
Process finished with exit code 0
--可以看到正常实现了我们所希望的结果,但是仔细思考BraveKnight的实现,我们可以发现在embarkOnQuest()方法中,我们执行调用了Minstrel的方法,同时又将Minstrel中的实例对象在构造BraveKnight的时候传入了进入,这样一来有点这个骑士硬是拉着一个吟游诗人对他的功绩进行传唱一样,对于吟游诗人来讲,他是否传唱这个骑士,不应该有这个骑士去决定,对他的事迹进行表彰也不应该是这个骑士强迫他完成的.因此,我们可以进MInstrel抽象为一个切面,在knights.xml中声明:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
4 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">
5 <bean id="knight" class="knights.BraveKnight">
6 <!--将id为quest的bean注入到knight中-->
7 <constructor-arg ref="quest"/>
8 </bean>
9
10 <bean id="quest" class="knights.SlayDragonQuest">
11 <constructor-arg value="#{T(System).out}"/>
12 </bean>
13 <bean id="minstrel" class="knights.Minstrel">
14 </bean>
15 <aop:config>
16 <!--定义切点-->
17 <aop:pointcut id="emback" expression="execution(* *.embarkOnQuest(..))"/>
18 <aop:aspect ref="minstrel">
19 <!--定义前置通知-->
20 <aop:before method="singBeforeQuest" pointcut-ref="emback"/>
21 <!--定义后置通知-->
22 <aop:after method="singAfterQuest" pointcut-ref="emback"/>
23 </aop:aspect>
24 </aop:config>
25 </beans>
--这里使用了Spring的aop配置命名空间把Minstrel bean声明为一个切面.首先,需要把Minstrel声明为一个bean,然后在<aop:aspect>元素中引用该bean.为了进一步定义切面,声明在embarkOnQuest()方法之前调用singBeforeQuest()方法,之后调用singAfterQuest()方法,同时在这两个通知中,使用pointcut-ref属性引用了名字为"emback"的切入点,切入点使用<aop:pointcut>元素定义,并且配置expression属性来选择所应用的通知,表达式的语法采用的是AspectJ的切点表达式语言.现在调用执行XML文件查看效果(此时已将BraveKnight中的Minstrel的引用和相关方法都已经删除):
1 package knights;
2
3
4 import org.springframework.context.support.ClassPathXmlApplicationContext;
5
6 /**
7 * @author : S K Y
8 * @version :0.0.1
9 */
10 public class ApplicationContext {
11 public static void main(String[] args) {
12 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("knights.xml");
13 Knight knight = applicationContext.getBean("knight", Knight.class);
14 knight.embarkOnQuest();
15
16 }
17 }
1 package knights;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class BraveKnight implements Knight {
8 private Quest quest;
9
10 public BraveKnight() {
11 }
12
13 public BraveKnight(Quest quest) {
14 this.quest = quest;
15 }
16
17
18 @Override
19 public void embarkOnQuest() {
20 quest.embark();
21 }
22 }
--运行结果
九月 06, 2019 9:13:38 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@49c2faae: startup date [Fri Sep 06 21:13:37 CST 2019]; root of context hierarchy
九月 06, 2019 9:13:38 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [knights.xml]
这个骑士是那么的勇敢,
骑士接收到任务去击败恶龙
,他的功绩值得我们赞赏
Process finished with exit code 0
--可以发现在代码之中完全看不到我们对于Minstrel的调用和实例化,但是Minstrel却正确的完成了他的职能(使用AOP也体现了接口编程的重要性,使用接口我们统一了骑士执行任务的方法为embackOnQuest(),这样一来对于AOP而言,Minstrel想要歌颂骑士的功绩就有了统一的入口).
四.容纳你的Bean
在基于Spring的应用中,你的应用对象生存于Spring容器(container)中,Spring容器负责创建对象,装配他们,配置他们并管理他们的整个生命周期,从生存到死亡(new 到 finalize):
--Spring的容器并不只是一个,Spring自带了多个容器的实现,可以归为两种不同的类型:
1.bean工厂(由org.springframework.beans.factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持.
2.应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件get感兴趣的事件监听者.
--但是bean工厂对于大多数的应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢迎.
五.使用应用上下文
Spring自带了多种类型的应用上下文:
AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文;
AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文;
ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,把应用上下文的定义文件作为类资源;
XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义.
六.bean的生命周期
在传统的Java应用中,bean的生命周期很简单,使用Java关键字new进行bean的实例化,然后该bean就可以使用了,一旦该bean不再被使用,就会由Java的自动垃圾回收机制回收.而Spring容器中的bean的生命周期就更加的复杂,正确理解Spring bean的生命周期,那么我们可以利用Spring提供的扩展点来自定义bean的创建过程,Spring应用上下文的生命周期过程:
1.Spring对bean进行实例化;
2.Spring将值和bean的引用注入到bean对应的属性中;
3.如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法;
4.如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
5.如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文引用传入进来;
6.如果bean实现了BeanPostProcessor接口,Spring将调用它们 的post-ProcessBeforeInitialization()方法;
7.如果bean实现了InitializingBean接口,Spring将调用它们的 after-PropertiesSet()方法。类似地,如果bean使用init- method声明了初始化方法,该方法也会被调用;
8.如果bean实现了BeanPostProcessor接口,Spring将调用它们 的post-ProcessAfterInitialization()方法;
9.此时,bean已经准备就绪,可以被应用程序使用了,它们将一直 驻留在应用上下文中,直到该应用上下文被销毁;
10.如果bean实现了DisposableBean接口,Spring将调用它的 destroy()接口方法。同样,如果bean使用destroy-method声明 了销毁方法,该方法也会被调用。
七.Spring模块
当我们下载Spring发布版本并查看其lib目录时,会发现里面有多个 JAR文件。在Spring 4.0中,Spring框架的发布版本包括了20个不同的 模块,每个模块会有3个JAR文件(二进制类库、源码的JAR文件以及 JavaDoc的JAR文件):
--这些模块为开发企业级应用提供了所需的一切,并且我们可以自由的选择合适自身的应用需求的Spring模块.Spring甚至提供了与其他第三方框架和类库的集成点,这样就无需自己辫编写代码了:
--Spring核心容器
容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、 配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供 了DI的功能。除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如E- mail、JNDI访问、EJB集成和调度。 所有的Spring模块都构建于核心容器之上。
--Spring的AOP模块
这个模块 是Spring应用系统中开发切面的基础。借助于AOP,可以将遍布系统的关注点(例如事务和安 全)从它们所应用的对象中解耦出来。
--数据访问与集成
使用JDBC编写代码通常会导致大量的样板式代码,例如获得数据库 连接、创建语句、处理结果集到最后关闭数据库连接。Spring的JDBC 和DAO(Data Access Object)模块抽象了这些样板式代码,使我们的 数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引 发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义 丰富的异常层,以后我们再也不需要解释那些隐晦专有的SQL错误信 息了. 对于那些更喜欢ORM(Object-Relational Mapping)工具而不愿意直接 使用JDBC的开发者,Spring提供了ORM模块。Spring的ORM模块建立 在对DAO的支持之上,并为多个ORM框架提供了一种构建DAO的简 便方式。Spring没有尝试去创建自己的ORM解决方案,而是对许多流 行的ORM框架进行了集成,包括Hibernate、Java Persisternce API、 Java Data Object和iBATIS SQLMaps(Mybatis)。Spring的事务管理支持所有的 ORM框架以及JDBC。
--Web与远程调用
MVC(Model-View-Controller)模式是一种普遍被接受的构建Web应 用的方法,Spring能够与多种流行的MVC框架进行集成,但它的Web和远程 调用模块自带了一个强大的MVC框架(Spring MVC),有助于在Web层提升应用的松 耦合水平.同时Spring还自带了一个 远程调用框架:HTTP invoker。Spring还提供了暴露和使用REST API 的良好支持。
--Instrumentnation
Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。 具体来讲,它为Tomcat提供了一个织入代理,能够为Tomcat传递类文 件,就像这些文件是被类加载器加载的一样。
--测试
Spring提供了测试模块以致力于Spring应 用的测试。
注:以上内容来自Spring实战(第四版)