Spring Boot 項目 (一) 構建多模塊工程


1 寫在前面

動力:

之前編寫的 Spring Boot 程序把所有的代碼都塞在一個整體的源文件目錄下,不利於后續的編碼。再加之前端使用了 Angular 組織代碼,后台也將重構成模塊化形式;

局限:

由於編寫模塊化代碼需要對模塊化代碼有一定基礎了解(比如 JDK9 與前端的模塊化),與 Maven 了解,並且對 Spring Boot 的架構有一定了解,所以在沒有足夠的學習之前,是不能完成模塊化 這一目標的。

2 從零構建一個模塊化 Web 工程

2.1 代碼結構分析

我們將編寫一個簡單的 司機與車輛 vehicleSys

首先我們的代碼將分為幾個模塊:

  1. vs-domain: 用於定義 實體類;
  2. vs-dao: 用於定義數據庫訪問接口;
  3. vs-service: 用於定義處理服務;
  4. vs-web: 用於定義與前端的數據交互;

2.2 工程結構構建

2.2.1 主 Project

首先新建一個工程:

我們的工程目錄並不存放任何實際代碼,而是管理各種模塊,因此在 Spring Initializr 初始化項目時,我們不需要勾選任何依賴,只有 Maven Type 需要使用 POM 

 

 可以看到 Spring 為我們生成了以下結構:

 

 

 瀏覽它生成的依賴:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

 

 可以看到 Spring 為我們添加了主要使用的測試依賴,由於 Springboot 2.2.1 開始默認提供了 JUNIT5,所以我們將在之后的測試中使用它(如果有測試的話):

 並且也添加了各種核心依賴,這里只列出當前工程使用到的核心依賴:

  • spring-core:Spring 核心;
  • snakeyaml:解析 Resources 下的 y(a)ml 配置文件;
  • jakarta.annotation-api:JAVA 注解包,包括 security 和 sql ;
  • jakarta.servlet-api:Servlet 核心;
  • spring-boot-starter-logging:提供包括 log-back(默認),slf4j 的日志支持;
  • spring-boot-autoconfigure:完成 Spring Boot 自動配置魔法的核心依賴;
  • jackson/gson:序列化工具,默認使用 jackson

2.2.2 vs-domain

在項目根目錄下右鍵,new - Module,需要注意的是我們這次將使用 Maven ,新建 vs-domain 模塊,一直 next 完成;

並且編寫 Driver 類與 Car 類,這里使用了 Lombok 簡化代碼,請在 vs-domain 的 POM 文件中添加 lombok 依賴 

 1 package pancc.vs.domain;
 2 
 3 import lombok.*;
 4 
 5 import java.io.Serializable;
 6 import java.util.Set;
 7 
 8 /**
 9  * @author pancc
10  * @version 1.0
11  */
12 @Getter
13 @Setter
14 @Builder
15 @EqualsAndHashCode
16 @NoArgsConstructor
17 @AllArgsConstructor
18 public class Driver implements Serializable {
19     private static final long serialVersionUID = -5208610208917077981L;
20 
21     private Long id;
22     private String name;
23     private Integer age;
24 
25 
26     @EqualsAndHashCode.Exclude
27     private Set<Car> cars;
28 }

 

package pancc.vs.domain;

import lombok.*;

import java.io.Serializable;

/**
 * @author pancc
 * @version 1.0
 */
@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class Car implements Serializable {

    private static final long serialVersionUID = 1925499860719739118L;

    private Long id;
    private String name;
    private Long price;

}

2.2.3 vs-dao

跟 vs-domain 一樣,我們在工程下添加 vs-domain 模塊,並且編寫代碼,需要注意的是我們現在暫時將重點放在模塊化上,所以 dao 只是 mock,並且由於我們需要用到之前編寫的 vs-domain 模塊中的實體類,因此需要在 vs-dao 模塊的 POM 文件中添加依賴

1     <dependencies>
2         <dependency>
3             <groupId>pancc.vs</groupId>
4             <artifactId>vs-domain</artifactId>
5             <version>0.0.1-SNAPSHOT</version>
6             <scope>compile</scope>
7         </dependency>
8     </dependencies>

然后編寫 mock 的 DriverDao:

 1 package pancc.vs.dao;
 2 
 3 import org.springframework.stereotype.Component;
 4 import pancc.vs.domain.Car;
 5 import pancc.vs.domain.Driver;
 6 
 7 import java.util.Arrays;
 8 import java.util.Collections;
 9 import java.util.HashSet;
10 import java.util.Set;
11 
12 
13 /**
14  * @author pancc
15  * @version 1.0
16  */
17 @Component
18 public class DriverDao {
19     private static final Set<Driver> DRIVER_SET;
20 
21     static {
22         DRIVER_SET = Collections.unmodifiableSet(
23                 Collections.singleton(
24                         Driver.builder().id(250L).name("Kate").cars(
25                                 new HashSet<>(
26                                         Arrays.asList(
27                                                 Car.builder().id(1L).name("Ben").price(2500L).build(),
28                                                 Car.builder().id(2L).name("Tesla").price(30000L).build()))
29                         ).build()));
30     }
31 
32     public Set<Driver> queryAll() {
33         return DRIVER_SET;
34     }
35 }

 

2.2.4 vs-service 

vs-service 模塊與 vs-dao 的編寫過程沒有區別,這里只貼出代碼:

 1 package pancc.vs.service;
 2 
 3 import org.springframework.stereotype.Service;
 4 import pancc.vs.dao.DriverDao;
 5 import pancc.vs.domain.Driver;
 6 
 7 import java.util.Set;
 8 
 9 /**
10  * @author pancc
11  * @version 1.0
12  */
13 @Service
14 public class DriverService {
15 
16     private final DriverDao driverDao;
17 
18     public DriverService(DriverDao driverDao) {
19         this.driverDao = driverDao;
20     }
21 
22     public Set<Driver> queryAll() {
23         return this.driverDao.queryAll();
24     }
25 }

 

有一點令人不快的是,對於習慣了以前的 Spring Boot 編程的人,此時的 idea 並沒有在 @Autowire 注解導航可用的 bean,這個問題將在之后解決;

2.2.5 vs-web

從這里開始我們必須注意,由於從主工程的 POM 文件中繼承的 spring-boot-starter-parent 並不包含 web 依賴,我們必須手動添加,同時也要添加使用到的 vs-service

 1     <dependencies>
 2         <dependency>
 3             <groupId>pancc.vs</groupId>
 4             <artifactId>vs-service</artifactId>
 5             <version>0.0.1-SNAPSHOT</version>
 6             <scope>compile</scope>
 7         </dependency>
 8 
 9         <dependency>
10             <groupId>org.springframework.boot</groupId>
11             <artifactId>spring-boot-starter-web</artifactId>
12             <version>2.1.11.RELEASE</version>
13         </dependency>
14     </dependencies>

 

編寫 DriverController :

 1 package pancc.vs.web.controller;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.web.bind.annotation.GetMapping;
 5 import org.springframework.web.bind.annotation.RequestMapping;
 6 import org.springframework.web.bind.annotation.RestController;
 7 import pancc.vs.domain.Driver;
 8 import pancc.vs.service.DriverService;
 9 
10 import java.util.Set;
11 
12 /**
13  * @author pancc
14  * @version 1.0
15  */
16 @RestController
17 @RequestMapping("driver")
18 public class DriverController {
19 
20     @Autowired
21     private DriverService driverService;
22 
23     @GetMapping("all")
24     Set<Driver> all() {
25         return this.driverService.queryAll();
26     }
27 }

可以看到,此時 IDEA 仍舊沒有為:@Autowired  注解導航 Bean

 

 

我們的目的是在 web 環境下使用, 讓我們為 vs-web 編寫啟動類 :

 1 package pancc.vs.web;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 
 6 /**
 7  * @author pancc
 8  * @version 1.0
 9  */
10 @SpringBootApplication
11 public class App {
12     public static void main(String[] args) {
13         SpringApplication.run(App.class, args);
14     }
15 }

請注意 App 所在的 Package 這為解決接下來的問題很有幫助。

2.3 構建之后的問題

現在我們的 vs-web 的目錄結構是這樣的:

 

 同時, IDEA 也標識了這是一個 Spring Boot 應用。運行它,很好,我們得到了一個運行錯誤:

Field driverService in pancc.vs.web.controller.DriverController required a bean of type 'pancc.vs.service.DriverService' that could not be found.

 

讓我們先查看 Controller,哦豁完蛋:

 

 

沒問題,我們將在下一節修復它;

2.3.1 回顧 Spring Boot 

在着手解決上邊的問題之前,讓我們來復習一下 Spring Boot 的核心注解:

When a class does not include a package declaration, it is considered to be in the “default package”. The use of the “default package” is generally discouraged and should be avoided. It can cause particular problems for Spring Boot applications that use the @ComponentScan@ConfigurationPropertiesScan@EntityScan, or @SpringBootApplication annotations, since every class from every jar is read.

 與

 We generally recommend that you locate your main application class in a root package above other classes. The @SpringBootApplication annotation is often placed on your main class, and it implicitly defines a base “search package” for certain items. For example, if you are writing a JPA application, the package of the @SpringBootApplication annotated class is used to search for @Entity items. Using a root package also allows component scan to apply only on your project.

這兩段話簡要來說就是 @SpringBootApplication 注解會將自身所在包作為默認包掃描各種組件:Entity, Repository, Service, Controller....

2.3.2 修復錯誤

讓我們梳理一下幾個類的包位置:

  • Apppancc.vs.web
  • DriverControllerpancc.vs.web.controller
  • DriverServicepancc.vs.service
  • DriverDaopancc.vs.dao
  • Driverpancc.vs.domain
  • Carpancc.vs.domain

可以看到,只有 DriverController  App 的包下,因此只有 DriverController  可以被掃描到;其他類則處於不同包下不能被掃描;

 

讓我們先修復 DriverService ,這是最近需要修復的:

修改在 App 上的注釋:  @SpringBootApplication(scanBasePackages = { "pancc.vs.service"})   

現在我們得到了一個沒有報錯的 DriverController ,同時也沒有起到 Controller 作用的 普通類:

繼續運行 App , 會得到另一個錯誤:  

Parameter 0 of constructor in pancc.vs.service.DriverService required a bean of type 'pancc.vs.dao.DriverDao' that could not be found.

請不要忽視了添加 App 所在的包掃描,修改成下面的任意 1 項:

 @SpringBootApplication(scanBasePackages = { "pancc.vs.service"},scanBasePackageClasses = {App.class}) 

 @SpringBootApplication(scanBasePackages = {"pancc.vs.web", "pancc.vs.service"}) 

現在我們的 IDEA 能很好地導航 DriverController 依賴的 DriverService ,不要忘記我們的問題還沒有修復完整,DriverService 內的 Bean 依賴 DriverDao 也需要修復,繼續修改 啟動注解:

 @SpringBootApplication(scanBasePackages = {"pancc.vs.web", "pancc.vs.service","pancc.vs.dao"}) 

現在我們的程序可以運行了,啟動它並用 IDEA 的 RestServices 測試端口 /driver/all

 

2.3.3 懶人向修復

1 通配符:你可以使用通配符匹配包掃描路徑。

 @SpringBootApplication(scanBasePackages = {"pancc.vs.*"}) 可以避免重復勞動。

 

2 轉移啟動類到包頂層:就像 2.3.1 Spring Boot 文檔中描述的一樣,將 App 置於頂層中

將 App 置於 包 pancc.vs 中,現在我們的包結構變成了:

  • Apppancc.vs
  • DriverControllerpancc.vs.web.controller
  • DriverServicepancc.vs.service
  • DriverDaopancc.vs.dao
  • Driverpancc.vs.domain
  • Carpancc.vs.domain

你可以刪掉  scanBasePackages 屬性了

2.4 打包

2.4.1 在打包之前

讓我們看看  spring-boot-starter-web 依賴中有哪些需要注意的內容

  • spring-boot-starter:spring 核心
  • spring-boot-starter-json:rest 支持
  • spring-boot-starter-tomcat:內置 web 容器
  • hibernate-validator:驗證注解
  • spring-web:web 支持
  • spring-webmvc:mvc 支持

2.4.2 使用外部容器

 spring-boot-starter-web 依賴中存在 scope 為 compile 的 spring-boot-starter-tomcat ,所以我們在打包的時候需要將內置容器排除,同時在開發的時候需要使用內置容器,所以使用 provided

1         <dependency>
2             <groupId>org.springframework.boot</groupId>
3             <artifactId>spring-boot-starter-tomcat</artifactId>
4             <scope>provided</scope>
5         </dependency>

同時,需要設置打包方式為 war ,可以設置打包的輸出名字:

 1     <packaging>war</packaging>
 2 
 3     <!--Maven >= 3-->
 4     <build>
 5         <finalName>vs-web</finalName>
 6     </build>
 7 
 8     <!--Maven < 3-->
 9 <!--
10     <plugin>
11         <groupId>org.apache.maven.plugins</groupId>
12         <artifactId>maven-jar-plugin</artifactId>
13         <version>2.3.2</version>
14         <configuration>
15             <finalName>vs-web</finalName>
16         </configuration>
17     </plugin>
18     -->

同時由於使用外部容器,我們需要讓啟動類 App 繼承 SpringBootServletInitializer,以便將 Servlet,Filter,ServletContextInitializer Beans 綁定到外部容器上:

 1 package pancc.vs.web;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 import org.springframework.boot.builder.SpringApplicationBuilder;
 6 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
 7 
 8 /**
 9  * @author pancc
10  * @version 1.0
11  */
12 @SpringBootApplication(scanBasePackages = {"pancc.vs.*"})
13 public class App extends SpringBootServletInitializer {
14 
15 
16     @Override
17     protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
18         return builder.sources(App.class);
19     }
20 
21     public static void main(String[] args) {
22         SpringApplication.run(App.class, args);
23     }
24 }

設置完,在 vs-web 模塊目錄下執行 mvn clean && mvn package

 

 

 可以看到 maven 同時編譯了依賴各個模塊的的 class 同時在 vs-web 模塊的 target 下生成了 vs-web.war 包

 

 

 查看 vs-web.war 包的結構,可以看到依賴的各個模塊被很好的打包進去了:

 

 

 

將 vs-web.war 復制到 ~\tomcat\webapps 目錄下,執行啟動程序並訪問 鏈接  http://localhost:8080/vs-web/driver/all:

 

 

 

2.4.3 使用可執行 jar 包

由於目標是可執行 Jar 包,所以 vs-web 需要保留內置的 Tomcat 容器,此時我們的 App 需要繼承 SpringBootServletInitializer, 並且需要使用到 spring-boot-maven-plugin 插件,用於重新打包 Maven 打包好的 Jar 包。

現在我們的 POM 文件變成了:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5     <parent>
 6         <artifactId>multiple</artifactId>
 7         <groupId>pancc.vs</groupId>
 8         <version>0.0.1-SNAPSHOT</version>
 9     </parent>
10     <modelVersion>4.0.0</modelVersion>
11     <artifactId>vs-web</artifactId>
12 
13     <dependencies>
14         <dependency>
15             <groupId>pancc.vs</groupId>
16             <artifactId>vs-service</artifactId>
17             <version>0.0.1-SNAPSHOT</version>
18             <scope>compile</scope>
19         </dependency>
20 
21         <dependency>
22             <groupId>org.springframework.boot</groupId>
23             <artifactId>spring-boot-starter-web</artifactId>
24             <version>2.1.11.RELEASE</version>
25         </dependency>
26     </dependencies>
27 
28     <packaging>jar</packaging>
29 
30     <build>
31         <plugins>
32             <plugin>
33                 <groupId>org.springframework.boot</groupId>
34                 <artifactId>spring-boot-maven-plugin</artifactId>
35                 <configuration>
36                     <layout>JAR</layout>
37                     <mainClass>pancc.vs.web.App</mainClass>
38                 </configuration>
39                 <executions>
40                     <execution>
41                         <goals>
42                             <goal>repackage</goal>
43                         </goals>
44                     </execution>
45                 </executions>
46             </plugin>
47         </plugins>
48         <finalName>vs-web</finalName>
49     </build>
50 </project>

在 vs-web 模塊目錄下執行 mvn clean && mvn package && java -jar ./target/vs-web.jar, 可以看到程序正確執行,之后便可以愉快的測試了:

 

訪問  http://localhost:8080/driver/all 查看結果吧

 

2.5 我們漏了什么?

請注意 2.4.3 中 打包的可執行 JAR 包的結構,它的主類變成了 org.springframework.boot.loader.JarLauncher,我們將在之后抽點時間來分析它的源碼;

 

在 2.3.1 與 2.3.2 中我們分析了 Spring Boot 注解的包掃描問題,其中注解掃描的內容還包括 @Entity  ,這是屬於 javax.persistence 的注解,我們將在 Spring Boot 項目 (二) 中使用它


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM