Allow static registration of RuntimeHints
Prior to this commit, we could only contribute `RuntimeHints` through
two mechanisms:
* `AotContributingBeanFactoryPostProcessor`, consdering the entire
  `BeanFactory` and designed for contributing both code and hints.
* `AotContributingBeanPostProcessor`, consdering beans one by one, but
  also designed for contributing both code and hints.
There are cases where libraries and applications want to contribute
`RuntimeHints` only, in a more static fashion: a dependency being
present, or a piece of infrastructure being considered by the
application context are good enough signals to contribute hints about
resources or reflection.
This commit adds the `RuntimeHintsRegistrar` contract for these cases.
Implementations can be declared as `spring.factories` and they will be
processed as soon as they're detected on the classpath. They can also be
declared with `@ImportRuntimeHints` and they will be processed if the
annotated bean definition is considered in the application context.
This annotation should be mainly used on configuration classes and on
bean methods.
```
@Configuration
@ImportRuntimeHints(CustomRuntimeHintsRegistrar.class)
public class MyConfiguration {
  @Bean
  @ImportRuntimeHints(OtherRuntimeHintsRegistrar.class)
  public MyBean myBean() {
    //...
  }
}
```
Closes gh-28160
			
			
This commit is contained in:
		
							parent
							
								
									591aa7f64b
								
							
						
					
					
						commit
						38019d2249
					
				|  | @ -0,0 +1,48 @@ | |||
| /* | ||||
|  * Copyright 2002-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.context.annotation; | ||||
| 
 | ||||
| import java.lang.annotation.Documented; | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
| 
 | ||||
| import org.springframework.aot.hint.RuntimeHintsRegistrar; | ||||
| 
 | ||||
| /** | ||||
|  * Indicates that one or more {@link RuntimeHintsRegistrar} implementations should be processed. | ||||
|  * <p>Unlike declaring {@link RuntimeHintsRegistrar} as {@code spring.factories}, | ||||
|  * {@code @ImportRuntimeHints} allows for more flexible use cases where registrations are only | ||||
|  * processed if the annotated configuration class or bean method is considered by the | ||||
|  * application context. | ||||
|  * | ||||
|  * @author Brian Clozel | ||||
|  * @since 6.0 | ||||
|  * @see org.springframework.aot.hint.RuntimeHints | ||||
|  */ | ||||
| @Target({ElementType.TYPE, ElementType.METHOD}) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Documented | ||||
| public @interface ImportRuntimeHints { | ||||
| 
 | ||||
| 	/** | ||||
| 	 * {@link RuntimeHintsRegistrar} implementations to process. | ||||
| 	 */ | ||||
| 	Class<? extends RuntimeHintsRegistrar>[] value(); | ||||
| 
 | ||||
| } | ||||
|  | @ -150,6 +150,7 @@ public class ApplicationContextAotGenerator { | |||
| 		for (String ppName : postProcessorNames) { | ||||
| 			postProcessors.add(beanFactory.getBean(ppName, AotContributingBeanFactoryPostProcessor.class)); | ||||
| 		} | ||||
| 		postProcessors.add(new RuntimeHintsPostProcessor()); | ||||
| 		sortPostProcessors(postProcessors, beanFactory); | ||||
| 		return postProcessors; | ||||
| 	} | ||||
|  |  | |||
|  | @ -0,0 +1,92 @@ | |||
| /* | ||||
|  * Copyright 2002-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.context.generator; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import org.apache.commons.logging.Log; | ||||
| import org.apache.commons.logging.LogFactory; | ||||
| 
 | ||||
| import org.springframework.aot.hint.RuntimeHintsRegistrar; | ||||
| import org.springframework.beans.BeanUtils; | ||||
| import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | ||||
| import org.springframework.beans.factory.generator.AotContributingBeanFactoryPostProcessor; | ||||
| import org.springframework.beans.factory.generator.BeanFactoryContribution; | ||||
| import org.springframework.beans.factory.generator.BeanFactoryInitialization; | ||||
| import org.springframework.context.annotation.ImportRuntimeHints; | ||||
| import org.springframework.core.io.support.SpringFactoriesLoader; | ||||
| import org.springframework.core.log.LogMessage; | ||||
| import org.springframework.lang.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * AOT {@code BeanFactoryPostProcessor} that processes {@link RuntimeHintsRegistrar} implementations | ||||
|  * declared as {@code spring.factories} or using {@link ImportRuntimeHints @ImportRuntimeHints} annotated | ||||
|  * configuration classes or bean methods. | ||||
|  * <p>This processor is registered by default in the {@link ApplicationContextAotGenerator} as it is | ||||
|  * only useful in an AOT context. | ||||
|  * | ||||
|  * @author Brian Clozel | ||||
|  * @see ApplicationContextAotGenerator | ||||
|  */ | ||||
| class RuntimeHintsPostProcessor implements AotContributingBeanFactoryPostProcessor { | ||||
| 
 | ||||
| 	private static final Log logger = LogFactory.getLog(RuntimeHintsPostProcessor.class); | ||||
| 
 | ||||
| 	@Override | ||||
| 	public BeanFactoryContribution contribute(ConfigurableListableBeanFactory beanFactory) { | ||||
| 		ClassLoader beanClassLoader = beanFactory.getBeanClassLoader(); | ||||
| 		List<RuntimeHintsRegistrar> registrars = | ||||
| 				new ArrayList<>(SpringFactoriesLoader.loadFactories(RuntimeHintsRegistrar.class, beanClassLoader)); | ||||
| 		Arrays.stream(beanFactory.getBeanNamesForAnnotation(ImportRuntimeHints.class)).forEach(beanDefinitionName -> { | ||||
| 			ImportRuntimeHints importRuntimeHints = beanFactory.findAnnotationOnBean(beanDefinitionName, ImportRuntimeHints.class); | ||||
| 			if (importRuntimeHints != null) { | ||||
| 				Class<? extends RuntimeHintsRegistrar>[] registrarClasses = importRuntimeHints.value(); | ||||
| 				for (Class<? extends RuntimeHintsRegistrar> registrarClass : registrarClasses) { | ||||
| 					logger.trace(LogMessage.format("Loaded [%s] registrar from annotated bean [%s]", registrarClass.getCanonicalName(), beanDefinitionName)); | ||||
| 					RuntimeHintsRegistrar registrar = BeanUtils.instantiateClass(registrarClass); | ||||
| 					registrars.add(registrar); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 		return new RuntimeHintsRegistrarContribution(registrars, beanClassLoader); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	static class RuntimeHintsRegistrarContribution implements BeanFactoryContribution { | ||||
| 
 | ||||
| 		private final List<RuntimeHintsRegistrar> registrars; | ||||
| 
 | ||||
| 		@Nullable | ||||
| 		private final ClassLoader beanClassLoader; | ||||
| 
 | ||||
| 		RuntimeHintsRegistrarContribution(List<RuntimeHintsRegistrar> registrars, @Nullable ClassLoader beanClassLoader) { | ||||
| 			this.registrars = registrars; | ||||
| 			this.beanClassLoader = beanClassLoader; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void applyTo(BeanFactoryInitialization initialization) { | ||||
| 			this.registrars.forEach(registrar -> { | ||||
| 				logger.trace(LogMessage.format("Processing RuntimeHints contribution from [%s]", registrar.getClass().getCanonicalName())); | ||||
| 				registrar.registerHints(initialization.generatedTypeContext().runtimeHints(), this.beanClassLoader); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,182 @@ | |||
| /* | ||||
|  * Copyright 2002-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.context.generator; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
| import java.net.URLClassLoader; | ||||
| import java.util.stream.Stream; | ||||
| 
 | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import org.springframework.aot.generator.DefaultGeneratedTypeContext; | ||||
| import org.springframework.aot.generator.GeneratedType; | ||||
| import org.springframework.aot.hint.ResourceBundleHint; | ||||
| import org.springframework.aot.hint.RuntimeHints; | ||||
| import org.springframework.aot.hint.RuntimeHintsRegistrar; | ||||
| import org.springframework.beans.BeanInstantiationException; | ||||
| import org.springframework.beans.factory.support.BeanDefinitionBuilder; | ||||
| import org.springframework.beans.factory.support.RootBeanDefinition; | ||||
| import org.springframework.context.annotation.AnnotationConfigUtils; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.context.annotation.ImportRuntimeHints; | ||||
| import org.springframework.context.support.GenericApplicationContext; | ||||
| import org.springframework.javapoet.ClassName; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||||
| 
 | ||||
| /** | ||||
|  * Tests for {@link RuntimeHintsPostProcessor}. | ||||
|  * | ||||
|  * @author Brian Clozel | ||||
|  */ | ||||
| class RuntimeHintsPostProcessorTests { | ||||
| 
 | ||||
| 	private DefaultGeneratedTypeContext generationContext; | ||||
| 
 | ||||
| 	private ApplicationContextAotGenerator generator; | ||||
| 
 | ||||
| 	@BeforeEach | ||||
| 	void setup() { | ||||
| 		this.generationContext = createGenerationContext(); | ||||
| 		this.generator = new ApplicationContextAotGenerator(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void shouldProcessRegistrarOnConfiguration() { | ||||
| 		GenericApplicationContext applicationContext = createContext(ConfigurationWithHints.class); | ||||
| 		this.generator.generateApplicationContext(applicationContext, this.generationContext); | ||||
| 		assertThatSampleRegistrarContributed(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void shouldProcessRegistrarOnBeanMethod() { | ||||
| 		GenericApplicationContext applicationContext = createContext(ConfigurationWithBeanDeclaringHints.class); | ||||
| 		this.generator.generateApplicationContext(applicationContext, this.generationContext); | ||||
| 		assertThatSampleRegistrarContributed(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void shouldProcessRegistrarInSpringFactory() { | ||||
| 		GenericApplicationContext applicationContext = createContext(); | ||||
| 		applicationContext.setClassLoader(new TestSpringFactoriesClassLoader()); | ||||
| 		this.generator.generateApplicationContext(applicationContext, this.generationContext); | ||||
| 		assertThatSampleRegistrarContributed(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() { | ||||
| 		GenericApplicationContext applicationContext = createContext(ConfigurationWithIllegalRegistrar.class); | ||||
| 		assertThatThrownBy(() -> this.generator.generateApplicationContext(applicationContext, this.generationContext)) | ||||
| 				.isInstanceOf(BeanInstantiationException.class); | ||||
| 	} | ||||
| 
 | ||||
| 	private void assertThatSampleRegistrarContributed() { | ||||
| 		Stream<ResourceBundleHint> bundleHints = this.generationContext.runtimeHints().resources().resourceBundles(); | ||||
| 		assertThat(bundleHints).anyMatch(bundleHint -> "sample".equals(bundleHint.getBaseName())); | ||||
| 	} | ||||
| 
 | ||||
| 	private GenericApplicationContext createContext(Class<?>... configClasses) { | ||||
| 		GenericApplicationContext applicationContext = new GenericApplicationContext(); | ||||
| 		AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext); | ||||
| 		for (Class<?> configClass : configClasses) { | ||||
| 			applicationContext.registerBeanDefinition(configClass.getSimpleName(), new RootBeanDefinition(configClass)); | ||||
| 		} | ||||
| 		applicationContext.registerBeanDefinition("runtimeHintsPostProcessor", | ||||
| 				BeanDefinitionBuilder.rootBeanDefinition(RuntimeHintsPostProcessor.class, RuntimeHintsPostProcessor::new).getBeanDefinition()); | ||||
| 		return applicationContext; | ||||
| 	} | ||||
| 
 | ||||
| 	private DefaultGeneratedTypeContext createGenerationContext() { | ||||
| 		ClassName mainGeneratedType = ClassName.get("com.example", "Test"); | ||||
| 		return new DefaultGeneratedTypeContext(mainGeneratedType.packageName(), packageName -> | ||||
| 				GeneratedType.of(ClassName.get(packageName, mainGeneratedType.simpleName()))); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@ImportRuntimeHints(SampleRuntimeHintsRegistrar.class) | ||||
| 	@Configuration(proxyBeanMethods = false) | ||||
| 	static class ConfigurationWithHints { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Configuration(proxyBeanMethods = false) | ||||
| 	static class ConfigurationWithBeanDeclaringHints { | ||||
| 
 | ||||
| 		@Bean | ||||
| 		@ImportRuntimeHints(SampleRuntimeHintsRegistrar.class) | ||||
| 		SampleBean sampleBean() { | ||||
| 			return new SampleBean(); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	public static class SampleRuntimeHintsRegistrar implements RuntimeHintsRegistrar { | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void registerHints(RuntimeHints hints, ClassLoader classLoader) { | ||||
| 			hints.resources().registerResourceBundle("sample"); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	static class SampleBean { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@ImportRuntimeHints(IllegalRuntimeHintsRegistrar.class) | ||||
| 	@Configuration(proxyBeanMethods = false) | ||||
| 	static class ConfigurationWithIllegalRegistrar { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	public static class IllegalRuntimeHintsRegistrar implements RuntimeHintsRegistrar { | ||||
| 
 | ||||
| 		public IllegalRuntimeHintsRegistrar(String arg) { | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void registerHints(RuntimeHints hints, ClassLoader classLoader) { | ||||
| 			hints.resources().registerResourceBundle("sample"); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	private static class TestSpringFactoriesClassLoader extends URLClassLoader { | ||||
| 
 | ||||
| 		TestSpringFactoriesClassLoader() { | ||||
| 			super(new URL[] {testLocation()}, RuntimeHintsPostProcessorTests.class.getClassLoader()); | ||||
| 		} | ||||
| 
 | ||||
| 		private static URL testLocation() { | ||||
| 			try { | ||||
| 				return new File("src/test/resources/org/springframework/context/annotation/runtimehints/").toURI().toURL(); | ||||
| 			} | ||||
| 			catch (MalformedURLException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.context.generator.RuntimeHintsPostProcessorTests.SampleRuntimeHintsRegistrar | ||||
|  | @ -0,0 +1,42 @@ | |||
| /* | ||||
|  * Copyright 2002-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.aot.hint; | ||||
| 
 | ||||
| import org.springframework.lang.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * Contract for registering {@link RuntimeHints} in a static fashion. | ||||
|  * <p>Implementations will contribute hints without any knowledge of the application context | ||||
|  * and can only use the given {@link ClassLoader} to conditionally contribute hints. | ||||
|  * <p>{@code RuntimeHintsRegistrar} can be declared as {@code spring.factories} entries; | ||||
|  * the registrar will be processed as soon as its declaration is found in the classpath. | ||||
|  * A standard no-arg constructor is required for implementations. | ||||
|  * | ||||
|  * @author Brian Clozel | ||||
|  * @since 6.0 | ||||
|  */ | ||||
| @FunctionalInterface | ||||
| public interface RuntimeHintsRegistrar { | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Contribute hints to the given {@link RuntimeHints} instance. | ||||
| 	 * @param hints the hints contributed so far for the application | ||||
| 	 * @param classLoader the classloader, or {@code null} if even the system ClassLoader isn't accessible | ||||
| 	 */ | ||||
| 	void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader); | ||||
| 
 | ||||
| } | ||||
		Loading…
	
		Reference in New Issue