Browse Source
Update `SpringBootContextLoader` so that it now implements the `AotContextLoader` interface. The `ContextLoaderHook` will abandon at `contextLoaded` if the test class is being AOT processed. This commit also introduces a new `AotApplicationContextInitializer` which allows us to plug-in an alternative AOT application context listener when the `SpringApplication` is running in test mode. Closes gh-31965pull/32405/head
7 changed files with 469 additions and 11 deletions
@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
|
||||
/* |
||||
* Copyright 2012-2022 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.test.context; |
||||
|
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.aot.AotDetector; |
||||
import org.springframework.aot.generate.InMemoryGeneratedFiles; |
||||
import org.springframework.aot.test.generate.compile.CompileWithTargetClassAccess; |
||||
import org.springframework.aot.test.generate.compile.TestCompiler; |
||||
import org.springframework.beans.factory.config.BeanDefinition; |
||||
import org.springframework.beans.factory.support.GenericBeanDefinition; |
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; |
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.context.support.GenericApplicationContext; |
||||
import org.springframework.test.context.BootstrapUtils; |
||||
import org.springframework.test.context.MergedContextConfiguration; |
||||
import org.springframework.test.context.TestContextBootstrapper; |
||||
import org.springframework.test.context.aot.AotContextLoader; |
||||
import org.springframework.test.context.aot.AotTestContextInitializers; |
||||
import org.springframework.test.context.aot.TestContextAotGenerator; |
||||
import org.springframework.test.util.ReflectionTestUtils; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.function.ThrowingConsumer; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link SpringBootContextLoader} when used in AOT mode. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
@CompileWithTargetClassAccess |
||||
class SpringBootContextLoaderAotTests { |
||||
|
||||
@Test |
||||
void loadContextForAotProcessingAndAotRuntime() { |
||||
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); |
||||
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); |
||||
Class<?> testClass = ExampleTest.class; |
||||
generator.processAheadOfTime(Stream.of(testClass)); |
||||
TestCompiler.forSystem().withFiles(generatedFiles).printFiles(System.out) |
||||
.compile(ThrowingConsumer.of((compiled) -> assertCompiledTest(testClass))); |
||||
} |
||||
|
||||
private void assertCompiledTest(Class<?> testClass) throws Exception { |
||||
try { |
||||
System.setProperty(AotDetector.AOT_ENABLED, "true"); |
||||
resetAotClasses(); |
||||
AotTestContextInitializers aotContextInitializers = new AotTestContextInitializers(); |
||||
TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(testClass); |
||||
MergedContextConfiguration mergedConfig = testContextBootstrapper.buildMergedContextConfiguration(); |
||||
ApplicationContextInitializer<ConfigurableApplicationContext> contextInitializer = aotContextInitializers |
||||
.getContextInitializer(testClass); |
||||
ConfigurableApplicationContext context = (ConfigurableApplicationContext) ((AotContextLoader) mergedConfig |
||||
.getContextLoader()).loadContextForAotRuntime(mergedConfig, contextInitializer); |
||||
assertThat(context).isExactlyInstanceOf(GenericApplicationContext.class); |
||||
String[] beanNames = context.getBeanNamesForType(ExampleBean.class); |
||||
BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition(beanNames[0]); |
||||
assertThat(beanDefinition).isNotExactlyInstanceOf(GenericBeanDefinition.class); |
||||
} |
||||
finally { |
||||
System.clearProperty(AotDetector.AOT_ENABLED); |
||||
resetAotClasses(); |
||||
} |
||||
} |
||||
|
||||
private void resetAotClasses() { |
||||
reset("org.springframework.test.context.aot.AotTestAttributesFactory"); |
||||
reset("org.springframework.test.context.aot.AotTestContextInitializersFactory"); |
||||
} |
||||
|
||||
private void reset(String className) { |
||||
Class<?> targetClass = ClassUtils.resolveClassName(className, null); |
||||
ReflectionTestUtils.invokeMethod(targetClass, "reset"); |
||||
} |
||||
|
||||
@SpringBootTest(classes = ExampleConfig.class, webEnvironment = WebEnvironment.NONE) |
||||
static class ExampleTest { |
||||
|
||||
} |
||||
|
||||
@SpringBootConfiguration |
||||
@Import(ExampleBean.class) |
||||
static class ExampleConfig { |
||||
|
||||
} |
||||
|
||||
static class ExampleBean { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,152 @@
@@ -0,0 +1,152 @@
|
||||
/* |
||||
* Copyright 2012-2022 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
/** |
||||
* A {@link ApplicationContextInitializer} wrapper used to initialize a |
||||
* {@link ConfigurableApplicationContext} using artifacts that were generated |
||||
* ahead-of-time. |
||||
* |
||||
* @param <C> the application context type |
||||
* @author Phillip Webb |
||||
* @since 3.0.0 |
||||
*/ |
||||
public abstract sealed class AotApplicationContextInitializer<C extends ConfigurableApplicationContext> |
||||
implements ApplicationContextInitializer<C> { |
||||
|
||||
private static final Log logger = LogFactory.getLog(AotApplicationContextInitializer.class); |
||||
|
||||
@Override |
||||
public final void initialize(C applicationContext) { |
||||
logger.debug(LogMessage.format("Initializing ApplicationContext using AOT initializer '%s'", getName())); |
||||
aotInitialize(applicationContext); |
||||
} |
||||
|
||||
abstract void aotInitialize(C applicationContext); |
||||
|
||||
abstract String getName(); |
||||
|
||||
static <C extends ConfigurableApplicationContext> AotApplicationContextInitializer<C> forMainApplicationClass( |
||||
Class<?> mainApplicationClass) { |
||||
String initializerClassName = mainApplicationClass.getName() + "__ApplicationContextInitializer"; |
||||
return new ReflectionDelegatingAotApplicationContextInitializer<>(initializerClassName); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link AotApplicationContextInitializer} by delegating to an existing |
||||
* initializer instance. |
||||
* @param <C> the application context type |
||||
* @param initializer the initializer to delegate to |
||||
* @return a new {@link AotApplicationContextInitializer} instance |
||||
*/ |
||||
public static <C extends ConfigurableApplicationContext> AotApplicationContextInitializer<C> of( |
||||
ApplicationContextInitializer<C> initializer) { |
||||
Assert.notNull(initializer, "Initializer must not be null"); |
||||
return new InstanceDelegatingAotApplicationContextInitializer<>(initializer.getClass().getName(), initializer); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link AotApplicationContextInitializer} by delegating to an existing |
||||
* initializer instance. |
||||
* @param <C> the application context type |
||||
* @param initializer the initializer to delegate to |
||||
* @param name the name of the initializer |
||||
* @return a new {@link AotApplicationContextInitializer} instance |
||||
*/ |
||||
public static <C extends ConfigurableApplicationContext> AotApplicationContextInitializer<C> of(String name, |
||||
ApplicationContextInitializer<C> initializer) { |
||||
Assert.notNull(name, "Name must not be null"); |
||||
Assert.notNull(initializer, "Initializer must not be null"); |
||||
return new InstanceDelegatingAotApplicationContextInitializer<>(name, initializer); |
||||
} |
||||
|
||||
/** |
||||
* {@link AotApplicationContextInitializer} that delegates to an initializer created |
||||
* via reflection. |
||||
* |
||||
* @param <C> the application context type |
||||
*/ |
||||
static final class ReflectionDelegatingAotApplicationContextInitializer<C extends ConfigurableApplicationContext> |
||||
extends AotApplicationContextInitializer<C> { |
||||
|
||||
private final String initializerClassName; |
||||
|
||||
ReflectionDelegatingAotApplicationContextInitializer(String initializerClassName) { |
||||
this.initializerClassName = initializerClassName; |
||||
} |
||||
|
||||
@Override |
||||
void aotInitialize(C applicationContext) { |
||||
ApplicationContextInitializer<C> initializer = createInitializer(applicationContext.getClassLoader()); |
||||
initializer.initialize(applicationContext); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private ApplicationContextInitializer<C> createInitializer(ClassLoader classLoader) { |
||||
Class<?> initializerClass = ClassUtils.resolveClassName(this.initializerClassName, classLoader); |
||||
Assert.isAssignable(ApplicationContextInitializer.class, initializerClass); |
||||
return (ApplicationContextInitializer<C>) BeanUtils.instantiateClass(initializerClass); |
||||
} |
||||
|
||||
@Override |
||||
String getName() { |
||||
return this.initializerClassName; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* {@link AotApplicationContextInitializer} that delegates to an existing initializer |
||||
* instance. |
||||
* |
||||
* @param <C> the application context type |
||||
*/ |
||||
static final class InstanceDelegatingAotApplicationContextInitializer<C extends ConfigurableApplicationContext> |
||||
extends AotApplicationContextInitializer<C> { |
||||
|
||||
private final String name; |
||||
|
||||
private final ApplicationContextInitializer<C> initializer; |
||||
|
||||
InstanceDelegatingAotApplicationContextInitializer(String name, ApplicationContextInitializer<C> initializer) { |
||||
this.name = name; |
||||
this.initializer = initializer; |
||||
} |
||||
|
||||
@Override |
||||
void aotInitialize(C applicationContext) { |
||||
this.initializer.initialize(applicationContext); |
||||
} |
||||
|
||||
@Override |
||||
String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
/* |
||||
* Copyright 2012-2022 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.context.support.GenericApplicationContext; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link AotApplicationContextInitializer}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class AotApplicationContextInitializerTests { |
||||
|
||||
@Test |
||||
void forMainApplicationClassUsesReflection() { |
||||
AotApplicationContextInitializer<ConfigurableApplicationContext> initializer = AotApplicationContextInitializer |
||||
.forMainApplicationClass(ExampleMain.class); |
||||
GenericApplicationContext applicationContext = new GenericApplicationContext(); |
||||
initializer.initialize(applicationContext); |
||||
assertThat(initializer.getName()).isEqualTo(ExampleMain__ApplicationContextInitializer.class.getName()); |
||||
assertThat(applicationContext.getId()).isEqualTo("ExampleMain"); |
||||
} |
||||
|
||||
@Test |
||||
void ofWhenInitializerIsNullThrowsException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> AotApplicationContextInitializer.of(null)) |
||||
.withMessage("Initializer must not be null"); |
||||
} |
||||
|
||||
@Test |
||||
void ofWithNameWhenInitializerIsNullThrowsException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> AotApplicationContextInitializer.of("test", null)) |
||||
.withMessage("Initializer must not be null"); |
||||
} |
||||
|
||||
@Test |
||||
void ofWithNameWhenNameIsNullThrowsException() { |
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> AotApplicationContextInitializer.of(null, new CustomApplicationContextInitializer())) |
||||
.withMessage("Name must not be null"); |
||||
} |
||||
|
||||
@Test |
||||
void ofUsesDelegation() { |
||||
AotApplicationContextInitializer<ConfigurableApplicationContext> initializer = AotApplicationContextInitializer |
||||
.of(new CustomApplicationContextInitializer()); |
||||
GenericApplicationContext applicationContext = new GenericApplicationContext(); |
||||
initializer.initialize(applicationContext); |
||||
assertThat(initializer.getName()).isEqualTo(CustomApplicationContextInitializer.class.getName()); |
||||
assertThat(applicationContext.getId()).isEqualTo("Custom"); |
||||
|
||||
} |
||||
|
||||
static class ExampleMain { |
||||
|
||||
} |
||||
|
||||
static class ExampleMain__ApplicationContextInitializer |
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> { |
||||
|
||||
@Override |
||||
public void initialize(ConfigurableApplicationContext applicationContext) { |
||||
applicationContext.setId("ExampleMain"); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class CustomApplicationContextInitializer |
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> { |
||||
|
||||
@Override |
||||
public void initialize(ConfigurableApplicationContext applicationContext) { |
||||
applicationContext.setId("Custom"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue