玖叶教程网

前端编程开发入门

自定义Spring Initializr目录结构

  1. 不入虎穴,焉得虎子

之前提过,请求的"/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文件内容)。

  1. 实践是检验真理的唯一标准

默认的输出压缩包包较少,我们以自定义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

最终效果如下(有小问题,欢迎留言讨论):

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言