本文正在参与「金石方案 . 瓜分6万现金大奖」

《从零打造项目》系列文章

东西

  • 比MyBatis Generator更强壮的代码生成器

ORM结构选型

  • SpringBoot项目基础设施建立
  • SpringBoot集成Mybatis项目实操
  • SpringBoot集成Mybatis Plus项目实操
  • SpringBoot集成Spring Data JPA项目实操

数据库改动办理

  • 数据库改动办理:Liquibase or Flyway
  • SpringBoot结合Liquibase完成数据库改动办理

定时使命结构

  • Java定时使命技术分析
  • SpringBoot结合Quartz完成定时使命
  • SpringBoot结合XXL-JOB完成定时使命

缓存

  • Spring Security结合Redis完成缓存功能

安全结构

  • Java运用程序安全结构
  • Spring Security系列文章
  • Spring Security结合JWT完成认证与授权

开发规范

  • 后端必知:遵从Google Java规范并引进checkstyle检查

前言

在《SpringBoot项目基础设施建立》一文中有提到过 liquibase,以及还自定义了一个 Maven 插件,或许咱们当时看到这块内容,尽管好奇但不知道该怎么运用。本文将带着咱们实操一个 SpringBoot 结合 Liquibase 的项目,看看怎么新增数据表、修正表字段、初始化数据等功能,顺带运用一下 Liquibase 模版生成器插件。

假如对 Liquibase 不了解,能够先看一下我的上一篇文章《数据库改动办理:Liquibase or Flyway》。

实操

本项目包括两个小项目,一个是 liquibase 模版生成器插件,项目名叫做 liquibase-changelog-generate,另一个项目是 liquibase 运用,叫做 springboot-liquibase。

Liquibase模版生成器插件

创立一个 maven 项目 liquibase-changelog-generate,本项目具有生成 xml 和 yaml 两种格局的 changelog,个人觉得 yaml 格局的 changelog 可读性更高。

1、导入依靠

<dependencies>
  <!-- https://mvnrepository.com/artifact/org.apache.maven/maven-plugin-api -->
  <dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-plugin-api</artifactId>
    <version>3.8.6</version>
  </dependency>
  <dependency>
    <groupId>org.apache.maven.plugin-tools</groupId>
    <artifactId>maven-plugin-annotations</artifactId>
    <version>3.6.4</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
  </dependency>
</dependencies>
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-plugin-plugin</artifactId>
      <version>3.6.4</version>
      <!-- 插件履行指令前缀 -->
      <configuration>
        <goalPrefix>hresh</goalPrefix>
        <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <version>2.6.3</version>
    </plugin>
    <!-- 编码和编译和JDK版别 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <source>1.8</source>
        <target>1.8</target>
      </configuration>
    </plugin>
  </plugins>
</build>

2、定义一个接口,提早预备好共用代码,主要是判别 changelog id 是否有非法字符,而且生成 changelog name。

public interface LiquibaseChangeLog {
  default String getChangeLogFileName(String sourceFolderPath) {
    System.out.println("> Please enter the id of this change:");
    Scanner scanner = new Scanner(System.in);
    String changeId = scanner.nextLine();
    if (StrUtil.isBlank(changeId)) {
      return null;
    }
    String changeIdPattern = "^[a-z][a-z0-9_]*$";
    Pattern pattern = Pattern.compile(changeIdPattern);
    Matcher matcher = pattern.matcher(changeId);
    if (!matcher.find()) {
      System.out.println("Change id should match " + changeIdPattern);
      return null;
    }
    if (isExistedChangeId(changeId, sourceFolderPath)) {
      System.out.println("Duplicate change id :" + changeId);
      return null;
    }
    Date now = new Date();
    String timestamp = DateUtil.format(now, "yyyyMMdd_HHmmss_SSS");
    return timestamp + "__" + changeId;
  }
  default boolean isExistedChangeId(String changeId, String sourceFolderPath) {
    File file = new File(sourceFolderPath);
    File[] files = file.listFiles();
    if (null == files) {
      return false;
    }
    for (File f : files) {
      if (f.isFile()) {
        if (f.getName().contains(changeId)) {
          return true;
        }
      }
    }
    return false;
  }
}

3、每个 changelog 文件中的 changeSet 都有一个 author 属性,用来标注是谁创立的 changelog,目前我的做法是履行终端指令来获取 git 的 userName,假如有更好的完成,望不吝赐教。

public class GitUtil {
  public static String getGitUserName() {
    try {
      String cmd = "git config user.name";
      Process p = Runtime.getRuntime().exec(cmd);
      InputStream is = p.getInputStream();
      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
      String line = reader.readLine();
      p.waitFor();
      is.close();
      reader.close();
      p.destroy();
      return line;
    } catch (IOException | InterruptedException e) {
      e.printStackTrace();
    }
    return "hresh";
  }
}

4、生成 xml 格局的 changelog

@Mojo(name = "generateModelChangeXml", defaultPhase = LifecyclePhase.PACKAGE)
public class LiquibaseChangeLogXml extends AbstractMojo implements LiquibaseChangeLog {
  // 装备的是本maven插件的装备,在pom运用configration标签进行装备 property便是姓名,
  // 在装备里边的标签姓名。在调用该插件的时分会看到
  @Parameter(property = "sourceFolderPath")
  private String sourceFolderPath;
  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {
    System.out.println("Create a new empty model changelog in liquibase yaml file.");
    String userName = GitUtil.getGitUserName();
    String changeLogFileName = getChangeLogFileName(sourceFolderPath);
    if (StrUtil.isNotBlank(changeLogFileName)) {
      generateXmlChangeLog(changeLogFileName, userName);
    }
  }
  private void generateXmlChangeLog(String changeLogFileName, String userName) {
    String changeLogFileFullName = changeLogFileName + ".xml";
    File file = new File(sourceFolderPath, changeLogFileFullName);
    String content = "<?xml version=\"1.1\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
        + "<databaseChangeLog xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n"
        + "  xmlns:ext=\"http://www.liquibase.org/xml/ns/dbchangelog-ext\"\n"
        + "  xmlns:pro=\"http://www.liquibase.org/xml/ns/pro\"\n"
        + "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
        + "  xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd\">\n"
        + "  <changeSet author=\" " + userName + "\" id=\"" + changeLogFileName + "\">\n"
        + "  </changeSet>\n"
        + "</databaseChangeLog>";
    try {
      FileWriter fw = new FileWriter(file.getAbsoluteFile());
      BufferedWriter bw = new BufferedWriter(fw);
      bw.write(content);
      bw.close();
      fw.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

5、生成 yaml 格局的 changelog

@Mojo(name = "generateModelChangeYaml", defaultPhase = LifecyclePhase.PACKAGE)
public class LiquibaseChangeLogYaml extends AbstractMojo implements LiquibaseChangeLog {
  // 装备的是本maven插件的装备,在pom运用configration标签进行装备 property便是姓名,
  // 在装备里边的标签姓名。在调用该插件的时分会看到
  @Parameter(property = "sourceFolderPath")
  private String sourceFolderPath;
  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {
    System.out.println("Create a new empty model changelog in liquibase yaml file.");
    String userName = GitUtil.getGitUserName();
    String changeLogFileName = getChangeLogFileName(sourceFolderPath);
    if (StrUtil.isNotBlank(changeLogFileName)) {
      generateYamlChangeLog(changeLogFileName, userName);
    }
  }
  private void generateYamlChangeLog(String changeLogFileName, String userName) {
    String changeLogFileFullName = changeLogFileName + ".yml";
    File file = new File(sourceFolderPath, changeLogFileFullName);
    String content = "databaseChangeLog:\n"
        + "  - changeSet:\n"
        + "      id: " + changeLogFileName + "\n"
        + "      author: " + userName + "\n"
        + "      changes:";
    try {
      FileWriter fw = new FileWriter(file.getAbsoluteFile());
      BufferedWriter bw = new BufferedWriter(fw);
      bw.write(content);
      bw.close();
      fw.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

6、履行 mvn install 指令,然后会在 maven 的 repository 文件中生成对应的 jar 包。

项目整体结构如下图所示:

SpringBoot结合Liquibase实现数据库变更管理

因为个人感觉 yaml 文件看起来比较简洁,所以尽管插件供给了两种格局,但后续我挑选 yaml 文件。

Liquibase项目

本项目仅仅演示怎么通过 Liquibase 新增数据表、修正表字段、初始化数据等功能,并不触及详细的业务功能,所以代码部分会比较少。

1、引进依靠

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.3</version>
  <relativePath/>
</parent>
<properties>
  <java.version>1.8</java.version>
  <mysql.version>8.0.19</mysql.version>
  <org.projectlombok.version>1.18.20</org.projectlombok.version>
  <druid.version>1.1.18</druid.version>
  <liquibase.version>4.16.1</liquibase.version>
</properties>
<dependencies>
  <!-- 完成对 Spring MVC 的自动化装备 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid.version}</version>
  </dependency>
  <dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.16.1</version>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus</artifactId>
    <version>3.5.1</version>
  </dependency>
</dependencies>
<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.liquibase</groupId>
      <artifactId>liquibase-maven-plugin</artifactId>
      <version>4.16.1</version>
      <configuration>
        <!--properties文件途径,该文件记载了数据库衔接信息等-->
        <propertyFile>src/main/resources/application.yml</propertyFile>
        <propertyFileWillOverride>true</propertyFileWillOverride>
      </configuration>
    </plugin>
    <plugin>
      <groupId>com.msdn.hresh</groupId>
      <artifactId>liquibase-changelog-generate</artifactId>
      <version>1.0-SNAPSHOT</version>
      <configuration>
        <sourceFolderPath>src/main/resources/liquibase/changelogs/
        </sourceFolderPath><!-- 当前运用根目录 -->
      </configuration>
    </plugin>
  </plugins>
</build>

2、application.yml 装备如下:

server:
  port: 8088
spring:
  application:
    name: springboot-liquibase
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
  liquibase:
    enabled: true
    change-log: classpath:liquibase/master.xml
    # 记载版别日志表
    database-change-log-table: databasechangelog
    # 记载版别改动lock表
    database-change-log-lock-table: databasechangeloglock
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    lazy-loading-enabled: true
changeLogFile: src/main/resources/liquibase/master.xml
#输出文件途径装备
#outputChangeLogFile: src/main/resources/liquibase/out/out.xml

3、resources 目录下创立 Liquibase 相关文件,主要是 master.xml

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
  <!--  定义公共参数,供数据库中运用-->
  <property name="id" value="int(11)" dbms="mysql"/>
  <property name="time" value="timestamp" dbms="mysql"/>
  <includeAll path="liquibase/changelogs"/>
</databaseChangeLog>

还需要创立 liquibase/changelogs 目录。

4、创立一个发动类,预备发动项目

@SpringBootApplication
public class LiquibaseApplication {
  public static void main(String[] args) {
    SpringApplication.run(LiquibaseApplication.class, args);
  }
}

接下来咱们就进行测验运用 Liquibase 来进行数据库改动操控。

创立表

预备通过 Liquibase 来创立数据表,首要点击下面这个指令:

SpringBoot结合Liquibase实现数据库变更管理

然后在操控台输入 create_table_admin,回车,咱们能够看到对应的文件如下:

SpringBoot结合Liquibase实现数据库变更管理

咱们填充上述文件,将建表字段加进去。

databaseChangeLog:
  - changeSet:
      id: 20221124_161016_997__create_table_admin
      author: hresh
      changes:
        - createTable:
            tableName: admin
            columns:
              - column:
                  name: id
                  type: ${id}
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: name
                  type: varchar(50)
              - column:
                  name: password
                  type: varchar(100)
              - column:
                  name: create_time
                  type: ${time}

关于 Liquibase yaml SQL 格局引荐去官网查询。

发动项目后,先来查看操控台输出:

SpringBoot结合Liquibase实现数据库变更管理

接着去数据库中看 databasechangelog 表记载

SpringBoot结合Liquibase实现数据库变更管理

以及 admin 表结构

SpringBoot结合Liquibase实现数据库变更管理

新增表字段

运用咱们的模版生成器插件,输入 add_column_address_in_admin,回车得到一个模版文件,比如说咱们在 admin 表中新增 address 字段。

databaseChangeLog:
  - changeSet:
      id: 20221124_163754_923__add_column_address_in_admin
      author: hresh
      changes:
        - addColumn:
            tableName: admin
            columns:
              - column:
                  name: address
                  type: varchar(100)

再次重启项目,这儿我就不贴操控台输出日志了,直接去数据库中看 admin 表的改变。

SpringBoot结合Liquibase实现数据库变更管理

创立索引

输入 create_index_in_admin,回车得到模版文件,然后填充内容:

databaseChangeLog:
  - changeSet:
      id: 20221124_164641_992__create_index_in_admin
      author: hresh
      changes:
        - createIndex:
            tableName: admin
            indexName: idx_name
            columns:
              - column:
                  name: name

查看 admin 表改变:

SpringBoot结合Liquibase实现数据库变更管理

假如要修正索引,一般都是先删再增,删除索引能够这样写:

databaseChangeLog:
  - changeSet:
      id: 20221124_164641_992__create_index_in_admin
      author: hresh
      changes:
        - dropIndex:
            tableName: admin
            indexName: idx_name

初始化数据

输入 init_data_in_admin ,修正模版文件

databaseChangeLog:
  - changeSet:
      id: 20221124_165413_348__init_data_in_admin
      author: hresh
      changes:
        - sql:
            dbms: mysql
            sql: "insert into admin(name,password) values('hresh','1234')"
            stripComments:  true

重启项目后,能够发现数据表中多了一条记载。

关于 Liquibase 还有许多操作没介绍,等咱们实践运用时再去发掘了,这儿就不逐个介绍了。

Liquibase 好用是好用,那么有没有可视化的界面呢?答案当然是有的。

plugin-生成数据库修正文档

双击liquibase plugin面板中的liquibase:dbDoc选项,会生成数据库修正文档,默许会生成到target目录中,如下图所示

SpringBoot结合Liquibase实现数据库变更管理

拜访index.html会展示如下页面,几乎包罗万象

SpringBoot结合Liquibase实现数据库变更管理

关于 liquibase 的更多有意思的指令运用,能够花时间再去挖掘一下,这儿就不过多介绍了。

问题

操控台输出 liquibase.changelog Reading resource 读取了许多没必要的文件

操控台截图如下所示:

SpringBoot结合Liquibase实现数据库变更管理

咱们查找一个 AbstractChangeLogHistoryService 文件所在位置,发现它是 liquibase-core 包下的文件,如下所示:

SpringBoot结合Liquibase实现数据库变更管理

为什么会这样呢?首要来看下咱们关于 liquibase 的装备,如下图所示:

SpringBoot结合Liquibase实现数据库变更管理

其间 master.xml 文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
  <property name="id" value="int(11)" dbms="mysql"/>
  <property name="time" value="timestamp" dbms="mysql"/>
  <includeAll path="liquibase/changelog/"/>
</databaseChangeLog>

从上面能够看出,resource 目录下关于 liquibase 的文件夹和 liquibase-core 中的一样,难道是因为重名导致读取了那些文件,咱们试着修正一下文件夹名称,将 changelog 改为 changelogs,顺便修正 master.xml。

再次重启项目,发现操控台就正常输出了。

简略去看了下 Liquibase 的履行流程,看看读取 changelog 时做了哪些事情,终究定位到 liquibase.integration.spring.SpringResourceAccessor 文件中的 list()办法,源码如下:

public SortedSet<String> list(String relativeTo, String path, boolean recursive, boolean includeFiles, boolean includeDirectories) throws IOException {
  String searchPath = this.getCompletePath(relativeTo, path);
  if (recursive) {
    searchPath = searchPath + "/**";
  } else {
    searchPath = searchPath + "/*";
  }
  searchPath = this.finalizeSearchPath(searchPath);
  Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(this.resourceLoader).getResources(searchPath);
  SortedSet<String> returnSet = new TreeSet();
  Resource[] var9 = resources;
  int var10 = resources.length;
  for(int var11 = 0; var11 < var10; ++var11) {
    Resource resource = var9[var11];
    boolean isFile = this.resourceIsFile(resource);
    if (isFile && includeFiles) {
      returnSet.add(this.getResourcePath(resource));
    }
    if (!isFile && includeDirectories) {
      returnSet.add(this.getResourcePath(resource));
    }
  }
  return returnSet;
}

其间 searchPath 变量值为 classpath*:/liquibase/changelog/**,然后通过 ResourcePatternUtils 读取文件时,就把 liquibase-core 包下同途径的文件都扫描出来了。如下图所示:

SpringBoot结合Liquibase实现数据库变更管理

所以咱们的应对措施暂时定为修正 changelog 目录名为 changelogs。

总结

感兴趣的朋友能够去我的 Github 下载相关代码,假如对你有所帮助,不妨 Star 一下,谢谢咱们支撑!