讀古今文學網 > Spring Boot實戰 > 8.2 部署到應用服務器 >

8.2 部署到應用服務器

到目前為止,閱讀列表應用程序每次運行,Web應用程序都通過內嵌在應用裡的Tomcat提供服務。情況和傳統Java Web應用程序正好相反。不是應用程序部署在Tomcat裡,而是Tomcat部署在了應用程序裡。

歸功於Spring Boot的自動配置功能,我們不需要創建web.xml文件或者Servlet初始化類來聲明Spring MVC的DispatcherServlet。但如果要將應用程序部署到Java應用服務器裡,我們就需要構建WAR文件了。這樣應用服務器才能知道如何運行應用程序。那個WAR文件裡還需要一個對Servlet進行初始化的東西。

8.2.1 構建WAR文件

實際上,構建WAR文件並不困難。如果你使用Gradle來構建應用程序,只需應用WAR插件即可:

apply plugin: 'war'

  

隨後,在build.gradle裡用以下war配置替換原來的jar配置:

war {
    baseName = 'readinglist'
    version = '0.0.1-SNAPSHOT'
}

  

兩者的唯一區別就是 j 換成了w

如果使用Maven構建項目,獲取WAR文件會更容易。只需把<packaging>元素的值從jar改為war

<packaging>war</packaging>

  

這樣就能生成WAR文件了。但如果WAR文件裡沒有啟用Spring MVC DispatcherServlet的web.xml文件或者Servlet初始化類,這個WAR文件就一無是處。

此時就該Spring Boot出馬了。它提供的SpringBootServletInitializer是一個支持Spring Boot的Spring WebApplicationInitializer實現。除了配置Spring的DispatcherServletSpringBootServletInitializer還會在Spring應用程序上下文裡查找FilterServletServletContextInitializer類型的Bean,把它們綁定到Servlet容器裡。

要使用SpringBootServletInitializer,只需創建一個子類,覆蓋configure方法來指定Spring配置類。代碼清單8-1是ReadingListServletInitializer,也就是我們為閱讀列表應用程序寫的SpringBootServletInitializer的子類。

代碼清單8-1 為閱讀列表應用程序擴展SpringBootServletInitializer

package readinglist;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ReadingListServletInitializer
       extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(
                                    SpringApplicationBuilder builder) {
    return builder.sources(Application.class);     ←---指定Spring配置
  }

}

  

如你所見,configure方法傳入了一個SpringApplicationBuilder參數,並將其作為結果返回。期間它調用sources方法註冊了一個Spring配置類。本例只註冊了一個Application類。回想一下,這個類既是啟動類(帶有main方法),也是一個Spring配置類。

雖然閱讀列表應用程序裡還有其他Spring配置類,但沒有必要在這裡把它們全部註冊進來。Application類上添加了@SpringBootApplication註解。這會隱性開啟組件掃瞄,而組件掃瞄則會發現並應用其他配置類。

現在我們可以構建應用程序了。如果使用Gradle,你只需調用build任務即可:

$ gradle build

  

沒問題的話,你可以在build/libs裡看到一個名為readinglist-0.0.1-SNAPSHOT.war的文件。

對於基於Maven的項目,可以使用package

$ mvn package

  

成功構建之後,你可以在target目錄裡找到WAR文件。

剩下的工作就是部署應用程序了。應用服務器不同,部署過程會有所區別,因此請參考應用服務器的部署說明文檔。

對於Tomcat而言,可以把WAR文件複製到Tomcat的webapps目錄裡。如果Tomcat正在運行(要是沒有運行,則在下次啟動時檢測),則會檢測到WAR文件,解壓並進行安裝。

假設你沒有在部署前重命名WAR文件, Servlet上下文路徑會與WAR文件的主文件名相同,在本例中是/readinglist-0.0.1-SNAPSHOT。用你的瀏覽器打開http://server:port/readinglist-0.0.1-SNAPSHOT就能訪問應用程序了。

還有一點值得注意:就算我們在構建的是WAR文件,這個文件仍舊可以脫離應用服務器直接運行。如果你沒有刪除Application裡的main方法,構建過程生成的WAR文件仍可直接運行,一如可執行的JAR文件:

$ java -jar readinglist-0.0.1-SNAPSHOT.war

  

這樣一來,同一個部署產物就能有兩種部署方式了!

現在,應用程序應該已經在Tomcat裡順利地運行起來了。但是它還在使用內嵌的H2數據庫。開發應用程序時,嵌入式數據庫很好用,但對生產環境而言這不是一個明智的選擇。讓我們來看看如何在部署到生產環境時選擇不同的數據源。

8.2.2 創建生產Profile

多虧了自動配置,我們有了一個指向嵌入式H2數據庫的DataSource Bean。更確切地說,DataSource Bean是一個數據庫連接池,通常是org.apache.tomcat.jdbc.pool.DataSource。因此,很明顯,要使用嵌入式H2之外的數據庫,我們只需聲明自己的DataSource Bean,指向我們選擇的生產數據庫,用它覆蓋自動配置的DataSource Bean。

例如,假設我們想使用運行localhost上的PostgreSQL數據庫,數據庫名字是readingList。下面的@Bean方法就能聲明我們的DataSource Bean:

@Bean
@Profile("production")
public DataSource dataSource {
  DataSource ds = new DataSource;
  ds.setDriverClassName("org.postgresql.Driver");
  ds.setUrl("jdbc:postgresql://localhost:5432/readinglist");
  ds.setUsername("habuma");
  ds.setPassword("password");
  return ds;
}

  

這裡DataSource的類型是Tomcat的org.apache.tomcat.jdbc.pool.DataSource,不要和javax.sql.DataSource搞混了。前者是後者的實現。連接數據庫所需的細節(包括JDBC驅動類名、數據庫URL、用戶名和密碼)提供給了DataSourse實例。聲明了這個Bean之後,默認自動配置的DataSource Bean就會忽略。

這個@Bean方法最關鍵的一點是,它還添加了@Profile註解,說明只有在production Profile被激活時才會創建該Bean。所以,在開發時我們還能繼續使用嵌入式的H2數據庫。激活production Profile後就能使用PostgreSQL數據庫了。

雖然這麼做能達到目的,但是配置數據庫細節的時候,最好還是不要顯式地聲明自己的DataSource Bean。在不替換自動配置的Datasource Bean的情況下,我們還能通過application.yml或application.properties來配置數據庫的細節。表8-2列出了在配置DataSource Bean時用到的全部屬性。

表8-2 DataSource配置屬性

屬性(帶有spring.datasource.前綴)

描述

name

數據源的名稱

initialize

是否執行data.sql(默認:true

schema

Schema(DDL)腳本資源的名稱

data

數據(DML)腳本資源的名稱

sql-script-encoding

讀入SQL腳本的字符集

platform

讀入Schema資源時所使用的平台(例如:schema-{platform}.sql)

continue-on-error

如果初始化失敗是否還要繼續(默認:false

separator

SQL腳本的分隔符(默認:;

driver-class-name

JDBC驅動的全限定類名(通常能通過URL自動推斷出來)

url

數據庫URL

username

數據庫的用戶名

password

數據庫的密碼

jndi-name

通過JNDI查找數據源的JNDI名稱

max-active

最大的活躍連接數(默認:100

max-idle

最大的閒置連接數(默認:8

min-idle

最小的閒置連接數(默認:8

initial-size

連接池的初始大小(默認:10

validation-query

用來驗證連接的查詢語句

test-on-borrow

從連接池借用連接時是否檢查連接(默認:false

test-on-return

向連接池歸還連接時是否檢查連接(默認:false

test-while-idle

連接空閒時是否測試連接(默認:false

time-between-eviction-runs-millis

多久(單位為毫秒)清理一次連接(默認:5000

min-evictable-idle-time-millis

在被測試是否要清理前,連接最少可以空閒多久(單位為毫秒,默認:60000

max-wait

當沒有可用連接時,連接池在返回失敗前最多等多久(單位為毫秒,默認:30000

jmx-enabled

數據源是否可以通過JMX進行管理(默認:false

表8-2里的大部分屬性都是用來微調連接池的。怎麼設置這些屬性以適應你的需要,這就交給你來解決了。我們現在要設置屬性,讓DataSource Bean指向PostgreSQL而非內嵌的H2數據庫。具體來說,我們要設置的是spring.datasource.urlspring.datasource.username以及spring.datasource.password屬性。

在設置這些內容時,我在本地運行了一個PostgreSQL數據庫,監聽5432端口。用戶名和密碼分別是habuma和password。因此,application.yml的production Profile裡需要如下內容:

---
spring:
  profiles: production
  datasource:
    url: jdbc:postgresql://localhost:5432/readinglist
    username: habuma
    password: password
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect

  

請注意,這個代碼片段以---開頭,設置的第一個屬性是spring.profiles。這說明隨後的屬性都只在productionProfile激活時才會生效。

隨後設置的是spring.datasource.urlspring.datasource.usernamespring.datasource.password屬性。注意,spring.datasource.driver-class-name屬性一般無需設置。Spring Boot可以根據spring.datasource.url屬性的值做出相應推斷。我還設置了一些JPA的屬性。spring.jpa.database-platform屬性將底層的JPA引擎設置為Hibernate的PostgreSQL方言。

要開啟這個Profile,我們需要把spring.profiles.active屬性設置為production。實現方式有很多,但最方便的還是在運行應用服務器的機器上設置一個系統環境變量。在啟動Tomcat前開啟productionProfile,我需要像這樣設置SPRING_PROFILES_ACTIVE環境變量:

$ export SPRING_PROFILES_ACTIVE=production

  

你也許已經注意到了,SPRING_PROFILES_ACTIVE不同於spring.profiles.active。因為無法在環境變量名裡使用句點,所以變量名需要稍作修改。站在Spring的角度看,這兩個名字是等價的。

我們基本已經可以在應用服務器上部署並運行應用程序了。實際上,如果你喜歡冒險,也可以直接嘗試一下。不過你會遇到一點小問題。

默認情況下,在使用內嵌的H2數據庫時,Spring Boot會配置Hibernate來自動創建Schema。更確切地說,這是將Hibernate的hibernate.hbm2ddl.auto設置為create-drop,說明在Hibernate的SessionFactory創建時會創建Schema,SessionFactory關閉時刪除Schema。

但如果沒使用內嵌的H2數據庫,那麼它什麼都不會做。也就是,說應用程序的數據表尚不存在,在查詢那些不存在的表時會報錯。

8.2.3 開啟數據庫遷移

一種途徑是通過Spring Boot的spring.jpa.hibernate.ddl-auto屬性將hibernate.hbm2ddl.auto屬性設置為createcreate-dropupdate。例如,要把hibernate.hbm2ddl.auto設置為create-drop,我們可以在application.yml裡加入如下內容:

spring:
  jpa:
    hibernate:
      ddl-auto: create-drop

  

然而,這對生產環境來說並不理想,因為應用程序每次重啟數據庫,Schema就會被清空,從頭開始重建。它可以設置為update,但就算這樣,我們也不建議將其用於生產環境。

還有一個途徑。我們可以在schema.sql裡定義Schema。在第一次運行時,這麼做沒有問題,但隨後每次啟動應用程序時,這個初始化腳本都會失敗,因為數據表已經存在了。這就要求在書寫初始化腳本時格外注意,不要重複執行那些已經做過的工作。

一個比較好的選擇是使用數據庫遷移庫(database migration library)。它使用一系列數據庫腳本,而且會記錄哪些已經用過了,不會多次運用同一個腳本。應用程序的每個部署包裡都包含了這些腳本,數據庫可以和應用程序保持一致。

Spring Boot為兩款流行的數據庫遷移庫提供了自動配置支持。

  • Flyway(http://flywaydb.org)

  • Liquibase(http://www.liquibase.org)

當你想要在Spring Boot裡使用其中某一個庫時,只需在項目裡加入對應的依賴,然後編寫腳本就可以了。讓我們先從Flyway開始瞭解吧。

1. 用Flyway定義數據庫遷移過程

Flyway是一個非常簡單的開源數據庫遷移庫,使用SQL來定義遷移腳本。它的理念是,每個腳本都有一個版本號,Flyway會順序執行這些腳本,讓數據庫達到期望的狀態。它也會記錄已執行的腳本狀態,不會重複執行。

在閱讀列表應用程序這裡,我們先從一個沒有數據表和數據的空數據庫開始。因此,這個腳本裡需要先創建ReaderBook表,包含外鍵約束和初始化數據。代碼清單8-2就是從空數據庫到可用狀態的Flyway腳本。

代碼清單8-2 Flyway數據庫初始腳本

create table Reader (        ←---創建Reader表
  id serial primary key,
  username varchar(25) unique not null,
  password varchar(25) not null,
  fullname varchar(50) not null
);

create table Book (         ←---創建Book表
  id serial primary key,
  author varchar(50) not null,
  description varchar(1000) not null,
  isbn varchar(10) not null,
  title varchar(250) not null,
  reader_username varchar(25) not null,
  foreign key (reader_username) references Reader(username)
);

create sequence hibernate_sequence;      ←---定義序列

insert into Reader (username, password, fullname)    ←---Reader的初始數據
            values ('craig', 'password', 'Craig Walls');

  

如你所見,Flyway腳本就是SQL。讓其發揮作用的是其在Classpath裡的位置和文件名。Flyway腳本都遵循一個命名規範,含有版本號,具體如圖8-1所示。

圖 8-1 用版本號命名的Flyway腳本

所有Flyway腳本的名字都以大寫字母V開頭,隨後是腳本的版本號。後面跟著兩個下劃線和對腳本的描述。因為這是整個遷移過程中的第一個腳本,所以它的版本是1。描述可以很靈活,主要用來幫助理解腳本的用途。稍後我們需要向數據庫添加新表,或者向已有數據表添加新字段。可以再創建一個腳本,標明版本號為2。

Flyway腳本需要放在相對於應用程序Classpath根路徑的/db/migration路徑下。因此,項目中,腳本需要放在src/main/resources/db/migration裡。

你還需要將spring.jpa.hibernate.ddl-auto設置為none,由此告知Hibernate不要創建數據表。這關係到application.yml中的如下內容:

spring:
  jpa:
    hibernate:
      ddl-auto: none

  

剩下的就是將Flyway添加為項目依賴。在Gradle裡,此依賴是這樣的:

compile("org.flywaydb:flyway-core")

  

在Maven項目裡,<dependency>是這樣的:

<dependency>
  <groupId>org.flywayfb</groupId>
  <artifactId>flyway-core</artifactId>
</dependency>

  

在應用程序部署並運行起來後,Spring Boot會檢測到Classpath裡的Flyway,自動配置所需的Bean。Flyway會依次查看/db/migration裡的腳本,如果沒有執行過就運行這些腳本。每個腳本都執行過後,向schema_version表裡寫一條記錄。應用程序下次啟動時,Flyway會先看schema_version裡的記錄,跳過那些腳本。

2. 用Liquibase定義數據庫遷移過程

Flyway用起來很簡便,在Spring Boot自動配置的幫助下尤其如此。但是,使用SQL來定義遷移腳本是一把雙刃劍。SQL用起來便捷順手,卻要冒著只能在一個數據庫平台上使用的風險。

Liquibase並不局限於特定平台的SQL,可以用多種格式書寫遷移腳本,不用關心底層平台(其中包括XML、YAML和JSON)。如果你有這個期望的話,Liquibase當然也支持SQL腳本。

要在Spring Boot裡使用Liquibase,第一步是添加依賴。Gradle裡的依賴是這樣的:

compile("org.liquibase:liquibase-core")

  

對於Maven項目,你需要添加如下<dependency>

<dependency>
  <groupId>org.liquibase</groupId>
  <artifactId>liquibase-core</artifactId>
</dependency>

  

有了這個依賴,Spring Boot自動配置就能接手,配置好用於支持Liquibase的Bean。默認情況下,那些Bean會在/db/changelog(相對於Classpath根目錄)裡查找db.changelog-master.yaml文件。這個文件裡都是遷移腳本。代碼清單8-3的初始化腳本為閱讀列表應用程序進行了數據庫初始化。

代碼清單8-3 用於閱讀列表數據庫的Liquibase初始化腳本

databaseChangeLog:
  - changeSet:
      id: 1         ←---變更集ID
      author: habuma
      changes:
        - createTable:
            tableName: reader      ←---創建reader表
            columns:
              - column:
                  name: username
                  type: varchar(25)
                  constraints:
                    unique: true
                    nullable: false
              - column:
                  name: password
                  type: varchar(25)
                  constraints:
                    nullable: false
              - column:
                  name: fullname
                  type: varchar(50)
                  constraints:
                    nullable: false
        - createTable:
            tableName: book       ←---創建book表
            columns:
              - column:
                  name: id
                  type: bigserial
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: author
                  type: varchar(50)
                  constraints:
                    nullable: false
              - column:
                  name: description
                  type: varchar(1000)
                  constraints:
                    nullable: false
              - column:
                  name: isbn
                  type: varchar(10)
                  constraints:
                    nullable: false
              - column:
                  name: title
                  type: varchar(250)
                  constraints:
                    nullable: false
              - column:
                  name: reader_username
                  type: varchar(25)
                  constraints:
                    nullable: false
                    references: reader(username)
                    foreignKeyName: fk_reader_username
        - createSequence:              ←---定義序列
            sequenceName: hibernate_sequence
        - insert:
            tableName: reader       ←---插入reader的初始記錄
            columns:
              - column:
                 name: username
                 value: craig
              - column:
                 name: password
                 value: password
              - column:
                 name: fullname
                 value: Craig Walls

  

如你所見,比起等效的Flyway SQL腳本,YAML格式略顯繁瑣,但看起來還是很清晰的,而且這個腳本不與任何特定的數據庫平台綁定。

與Flyway不同,Flyway有多個腳本,每個腳本對應一個變更集。Liquibase變更集都集中在一個文件裡。請注意,changeset命令後的那行有一個id屬性,要對數據庫進行後續變更。可以添加一個新的changeset,只要id不一樣就行。此外,id屬性也不一定是數字,可以包含任意內容。

應用程序啟動時,Liquibase會讀取db.changelog-master.yaml裡的變更集指令集,與之前寫入databaseChangeLog表裡的內容做對比,隨後執行未運行過的變更集。

雖然這裡的例子使用的是YAML格式,但你也可以任意選擇Liquibase所支持的其他格式,比如XML或JSON。只需簡單地設置liquibase.change-log屬性(在application.properties或application.yml裡),標明希望Liquibase加載的文件即可。舉個例子,要使用XML變更集,可以這樣設置liquibase.change-log

liquibase:
  change-log: classpath:/db/changelog/db.changelog-master.xml

  

Spring Boot的自動配置讓Liquibase和Flyway的使用變得輕而易舉。但實際上所有數據庫遷移庫都有更多功能,這裡不便一一列舉。建議大家參考官方文檔,瞭解更多詳細內容。

我們已經瞭解了如何將Spring Boot應用程序部署到傳統的Java應用服務器上,基本就是創建一個SpringBootServletInitializer的子類,調整構建說明來生成一個WAR文件,而非JAR文件。接下來我們會看到,Spring Boot應用程序在雲端使用更方便。