搭建项目框架


对于Warp Exchange项目,我们以Maven为构建工具,把每个模块作为一个Maven的项目管理,并抽取出公共逻辑放入common模块,结构如下:

  • common:公共代码;
  • config:配置服务器;
  • push:推送服务;
  • quotation:行情服务;
  • trading-api:交易API服务;
  • trading-engine:交易引擎;
  • trading-sequencer:定序服务;
  • ui:用户Web界面。

为了简化版本和依赖管理,我们用parent模块管理最基础的pom.xml,其他模块直接从parent继承,能大大简化各自的pom.xmlparent模块pom.xml内容如下:

  1. <project xmlns="http://maven.apache.org/POM/4.0.0"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  4. http://maven.apache.org/xsd/maven-4.0.0.xsd"
  5. >
  6. <modelVersion>4.0.0</modelVersion>
  7. <groupId>com.itranswarp.exchange</groupId>
  8. <artifactId>parent</artifactId>
  9. <version>1.0</version>
  10. <packaging>pom</packaging>
  11. <!-- 继承自SpringBoot Starter Parent -->
  12. <parent>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-parent</artifactId>
  15. <!-- SpringBoot版本 -->
  16. <version>3.0.0</version>
  17. </parent>
  18. <properties>
  19. <!-- 项目版本 -->
  20. <project.version>1.0</project.version>
  21. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  22. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  23. <!-- Java编译和运行版本 -->
  24. <maven.compiler.source>17</maven.compiler.source>
  25. <maven.compiler.target>17</maven.compiler.target>
  26. <java.version>17</java.version>
  27. <!-- 定义第三方组件的版本 -->
  28. <pebble.version>3.2.0</pebble.version>
  29. <springcloud.version>2022.0.0</springcloud.version>
  30. <springdoc.version>2.0.0</springdoc.version>
  31. <vertx.version>4.3.1</vertx.version>
  32. </properties>
  33. <!-- 引入SpringCloud依赖 -->
  34. <dependencyManagement>
  35. <dependencies>
  36. <dependency>
  37. <groupId>org.springframework.cloud</groupId>
  38. <artifactId>spring-cloud-dependencies</artifactId>
  39. <version>${springcloud.version}</version>
  40. <type>pom</type>
  41. <scope>import</scope>
  42. </dependency>
  43. </dependencies>
  44. </dependencyManagement>
  45. <!-- 共享的依赖管理 -->
  46. <dependencies>
  47. <!-- 依赖JUnit5 -->
  48. <dependency>
  49. <groupId>org.junit.jupiter</groupId>
  50. <artifactId>junit-jupiter-api</artifactId>
  51. <scope>test</scope>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.junit.jupiter</groupId>
  55. <artifactId>junit-jupiter-params</artifactId>
  56. <scope>test</scope>
  57. </dependency>
  58. <dependency>
  59. <groupId>org.junit.jupiter</groupId>
  60. <artifactId>junit-jupiter-engine</artifactId>
  61. <scope>test</scope>
  62. </dependency>
  63. <!-- 依赖SpringTest -->
  64. <dependency>
  65. <groupId>org.springframework</groupId>
  66. <artifactId>spring-test</artifactId>
  67. <scope>test</scope>
  68. </dependency>
  69. </dependencies>
  70. <build>
  71. <pluginManagement>
  72. <plugins>
  73. <!-- 引入创建可执行Jar的插件 -->
  74. <plugin>
  75. <groupId>org.springframework.boot</groupId>
  76. <artifactId>spring-boot-maven-plugin</artifactId>
  77. </plugin>
  78. </plugins>
  79. </pluginManagement>
  80. </build>
  81. </project>

上述pom.xml中,除了写死的Spring Boot版本、Java运行版本、项目版本外,其他引入的版本均以<xxx.version>1.23</xxx.version>的形式定义,以便后续可以用${xxx.version}引用版本号,避免了同一个组件出现多个写死的版本定义。

对其他业务模块,引入parentpom.xml可大大简化配置。以ui模块为例,其pom.xml如下:

  1. <project xmlns="http://maven.apache.org/POM/4.0.0"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  4. http://maven.apache.org/xsd/maven-4.0.0.xsd"
  5. >
  6. <modelVersion>4.0.0</modelVersion>
  7. <!-- 指定Parent -->
  8. <parent>
  9. <groupId>com.itranswarp.exchange</groupId>
  10. <artifactId>parent</artifactId>
  11. <version>1.0</version>
  12. <!-- Parent POM的相对路径 -->
  13. <relativePath>../parent/pom.xml</relativePath>
  14. </parent>
  15. <!-- 当前模块名称 -->
  16. <artifactId>ui</artifactId>
  17. <dependencies>
  18. <!-- 依赖SpringCloud Config客户端 -->
  19. <dependency>
  20. <groupId>org.springframework.cloud</groupId>
  21. <artifactId>spring-cloud-starter-config</artifactId>
  22. </dependency>
  23. <!-- 依赖SpringBoot Actuator -->
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-actuator</artifactId>
  27. </dependency>
  28. <!-- 依赖Common模块 -->
  29. <dependency>
  30. <groupId>com.itranswarp.exchange</groupId>
  31. <artifactId>common</artifactId>
  32. <version>${project.version}</version>
  33. </dependency>
  34. <!-- 依赖第三方模块 -->
  35. <dependency>
  36. <groupId>io.pebbletemplates</groupId>
  37. <artifactId>pebble-spring-boot-starter</artifactId>
  38. <version>${pebble.version}</version>
  39. </dependency>
  40. </dependencies>
  41. <build>
  42. <!-- 指定输出文件名 -->
  43. <finalName>${project.artifactId}</finalName>
  44. <!-- 创建SpringBoot可执行jar -->
  45. <plugins>
  46. <plugin>
  47. <groupId>org.springframework.boot</groupId>
  48. <artifactId>spring-boot-maven-plugin</artifactId>
  49. </plugin>
  50. </plugins>
  51. </build>
  52. </project>

因为我们在parentpom.xml中引入了Spring Cloud的依赖管理,因此,无需指定相关组件的版本。只有我们自己编写的组件和未在Spring Boot和Spring Cloud中引入的组件,才需要指定版本。

最后,我们还需要一个build模块,把所有模块放到一起编译。建立build文件夹并创建pom.xml如下:

  1. <project xmlns="http://maven.apache.org/POM/4.0.0"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  4. http://maven.apache.org/maven-v4_0_0.xsd"
  5. >
  6. <modelVersion>4.0.0</modelVersion>
  7. <groupId>com.itranswarp.exchange</groupId>
  8. <artifactId>build</artifactId>
  9. <version>1.0</version>
  10. <packaging>pom</packaging>
  11. <name>Warp Exchange</name>
  12. <!-- 按相对路径列出所有模块 -->
  13. <modules>
  14. <module>../common</module>
  15. <module>../config</module>
  16. <module>../parent</module>
  17. <module>../push</module>
  18. <module>../quotation</module>
  19. <module>../trading-api</module>
  20. <module>../trading-engine</module>
  21. <module>../trading-sequencer</module>
  22. <module>../ui</module>
  23. </modules>
  24. </project>

我们还需要创建目录config-repo来存储Spring Cloud Config服务器端的配置文件。

最后,将所有模块导入IDE,可正常开发、编译、运行。如果要在命令行模式下运行,进入build文件夹使用Maven编译即可:

  1. warpexchange $ cd build && mvn clean package

本地开发环境

在本地开发时,我们需要经常调试代码。除了安装JDK,选择一个IDE外,我们还需要在本地运行MySQL、Redis、Kafka,以及Kafka依赖的ZooKeeper服务。

考虑到手动安装各个服务在不同操作系统下的差异,以及初始化数据非常麻烦,我们使用Docker Desktop来运行这些基础服务,需要在build目录下编写一个docker-compose.yml文件定义我们要运行的所有服务:

  1. version: "3"
  2. services:
  3. zookeeper:
  4. image: bitnami/zookeeper:3.5
  5. container_name: zookeeper
  6. ports:
  7. - "2181:2181"
  8. environment:
  9. - ALLOW_ANONYMOUS_LOGIN=yes
  10. volumes:
  11. - "./docker/zookeeper-data:/bitnami"
  12. kafka:
  13. image: bitnami/kafka:3.0
  14. container_name: kafka
  15. ports:
  16. - "9092:9092"
  17. depends_on:
  18. - zookeeper
  19. environment:
  20. - KAFKA_BROKER_ID=1
  21. - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
  22. - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092
  23. - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
  24. - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
  25. - ALLOW_PLAINTEXT_LISTENER=yes
  26. volumes:
  27. - "./docker/kafka-data:/bitnami"
  28. redis:
  29. image: redis:6.2
  30. container_name: redis
  31. ports:
  32. - "6379:6379"
  33. volumes:
  34. - "./docker/redis-data:/data"
  35. mysql:
  36. image: mysql:8
  37. container_name: mysql
  38. ports:
  39. - "3306:3306"
  40. command: --default-authentication-plugin=mysql_native_password
  41. environment:
  42. - MYSQL_ROOT_PASSWORD=password
  43. volumes:
  44. - "./sql/schema.sql:/docker-entrypoint-initdb.d/1-schema.sql:ro"
  45. - "./docker/mysql-data:/var/lib/mysql"

在上述docker-compose.yml文件中,我们定义了MySQL、Redis、Kafka以及Kafka依赖的ZooKeeper服务,各服务均暴露标准端口,且MySQL的root口令设置为password,第一次启动MySQL时,使用sql/schema.sql文件初始化数据库表结构。所有数据盘均挂载到build目录下的docker目录。

build目录下运行docker-compose up -d即可启动容器:

  1. build $ docker-compose up -d
  2. Creating network "build_default" with the default driver
  3. Creating zookeeper ... done
  4. Creating mysql ... done
  5. Creating redis ... done
  6. Creating kafka ... done

在Docker Desktop中可看到运行状态:

docker-desktop

如果要删除开发环境的所有数据,首先停止运行Docker容器进程并删除,然后删除build目录下的docker目录,重新运行docker-compose即可。

Spring Cloud Config

Spring Cloud Config是Spring Cloud的一个子项目,它的主要目的是解决多个Spring Boot应用启动时,应该如何读取配置文件的问题。

对于单体应用,即一个独立的Spring Boot应用,我们会把配置写在application.yml文件中。如果配置需要针对多个环境,可以用---分隔并标注好环境:

  1. # application.yml
  2. # 通用配置:
  3. spring:
  4. datasource:
  5. url: jdbc:mysql://localhost/test
  6. ---
  7. # test profile:
  8. spring:
  9. config:
  10. activate:
  11. on-profile: test
  12. datasource:
  13. url: jdbc:mysql://172.16.0.100/test

这种配置方式针对单个Spring Boot应用是可行的,但是,针对分布式应用,有多个Spring Boot应用需要启动时,分散在各个应用中的配置既不便于管理,也不便于复用相同的配置。

Spring Cloud Config提供了一个通用的分布式应用的配置解决方案。它把配置分为两部分:

  • Config Server:配置服务器,负责读取所有配置;
  • Config Client:嵌入到各个Spring Boot应用中,本地无配置信息,启动时向服务器请求配置。

我们先来看看如何搭建一个Spring Cloud Config Server,即配置服务器。

首先,在config模块中引入spring-cloud-config-server依赖:

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-config-server</artifactId>
  4. </dependency>

然后,编写一个ConfigApplication入口,标注@EnableConfigServer

  1. @EnableConfigServer
  2. @SpringBootApplication
  3. public class ConfigApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ConfigApplication.class, args);
  6. }
  7. }

最后,在application.yml中设置如何搜索配置。Spring Cloud Config支持多种配置方式,包括从本地文件、Git仓库、数据库等多个地方读取配置。这里我们选择以本地文件的方式读取配置文件,这也是最简单的一种配置方式:

  1. # 配置服务器的端口,通常设置为8888:
  2. server:
  3. port: 8888
  4. spring:
  5. application:
  6. name: config-server
  7. profiles:
  8. # 从文件读取配置时,Config Server激活的profile必须设定为native:
  9. active: native
  10. cloud:
  11. config:
  12. server:
  13. native:
  14. # 设置配置文件的搜索路径:
  15. search-locations: file:./config-repo, file:../config-repo, file:../../config-repo

config-repo目录下,存放的就是一系列配置文件:

  1. config-repo/
  2. ├── application-default.yml
  3. ├── application-test.yml
  4. ├── application.yml
  5. ├── push.yml
  6. ├── quotation.yml
  7. ├── trading-api.yml
  8. ├── trading-engine.yml
  9. ├── trading-sequencer.yml
  10. ├── ui-default.yml
  11. └── ui.yml

至此,配置服务器就完成了,直接运行ConfigApplication即可启动配置服务器。在开发过程中,保持配置服务器在后台运行即可。

接下来,对于每个负责业务的Spring Boot应用,我们需要从Spring Cloud Config Server读取配置。读取配置并不是说本地零配置,还是需要一点基础配置信息。以ui项目为例,编写application.yml如下:

  1. spring:
  2. application:
  3. # 设置app名称:
  4. name: ui
  5. config:
  6. # 导入Config Server地址:
  7. import: configserver:${CONFIG_SERVER:http://localhost:8888}

上述默认的Config Server配置为http://localhost:8888,也可以通过环境变量指定Config Server的地址。

下一步是在ui模块的pom.xml中添加依赖:

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-config</artifactId>
  4. </dependency>

接下来正常启动UIApplication,该应用就会自动从Config Server读取配置。由于我们指定了应用的名称是ui,且默认的profiledefault,因此,Config Server将返回以下4个配置文件:

  • ui-default.yml
  • application-default.yml
  • ui.yml
  • application.yml

前面的配置文件优先级较高,后面的配置文件优先级较低。如果出现相同的配置项,则在优先级高的配置生效。

我们可以在浏览器访问http://localhost:8888/ui/default看到Config Server返回的配置,它是一个JSON文件:

  1. {
  2. "name": "ui",
  3. "profiles": [
  4. "default"
  5. ],
  6. "label": null,
  7. "version": null,
  8. "state": null,
  9. "propertySources": [
  10. {
  11. "name": "file:../config-repo/ui-default.yml",
  12. "source": {...}
  13. },
  14. {
  15. "name": "file:../config-repo/application-default.yml",
  16. "source": {...}
  17. },
  18. {
  19. "name": "file:../config-repo/ui.yml",
  20. "source": {...}
  21. },
  22. {
  23. "name": "file:../config-repo/application.yml",
  24. "source": {...}
  25. }
  26. ]
  27. }

如果我们启动UIApplication时传入SPRING_PROFILES_ACTIVE=test,将profile设置为test,则Config Server返回的文件如下:

  • ui-test.yml
  • application-test.yml
  • ui.yml
  • application.yml

可以通过http://localhost:8888/ui/test查看返回的配置。由于文件ui-test.yml不存在,因此,实际配置由3个文件合并而成。

我们可以很容易地看到,一个Spring Boot应用在启动时,首先要设置自己的name并导入Config Server的URL,再根据当前活动的profile,由Config Server返回多个配置文件:

  • {name}-{profile}.yml
  • application-{profile}.yml
  • {name}.yml
  • application.yml

其中,{name}-{xxx}.yml是针对某个应用+某个profile的特定配置,{name}.yml是针对某个应用+所有profile的配置,application-{profile}.yml是针对某个profile的全局配置,application.yml是所有应用的全局配置。搭配各种配置文件就可以灵活组合配置。一般来说,全局默认的配置放在application.yml中,例如数据库连接:

  1. spring:
  2. datasource:
  3. url: jdbc:mysql://localhost/test

这样保证了默认连接到本地数据库,在生产环境中会直接报错而不是连接到错误的数据库。

在生产环境,例如profile设置为prod,则可以将数据库连接写在application-prod.yml中,使得所有生产环境的应用读取到的数据库连接是一致的:

  1. spring:
  2. datasource:
  3. url: jdbc:mysql://172.16.0.100/prod_db

某个应用自己特定的配置则应当放到{name}.yml{name}-{profile}.yml中。

在设置好各个配置文件后,应当通过浏览器检查Config Server返回的配置是否符合预期。

Spring Cloud Config还支持配置多个profile,以及从加密的配置源读取配置等。如果遇到更复杂的需求,可参考Spring Cloud Config的文档

环境变量

需要特别注意,在config-repo的配置文件里,使用的环境变量,不是Config Server的环境变量,而是具体某个Spring Boot应用的环境变量。

我们举个例子:假定ui.yml定义如下:

  1. server:
  2. port: ${APP_PORT:8000}

UIApplication启动时,它获得的配置为server.port=${APP_PORT:8000}。Config Server不会替换任何环境变量,而是将它们原封不动地返回给UIApplication,由UIApplication根据自己的环境变量解析后获得最终配置。如果我们启动UIApplication时传入环境变量:

  1. $ java -DAPP_PORT=7000 -jar ui.jar

UIApplication最终读取的配置server.port7000

可见,使用Spring Cloud Config时,读取配置文件步骤如下:

  1. 启动XxxApplication时,读取自身的application.yml,获得name和Config Server地址;
  2. 根据nameprofile和Config Server地址,获得一个或多个有优先级的配置文件;
  3. 按优先级合并配置项;
  4. 如果配置项中存在环境变量,则使用Xxx应用本身的环境变量去替换占位符。

环境变量通常用于配置一些敏感信息,如数据库连接口令,它们不适合明文写在config-repo的配置文件里。

常见错误

启动一个Spring Boot应用时,如果出现Unable to load config data错误:

  1. java.lang.IllegalStateException: Unable to load config data from 'configserver:http://localhost:8888'
  2. at org.springframework.boot.context.config.StandardConfigDataLocationResolver.getReferences
  3. at ...

需要检查是否在pom.xml中引入了spring-cloud-starter-config,因为没有引入该依赖时,应用无法解析本地配置的import: configserver:xxx

如果在启动一个Spring Boot应用时,Config Server没有运行,通常错误信息是因为没有读取到配置导致无法创建某个Bean。

参考源码

可以从GitHubGitee下载源码。

GitHubmichaelliaowarpexchange/

▸ build)

▸ sql)

▤ schema.sql)

▤ docker-compose.yml)

▤ pom.xml)

▸ common)

▸ src/main/resources)

▤ logback-spring.xml)

▤ pom.xml)

▸ config)

▸ src/main)

▸ java/com/itranswarp/exchange/config)

▤ ConfigApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ config-repo)

▤ application-default.yml)

▤ application-test.yml)

▤ application.yml)

▤ push.yml)

▤ quotation.yml)

▤ trading-api.yml)

▤ trading-engine.yml)

▤ trading-sequencer.yml)

▤ ui-default.yml)

▤ ui.yml)

▸ parent)

▤ pom.xml)

▸ push)

▸ src/main)

▸ java/com/itranswarp/exchange/push)

▤ PushApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ quotation)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ QuotationApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-api)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ TradingApiApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-engine)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ TradingEngineApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-sequencer)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ TradingSequencerApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ ui)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ UIApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▤ .gitignore)

▤ LICENSE)

▤ README.md)

小结

我们以Spring Boot为基础,并通过Maven的模块化配置搭建了项目的基本结构,依赖的基础组件通过Docker Desktop运行并初始化数据。对于多个服务组成的分布式应用来说,使用Spring Cloud Config可满足应用的配置需求。

读后有收获可以支付宝请作者喝咖啡:

搭建项目框架 - 图2