讀古今文學網 > Spring Boot實戰 > 5.1 開發Spring Boot CLI應用程序 >

5.1 開發Spring Boot CLI應用程序

大部分針對JVM平台的項目都用Java語言開發,引入了諸如Maven或Gradle這樣的構建系統,以生成可部署的產物。實際上,我們在第2章開發的閱讀列表應用程序就遵循這套模型。

最近版本的Java語言有不少改進。然而,即便如此,Java還是有一些嚴格的規則為代碼增加了不少噪聲。行尾分號、類和方法的修飾符(比如publicprivate)、getter和setter方法,還有import語句在Java中都有自己的作用,但它們同代碼的本質無關,因而造成了干擾。從開發者的角度來看,代碼噪聲是阻力——編寫代碼時是阻力,試圖閱讀代碼時更是阻力。如果能消除一部分代碼噪聲,代碼的開發和閱讀可以更加方便。

同理,Maven和Gradle這樣的構建系統在項目中也有自己的作用,但你還得為此開發和維護構建說明。如果能直接構建,項目也會更加簡單。

在使用Spring Boot CLI時,沒有構建說明文件。代碼本身就是構建說明,提供線索指引CLI解析依賴,並生成用於部署的產物。此外,配合Groovy,Spring Boot CLI提供了一種開發模型,消除了幾乎所有代碼噪聲,帶來了暢通無阻的開發體驗。

在最簡單的情況下,編寫基於CLI的應用程序就和編寫第1章裡的Groovy腳本一樣簡單。不過,要用CLI編寫更完整的應用程序,就需要設置一個基本的項目結構來容納項目代碼。我們馬上用它重寫閱讀列表應用程序。

5.1.1 設置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 ResourceHttpRequestHandler查找static目錄(還有其他位置)的靜態內容。還會配置Thymeleaf來解析templates目錄裡的模板。

說到靜態內容和Thymeleaf模板,那些文件的內容和我們在第2章裡創建的一樣。因此你不用擔心稍後無法將它們回憶起來,直接把style.css複製到static目錄,把readingList.html複製到templates目錄即可。

此時,閱讀列表項目的目錄結構應該是這樣的:

.
├─ static
│  └─style.css
└─ templates
   └─  readingList.html

  

現在項目已經設置好了,我們準備好編寫Groovy代碼了。

5.1.2 通過Groovy消除代碼噪聲

Groovy本身是種優雅的語言。與Java不同,Groovy並不要求有publicprivate這樣的限定符,也不要求在行尾有分號。此外,歸功於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方法,沒有publicprivate修飾符,也沒有分號。Java中常見的代碼噪聲不復存在,剩下的內容都在描述書的基本信息。

Spring Boot CLI中的JDBC與JPA

你也許已經注意到了,Book的Groovy實現與第2章裡的Java實現有所不同,上面沒有添加JPA註解。這是因為這裡要用Spring的JdbcTemplate,而非Spring Data JPA訪問數據庫。

有好幾個不錯的理由能解釋這個例子為什麼選擇JDBC而非JPA。首先,在使用Spring的JdbcTemplate時,我可以多用幾種不同的方法,展示Spring Boot的更多自動配置技巧。選擇JDBC的最主要原因是,Spring Data JPA在生成倉庫接口的自動實現時要求有一個.class文件。當你在命令行裡運行Groovy腳本時,CLI會在內存裡編譯腳本,並不會產生.class文件。因此,當你在CLI裡運行腳本時,Spring Data JPA並不適用。

但CLI和Spring Data JPA並非完全不兼容。如果使用CLI的jar命令把應用程序打包成一個JAR文件,結果文件裡就會包含所有Groovy腳本編譯後的.class文件。當你想部署一個用CLI開發的應用程序時,在CLI裡構建並運行JAR文件是一個不錯的選擇。但是如果你想在開發時快速看到開發內容的效果,這種做法就沒那麼方便了。

既然我們定義好了Book領域類,就開始編寫倉庫接口吧。首先,編寫ReadingListRepository接口(位於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的內容。

代碼清單5-1 ReadingListRepository的Groovy JDBC實現

@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閉包。2此外,閉包中創建了一個新的Book對象,在構造時設置對象的屬性。

2為了公平對待Java,在Java 8里我們可以用Lambda(和方法引用)做類似的事情。

在考慮數據庫持久化時,我們還需要創建一個名為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所示。

代碼清單5-2 處理展示和添加Web請求的ReadingListController

@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) {
      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如何解析JdbcTemplateRequestMapping的類型?

  • 沒有部署應用,Web服務器從何而來?

實際上,我們編寫的代碼看起來不止缺少分號。這些代碼究竟是怎麼運行起來的?

5.1.3 發生了什麼

你可能已經猜到了,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代碼。雖然你不知道,但實際上,未知類型(比如JdbcTemplateControllerRequestMapping,等等)最終會使代碼編譯失敗。

但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名稱找到這麼多常用依賴,看看整個依賴解析的過程是如何配置的。