spring boot 继承抽象类 构造函数注入_Spring实战1:Spring初探

现在的Java程序员赶上了好时候。在将近20年的历史中,Java的发展历经沉浮。尽管有很多为人诟病的产品,例如applets、EJB、Java Data Object(JDO)和数不清的日志框架,Java还是发展为一个庞大且丰富的开发平台,很多企业级应用都是基于JVM平台构建。Spring是JVM开发平台中的一颗明珠。

Spring最开始出现的目的是替代企业级开发框架EJB,相比EJB,Spring提供更轻量和更易用的编程模型。Spring的重要特点是非侵入式增强POJO(plain old java object)的能力。

在后续的发展过程中,EJB也效仿Spring的做法提供了简单的以POJO为中心的编程模型,现在的EJB框架也拥有依赖注入(DI)和面向切面编程(AOP)能力,可以论证是受Spring成功的影响。

尽管J2EE一直在追赶Spring的发展,但是Spring本身也没有停止进步。现在,Spring在一些J2EE刚刚涉入或者完全没有涉入的领域飞速发展:移动开发、社交API整合、NoSQL数据库、云计算和大数据。就目前来看,Spring的未来一片光明。

重要的事情再强调一遍:现在的Java程序员赶上了好时候。

这篇文章会从一个比较高的层次探索Spring,介绍Spring框架解决了哪些主要问题。

1.1 简化Java开发

Spring是一种开源框架,由Rod Johnson发明,并在其著作《Expert One-on-One:J2EE设计与开发》。Spring的初衷是降低企业级开发的复杂性,并试图通过POJO对象实现之前EJB这类重型框架才能实现的功能。Spring不仅仅对服务端开发有用,任何Java应用都可受益于Spring的简洁、易测试和低耦合等特性。

Spring框架中使用beansJavaBeans来表示应用程序中的组件,但这并不意味着该组件必须严格满足Java Bean的规范。

Spring做了很多事情,但是归根到底是一些基本的思路,而所有这些思路最终都导向Spring的使命:简化Java开发

Spring通过下列四种策略来简化Java开发:

  • 基于POJO的轻量级、最小侵入式开发;
  • 通过依赖注入和面向接口编程实现松耦合;
  • 通过面向切面编程和惯例实现声明式编程;
  • 通过面向切面编程和模板消除样板式代码(boierplate code)

几乎Spring的每条特性都可以追溯到这四条策略之一,接下来分别对这四条策略进行阐述,并给出具体的代码说明Spring如何简化Java开发。

1.1.1 激发POJO的能力

如果你做Java开发足够久,你应该遇到过很多会束缚程序员能力的开发框架,这些框架要求程序员继承框架提供的类或者实现它提供的接口,例如EJB框架中的session beans,另外,在EJB之前的很多框架中也有类似的侵入式编程模型,如Struts、WebWork、Tapestry等等。

Spring尽量避免让自己的API污染你的应用代码。Spring几乎不会强制要求开发人员实现某个Spring提供的接口或者继承某个Spring提供的类,在Spring应用中的Java类看起来和普通类一样,不过,Spring现在经常使用注解来修饰Java类,但是这个类还是一个POJO。

举个代码例子说明,看如下的HelloWorldBean

package com.spring.sample;public class HelloWorldBean { public String sayHello() { return "Hello World"; }}

可以看出,这就是一个简单的Java类-POJO,没有什么特殊的标志表明它是一个Spring组件。Spring这种非侵入式编程模型使得这个类在Spring和非Spring框架下具备相同的功能。

尽管形式非常简单,POJO的能力值却可能非常高,例如Spring可以通过依赖注入编织这些POJOs来激发POJO的能力。

1.1.2 依赖注入

依赖注入听起来比较吓人,貌似一种非常复杂的编程技术或者设计模式。实际上依赖注入并不复杂,通过在工程中应用依赖注入技术,可以得到更简单、更容易理解和测试的代码。

How DI works

除了Hello-world级别的程序,稍微复杂一点的Java应用都需要多个类配合实现功能。一般而言,每个类自己负责获取它要合作的类对象的引用,这会导致代码高度耦合且难以测试。

首先看如下代码:

package com.spring.sample.knights;public class DamselRescuingKnight implements Knight { private RescueDamselQuest quest; public DamselRescuingKnight() { this.quest = new RescueDamselQuest(); //与RescueDamselQuest紧耦合 } public void embarkOnQuest() { quest.emark(); }}

可以看出,DamselRescuingKnight在它的构造函数中创建了自己的Quest实例——RescueDamselQuest实例,这使得DamselRescuingKnightRescueDamselQuest紧密耦合,如果需要刺杀Damsel,则这个刀可以使用,但是如果需要刺杀恐龙,则这个刀就派不上用场了。

更糟的是,给DamselRescuingKnight写单元测试很不方便,在这个测试中,你必须确认:当调用knight的emarkOnQuest函数时,quest的embark函数也正确调用,但这并不容易。

耦合是一头双头怪:一方面,紧耦合的代码难以测试、难以复用并且难以理解,并且经常陷入“修复一个bug但引入一个新的bug”的开发怪圈中;另一方面,应用程序必须存在适当的耦合,否则该应用无法完成任何功能。总之,耦合是必要的,但是应该控制组件之间的耦合程度。

通过使用依赖注入(DI)技术,对象之间的依赖关系由Spring框架提供的容器进行管理,而不需要某个对象主动创建自己需要的引用,如下图所示:

39852d31f9ce34f319bc6288059aff57.png

依赖注入的作用

再看一个BraveKnight类的例子:

package com.spring.sample.knights;public class BraveKnight implements Knight { private Quest quest; public BraveKnight(Quest quest) { // Quest实例被注入 this.quest = quest; } public void embarkOnQuest() { quest.emark(); }}

该对象不再局限于一种quest实例,在构造过程中利用构造函数的参数传入quest实例,这种类型的依赖注入称为构造注入

还有一点需要注意,使用接口定义quest实例,这就是面向接口编程,使得BraveKnight不再局限于某种特定的Quest实现,这就是DI带来的最大的好处——松耦合。

实现依赖注入

在上述例子代码可以看出,Spring相当于将依赖注入的位置从BraveKnight类中剥离出来,那么具体的依赖注入代码如何写呢?开发人员如何规定给BraveKnight注入哪个Quest实现,例如SlayDragonQuest

package com.spring.sample.knights;import java.io.PrintStream;public class SlayDragonQuest implements Quest { private PrintStream stream; public SlayDragonQuest(PrintStream stream) { this.stream = stream; } public void emark() { stream.println("Embarking on quest to slay the dragon!"); }}

在Spirng框架中,最通用的方法是通过写XML配置文件来定义组件之间的依赖关系,如下所示:

<?xml version="1.0" encoding="UTF-8"?>

在这个xml配置文件中分别定义了BraveKnightSlayDragonQuest两个bean:在BraveKnightbean的定义中,通过构造器函数传入一个SlayDragonQuest的引用;在SlayDragonQuest的定义中,通过SpEL语言将System.out传入它的构造函数。

Spring 3.0引入了JavaConfig,这种写法比xml文件的好处是具备类型安全检查,例如,上面XML配置文件可以这么写:

package com.spring.sample.knights.config;import com.spring.sample.knights.BraveKnight;import com.spring.sample.knights.Knight;import com.spring.sample.knights.Quest;import com.spring.sample.knights.SlayDragonQuest;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class KnightConfig { @Bean public Knight knight() { return new BraveKnight(quest()); } @Bean public Quest quest() { return new SlayDragonQuest(System.out); }}

不论是基于XML的配置还是基于Java文件的配置,都由Spring框架负责管理beans之间的依赖关系。

启动依赖注入

在Spring应用中,由application context负责加载beans,并将这些beans根据配置文件编织在一起。Spring框架提供了几种application context的实现,如果使用XML格式的配置文件,则使用ClassPathXmlApplicationContext;如果使用Java文件形式的配置文件,则使用AnnotationConfigApplicationContext

package com.spring.sample.knights;import com.spring.sample.knights.config.KnightConfig;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class KnightMain { public static void main(String[] args) {// ClassPathXmlApplicationContext context =// new ClassPathXmlApplicationContext("classpath:/knight.xml"); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(KnightConfig.class); Knight knight = context.getBean(Knight.class); knight.embarkOnQuest(); context.close(); }}

上述代码中,根据KnightConfig.java文件创建Spring应用上下文,可以把该应用上下文看成对象工厂,来获取idknight的bean。

如果你想了解更多关于DI的知识,可以查看Dhanji R. Prasanna's Dependency Injectionhttps://www.manning.com/books/dependency-injection一书。

1.1.3 切面编程

依赖注入(DI)实现了模块之间的松耦合,而利用面向切面编程(AOP)可以将涉及整个应用的基础功能(安全、日志)放在一个可复用的模块中。

AOP是一种在软件系统中实现关注点分离的技术。软件系统由几个模块构成,每个模块负责一种功能,不过在系统中有些需求需要涉及到所有的模块,例如日志、事务管理和安全等。如果将这些需求相关的代码都分散在各个模块中,一方面是不方便维护、另一方面是与原来每个模块的业务逻辑代码混淆在一起,不符合单一职责原则。

  • 实现系统级别处理的代码分散在多个子模块中,这意味着如果要修改这些处理代码,则要在每个模块中都进行修改。即使将这些代码封装到一个模块中,在没给个子模块中只保留对方法的调用,这些方法调用还是在各个模块中重复出现。
  • 业务逻辑代码与非核心功能的代码混淆在一起。例如,一个添加address book的方法应该只关心如何添加address book,而不应该关心该操作是否安全或者是否能够实现事务处理。

下面这张图可以体现这种复杂性,左边的业务逻辑模块与右边的系统服务模块沟通太过密切,每个业务模块需要自己负责调用这些系统服务模块。

c4f6cd544d2e24bfd42877388535267a.png

业务逻辑模块与系统服务模块过度交互

AOP可以模块化这些系统服务,然后利用声明式编程定义该模块需要应用到那些业务逻辑模块上。这使得业务模块更简洁,更专注于处理业务逻辑,简而言之,切面(aspects)确保POJO仍然是普通的Java类。

可以将切面想象为覆盖在一些业务模块上的毯子,如下图所示。在系统中有一些模块负责核心的业务逻辑,利用AOP可以为所有这些模块增加额外的功能,而且核心业务模块无需知道切面模块的存在。

dde44863c63d1bb6c52e942a73727c62.png

切面就像毯子一样覆盖在几个核心业务模块之上

AOP实践

继续上面的例子,如果需要一个人记录BraveKnight的所作所为,下面代码是该日志服务:

package com.spring.sample.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!"); }}

然后在XML文件中定义Minstrel对应的切面:

<?xml version="1.0" encoding="UTF-8"?>

在这个配置文件中增加了aop配置名字空间。首先定义Minstrel的bean,然后利用标签定义aop相关的配置;然后在节点中引用minstrel,定义方面;aspect负责将pointcut和要执行的函数(before、after或者around)连接在一起。

还有一种更先进的写法,利用注解和Java配置文件,可以参考aop docs

Spring框架中的一些子模块也是基于AOP实现的,例如负责事务处理和负责安全的模块。

1.1.4 使用模板消除重复代码

在编程过程中有没有感觉经常需要写重复无用的代码才能实现简单的功能,最经典的例子是JDBC的使用,这些代码就是样板式代码(boilerplate code)。

以JDBC的使用举个例子,这种原始的写法你一定见过:

public Employee getEmployeeById(long id) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = dataSource.getConnection(); stmt = conn.prepareStatement("select id, name 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.setName(rs.getString("name")); } 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的接口,其他JMS、JNDI以及REST服务的客户端API等也有类似的情况出现。

Spring试图通过模板来消除重复代码,这里所用的是模板设计模式。对于JDBC接口,Spring提供了JdbcTemplate模板来消除上面那个代码片段中的样板式代码,例子代码如下:

public Employee getEmployeeById(long id) { return jdbcTemplate.queryForObject( "select id, name from employee where id=?
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页