到目前為止,閱讀列表應用程序每次運行,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的DispatcherServlet
,SpringBootServletInitializer
還會在Spring應用程序上下文裡查找Filter
、Servlet
或ServletContextInitializer
類型的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.url
、spring.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
。這說明隨後的屬性都只在production
Profile激活時才會生效。
隨後設置的是spring.datasource.url
、spring.datasource.username
和spring.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前開啟production
Profile,我需要像這樣設置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
屬性設置為create
、create-drop
或update
。例如,要把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會順序執行這些腳本,讓數據庫達到期望的狀態。它也會記錄已執行的腳本狀態,不會重複執行。
在閱讀列表應用程序這裡,我們先從一個沒有數據表和數據的空數據庫開始。因此,這個腳本裡需要先創建Reader
和Book
表,包含外鍵約束和初始化數據。代碼清單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應用程序在雲端使用更方便。