Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Commit 4498c7c

Browse files
committed
Ensure TestExecutionListeners are loaded in native image
Prior to this commit, TestExecutionListener (TEL) implementations were discovered via the `spring.factories` mechanism (via AOT StaticSpringFactories), but they could not be loaded since the necessary native image reflection configuration was missing. This commit addresses this by introducing a TestExecutionListenerFactoriesCodeContributor that only applies to "org.springframework.test.context.TestExecutionListener" factory entries. This FactoriesCodeContributor ensures that the fully qualified class names for TELs can still be discovered via the SpringFactoriesLoader, and it additionally registers reflection configuration allowing the TELs to be instantiated reflectively. In addition, this commit introduces a substitution for AbstractTestContextBootstrapper.instantiateListeners() that serves as a workaround for odd behavior in a GraalVM native image. Specifically, if a NoClassDefFoundError is thrown during the native image build while attempting to register a particular class for reflection, that class will be magically "available" for loading via reflection within the native image without throwing a NoClassDefFoundError (even though the class cannot be loaded). As a result, an attempt to instantiate that nonexistent class via reflection within the native image will result in a NoSuchMethodException for the constructor. Since AbstractTestContextBootstrapper.instantiateListeners() catches NoClassDefFoundError in order to skip TestExecutionListeners that cannot be loaded, within the native image our substitution has to also catch NoSuchMethodException in order to skip such TestExecutionListeners. Closes gh-1217
1 parent 247982a commit 4498c7c

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

spring-aot/src/main/java/org/springframework/aot/factories/FactoriesCodeContributors.java

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class FactoriesCodeContributors {
3838

3939
FactoriesCodeContributors(AotOptions aotOptions) {
4040
this.contributors = Arrays.asList(new IgnoredFactoriesCodeContributor(),
41+
new TestExecutionListenerFactoriesCodeContributor(),
4142
new TestAutoConfigurationFactoriesCodeContributor(aotOptions),
4243
new NoArgConstructorFactoriesCodeContributor(),
4344
new PrivateFactoriesCodeContributor(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2019-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.factories;
18+
19+
import com.squareup.javapoet.ClassName;
20+
21+
import org.springframework.aot.build.context.BuildContext;
22+
import org.springframework.nativex.domain.reflect.ClassDescriptor;
23+
import org.springframework.nativex.hint.Flag;
24+
25+
/**
26+
* {@link FactoriesCodeContributor} that contributes source code for
27+
* {@link org.springframework.test.context.TestExecutionListener} implementations.
28+
* <p>Instead of instantiating them statically, we make sure that
29+
* {@link org.springframework.core.io.support.SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)}
30+
* will return their names and that reflection metadata is registered for native images.
31+
*
32+
* @author Sam Brannen
33+
* @author Brian Clozel
34+
* @see org.springframework.nativex.substitutions.framework.test.Target_AbstractTestContextBootstrapper
35+
* @see org.springframework.test.context.support.AbstractTestContextBootstrapper.instantiateListeners
36+
*/
37+
class TestExecutionListenerFactoriesCodeContributor implements FactoriesCodeContributor {
38+
39+
@Override
40+
public boolean canContribute(SpringFactory factory) {
41+
return factory.getFactoryType().getClassName().equals("org.springframework.test.context.TestExecutionListener");
42+
}
43+
44+
@Override
45+
public void contribute(SpringFactory factory, CodeGenerator code, BuildContext context) {
46+
ClassName factoryTypeClass = ClassName.bestGuess(factory.getFactoryType().getClassName());
47+
generateReflectionMetadata(factory, context);
48+
code.writeToStaticBlock(builder -> {
49+
builder.addStatement("names.add($T.class, $S)", factoryTypeClass,
50+
factory.getFactory().getClassName());
51+
});
52+
}
53+
54+
private void generateReflectionMetadata(SpringFactory factory, BuildContext context) {
55+
String factoryClassName = factory.getFactory().getClassName();
56+
ClassDescriptor factoryDescriptor = ClassDescriptor.of(factoryClassName);
57+
factoryDescriptor.setFlag(Flag.allDeclaredConstructors);
58+
context.describeReflection(reflect -> reflect.add(factoryDescriptor));
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.nativex.substitutions.framework.test;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.List;
22+
23+
import com.oracle.svm.core.annotate.Alias;
24+
import com.oracle.svm.core.annotate.Substitute;
25+
import com.oracle.svm.core.annotate.TargetClass;
26+
27+
import org.apache.commons.logging.Log;
28+
29+
import org.springframework.beans.BeanInstantiationException;
30+
import org.springframework.beans.BeanUtils;
31+
import org.springframework.nativex.substitutions.OnlyIfPresent;
32+
import org.springframework.test.context.TestExecutionListener;
33+
34+
/**
35+
* Native substitution for {@link org.springframework.test.context.support.AbstractTestContextBootstrapper}.
36+
*
37+
* <p>Necessary since class instantiation via reflection can result in a
38+
* {@link NoSuchMethodException} instead of a {@link NoClassDefFoundError}
39+
* within a native image, IF a {@link NoClassDefFoundError} was thrown during
40+
* native image build time while attempting to register the class for reflection.
41+
* In that case, the reflection registration still occurs, and we unfortunately
42+
* see different behavior at runtime within the native image.
43+
*
44+
* <p>In summary, the only change this substitution makes is the following
45+
* which additionally checks for a {@code NoSuchMethodException}:
46+
* {@code if (cause instanceof NoClassDefFoundError || cause instanceof NoSuchMethodException)}.
47+
*
48+
* @author Sam Brannen
49+
*/
50+
@TargetClass(className = "org.springframework.test.context.support.AbstractTestContextBootstrapper", onlyWith = OnlyIfPresent.class)
51+
final class Target_AbstractTestContextBootstrapper {
52+
53+
@Alias
54+
private Log logger;
55+
56+
57+
@Substitute
58+
private List<TestExecutionListener> instantiateListeners(Collection<Class<? extends TestExecutionListener>> classes) {
59+
List<TestExecutionListener> listeners = new ArrayList<>(classes.size());
60+
for (Class<? extends TestExecutionListener> listenerClass : classes) {
61+
try {
62+
listeners.add(BeanUtils.instantiateClass(listenerClass));
63+
}
64+
catch (BeanInstantiationException ex) {
65+
Throwable cause = ex.getCause();
66+
// Within a native image, NoSuchMethodException may be thrown instead of NoClassDefFoundError.
67+
if (cause instanceof NoClassDefFoundError || cause instanceof NoSuchMethodException) {
68+
// TestExecutionListener not applicable due to a missing dependency
69+
if (logger.isDebugEnabled()) {
70+
logger.debug(String.format(
71+
"Skipping candidate TestExecutionListener [%s] due to a missing dependency. " +
72+
"Specify custom listener classes or make the default listener classes " +
73+
"and their required dependencies available. Offending class: [%s]",
74+
listenerClass.getName(), cause.getMessage()));
75+
}
76+
}
77+
else {
78+
throw ex;
79+
}
80+
}
81+
}
82+
return listeners;
83+
}
84+
85+
}

0 commit comments

Comments
 (0)