spring initializr源码分析——代码生成器

spring initializr 通过web ui/web service api的方式,可以快速生成spring boot项目。github地址:https://github.com/spring-io/initializr/

使用起来非常简单,在页面上配置项目属性,如项目类型、语言、spring boot版本、依赖项等,然后点击generate project,即可生成一个单module的spring boot项目。最终生成的是一个zip包,提供给用户下载。

 

在实际应用中,可以考虑以spring initializr为原型,对源码进行改造,开发属于团队内部的代码生成器。

 

这里简单对spring initializr源码进行分析。

spring initializr本身也是spring boot项目,启动类是  io.spring.initializr.service.InitializrService 。(强烈推荐clone代码到本地,直接启动InitializrService即可,没有其他依赖服务)

META-INF/spring.factories 配置如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.spring.initializr.web.autoconfigure.InitializrAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
io.spring.initializr.web.autoconfigure.CloudfoundryEnvironmentPostProcessor

在SpringApplication启动时会主动去加载配置类 InitializrAutoConfiguration,直接看源码:

@Configuration
@EnableConfigurationProperties(InitializrProperties.class)
@AutoConfigureAfter({ CacheAutoConfiguration.class, JacksonAutoConfiguration.class,
        WebClientAutoConfiguration.class })
public class InitializrAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(InitializrMetadataProvider.class)
    public InitializrMetadataProvider initializrMetadataProvider(
            InitializrProperties properties, // 读取application.yml前缀为initializr的配置
            ObjectMapper objectMapper,
            RestTemplateBuilder restTemplateBuilder) {
        InitializrMetadata metadata = InitializrMetadataBuilder 
                .fromInitializrProperties(properties).build(); // 根据配置生成 metadata
        return new DefaultInitializrMetadataProvider(metadata,
                objectMapper, restTemplateBuilder.build());
    }
}

这里的操作是读取application.yml前缀为initializr配置项,然后根据配置项生成默认的metadata。这个metadata是用于生成项目用的源数据,相当于一个配置集合。metadata主要定义了以下几个属性:

  • env - 环境变量
  • dependencies - 依赖项
  • types - 项目类型:maven/gradle
  • packagings - 打包方式:war/jar
  • javaVersions - jdk版本
  • languages - 使用的语言:java/kotlin/groovy
  • bootVersions - spring boot版本

以上是启动过程中完成的工作。

 

用户的访问操作对应的Controller类为 io.spring.initializr.web.project.MainController。

访问主页面,即触发执行以下代码

@RequestMapping(value = "/", produces = "text/html")
public String home(Map<String, Object> model) {
    renderHome(model);
    return "home";
}

可以看出这段代码完成的主要逻辑是根据用户请求参数,生成model对象,再用model对象去渲染 home.html, 完成渲染后,再返回给客户端。这里主要看渲染的过程,及renderHome()方法的实现。

    /**
     * Render the home page with the specified template.
     */
    protected void renderHome(Map<String, Object> model) {
        InitializrMetadata metadata = metadataProvider.get(); // 获取metadata

        model.put("serviceUrl", generateAppUrl());

// 从metadata中读取默认的配置型,并赋值给model对象 BeanWrapperImpl wrapper
= new BeanWrapperImpl(metadata);
      
for (PropertyDescriptor descriptor : wrapper.getPropertyDescriptors()) { if ("types".equals(descriptor.getName())) { model.put("types", removeTypes(metadata.getTypes())); } else { model.put(descriptor.getName(), wrapper.getPropertyValue(descriptor.getName())); } } // Google analytics support model.put("trackingCode", metadata.getConfiguration().getEnv().getGoogleAnalyticsTrackingCode()); }

renderHome() 方法的处理逻辑很简单,就是把metadata对象的字段读取出来转成map对象赋值给model。前端再根据model对象渲染页面,页面上项目类型、语言、spring boot版本、依赖项等可选内容以及项目属性的默认填充值都是根据model对象渲染出来的。可见application.yml涵盖了的是所有配置项的全集。

 

下面介绍点击generate project时,服务做了哪些操作。

 

点击generate project发送的请求如下:

http://start.spring.io/starter.zip?type=gradle-project&language=java&bootVersion=1.5.10.RELEASE&baseDir=demo&groupId=com.example&artifactId=demo&name=demo&description=Demo+project+for+Spring+Boot&packageName=com.example.demo&packaging=jar&javaVersion=1.8&autocomplete=&generate-project=&style=web

进行拆分后的参数列表,这些参数光看参数名就可以知道其含义:

  • type=gradle-project
  • language=java
  • bootVersion=1.5.10.RELEASE
  • baseDir=demo
  • groupId=com.example
  • artifactId=demo
  • name=demo
  • description=Demo+project+for+Spring+Boot
  • packageName=com.example.demo
  • packaging=jar
  • javaVersion=1.8
  • style=web

访问的接口是starter.zip,位于 io.spring.initializr.web.project.MainController#springZip()

    @RequestMapping("/starter.zip")
    @ResponseBody
    public ResponseEntity<byte[]> springZip(BasicProjectRequest basicRequest)
            throws IOException {
        ProjectRequest request = (ProjectRequest) basicRequest;
        File dir = projectGenerator.generateProjectStructure(request); // 生成代码的关键代码

        File download = projectGenerator.createDistributionFile(dir, ".zip");

        String wrapperScript = getWrapperScript(request);
        new File(dir, wrapperScript).setExecutable(true);
        Zip zip = new Zip();
        zip.setProject(new Project());
        zip.setDefaultexcludes(false);
        ZipFileSet set = new ZipFileSet();
        set.setDir(dir);
        set.setFileMode("755");
        set.setIncludes(wrapperScript);
        set.setDefaultexcludes(false);
        zip.addFileset(set);
        set = new ZipFileSet();
        set.setDir(dir);
        set.setIncludes("**,");
        set.setExcludes(wrapperScript);
        set.setDefaultexcludes(false);
        zip.addFileset(set);
        zip.setDestFile(download.getCanonicalFile());
        zip.execute();
        return upload(download, dir, generateFileName(request, "zip"), "application/zip");
    }

springZip主要执行的是打包的过程,主要看 projectGenerator.generateProjectStructure(request),这一步是生成代码的关键步骤。

    /**
     * Generate a project structure for the specified {@link ProjectRequest}. Returns a directory containing the
     * project.
     */
    public File generateProjectStructure(ProjectRequest request) {
        try {
       // 根据请求生成model,这里的model非全集model Map
<String, Object> model = resolveModel(request);
       // 生成代码 File rootDir
= generateProjectStructure(request, model); publishProjectGeneratedEvent(request); return rootDir; } catch (InitializrException ex) { publishProjectFailedEvent(request, ex); throw ex; } }

resolveModel方法的代码非常长,这里就不贴了。处理逻辑不复杂,简单地说就是根据用户请求request,以及项目启动时生成的metadata,生成出一个model。这里的metadata相当于一个配置的全集,用户请求request指明了项目的个性化配置,根据个性化配置,从配置全集中进行筛选和组装出model。这个model对象会用于下一步生成代码使用。

 

这里可以看一下model包含了哪些内容,下面的截图是我在本地debug时生成的。

 

好了。下面看下是如何根据model生成代码的。

    /**
     * Generate a project structure for the specified {@link ProjectRequest} and resolved
     * model.
     */
    protected File generateProjectStructure(ProjectRequest request,
            Map<String, Object> model) {
        File rootDir;
        try {
            rootDir = File.createTempFile("tmp", "", getTemporaryDirectory());
        }
        catch (IOException e) {
            throw new IllegalStateException("Cannot create temp dir", e);
        }
        addTempFile(rootDir.getName(), rootDir);
        rootDir.delete();
        rootDir.mkdirs();

        File dir = initializerProjectDir(rootDir, request);

        if (isGradleBuild(request)) {
            String gradle = new String(doGenerateGradleBuild(model));
            writeText(new File(dir, "build.gradle"), gradle);
            writeGradleWrapper(dir, Version.safeParse(request.getBootVersion()));
        }
        else {
            String pom = new String(doGenerateMavenPom(model));
            writeText(new File(dir, "pom.xml"), pom);
            writeMavenWrapper(dir);
        }

        generateGitIgnore(dir, request);

        String applicationName = request.getApplicationName();
        String language = request.getLanguage();

        String codeLocation = language;
        File src = new File(new File(dir, "src/main/" + codeLocation),
                request.getPackageName().replace(".", "/"));
        src.mkdirs();
        String extension = ("kotlin".equals(language) ? "kt" : language);
        write(new File(src, applicationName + "." + extension),
                "Application." + extension, model);

        if ("war".equals(request.getPackaging())) {
            String fileName = "ServletInitializer." + extension;
            write(new File(src, fileName), fileName, model);
        }

        File test = new File(new File(dir, "src/test/" + codeLocation),
                request.getPackageName().replace(".", "/"));
        test.mkdirs();
        setupTestModel(request, model);
        write(new File(test, applicationName + "Tests." + extension),
                "ApplicationTests." + extension, model);

        File resources = new File(dir, "src/main/resources");
        resources.mkdirs();
        writeText(new File(resources, "application.properties"), "");

        if (request.hasWebFacet()) {
            new File(dir, "src/main/resources/templates").mkdirs();
            new File(dir, "src/main/resources/static").mkdirs();
        }
        return rootDir;
    }

这段代码虽长,但理解起来也不复杂,根据项目的属性model对象,生成对应的文件夹和文件。比如src/main/java、src/main/resources、application.properties等。文件夹生成直接调用File.mkdirs()方法即可。那么文件是如何生成的呢?

 

首先对于一些默认的配置文件,比如maven/gradle  wrapper等,直接拷贝即可。对于一些需要定制的文件,那么就通过模板渲染的方式去生成。这里的涉及的代码如下:

  // 写入文件, target表示目标文件,templateName表示模板名称,model表示通过解析获得的项目配置
public void write(File target, String templateName, Map<String, Object> model) { String body = templateRenderer.process(templateName, model); writeText(target, body); }
// 根据model渲染模板,生成文件内容
public String process(String name, Map<String, ?> model) { try { Template template = getTemplate(name); return template.execute(model); } catch (Exception e) { log.error("Cannot render: " + name, e); throw new IllegalStateException("Cannot render template", e); } }
// 定位模板
public Template getTemplate(String name) { if (cache) { return this.templateCaches.computeIfAbsent(name, this::loadTemplate); } return loadTemplate(name); }

这里举一个例子。模板Application.java,位于/initializr/initializr-generator/src/main/resources/templates/Application.java,内容如下:

package {{packageName}};

import org.springframework.boot.SpringApplication;
{{applicationImports}}

{{applicationAnnotations}}
public class {{applicationName}} {

    public static void main(String[] args) {
        SpringApplication.run({{applicationName}}.class, args);
    }
}

这里的{{packageName}} {{applicationImports}} 为占位符,通过model中的属性进行替换,获得最终的Application.java。

这里的模板使用了mustcache框架,跟freemarker有点类似。

 

目前initializr提供了以下模板

 

至此,代码生成的逻辑已经差不多讲完了。简单的说,就是根据用户request,结合预设的配置全集metadata生成对应的model,再根据model渲染模板,生成指定的文件和文件夹,再打包返回给用户。

如果要定制代码生成器,可以参考这种做法,主要是改造metadata和模板,可以增加或者修改metadata和模板来满足定制化的需求。

 

  没有英汉互译结果
  请尝试网页搜索

posted on 2018-03-15 11:45  归晚ok  阅读(1277)  评论(1编辑  收藏  举报

导航