- 不入虎穴,焉得虎子
之前提过,请求的"/starter.zip"可以在Spring Initializr的initializr-web模块下的ProjectGenerationController类找到,此类的bean定义是通过spring-boot的AutoConfiguration机制,由InitializrAutoConfiguration定义的DefaultProjectGenerationController。
跟进io.spring.initializr.web.controller.ProjectGenerationController#springZip:
@RequestMapping("/starter.zip")
public ResponseEntity<byte[]> springZip(R request) throws IOException {
ProjectGenerationResult result = this.projectGenerationInvoker.invokeProjectStructureGeneration(request);
Path archive = createArchive(result, "zip", ZipArchiveOutputStream::new, ZipArchiveEntry::new,
ZipArchiveEntry::setUnixMode);
return upload(archive, result.getRootDirectory(), generateFileName(request, "zip"), "application/zip");
}
首先第一步返回的ProjectGenerationResult本身比较简单,只有两个字段,分别是项目描述,包含开发语言及版本、构建系统、依赖、包名等;另一个是根路径。第二步为在临时目录创建压缩包,第三步为读取压缩包得到字节流,然后删除临时文件,最终将流返回给浏览器。先跟进第一步代码:
public ProjectGenerationResult invokeProjectStructureGeneration(R request) {
InitializrMetadata metadata = this.parentApplicationContext.getBean(InitializrMetadataProvider.class).get();
try {
ProjectDescription description = this.requestConverter.convert(request, metadata);
ProjectGenerator projectGenerator = new ProjectGenerator((
projectGenerationContext) -> customizeProjectGenerationContext(projectGenerationContext, metadata));
ProjectGenerationResult result = projectGenerator.generate(description,
generateProject(description, request));
addTempFile(result.getRootDirectory(), result.getRootDirectory());
return result;
}
catch (ProjectGenerationException ex) {
publishProjectFailedEvent(request, metadata, ex);
throw ex;
}
}
metadata bean定义可以在InitializrAutoConfiguration中找到,主要是将application.yml的配置转化成DefaultInitializrMetadataProvider。
接着是将请求信息和metadata一起计算,得到ProjectDescription。
之后是重点了,创建ProjectGenerator后进行generate。此处继续跟进:
public <T> T generate(ProjectDescription description, ProjectAssetGenerator<T> projectAssetGenerator)
throws ProjectGenerationException {
try (ProjectGenerationContext context = this.contextFactory.get()) {
registerProjectDescription(context, description);
registerProjectContributors(context, description);
this.contextConsumer.accept(context);
context.refresh();
try {
return projectAssetGenerator.generate(context);
}
catch (IOException ex) {
throw new ProjectGenerationException("Failed to generate project", ex);
}
}
}
将description注册到ProjectGenerationContext,然后用类似加载AutoConfiguration的方式(此处为读取META-INF/spring.factories下key为io.spring.initializr.generator.project.ProjectGenerationConfiguration的值)注册ProjectGenerationConfiguration到ProjectGenerationContext。 在start-site的相应文件下,内容如下:
io.spring.initializr.generator.project.ProjectGenerationConfiguration=\
io.spring.start.site.extension.build.gradle.GradleProjectGenerationConfiguration,\
io.spring.start.site.extension.build.maven.MavenProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.DependencyProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.observability.ObservabilityProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.solace.SolaceProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springamqp.SpringAmqpProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springboot.SpringBootProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springcloud.SpringCloudProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springdata.SpringDataProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springintegration.SpringIntegrationProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springrestdocs.SpringRestDocsProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.testcontainers.TestcontainersProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.vaadin.VaadinProjectGenerationConfiguration,\
io.spring.start.site.extension.description.DescriptionProjectGenerationConfiguration,\
io.spring.start.site.extension.code.groovy.GroovyProjectGenerationConfiguration,\
io.spring.start.site.extension.code.kotlin.KotlinProjectGenerationConfiguration
在initializr-generator-spring的相应文件有如下内容:
io.spring.initializr.generator.project.ProjectGenerationConfiguration=\
io.spring.initializr.generator.spring.build.BuildProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.build.gradle.GradleProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.build.maven.MavenProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.code.SourceCodeProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.code.groovy.GroovyProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.code.java.JavaProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.code.kotlin.KotlinProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.configuration.ApplicationConfigurationProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.documentation.HelpDocumentProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.scm.git.GitProjectGenerationConfiguration
accept这块的lambda内容在ProjectGenerationInvoker,主要是注册一些bean,暂时跳过。
ProjectGenerationContext是AnnotationConfigApplicationContext的子类,即AbstractApplicationContext的子类。此处内容可以参考spring源码解析部分,大概工作主要是创建BeanFactory、加载bean定义及初始化单例bean。
最后是生成,生成什么,想必大家很关注了。不过这个ProjectAssetGenerator,是在ProjectGenerationInvoker定义的,也是lambda方法,就是使用DefaultProjectAssetGenerator的generate方法,然后发布ProjectGeneratedEvent事件。 继续跟进generate方法:
@Override
public Path generate(ProjectGenerationContext context) throws IOException {
ProjectDescription description = context.getBean(ProjectDescription.class);
Path projectRoot = resolveProjectDirectoryFactory(context).createProjectDirectory(description);
Path projectDirectory = initializerProjectDirectory(projectRoot, description);
List<ProjectContributor> contributors = context.getBeanProvider(ProjectContributor.class).orderedStream()
.collect(Collectors.toList());
for (ProjectContributor contributor : contributors) {
contributor.contribute(projectDirectory);
}
return projectRoot;
}
ProjectGenerationConfiguration定义了ProjectContributor bean及ProjectContributor依赖的其它bean。所以需要先加载ProjectGenerationConfiguration,然后再refresh!
ProjectContributor实现类有多个,比如MavenBuildProjectContributor负责写“pom.xml”文件,内容是由MavenBuildWriter提供。 而对于GitIgnoreContributor,“.gitignore”文件由它写,但内容来自GitProjectGenerationConfiguration(参见上面提供的initializr-generator-spring下的spring.factories文件内容)。
- 实践是检验真理的唯一标准
默认的输出压缩包包较少,我们以自定义web包下创建一个Controller为例。首先,创建一个ProjectGenerationConfiguration,如下:
package io.spring.start.site.extension.code.java;
import io.spring.initializr.generator.language.*;
import io.spring.initializr.generator.language.java.*;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.contributor.ProjectContributor;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.function.Supplier;
/**
* @see io.spring.initializr.generator.spring.code.MainSourceCodeProjectContributor
*/
public class SpringWebContributor<T extends TypeDeclaration, C extends CompilationUnit<T>, S extends SourceCode<T, C>> implements ProjectContributor {
private final ProjectDescription description;
private final SourceCodeWriter<S> sourceWriter;
private final Supplier<S> sourceFactory;
public SpringWebContributor(ProjectDescription description, Supplier<S> sourceFactory,
SourceCodeWriter<S> sourceWriter) {
this.description = description;
this.sourceWriter = sourceWriter;
this.sourceFactory = sourceFactory;
}
@Override
public void contribute(Path projectRoot) throws IOException {
S sourceCode = this.sourceFactory.get();
// description.getApplicationName() == projectMetadata.name + Application
String className = description.getName() + "Controller";
C compilationUnit = sourceCode.createCompilationUnit(description.getPackageName() + ".web", className);// file name
T exampleControllerType = compilationUnit.createTypeDeclaration(className);// class name
exampleControllerType.annotate(Annotation.name("org.springframework.stereotype.Controller"));
boolean lombokExist = description.getRequestedDependencies().containsKey("lombok");
if(lombokExist) {
exampleControllerType.annotate(Annotation.name("lombok.extern.slf4j.Slf4j"));
} else {
((JavaTypeDeclaration) exampleControllerType).addFieldDeclaration(
JavaFieldDeclaration.field("logger").modifiers(Modifier.PRIVATE)
.value("LoggerFactory.getLogger(getClass())").returning("org.slf4j.Logger")
);
}
Parameter parameter = new Parameter("java.lang.String", "echoStr");// bug! parameter cannot be annotated?
//parameter.annotate(Annotation.name("org.springframework.web.bind.annotation.RequestParam"))
JavaMethodDeclaration method = JavaMethodDeclaration.method("echo").modifiers(Modifier.PUBLIC)
.returning("java.lang.Object")
.parameters(parameter).body(
new JavaExpressionStatement(new JavaMethodInvocation("logger", "debug", "echoStr")),
new JavaReturnStatement(new JavaExpression())// bug! return what if USE JavaSourceCodeWriter?
);
method.annotate(Annotation.name("org.springframework.web.bind.annotation.GetMapping",
t->t.attribute("value", String.class, "/echo")));
method.annotate(Annotation.name("org.springframework.web.bind.annotation.ResponseBody"));
((JavaTypeDeclaration) exampleControllerType).modifiers(Modifier.PUBLIC);
((JavaTypeDeclaration) exampleControllerType).addMethodDeclaration(method);
sourceWriter.writeTo(description.getBuildSystem().getMainSource(projectRoot, this.description.getLanguage()), //src/main/java
sourceCode);
}
/**
* after main/test source code
* @return
*/
@Override
public int getOrder() {
return 100;
}
}
package io.spring.start.site.extension.code.java;
import io.spring.initializr.generator.condition.ConditionalOnLanguage;
import io.spring.initializr.generator.io.IndentingWriterFactory;
import io.spring.initializr.generator.language.java.JavaLanguage;
import io.spring.initializr.generator.language.java.JavaSourceCode;
import io.spring.initializr.generator.language.java.JavaSourceCodeWriter;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.ProjectGenerationConfiguration;
import org.springframework.context.annotation.Bean;
/**
* @see io.spring.initializr.generator.spring.code.java.JavaProjectGenerationConfiguration
*/
@ProjectGenerationConfiguration
@ConditionalOnLanguage(JavaLanguage.ID)
public class SpringWebConfiguration {
private final ProjectDescription description;
private final IndentingWriterFactory indentingWriterFactory;
public SpringWebConfiguration(ProjectDescription description, IndentingWriterFactory indentingWriterFactory) {
this.description = description;
this.indentingWriterFactory = indentingWriterFactory;
}
@Bean
public SpringWebContributor springWebContributor() {
return new SpringWebContributor(description, JavaSourceCode::new, new JavaSourceCodeWriter(indentingWriterFactory));
}
}
然后,在项目的META-INF/spring.factories下增加相应信息。我代码就在start-site下,所以spring.factories文件已存在,追加值即可。
io.spring.initializr.generator.project.ProjectGenerationConfiguration=\
io.spring.start.site.extension.build.gradle.GradleProjectGenerationConfiguration,\
io.spring.start.site.extension.build.maven.MavenProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.DependencyProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.observability.ObservabilityProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.solace.SolaceProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springamqp.SpringAmqpProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springboot.SpringBootProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springcloud.SpringCloudProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springdata.SpringDataProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springintegration.SpringIntegrationProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.springrestdocs.SpringRestDocsProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.testcontainers.TestcontainersProjectGenerationConfiguration,\
io.spring.start.site.extension.dependency.vaadin.VaadinProjectGenerationConfiguration,\
io.spring.start.site.extension.description.DescriptionProjectGenerationConfiguration,\
io.spring.start.site.extension.code.groovy.GroovyProjectGenerationConfiguration,\
io.spring.start.site.extension.code.kotlin.KotlinProjectGenerationConfiguration,\
io.spring.start.site.extension.code.java.SpringWebConfiguration
最终效果如下(有小问题,欢迎留言讨论):
本文暂时没有评论,来添加一个吧(●'◡'●)