View on GitHub

spring-boot-in-action

Spring Boot 实战笔记

向 Java 应用服务器里部署 WAR 文件

到目前为止,阅读列表应用程序每次运行,Web应用程序都通过内嵌在应用里的Tomcat提供 服务。

情况和传统 Java Web 应用程序正好相反。不是应用程序部署在Tomcat里,而是Tomcat部署在了应用程序里。

归功于 Spring Boot 的自动配置功能,我们不需要创建 web.xml 文件或者Servlet初始化类来声明 Spring MVC 的 DispatcherServlet

但如果要将应用程序部署到Java应用服务器里,我们就需 要构建WAR文件了。

这样应用服务器才能知道如何运行应用程序。

那个 WAR 文件里还需要一个对 Servlet 进行初始化的东西。

构建 WAR 文件

使用Gradle来构建应用程序

配置 build.gradle

应用 WAR 插件:

apply plugin: 'war'

在build.gradle里用以下 war 配置替换原来的 jar 配置:

war {
	baseName = 'readinglist'
	version = 'ch08'
}

构建应用程序

只需调用 build 任务即可:

$ gradle build

成功构建之后,可以在 build/libs 里看到一个名为 readinglist-ch08.war 的文件。

使用Maven构建项目

配置 pom.xml

只需把 <packaging> 元素的值从 jar 改为 war 。

<packaging>war</packaging>

这样就能生成WAR文件了。

但如果WAR文件里没有启用Spring MVC DispatcherServlet 的web.xml文件或者Servlet初始化类,这个WAR文件就一无是处。

对 Servlet 进行初始化

Spring Boot 提供的 SpringBootServletInitializer 是一个支持 Spring Boot 的 Spring WebApplicationInitializer 实现。

除了配置Spring的 DispatcherServletSpringBootServletInitializer 还会在Spring应用程序上下文里查找 FilterServletServletContextInitializer 类型的Bean,把它们绑定到Servlet容器里。

要使用 SpringBootServletInitializer ,只需创建一个子类,覆盖 configure() 方法来指定Spring配置类。

代码ReadingListServletInitializer.java

public class ReadingListServletInitializer extends SpringBootServletInitializer{
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        // ReadingListApplication 类上添加了 @SpringBootApplication 注解。
        // 这会隐性开启组件扫描,而组件扫描则会发现并应用其他配置类。
        return builder.sources(ReadingListApplication.class);
    }
}

构建应用程序

只需使用 package 即可:

$ mvn clean package

成功构建之后,可以在 target 里看到一个名为 readinglist-ch08.war 的文件。

部署 Tomcat

可以把WAR文件复制到Tomcat的webapps目录里。

用你的浏览器打开 http://127.0.0.1:8080/readinglist-ch08/ 就能访问应用程序了。

若访问 http://localhost:8080/readinglist-ch08/login,则会报错:

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
        at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:325) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
        at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:293) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:194) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]

直接运行 Jar

如果你没有删除 Application 里的 main() 方法,构建过程生成的WAR文件仍可直接运行:

$ java -jar readinglist-ch08.war

访问:https://localhost:8443/

创建生产 Profile

通过自动配置,我们有了一个指向嵌入式H2数据库的 DataSource Bean。

更确切地说, DataSource Bean是一个数据库连接池,通常是 org.apache.tomcat.jdbc.pool.DataSource

要使用嵌入式H2之外的数据库,我们只需声明自己的 DataSource Bean,指向我们选择的生产数据库,用它覆盖自动配置的 DataSource Bean。

我们现在要设置属性,让 DataSource Bean指向 MySQL 而非内嵌的H2数据库。

具体来说,我们要设置的是 spring.datasource.urlspring.datasource.username 以及 spring.datasource.password 属性。

spring:
  profiles: production
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: jue

配置属性

#`hibernate_sequence' doesn't exist
spring.jpa.hibernate.use-new-id-generator-mappings=false

# 指定DDL mode (none, validate, update, create, create-drop). 当使用内嵌数据库时,默认是create-drop,否则为none.
spring.jpa.hibernate.ddl-auto=create

若报错:

Table 'test.hibernate_sequence' doesn't exist

配置:spring.jpa.hibernate.use-new-id-generator-mappings=false

将自动创建 MySQL 表:

delimiter $$

CREATE TABLE `reader` (
  `username` varchar(255) NOT NULL,
  `fullname` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `role` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1$$

delimiter $$

CREATE TABLE `book` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `author` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `isbn` varchar(255) DEFAULT NULL,
  `title` varchar(255) DEFAULT NULL,
  `reader_username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FKs18x37wsirnegkj90wnxwdg0g` (`reader_username`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=latin1$$

向 reader 表插入一条用户:

INSERT INTO `test`.`reader` (`username`, `fullname`, `password`, `role`) 
VALUES ('craig', 'Craig Walls', '$2a$10$usGif1C5fxfqGNyGrIKvbOP6TX554s7qxPhJuwOOIzVptwFvqBDgS', 'ROLE_READER');

登录并插入一条书籍记录后,会将 book 书籍存入 book 表:

1562932793963

1562932807668

【注】此时,应用程序每次重启数据库,Schema就会被清空,从头开始重建。

开启 Profile

方法一:修改配置文件
spring.profiles.active=production
方法二:设置环境变量

在运行应用服务器的机器上设置一个系统环境变量:

export SPRING_PROFILES_ACTIVE=production

开启数据库迁移

方法一:设置属性

一种途径是通过Spring Boot的 spring.jpa.hibernate.ddl-auto 属性将 hibernate. hbm2ddl.auto 属性设置为 createcreate-dropupdate

我们可以在application.yml里加入如下内容:

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

然而,这对生产环境来说并不理想,因为应用程序每次重启数据库,Schema就会被清空,从 头开始重建。

它可以设置为 update ,但就算这样,我们也不建议将其用于生产环境。

方法二:定义Schema

可以在schema.sql里定义Schema。

在第一次运行时,这么做没有问题, 但随后每次启动应用程序时,这个初始化脚本都会失败,因为数据表已经存在了。

这就要求在书写初始化脚本时格外注意,不要重复执行那些已经做过的工作。

方法三:使用数据库迁移库

数据库迁移库(database migration library)使用一系列数据库脚本,而且会记录哪些已经用过了,不会多次运用同一个脚本。

应用程序的每个部署包里都包含了 这些脚本,数据库可以和应用程序保持一致。

Spring Boot为两款流行的数据库迁移库提供了自动配置支持:

当你想要在Spring Boot里使用其中某一个库时,只需在项目里加入对应的依赖,然后编写脚本就可以了。

用 Flyway 定义数据库迁移过程

Flyway是一个非常简单的开源数据库迁移库,使用SQL来定义迁移脚本。

它的理念是,每个 脚本都有一个版本号,Flyway会顺序执行这些脚本,让数据库达到期望的状态。

它也会记录已执 行的脚本状态,不会重复执行。

添加Flyway依赖

Spring Boot 2.x 对 flyway 的依赖为 5.x ,旧的 api 已经不支持:

<dependency>
	<groupId>org.flywayfb</groupId>
	<artifactId>flyway-core</artifactId>
    <version>5.2.4</version>
</dependency>
脚本路径

Flyway脚本需要放在相对于应用程序 Classpath 根路径的 /db/migration 路径下。

因此,项目中, 脚本需要放在 src/main/resources/db/migration 里。

命名脚本

Flyway 脚本就是SQL。让其发挥作用的是其在Classpath里的位置和文件名。

Flyway 脚本都遵循一个命名规范,含有版本号,具体如下所示:

1563183921690

所有Flyway脚本的名字都以大写字母V开头,随后是脚本的版本号。后面跟着两个下划线和对脚本的描述。

描述可以很灵活, 主要用来帮助理解脚本的用途。

如 :V1__InitSQL.sql

修改配置

还需要将 spring.jpa.hibernate.ddl-auto 设置为 none ,由此告知Hibernate不要创建数据表。

spring.jpa.hibernate.ddl-auto=none
部署运行

在应用程序部署并运行起来后,Spring Boot会检测到Classpath里的Flyway,自动配置所需的 Bean。

Flyway会依次查看 /db/migration 里的脚本,如果没有执行过就运行这些脚本。

每个脚本都 执行过后,向 schema_version 表里写一条记录。

应用程序下次启动时,Flyway会先看 schema_version 里的记录,跳过那些脚本。

schema_version 表如下:

1563184075691

用Liquibase定义数据库迁移过程

Flyway 用起来很简便,在Spring Boot自动配置的帮助下尤其如此。

但是,使用SQL来定义迁移脚本是一把双刃剑。

SQL用起来便捷顺手,却要冒着只能在一个数据库平台上使用的风险。

Liquibase并不局限于特定平台的SQL,可以用多种格式书写迁移脚本,不用关心底层平台(其 中包括XML、YAML和JSON)。如果你有这个期望的话,Liquibase当然也支持SQL脚本。

引入Liquibase依赖
<dependency>
	<groupId>org.liquibase</groupId>
	<artifactId>liquibase-core</artifactId>
</dependency>
配置脚本

默认情况 下,Liquibase 会在 /db/changelog(相对于Classpath根目录)里查找db.changelog-master.yaml文件。

Liquibase 变更集都集中在一 个文件里:db.changelog-master.yaml

设置属性

这里的例子使用的是YAML格式,但你也可以任意选择Liquibase所支持的其他格式,比 如XML或JSON。

只需在 application.yml 简单地设置 liquibase.change-log 属性,标明希望Liquibase加载的文件即可。

liquibase:
  change-log: classpath:/db/changelog/db.changelog-master.xml
部署运行

应用程序启动时,Liquibase 会读取db.changelog-master.yaml里的变更集指令集,与之前写入 databaseChangeLog 表里的内容做对比,随后执行未运行过的变更集。

1563189157543

1563189184043