开发Spring Boot CLI应用程序
大部分针对 JVM 平台的项目都用 Java 语言开发,引入了诸如 Maven 或 Gradle 这样的构建系统,以生成可部署的产物。实际上,我们在第 2 章开发的阅读列表应用程序就遵循这套模型。
最近版本的 Java 语言有不少改进。然而,即便如此,Java 还是有一些严格的规则为代码增加了不少噪声。行尾分号、类和方法的修饰符(比如 public 和 private)、getter 和 setter 方法,还有 import 语句在 Java 中都有自己的作用,但它们同代码的本质无关,因而造成了干扰。从开发者的角度来看,代码噪声是阻力——编写代码时是阻力,试图阅读代码时更是阻力。如果能消除一部分代码噪声,代码的开发和阅读可以更加方便。
同理,Maven 和 Gradle 这样的构建系统在项目中也有自己的作用,但你还得为此开发和维护构建说明。如果能直接构建,项目也会更加简单。
在使用 Spring Boot CLI 时,没有构建说明文件。代码本身就是构建说明,提供线索指引 CLI 解析依赖,并生成用于部署的产物。此外,配合 Groovy, Spring Boot CLI 提供了一种开发模型,消除了几乎所有代码噪声,带来了畅通无阻的开发体验。
在最简单的情况下,编写基于 CLI 的应用程序就和编写第 1 章里的 Groovy 脚本一样简单。不过,要用 CLI 编写更完整的应用程序,就需要设置一个基本的项目结构来容纳项目代码。我们马上用它重写阅读列表应用程序。
设置CLI项目
我们要做的第一件事是创建目录结构,容纳项目。与基于 Maven 或 Gradle 的项目不同,Spring Boot CLI 项目并没有严格的项目结构要求。实际上,最简单的 Spring Boot CLI 应用程序就是一个 Groovy 脚本,可以放在文件系统的任意目录里。对阅读列表应用程序而言,你应该创建一个干净的新目录来存放代码,把它们和你电脑上的其他东西分开。
$ mkdir readinglist
此处我将目录命名为 readinglist,但你可以随意命名。比起找个地方放置代码,名字并不重要。
我们还需要两个额外的目录存放静态 Web 内容和 Thymeleaf 模板。在 readinglist 目录里创建两个新的目录,名为 static 和 templates。
$ cd readinglist
$ mkdir static
$ mkdir templates
这些目录的名字和基于 Java 的项目中 src/main/resources 里的目录同名。虽然 Spring Boot 并不像 Maven 和 Gradle 那样,对目录结构有严格的要求,但 Spring Boot 会自动配置一个 Spring Resource-HttpRequestHandler 查找 static 目录(还有其他位置)的静态内容。还会配置 Thymeleaf 来解析 templates 目录里的模板。
说到静态内容和 Thymeleaf 模板,那些文件的内容和我们在第 2 章里创建的一样。因此你不用担心稍后无法将它们回忆起来,直接把 style.css 复制到 static 目录,把 readingList.html 复制到 templates 目录即可。
此时,阅读列表项目的目录结构应该是这样的:

现在项目已经设置好了,我们准备好编写 Groovy 代码了。
通过Groovy消除代码噪声
Groovy 本身是种优雅的语言。与 Java 不同,Groovy 并不要求有 public 和 private 这样的限定符,也不要求在行尾有分号。此外,归功于 Groovy 的简化属性语法(GroovyBeans), JavaBean 的标准访问方法没有存在的必要了。
随之而来的结果是,用 Groovy 编写 Book 领域类相当简单。如果在阅读列表项目的根目录里创建一个新的文件,名为 Book.groovy,那么在这里编写如下 Groovy 类。
class Book {
Long id
String reader
String isbn
String title
String author
String description
}
如你所见,Groovy 类与它的 Java 类相比,大小完全不在一个量级。这里没有 setter 和 getter 方法,没有 public 和 private 修饰符,也没有分号。Java 中常见的代码噪声不复存在,剩下的内容都在描述书的基本信息。
既然我们定义好了 Book 领域类,就开始编写仓库接口吧。首先,编写 ReadingList-Repository 接口(位于 ReadingListRepository.groovy):
interface ReadingListRepository {
List<Book> findByReader(String reader)
void save(Book book)
}
除了没有分号,以及接口上没有 public 修饰符,ReadingListRepository 的 Groovy 版本和与之对应的 Java 版本并无二致。最显著的区别是它没有扩展 JpaRepository。本章我们不用 Spring Data JPA,既然如此,我们就不得不自己实现 ReadingListRepository。代码清单5-1就是 JdbcReadingListRepository.groovy 的内容。
@Repository
class JdbcReadingListRepository implements ReadingListRepository {
@Autowired
JdbcTemplate jdbc // 注入JdbcTemplate
List<Book> findByReader(String reader) {
jdbc.query(
"select id, reader, isbn, title, author, description " +
"from Book where reader=?",
{ rs, row ->
new Book(id: rs.getLong(1),
reader: rs.getString(2),
isbn: rs.getString(3),
title: rs.getString(4),
author: rs.getString(5),
description: rs.getString(6))
} as RowMapper, // RowMapper闭包
reader)
}
void save(Book book) {
jdbc.update("insert into Book " +
"(reader, isbn, title, author, description) " +
"values (?, ?, ?, ?, ?)",
book.reader,
book.isbn,
book.title,
book.author,
book.description)
}
}
以上代码的大部分内容在实现一个典型的基于 JdbcTemplate 的仓库。它自动注入了一个 JdbcTemplate 对象的引用,用它查询数据库获取图书(在 findByReader() 方法里),将图书保存到数据库(在 save() 方法里)。
因为编写过程采用了 Groovy,所以我们在实现中可以使用一些 Groovy 的语法糖。举个例子,在 findByReader() 里,调用 query() 时可以在需要 RowMapper 实现的地方传入一个 Groovy 闭包。此外,闭包中创建了一个新的 Book 对象,在构造时设置对象的属性。
在考虑数据库持久化时,我们还需要创建一个名为 schema.sql 的文件。其中包含创建 Book 表所需的 SQL。仓库在发起查询时依赖这个数据表:
create table Book (
id identity,
reader varchar(20) not null,
isbn varchar(10) not null,
title varchar(50) not null,
author varchar(50) not null,
description varchar(2000) not null
);
稍后我会解释如何使用 schema.sql。现在你只需要知道,把它放在Classpath的根目录(即项目的根目录),就能创建出查询用的 Book 表了。
Groovy 的所有部分差不多都齐全了,但还有一个 Groovy 类必须要写。这样 Groovy 化的阅读列表应用程序才完整。我们需要编写一个 ReadingListController 的 Groovy 实现来处理 Web 请求,为浏览器提供阅读列表。在项目的根目录,要创建一个名为 ReadingListController.groovy 的文件,内容如代码清单5-2所示。
@Controller
@RequestMapping("/")
class ReadingListController {
String reader = "Craig"
@Autowired
ReadingListRepository readingListRepository // 注入ReadingListRepository
@RequestMapping(method=RequestMethod.GET)
def readersBooks(Model model) {
List<Book> readingList =
readingListRepository.findByReader(reader) // 获取阅读列表
if (readingList != null) {
model.addAttribute("books", readingList) // 设置模型
}
"readingList" // 返回视图名称
}
@RequestMapping(method=RequestMethod.POST)
def addToReadingList(Book book) {
book.setReader(reader)
readingListRepository.save(book) // 保存图书
"redirect:/" // POST后重定向
}
这个 ReadingListController 和第 2 章里的版本有很多相似之处。主要的不同在于,Groovy 的语法消除了类和方法的修饰符、分号、访问方法和其他不必要的代码噪声。
你还会注意到,两个处理器方法都用 def 而非 String 来定义。两者都没有显式的 return 语句。如果你喜欢在方法上说明类型,喜欢显式的 retrun 语句,加上就好了——Groovy 并不在意这些细节。
在运行应用程序之前,还要做一件事。那就是创建一个新文件,名为 Grabs.groovy,内容包括如下三行:
@Grab("h2")
@Grab("spring-boot-starter-thymeleaf")
class Grabs {}
稍后我们再来讨论这个类的作用,现在你只需要知道类上的 @Grab 注解会告诉 Groovy 在启动应用程序时自动获取一些依赖的库。
不管你信还是不信,我们已经可以运行这个应用程序了。我们创建了一个项目目录,向其中复制了一个样式表和 Thymeleaf 模板,填充了一些 Groovy 代码。接下来,用Spring Boot CLI(在项目目录里)运行即可:
$ spring run .
几秒后,应用程序完全启动。打开浏览器,访问 http://localhost:8080 。如果一切正常,你应该就能看到和第 2 章一样的阅读列表应用程序。
成功啦!只用了几页纸的篇幅,你就写出了简单而又完整的 Spring 应用程序!
此时此刻你也许会好奇这是怎么办到的。
-
没有 Spring 配置,Bean 是如何创建并组装的?JdbcTemplate Bean 又是从哪来的?
-
没有构建文件,Spring MVC 和 Thymeleaf 这样的依赖库是哪来的?
-
没有 import 语句。如果不通过 import 语句来指定具体的包,Groovy如何解析 Jdbc-Template 和 RequestMapping 的类型?
-
没有部署应用,Web 服务器从何而来?
实际上,我们编写的代码看起来不止缺少分号。这些代码究竟是怎么运行起来的?
发生了什么
你可能已经猜到了,Spring Boot CLI 在这里不仅仅是便捷地使用 Groovy 编写了 Spring 应用程序。Spring Boot CLI 施展了很多技能。
-
CLI 可以利用 Spring Boot 的自动配置和起步依赖。
-
CLI 可以检测到正在使用的特定类,自动解析合适的依赖库来支持那些类。
-
CLI 知道多数常用类都在哪些包里,如果用到了这些类,它会把那些包加入 Groovy 的默认包里。
-
应用自动依赖解析和自动配置后,CLI 可以检测到当前运行的是一个 Web 应用程序,并自动引入嵌入式 Web 容器(默认是 Tomcat)供应用程序使用。
仔细想想,这些才是 CLI 提供的最重要的特性。Groovy 语法只是额外的福利!
通过 Spring Boot CLI 运行阅读列表应用程序,表面看似平凡无奇,实则大有乾坤。CLI 尝试用内嵌的 Groovy 编译器来编译 Groovy 代码。虽然你不知道,但实际上,未知类型(比如 JdbcTemplate、Controller 及 RequestMapping,等等)最终会使代码编译失败。
但 CLI 不会放弃,它知道只要把 Spring Boot JDBC 起步依赖加入 Classpath 就能找到 JdbcTemplate。它还知道把 Spring Boot 的 Web 起步依赖加入 Classpath 就能找到 Spring MVC 的相关类。因此,CLI 会从 Maven 仓库(默认为 Maven 中心仓库)里获取那些依赖。
如果此时CLI重新编译,那还是会失败,因为缺少import语句。但CLI知道很多常用类的包。利用定制Groovy编译器默认包导入的功能之后,CLI把所有需要用到的包都加入了Groovy编译器的默认导入列表。
现在CLI可以尝试再一次编译了。假设没有其他CLI能力范围外的问题(比如,存在CLI不知道的语法或类型错误),代码就能完成编译。CLI将通过内置的启动方法(与基于Java的例子里的main()方法类似)运行应用程序。
此时,Spring Boot自动配置就能发挥作用了。它发现Classpath里存在Spring MVC(因为CLI解析了Web起步依赖),就自动配置了合适的Bean来支持Spring MVC,还有嵌入式Tomcat Bean供应用程序使用。它还发现Classpath里有JdbcTemplate,所以自动创建了JdbcTemplate Bean,注入了同样自动创建的DataSource Bean。
说起DataSource Bean,这只是Spring Boot自动配置创建的众多Bean中的一个。Spring Boot还自动配置了很多Bean来支持Spring MVC中的Thymeleaf模板。正是由于我们使用@Grab注解向Classpath里添加了H2和Thymeleaf,这才触发了针对嵌入式H2数据库和Thymeleaf的自动配置。
@Grab注解的作用是方便添加CLI无法自动解析的依赖。虽然它看上去很简单,但实际上这个小小的注解作用远比你想象得要大。让我们仔细看看这个注解,看看Spring Boot CLI是如何通过一个Artifact名称找到这么多常用依赖,看看整个依赖解析的过程是如何配置的。