背景#

什么是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项目。

References#

Source Code#