• 基于start.spring.io,定制你的Java脚手架


    一、背景:为什么要做?

    创建工程的痛点

    2020 年,我们公司迎来了业务发展的迅猛期,滋生大量创建工程的需求。总体来说,创建工程面临着以下几个问题。

    1. 在创建工程时,多采用 copy 历史工程,并在上面进行修改的方式,造成了新工程里遗留了一些老旧的“垃圾";
    2. 各团队所建工程分层方式不一,结构混乱,甚至有的包职责相同,命名却不一样,难以形成共识传递下去;
    3. 所依赖组件版本不一,比如jacksonguava包,难以形成技术演进,或者说技术演进兼容性问题很难解决;

    业内方案参考

    start.spring.io整合了Gradle, Maven工程,语言支持Java,Kotlin,Groovy

    start.spring.io主页面

    start.aliyun.comstart.spring.io基础上增加了不同应用架构的选择:MVC, 分层架构, COLA。同时也增加阿里的开源组件例如Spring Cloud Alibaba

    start.aliyun.com主页面

    同时也增加了【一键运行】功能,【分享】功能可以保存分享至自己的账号下。

    二、构思:做成什么样?

    脚手架画像

    1. 能快速创建一个最小可运行工程;
    2. 能规范工程命名、服务应用架构分层, 增加代码结构规范、可理解性;
    3. 能快速集成 CI/CD,代码驱动 API 接口文档生成,提升开发效率;
    4. 能统一第三方组件版本号;

    1.0 版本

    为了快速落地脚手架,我们使用了 Maven Archetype 来实现。首先创建一个规范化的工程。

    工程结构需分层清晰,像斑马的条纹,因此取名为zebra。工程已开源:https://github.com/dbses/zebra

    规范的示例工程

    然后使用 Maven 的maven-archetype-plugin插件,生成脚手架。

    <plugin>
        <groupId>org.apache.maven.pluginsgroupId>
        <artifactId>maven-archetype-pluginartifactId>
        <version>3.0.0version>
        <configuration>
            <propertyFile>archetype.propertiespropertyFile>
            <encoding>UTF-8encoding>
            <archetypeFilteredExtentions>java,xml,yml,sh,groovy,mdarchetypeFilteredExtentions>
        configuration>
    plugin>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    生成的脚手架如下:

    示例工程的脚手架

    脚手架中生成的代码不是可编译的代码,它包含了一些变量。

    #set( $symbol_pound = '#' )
    #set( $symbol_dollar = '$' )
    #set( $symbol_escape = '\' )
    package ${package};
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.feign.EnableFeignClients;
    
    /**
     * @author dbses
     */
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class WebApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(WebApplication.class, args);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    最后,就可以将脚手架打包并上传至仓库。

    # 1. 修改项目代码,在项目目录执行
    cd ${projectRoot}
    mvn archetype:create-from-project
    # 2. 然后在 target/generated-sources/archetype 目录下执行下一步的操作
    cd target/generated-sources/archetype
    # 3. 脚手架打包
    mvn clean install
    # 4. 上传脚手架
    mvn clean deploy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    用户可以通过以下命令下载脚手架包。

    mvn dependency:get    \
      -DremoteRepositories={仓库地址}    \
      -DgroupId={脚手架的groupId}    \
      -DartifactId={脚手架的artifactId}    \
      -Dversion={版本}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下载完成后,使用脚手架生成新工程。

    mvn archetype:generate    \
      -DarchetypeGroupId={脚手架的groupId}    \
      -DarchetypeArtifactId={脚手架的artifactId}    \
      -DarchetypeVersion={脚手架版本}    \
      -DgroupId={工程的groupId}    \
      -DartifactId={工程的artifactId}    \
      -Dversion={工程版本号}    \
      -Dpackage={工程包名}    \
      -DinteractiveMode=false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.0 版本

    脚手架 1.0 版本解决了上述的大多痛点,但也有一些无法实现的或者可以做得更好的。例如:基础组件依赖无法管理,用户无法灵活的选择工程所需要的依赖等。参考start.spring.io,我们发现可以做的还有很多,于是启动 2.0 版本的开发。

    最终形态:

    脚手架2.0主页面

    三、实现:怎么做的?

    相比于start.spring.io,主要变化是增加了分层应用架构,整合了公司自己的组件库,并且新开发了【一键运行】功能。

    主要实现

    当前端组织好参数后,最终通过 HTTP 请求将参数传递给后端,后端接收到的参数如下。

    ProjectGenerationController

    后端接收到工程类型为maven-project,并通过如下配置将之识别为zebra-project,即分层架构。

    initializr:
        types:
        - name: None
          id: based-project
          description: Generate a Maven based project archive.
          tags:
            build: maven
            format: based-project
          action: /starter.zip
        - name: 分层架构
          id: maven-project
          description: Generate a Zebra project archive.
          tags:
            build: maven
            format: zebra-project
          default: true
          action: /starter.zip
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们自定义zebra-project分层架构的代码实现。这里定义一些 BuildCustomizer,实现工程的一些定制,例如:用户选择了 spring-boot-starter,程序应该在pom.xml生成相应的 dependency。代码如下:

    Spring Initializr 提供了 BuildCustomizer 接口的扩展性)

    @ProjectGenerationConfiguration
    @ConditionalOnProjectFormat(ZebraProjectFormat.ID)
    public class ZebraProjectGenerationConfiguration {
        
        // 省略代码
        
        @Bean
        public BuildCustomizer<Build> springBootStarterBuildCustomizer() {
            return (build) -> {
                build.dependencies().add(SPRINGBOOT_STARTER_ID,
                        Dependency.withCoordinates("org.springframework.boot", "spring-boot-starter"));
                build.dependencies().add("starter-test",
                        Dependency.withCoordinates("org.springframework.boot", "spring-boot-starter-test")
                                .scope(DependencyScope.TEST_COMPILE));
            };
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    再定义一些 Contributor,实现工程各个部分结构的生成,例如根目录pom.xml文件。

    @ProjectGenerationConfiguration
    @ConditionalOnProjectFormat(ZebraProjectFormat.ID)
    public class ProjectContributorAutoConfiguration {
        
        // 省略代码
        
        @Bean
        public ZebraRootPomProjectContributor zebraRootPomProjectContributor(
                MavenBuild build,
                IndentingWriterFactory indentingWriterFactory) {
            return new ZebraRootPomProjectContributor(build, indentingWriterFactory);
        }
    
        @Bean
        public ApplicationYmlProjectContributor bootstrapYmlProjectContributor(ProjectDescription description) {
            return new ApplicationYmlProjectContributor(description);
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ”项目添加了什么依赖,工程就生成对应的代码“,对于这个功能点,是通过@ConditionalOnRequestedDependency注解来实现的。

    @Bean
    @ConditionalOnRequestedDependency("web")
    public OrderServiceImplCodeProjectContributor orderServiceImplCodeProjectContributor() {
        return new OrderServiceImplCodeProjectContributor(this.description);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    例如OrderServiceImplCodeProjectContributor代码类的生成,只当用户选择了 web 依赖才会生成。

    public class OrderServiceImplCodeProjectContributor implements ProjectContributor {
        // 省略代码
        @Override
        public void contribute(Path projectRoot) throws IOException {
            JavaCompilationUnit javaCompilationUnit = javaSourceCode.createCompilationUnit(
                    this.description.getPackageName() + ".restful", "OrderServiceImpl");
            JavaTypeDeclaration javaTypeDeclaration = javaCompilationUnit.createTypeDeclaration("OrderServiceImpl");
            customize(javaTypeDeclaration);
    
            Path servicePath = ContributorSupport.getServicePath(projectRoot, description.getArtifactId());
            this.javaSourceCodeWriter.writeTo(
                    new SourceStructure(servicePath.resolve("src/main/"), new JavaLanguage()),
                    javaSourceCode);
        }
        // 省略代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    依赖包管理

    增加如下配置即可实现新组件库的增加。

    initializr:
      dependencies:
        - name: 基础组件库
          bom: infra
          repository: my-rep
          content:
            - name: Example
              id: example
              groupId: com.dbses.open
              artifactId: example-spring-boot-starter
              description: 示例组件说明
              starter: true
              links:
                - rel: guide
                  href: {用户手册}
                  description: Example 快速开始
                - rel: reference
                  href: {参考文档}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    一键运行

    一键运行功能是把生成好的工程上传至公司的代码仓库(Gitlab),并做好新工程的 CICD 配置(Jenkins),然后将工程部署到云容器(Kubernetes)的过程。

    一键运行页面

    前端使用的是 React 组件是抖音的 Semi。Gitlab Groups 下拉列表是通过 Gitlab API 授权获取的,这个授权过程如下:

    Gitlab授权流程图

    授权完成后,点击确认的后续过程:

    一键运行的业务流程

    createGitlabProjectProcessor业务处理:

    • 完成 gitlab工程的创建;
    • 生成新工程,并上传至gitlab

    createDevopsProcessor业务处理:生成并上传工程服务部署模板;

    cicdTriggerProcessor业务处理:触发PRECI操作(后续操作由jenkins回调衔接);

    到这里,大致的实现就讲完了。如果你也想搭建一个工程脚手架,欢迎和我交流。

    公众号:【杨同学technotes】

  • 相关阅读:
    JWT 登录
    MySQL进阶-详解SQL实现事务的原理
    彻底理解Java并发:synchronized关键字
    零代码编程:用ChatGPT批量采集bookroo网页上的英文书目列表
    内网环境基于 k8s 的大型网站电商解决方案(一)
    常见Web安全
    使用AssertJ让单元测试和TDD更加简单
    测试除了点点点,还有哪些内容呢?
    堆排序(Heap Sort)
    适配器模式:类适配器模式 对象适配器模式 详细讲解
  • 原文地址:https://blog.csdn.net/yang237061644/article/details/127587876