Merge pull request #45202 from nosan
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Trigger Docs Build (push) Blocked by required conditions Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:22], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:22], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:windows-latest name:Windows]) (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:false version:17]) (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:true version:21]) (push) Waiting to run Details

* gh-45202:
  Polish "Use ClassLoader with ArchitectureCheck"
  Use ClassLoader with ArchitectureCheck

Closes gh-45202
This commit is contained in:
Andy Wilkinson 2025-04-17 15:47:15 +01:00
commit 3aa352a2cd
7 changed files with 190 additions and 143 deletions

View File

@ -18,11 +18,15 @@ package org.springframework.boot.build.architecture;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.stream.Stream;
@ -33,11 +37,13 @@ import com.tngtech.archunit.lang.EvaluationResult;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
import org.gradle.api.Transformer;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.IgnoreEmptyDirectories;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
@ -58,6 +64,7 @@ import org.gradle.api.tasks.VerificationException;
* @author Scott Frederick
* @author Ivan Malutin
* @author Phillip Webb
* @author Dmytro Nosan
*/
public abstract class ArchitectureCheck extends DefaultTask {
@ -80,14 +87,17 @@ public abstract class ArchitectureCheck extends DefaultTask {
}
@TaskAction
void checkArchitecture() throws IOException {
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeViolationReport(violations, outputFile);
if (!violations.isEmpty()) {
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");
}
void checkArchitecture() throws Exception {
withCompileClasspath(() -> {
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeViolationReport(violations, outputFile);
if (!violations.isEmpty()) {
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");
}
return null;
});
}
private List<Path> classFilesPaths() {
@ -98,6 +108,22 @@ public abstract class ArchitectureCheck extends DefaultTask {
return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses));
}
private void withCompileClasspath(Callable<?> callable) throws Exception {
ClassLoader previous = Thread.currentThread().getContextClassLoader();
try {
List<URL> urls = new ArrayList<>();
for (File file : getCompileClasspath().getFiles()) {
urls.add(file.toURI().toURL());
}
ClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), getClass().getClassLoader());
Thread.currentThread().setContextClassLoader(classLoader);
callable.call();
}
finally {
Thread.currentThread().setContextClassLoader(previous);
}
}
private void writeViolationReport(List<EvaluationResult> violations, File outputFile) throws IOException {
outputFile.getParentFile().mkdirs();
StringBuilder report = new StringBuilder();
@ -126,6 +152,10 @@ public abstract class ArchitectureCheck extends DefaultTask {
return this.classes.getAsFileTree();
}
@InputFiles
@Classpath
public abstract ConfigurableFileCollection getCompileClasspath();
@Optional
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -49,6 +49,7 @@ public class ArchitecturePlugin implements Plugin<Project> {
TaskProvider<ArchitectureCheck> checkPackageTangles = project.getTasks()
.register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class,
(task) -> {
task.getCompileClasspath().from(sourceSet.getCompileClasspath());
task.setClasses(sourceSet.getOutput().getClassesDirs());
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
task.dependsOn(sourceSet.getProcessResourcesTaskName());

View File

@ -16,22 +16,20 @@
package org.springframework.boot.build.architecture;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.testfixtures.ProjectBuilder;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link ArchitectureCheck}.
@ -39,188 +37,206 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Andy Wilkinson
* @author Scott Frederick
* @author Ivan Malutin
* @author Dmytro Nosan
*/
class ArchitectureCheckTests {
@TempDir
File temp;
private Path projectDir;
@Test
void whenPackagesAreTangledTaskFailsAndWritesAReport() throws Exception {
prepareTask("tangled", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
private Path buildFile;
@BeforeEach
void setup(@TempDir Path projectDir) {
this.projectDir = projectDir;
this.buildFile = projectDir.resolve("build.gradle");
}
@Test
void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport() throws Exception {
prepareTask("untangled", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
}
File failureReport(ArchitectureCheck architectureCheck) {
return architectureCheck.getProject()
.getLayout()
.getBuildDirectory()
.file("checkArchitecture/failure-report.txt")
.get()
.getAsFile();
void whenPackagesAreTangledTaskFailsAndWritesAReport() throws IOException {
runGradleWithCompiledClasses("tangled",
shouldHaveFailureReportWithMessage("slices matching '(**)' should be free of cycles"));
}
@Test
void whenBeanPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws Exception {
prepareTask("bpp/nonstatic", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport() throws IOException {
runGradleWithCompiledClasses("untangled", shouldHaveEmptyFailureReport());
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersTaskFailsAndWritesAReport() throws Exception {
prepareTask("bpp/unsafeparameters", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenBeanPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws IOException {
runGradleWithCompiledClasses("bpp/nonstatic",
shouldHaveFailureReportWithMessage(
"methods that are annotated with @Bean and have raw return type assignable "
+ "to org.springframework.beans.factory.config.BeanPostProcessor"));
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersTaskFailsAndWritesAReport() throws IOException {
runGradleWithCompiledClasses("bpp/unsafeparameters",
shouldHaveFailureReportWithMessage(
"methods that are annotated with @Bean and have raw return type assignable "
+ "to org.springframework.beans.factory.config.BeanPostProcessor"));
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasSafeParametersTaskSucceedsAndWritesAnEmptyReport()
throws Exception {
prepareTask("bpp/safeparameters", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
throws IOException {
runGradleWithCompiledClasses("bpp/safeparameters", shouldHaveEmptyFailureReport());
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport()
throws Exception {
prepareTask("bpp/noparameters", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
throws IOException {
runGradleWithCompiledClasses("bpp/noparameters", shouldHaveEmptyFailureReport());
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws Exception {
prepareTask("bfpp/nonstatic", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenBeanFactoryPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws IOException {
runGradleWithCompiledClasses("bfpp/nonstatic",
shouldHaveFailureReportWithMessage("methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor"));
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersTaskFailsAndWritesAReport() throws Exception {
prepareTask("bfpp/parameters", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersTaskFailsAndWritesAReport() throws IOException {
runGradleWithCompiledClasses("bfpp/parameters",
shouldHaveFailureReportWithMessage("methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor"));
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport()
throws Exception {
prepareTask("bfpp/noparameters", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
throws IOException {
runGradleWithCompiledClasses("bfpp/noparameters", shouldHaveEmptyFailureReport());
}
@Test
void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport() throws Exception {
prepareTask("resources/loads", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport() throws IOException {
runGradleWithCompiledClasses("resources/loads", shouldHaveFailureReportWithMessage(
"no classes should call method where target owner type org.springframework.util.ResourceUtils and target name 'getURL'"));
}
@Test
void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport() throws Exception {
prepareTask("resources/noloads", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport() throws IOException {
runGradleWithCompiledClasses("resources/noloads", shouldHaveEmptyFailureReport());
}
@Test
void whenClassDoesNotCallObjectsRequireNonNullTaskSucceedsAndWritesAnEmptyReport() throws Exception {
prepareTask("objects/noRequireNonNull", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
void whenClassDoesNotCallObjectsRequireNonNullTaskSucceedsAndWritesAnEmptyReport() throws IOException {
runGradleWithCompiledClasses("objects/noRequireNonNull", shouldHaveEmptyFailureReport());
}
@Test
void whenClassCallsObjectsRequireNonNullWithMessageTaskFailsAndWritesReport() throws Exception {
prepareTask("objects/requireNonNullWithString", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenClassCallsObjectsRequireNonNullWithMessageTaskFailsAndWritesReport() throws IOException {
runGradleWithCompiledClasses("objects/requireNonNullWithString", shouldHaveFailureReportWithMessage(
"no classes should call method Objects.requireNonNull(Object, String)"));
}
@Test
void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport() throws Exception {
prepareTask("objects/requireNonNullWithSupplier", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport() throws IOException {
runGradleWithCompiledClasses("objects/requireNonNullWithSupplier", shouldHaveFailureReportWithMessage(
"no classes should call method Objects.requireNonNull(Object, Supplier)"));
}
@Test
void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport() throws Exception {
prepareTask("string/toUpperCase", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty()
.content()
.contains("because String.toUpperCase(Locale.ROOT) should be used instead");
});
void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport() throws IOException {
runGradleWithCompiledClasses("string/toUpperCase",
shouldHaveFailureReportWithMessage("because String.toUpperCase(Locale.ROOT) should be used instead"));
}
@Test
void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport() throws Exception {
prepareTask("string/toLowerCase", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty()
.content()
.contains("because String.toLowerCase(Locale.ROOT) should be used instead");
});
void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport() throws IOException {
runGradleWithCompiledClasses("string/toLowerCase",
shouldHaveFailureReportWithMessage("because String.toLowerCase(Locale.ROOT) should be used instead"));
}
@Test
void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail() throws Exception {
prepareTask("string/toLowerCaseWithLocale", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail() throws IOException {
runGradleWithCompiledClasses("string/toLowerCaseWithLocale", shouldHaveEmptyFailureReport());
}
@Test
void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail() throws Exception {
prepareTask("string/toUpperCaseWithLocale", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail() throws IOException {
runGradleWithCompiledClasses("string/toUpperCaseWithLocale", shouldHaveEmptyFailureReport());
}
private void prepareTask(String classes, ThrowingConsumer<ArchitectureCheck> callback) throws Exception {
File projectDir = new File(this.temp, "project");
projectDir.mkdirs();
copyClasses(classes, projectDir);
Project project = ProjectBuilder.builder().withProjectDir(projectDir).build();
project.getTasks().register("checkArchitecture", ArchitectureCheck.class, (task) -> {
task.setClasses(project.files("classes"));
callback.accept(task);
});
@Test
void whenBeanPostProcessorBeanMethodIsNotStaticWithExternalClass() throws IOException {
Files.writeString(this.buildFile, """
plugins {
id 'java'
id 'org.springframework.boot.architecture'
}
repositories {
mavenCentral()
}
java {
sourceCompatibility = 17
}
dependencies {
implementation("org.springframework.integration:spring-integration-jmx:6.3.9")
}
""");
Path testClass = this.projectDir.resolve("src/main/java/boot/architecture/bpp/external/TestClass.java");
Files.createDirectories(testClass.getParent());
Files.writeString(testClass, """
package org.springframework.boot.build.architecture.bpp.external;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.monitor.IntegrationMBeanExporter;
public class TestClass {
@Bean
IntegrationMBeanExporter integrationMBeanExporter() {
return new IntegrationMBeanExporter();
}
}
""");
runGradle(shouldHaveFailureReportWithMessage("methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanPostProcessor "));
}
private void copyClasses(String name, File projectDir) throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource root = resolver.getResource("classpath:org/springframework/boot/build/architecture/" + name);
FileSystemUtils.copyRecursively(root.getFile(),
new File(projectDir, "classes/org/springframework/boot/build/architecture/" + name));
private Consumer<GradleRunner> shouldHaveEmptyFailureReport() {
return (gradleRunner) -> {
assertThat(gradleRunner.build().getOutput()).contains("BUILD SUCCESSFUL")
.contains("Task :checkArchitectureMain");
assertThat(failureReport()).isEmptyFile();
};
}
private Consumer<GradleRunner> shouldHaveFailureReportWithMessage(String message) {
return (gradleRunner) -> {
assertThat(gradleRunner.buildAndFail().getOutput()).contains("BUILD FAILED")
.contains("Task :checkArchitectureMain FAILED");
assertThat(failureReport()).content().contains(message);
};
}
private void runGradleWithCompiledClasses(String path, Consumer<GradleRunner> callback) throws IOException {
ClassPathResource classPathResource = new ClassPathResource(path, getClass());
FileSystemUtils.copyRecursively(classPathResource.getFile().toPath(),
this.projectDir.resolve("classes").resolve(classPathResource.getPath()));
Files.writeString(this.buildFile, """
plugins {
id 'java'
id 'org.springframework.boot.architecture'
}
sourceSets {
main {
output.classesDirs.setFrom(file("classes"))
}
}
""");
runGradle(callback);
}
private void runGradle(Consumer<GradleRunner> callback) {
callback.accept(GradleRunner.create()
.withProjectDir(this.projectDir.toFile())
.withArguments("checkArchitectureMain")
.withPluginClasspath());
}
private Path failureReport() {
return this.projectDir.resolve("build/checkArchitectureMain/failure-report.txt");
}
}

View File

@ -103,7 +103,7 @@ class FilterOrderingIntegrationTests {
}
@Bean
WebServerFactoryCustomizerBeanPostProcessor ServletWebServerCustomizerBeanPostProcessor() {
static WebServerFactoryCustomizerBeanPostProcessor servletWebServerCustomizerBeanPostProcessor() {
return new WebServerFactoryCustomizerBeanPostProcessor();
}

View File

@ -211,12 +211,12 @@ class HttpEncodingAutoConfigurationTests {
static class MinimalWebAutoConfiguration {
@Bean
MockServletWebServerFactory MockServletWebServerFactory() {
MockServletWebServerFactory mockServletWebServerFactory() {
return new MockServletWebServerFactory();
}
@Bean
WebServerFactoryCustomizerBeanPostProcessor ServletWebServerCustomizerBeanPostProcessor() {
static WebServerFactoryCustomizerBeanPostProcessor servletWebServerCustomizerBeanPostProcessor() {
return new WebServerFactoryCustomizerBeanPostProcessor();
}

View File

@ -1098,7 +1098,7 @@ class WebMvcAutoConfigurationTests {
}
@Bean
WebServerFactoryCustomizerBeanPostProcessor ServletWebServerCustomizerBeanPostProcessor() {
static WebServerFactoryCustomizerBeanPostProcessor servletWebServerCustomizerBeanPostProcessor() {
return new WebServerFactoryCustomizerBeanPostProcessor();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -183,7 +183,7 @@ class WebSocketServletAutoConfigurationTests {
}
@Bean
WebServerFactoryCustomizerBeanPostProcessor ServletWebServerCustomizerBeanPostProcessor() {
static WebServerFactoryCustomizerBeanPostProcessor servletWebServerCustomizerBeanPostProcessor() {
return new WebServerFactoryCustomizerBeanPostProcessor();
}