探索定制Spring Initializr
目录
背景#
什么是Spring Initializr#
Spring Initializr是一个用于生成Spring Boot项目的在线工具。它可以帮助开发者快速创建一个包含所需依赖和配置的Spring Boot项目骨架,从而节省了手动配置项目的时间和精力。
web界面: start.spring.io
实际工作中,我们可能会怎么初始化spring boot微服务项目呢?找一个现有项目,删除各种不必要的文件,改改就好,但是这样做往往会让初始项目不太干净。
需求#
- 对于指定的项目类型,比如
web项目,自动添加常用的依赖,比如spring-boot-starter-web,spring-boot-starter-data-jpa等。 - 自动添加
git仓库初始化,并添加.gitignore文件。 - 自动添加
ci/cd配置文件,比如GitHub Actions的workflow文件。 - 支持多种项目类型,比如
web,cli,consumer,cronjob等。
本次探索主要针对web项目类型进行定制。
定制Spring Initializr#
生成基础项目#
访问start.spring.io,选择项目类型为Maven Project, 语言为Java, Java版本为25, Spring Boot版本为3.5.7, 项目元数据根据需要填写,选择依赖Spring Web,,点击Generate`按钮,下载生成的项目压缩包。
修改pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-web</artifactId>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator-spring</artifactId>
</dependency>
<!-- other dependencies -->
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-bom</artifactId>
<version>0.22.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
当前最新版本为0.22.0,可以在maven central查看最新版本。该版本并不兼容Spring Boot 4。
启动项目,访问http://localhost:8080/。
使用浏览器和命令行访问会看到不同的输出,输出内容可以对应到start.spring.io的首页内容。
$ curl http://localhost:8080
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Initializr :: http://localhost:8080
This service generates quickstart projects that can be easily customized.
Possible customizations include a project's dependencies, Java version, and
build system or build structure. See below for further details.
The services uses a HAL based hypermedia format to expose a set of resources
to interact with. If you access this root resource requesting application/json
as media type the response will contain the following links:
+-----+-------------+
| Rel | Description |
+-----+-------------+
+-----+-------------+
The URI templates take a set of parameters to customize the result of a request
to the linked resource.
+-----------------+------------------------------------------+------------------------------+
| Parameter | Description | Default value |
+-----------------+------------------------------------------+------------------------------+
| applicationName | application name | DemoApplication |
| artifactId | project coordinates (infer archive name) | demo |
| baseDir | base directory to create in the archive | no base dir |
| bootVersion | spring boot version | |
| dependencies | dependency identifiers (comma-separated) | none |
| description | project description | Demo project for Spring Boot |
| groupId | project coordinates | com.example |
| javaVersion | language level | |
| language | programming language | |
| name | project name (infer application name) | demo |
| packageName | root package | com.example.demo |
| packaging | project packaging | |
| type | project type | |
| version | project version | 0.0.1-SNAPSHOT |
+-----------------+------------------------------------------+------------------------------+
The following section has a list of supported identifiers for the comma-separated
list of "dependencies".
+----+-------------+------------------+
| Id | Description | Required version |
+----+-------------+------------------+
+----+-------------+------------------+
Examples:
To create a default demo.zip:
$ curl -G http://localhost:8080/starter.zip -o demo.zip
To create a web project using Java 11:
$ curl -G http://localhost:8080/starter.zip -d dependencies=web \
-d javaVersion=11 -o demo.zip
To create a web/data-jpa gradle project unpacked:
$ curl -G http://localhost:8080/starter.tgz -d dependencies=web,data-jpa \
-d type=gradle-project -d baseDir=my-dir | tar -xzvf -
To generate a Maven POM with war packaging:
$ curl -G http://localhost:8080/pom.xml -d packaging=war -o pom.xml
浏览器访问会输出Json格式的数据,命令行访问会输出文本格式的数据。
{
"_links": {
"dependencies": {
"href": "http://localhost:8080/dependencies{?bootVersion}",
"templated": true
}
},
"dependencies": {
"type": "hierarchical-multi-select",
"values": []
},
"type": {
"type": "action",
"values": []
},
"packaging": {
"type": "single-select",
"values": []
},
"javaVersion": {
"type": "single-select",
"values": []
},
"language": {
"type": "single-select",
"values": []
},
"bootVersion": {
"type": "single-select",
"values": []
},
"groupId": {
"type": "text",
"default": "com.example"
},
"artifactId": {
"type": "text",
"default": "demo"
},
"version": {
"type": "text",
"default": "0.0.1-SNAPSHOT"
},
"name": {
"type": "text",
"default": "demo"
},
"description": {
"type": "text",
"default": "Demo project for Spring Boot"
},
"packageName": {
"type": "text",
"default": "com.example.demo"
}
}
配置文件#
大多数配置可通过application.properties文件中以initializr为命名空间的配置项实现。可以通过override这些配置项来定制生成的项目。
由于该配置具有高度层级化特性,官方推荐使用yaml格式。
默认的配置文件为application.yml。
initializr:
boot-versions:
- id: 3.5.7
default: true
java-versions:
- id: 25
default: true
- id: 17
default: false
languages:
- id: java
default: true
group-id:
value: dev.meirong.showcase
artifact-id:
value: my-app
description:
value: Spring Boot Showcase
packagings:
- id: jar
name: Jar
default: true
types:
- id: maven-project
default: true
description: Generate a Maven based project archive
tags:
build: maven
format: project
action: /starter.zip
通过命令行访问http://localhost:8080/,可以看到配置已经生效。
The URI templates take a set of parameters to customize the result of a request
to the linked resource.
+-----------------+------------------------------------------+-----------------------------+
| Parameter | Description | Default value |
+-----------------+------------------------------------------+-----------------------------+
| applicationName | application name | DemoApplication |
| artifactId | project coordinates (infer archive name) | my-app |
| baseDir | base directory to create in the archive | no base dir |
| bootVersion | spring boot version | 3.5.7 |
| dependencies | dependency identifiers (comma-separated) | none |
| description | project description | Spring Boot Showcase |
| groupId | project coordinates | dev.meirong.showcase |
| javaVersion | language level | 25 |
| language | programming language | java |
| name | project name (infer application name) | demo |
| packageName | root package | dev.meirong.showcase.my_app |
| packaging | project packaging | jar |
| type | project type | maven-project |
| version | project version | 0.0.1-SNAPSHOT |
+-----------------+------------------------------------------+-----------------------------+
通过ProjectGenerationConfiguration定制#
ProjectGenerationConfiguration是Spring Initializr提供的一个特殊的@Configuration类,可以通过实现该类来定制生成的项目。
每个项目生成请求都可能有不同的参数, 比如选择的依赖,项目类型等。这些参数需要在请求处理过程中生效,所以相关的处理bean不能是spring boot应用启动时就初始化的单例bean。
在应用初始化的时候,需要排除该类的自动配置。
@ComponentScan(
basePackages = "dev.meirong.showcase.initializr_demo",
excludeFilters =
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = ProjectGenerationConfiguration.class))
public class InitializrDemoApplication {
public static void main(String[] args) {
SpringApplication.run(InitializrDemoApplication.class, args);
}
}
They are located using the SpringFactoriesLoader mechanism
需要在src/main/resources/META-INF/spring.factories文件中注册自定义的ProjectGenerationConfiguration实现类。
io.spring.initializr.generator.project.ProjectGenerationConfiguration=\
dev.meirong.showcase.initializr_demo.CustomProjectGenerationConfiguration
官网有相关资料可以参考
添加自定义依赖#
需要在application.yml中添加自定义依赖。
initializr:
......
......
env:
repositories:
# 使用 'confluent' 作为 ID,以便在依赖中引用
confluent-repo:
name: Confluent Maven Repository
url: https://packages.confluent.io/maven/
dependencies:
# --- 组 1: Web/基础 ---
- name: Web
content:
- name: Web
id: web
description: Full-stack web development with Tomcat and Spring MVC
groupId: org.springframework.boot
artifactId: spring-boot-starter-web
# Spring Boot Starter Web 默认是平台管理的,无需指定坐标
- name: Actuator
id: actuator
description: Production-ready features to help you monitor and manage your application
groupId: org.springframework.boot
artifactId: spring-boot-starter-actuator
# 默认是平台管理的
- name: Kafka JSON Schema
id: kafka-json-schema
groupId: io.confluent
artifactId: kafka-json-schema-serializer
version: 8.1.0
repository: confluent-repo
如果是spring boot starter依赖,可以不指定版本号,版本号会由spring boot的依赖管理自动添加。
如果是第三方依赖,需要指定版本号。如果是私有仓库的依赖,还需要指定repository。可以参考例子中的kafka-json-schema依赖。
此时创建项目的时候,可以通过dependencies参数指定需要添加的依赖。
curl -G http://localhost:8080/starter.zip -d dependencies=web,actuator,kafka-json-schema -o demo.zip
但是我希望的我项目生成的时候自动已经添加这些依赖,而不是每次都需要手动指定。一个方式是写个script来自动添加这些参数,另一种方式是实现BuildCustomizer接口来自动添加这些依赖。
public class DependencyCustomizer implements BuildCustomizer<MavenBuild> {
@Override
public void customize(MavenBuild mavenBuild) {
// spring boot parent
// Web/基础
mavenBuild.dependencies().add("web");
mavenBuild.dependencies().add("actuator");
mavenBuild.dependencies().add("validation");
// ....... other dependencies
}
}
添加git初始化#
我希望在项目生成的时候,自动添加git初始化,并添加.gitignore文件。
执行这类操作,可以通过实现ProjectContributor接口来实现。实际上生成项目的时候,服务端是会生成一个临时目录来存放项目,在这个目录下执行各种操作,最后将该目录打包成zip文件返回给客户端。
public class GitInitProjectContributor implements ProjectContributor {
private static final Logger log = LoggerFactory.getLogger(GitInitProjectContributor.class);
@Override
public void contribute(Path projectRoot) throws IOException {
log.info("Initializing Git repository in {}", projectRoot);
try {
ProcessBuilder processBuilder = new ProcessBuilder("git", "init");
processBuilder.directory(projectRoot.toFile());
processBuilder.redirectErrorStream(true); // Merge error and output streams
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
// Note: The process output is not captured here, but will appear in the main
// application logs.
throw new IOException("Failed to run 'git init', exit code: " + exitCode);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for 'git init' to complete", ex);
}
}
}
要定制.gitignore文件,也有GitIgnoreCustomizer可以使用。
@Bean
GitIgnoreCustomizer customGitIgnoreCustomizer() {
return (gitIgnore) -> {
// IDE 相关
gitIgnore.getGeneral().add("*.iml");
gitIgnore.getGeneral().add("*.ipr");
gitIgnore.getGeneral().add("*.iws");
gitIgnore.getGeneral().add(".idea/");
gitIgnore.getGeneral().add(".vscode/");
// ... other ignores
}
}
Maven Plugin定制#
类似于依赖定制,可以通过实现BuildCustomizer接口来定制Maven插件。
ublic class PluginCustomizer implements BuildCustomizer<MavenBuild> {
@Override
public void customize(MavenBuild mavenBuild) {
mavenBuild
.properties()
.property("spotbugs-maven-plugin.version", "4.9.8.1")
.property("spotbugs.version", "4.9.8")
.property("spotless-maven-plugin.version", "3.0.0");
mavenBuild.settings().finalName("app");
// Plugins
mavenBuild.plugins().add("org.springframework.boot", "spring-boot-maven-plugin");
mavenBuild
.plugins()
.add(
"com.github.spotbugs",
"spotbugs-maven-plugin",
plugin -> {
plugin.version("${spotbugs-maven-plugin.version}");
});
mavenBuild
.plugins()
.add(
"com.diffplug.spotless",
"spotless-maven-plugin",
plugin -> {
plugin.version("${spotless-maven-plugin.version}");
});
// ignore other plugins
}
}
使用模版来生成项目目录结构和文件#
使用ProjectContributor接口,基本可以对项目内容做任何变更,包括添加文件,修改文件等。
通过TemplateRenderer可以使用模版引擎来生成文件内容。Spring Boot Initializr内置了Mustache模版引擎。
很多公司的项目生成器都是通过模版引擎来生成项目文件的。好不好用就我看来只是模版插件的扩展性问题。不同场景往往需要不同项目模板。
public class YamlConfigurationCustomizer implements ProjectContributor {
private final ProjectDescription description;
private final TemplateRenderer templateRenderer;
public YamlConfigurationCustomizer(ProjectDescription description, TemplateRenderer templateRenderer) {
this.description = description;
this.templateRenderer = templateRenderer;
}
@Override
public void contribute(Path projectRoot) throws IOException {
Path resourcesDir = projectRoot.resolve("src/main/resources");
// 确保父目录存在
if (!Files.exists(resourcesDir)) {
Files.createDirectories(resourcesDir);
}
Path propertiesFile = resourcesDir.resolve("application.properties");
if (Files.exists(propertiesFile)) {
Files.delete(propertiesFile);
}
Path yamlFile = resourcesDir.resolve("application.yaml");
// create file if not exists
if (!Files.exists(yamlFile)) {
Files.createFile(yamlFile);
}
String artifactId = description.getArtifactId();
Map<String, String> model = new HashMap<>();
model.put("artifactId", artifactId);
String yamlContent = this.templateRenderer.render("application.yaml", model);
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(yamlFile))) {
writer.println(yamlContent);
}
}
}
总结#
虽然暂时没空去探索所有源码和实现,但是定制Spring Initializr还是比较简单的。通过配置文件和实现一些接口,就可以满足大部分定制需求。 这类公司的代码生成器,对公司内部统一技术栈和规范,提升开发效率,减少重复工作都有很大帮助。
实际上shopee的
spkit项目早期也是一个基于模版的代码生成工具,只是用于golang项目。