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和模板来满足定制化的需求。
请尝试网页搜索