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 model) {
renderHome(model);
return "home";
}
可以看出这段代码完成的主要逻辑是根据用户请求参数,生成model对象,再用model对象去渲染 home.html, 完成渲染后,再返回给客户端。这里主要看渲染的过程,及renderHome()方法的实现。
/**
* Render the home page with the specified template.
*/
protected void renderHome(Map 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 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 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 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 model) {
String body = templateRenderer.process(templateName, model);
writeText(target, body);
}
// 根据model渲染模板,生成文件内容
public String process(String name, Map 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和模板来满足定制化的需求。
原文地址