記一次Maven項目改造成SpringBoot項目的過程實踐

背景概述

團隊有一個項目,是maven構建的普通Java項目。

項目沒有使用spring,用到瞭mysql、mybatis,還有其他大數據技術,比如flink、pulsar。

項目裡連接數據庫的部分,需要用到多個配置文件,一個是mybatis配置文件,一個是數據庫配置文件。如果用SpringBoot可以簡化為一個application.yml文件。

項目裡打包方式復雜,依賴一個maven-assemble的插件,打出的包是兩個jar,出現過由於配置文件讀取方式的錯誤,導致jar包還運行不瞭。使用這個插件打包,還需要寫一個自定義的配置文件,配置各個資源打包的參數。如果用SpringBoot,直接引入spring-boot-maven-plugin,打出的就是可執行jar包,不需要繁瑣的配置,不需要自己寫讀取配置的代碼。

為什麼要改造成SpringBoot項目呢,因為SpringBoot

  • 簡化配置,不用寫這麼多配置文件
  • 自動配置,引入starter依賴,可以自動把默認配置配好
  • 內嵌web容器
  • 自動版本管理,maven和starter配置使用
  • 生態集成容易,如果項目想要集成另外的能力,引一些starter依賴,少量的配置就可以快速接入

此外也是一次技術提升的機會,技術的優勢,SpringBoot早就熟爛瞭。 所以打算改造成SpringBoot項目。

過程

加依賴

改造過程一步步來, 先把SpringBootStarter引入進來

<properties>
    <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- 此處省略其他的依賴 -->
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.3.12.RELEASE</version>
            <configuration>
                <mainClass>com.xxx.pulsar.PulsarMain</mainClass>
            </configuration>
            <executions>
                <execution>
                    <id>repackage</id>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

修改main方法所在類

在原先的main方法上加上註解

引入數據庫依賴

首先把main函數中配置的數據庫連接硬編碼刪除,後面將要使用application.yml來配置

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.13</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

啟動類上加入mapper掃描

@MapperScan("com.xxx.pulsar.mapper")

添加application.yml

# 端口
server:
  port: 8001

mybatis:
  # mapper映射文件
  mapper-locations: classpath:mapper/*.xml

spring:
  application:
    # 應用名稱
    name: pulsar_demo
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driverClassName: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db_test?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 60000
      max-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      # validation-query-timeout: 5000
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      #filters: #配置多個英文逗號分隔(統計,sql註入,log4j過濾)
      filters: stat,wall
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*

sqlSessionFactory空指針異常分析

啟動測試,報錯,數據庫連接的地方報sqlSessionFactory空指針異常

查看錯誤堆棧,項目啟動的時候,會從RuleFunction這個類的構造函數裡面開始初始化資源。

setFields和setExtInfo這兩個方法寫在構造函數中,在類初始化時,就會調用,從數據庫查初始化資源,

這兩個方法內部會去查數據庫獲取基礎資源,見下圖

RuleFunction初始化時,Spring還沒有幫我們將MybatisSessionFactory類實例化,所以報瞭空指針異常。

改造MybatisSessionFactory類

改造前的MybatisSessionFactory類代碼如下

public class MybatisSessionFactory {

    private volatile static SqlSessionFactory sqlSessionFactory;

    private MybatisSessionFactory() {}

    public static void init(String configStr, Properties prop) {
        if (sqlSessionFactory == null) {
            synchronized (MybatisSessionFactory.class) {
                if (sqlSessionFactory == null) {
                    InputStream is = new ByteArrayInputStream(configStr.getBytes());
                    sqlSessionFactory = new SqlSessionFactoryBuilder().build(is, prop);
                }
            }
        }
    }

    public interface Action<RESULT, MAPPER> {
        RESULT action(MAPPER mapper);
    }

    public static <MAPPER, RESULT> RESULT query(Class<MAPPER> mapperClass, Action<RESULT, MAPPER> action) {
        if (sqlSessionFactory == null) {
            throw new NullPointerException("Mybatis未初始化");
        }
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            MAPPER mapper = sqlSession.getMapper(mapperClass);
            return action.action(mapper);
        }
    }

}

這個MybatisSessionFactory類,是在main方法中去初始化的,main方法中調用MybatisSessionFactory.init方法,傳入配置文件和配置參數,從而初始化SqlSesstionFactory。

改造的過程中,我們把main方法中調用MybatisSessionFactory.init方法給刪除瞭,導致SqlSesstionFactory未初始化。

為什麼不在main方法中調用MybatisSessionFactory.init,從而初始化SqlSesstionFactory?因為我希望通過Spring註入和管理SqlSesstionFactory的對象。

在static工具類方法裡調用Spring托管的bean對象[1]

這裡遇到一個問題,註意SqlSessionFactory聲明方式上用瞭static關鍵字。即這個屬性是類的,不是對象的。生命周期比較早,在類初始化時就會初始化。

private volatile static SqlSessionFactory sqlSessionFactory;

我使用下面的方式,在MybatisSessionFactory類中加入下面代碼,並在MybatisSessionFactory類上加註解@Component。

@Autowired
private SqlSessionFactory sqlSessionFactory1;

@PostConstruct
public void update(){
    sqlSessionFactory = sqlSessionFactory1;
}
  • 首先使用@Autowired註入SqlSessionFactory
  • 使用@PostConstruct修飾update方法,方法名任意,不能有參數。這樣是為瞭保證這個順序:依賴註入之後,才執行update方法。該註解的方法在整個Bean初始化中的執行順序:Constructor(構造方法) -> @Autowired(依賴註入) -> @PostConstruct(註釋的方法)

RuleFunction類改造

還要改一個地方,初始化數據庫資源的入口方法是在RuleFunction類的構造函數中調用的。由於構造函數會先於依賴註入執行,需要把setFields和setExtInfo這兩個方法提取出來,且需要在依賴註入後執行。

修改成如下,並在RuleFunction類上加註解@Component。

改造前的執行流程

  • main方法內部調用MybatisSessionFactory的init方法
  • MybatisSessionFactory的init方法中new一個SqlSessionFactory
  • RuleFunction初始化時,調用自身構造方法
  • RuleFunction調用MybatisSessionFactory的query方法查詢數據庫

改造後的執行流程

  • PulsarMain和MybatisSessionFactory是松耦合的,
  • MybatisSessionFactory初始化時,因為通過@Autowired註解註入瞭SqlSessionFactory,所以需要初始化SqlSessionFactory,SqlSessionFactory初始化過程中會去使用配置文件中的數據庫連接參數初始化。
  • MybatisSessionFactory初始化完成後,由於MybatisSessionFactory.update方法使用瞭@PostConstruct註解,會執行update方法,將SqlSessionFactory賦值給靜態屬性sqlSessionFactory。
  • 後續RuleFunction的setFields方法執行過程中,就可以使用MybatisSessionFactory的query方法查詢數據庫瞭

總結

這次改造過程,對類加載過程、對象的實例化、static關鍵字、spring bean的生命周期有瞭更深入的理解。

  • 類加載過程,會初始化調用static修飾的屬性、方法、代碼塊
    • 類加載過程[2]:加載、鏈接、初始化
    • 其中鏈接的過程:驗證、準備、解析
  • 類初始化後,可以通過new關鍵字實例化一個對象,其它方式:通過反射api實例化
  • spring bean的生命周期[3]:實例化、屬性賦值、初始化、銷毀

擴展

對於這個問題抽象一下:Spring項目中,如果需要在一個類初始化時加載數據庫資源,可以有哪些方式?

參考

  • [1]靜態方法(工具類)中調用Spring管理的Bean
  • [2]類加載過程
  • [3]Spring Bean 的生命周期
  • Markdown 基於 Mermaid 的時序圖、流程圖和甘特圖

到此這篇關於記一次Maven項目改造成SpringBoot項目的過程實踐的文章就介紹到這瞭,更多相關Maven改造成SpringBoot項目內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: