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:
Brian Clozel
2022-04-14 11:53:46 +02:00
parent 591aa7f64b
commit 38019d2249
6 changed files with 366 additions and 0 deletions

View File

@@ -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);
}
}
}
}