Skip to content

Commit 9244090

Browse files
committed
Support DI of individual constructor args in @nested tests
Prior to this commit it was impossible to have an individual constructor argument in a @nested (i.e., inner) test class injected via @Autowired, @qualifier, or @value. This is due to a bug in javac on JDK versions prior to 9, whereby annotation lookups performed directly via the java.lang.reflect.Parameter API fail for inner class constructors. Specifically, the parameter annotations array in the compiled byte code for the user's test class excludes an entry for the implicit enclosing instance parameter for an inner class constructor. This commit introduces a workaround in ParameterAutowireUtils for this off-by-one error by transparently looking up annotations on the preceding Parameter object (i.e., index - 1). In addition, this commit relies on the change recently introduced in MethodParameter in order to compensate for the same JDK bug (see SPR-16652). Issue: SPR-16653
1 parent 17703e5 commit 9244090

File tree

3 files changed

+125
-22
lines changed

3 files changed

+125
-22
lines changed

spring-test/src/main/java/org/springframework/test/context/junit/jupiter/ParameterAutowireUtils.java

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.Executable;
2123
import java.lang.reflect.Parameter;
2224
import java.util.Optional;
2325

@@ -32,6 +34,7 @@
3234
import org.springframework.core.annotation.AnnotatedElementUtils;
3335
import org.springframework.core.annotation.SynthesizingMethodParameter;
3436
import org.springframework.lang.Nullable;
37+
import org.springframework.util.ClassUtils;
3538

3639
/**
3740
* Collection of utilities related to autowiring of individual method parameters.
@@ -43,6 +46,26 @@
4346
*/
4447
abstract class ParameterAutowireUtils {
4548

49+
private static final AnnotatedElement EMPTY_ANNOTATED_ELEMENT = new AnnotatedElement() {
50+
51+
@Override
52+
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
53+
return null;
54+
}
55+
56+
@Override
57+
public Annotation[] getAnnotations() {
58+
return new Annotation[0];
59+
}
60+
61+
@Override
62+
public Annotation[] getDeclaredAnnotations() {
63+
return new Annotation[0];
64+
}
65+
};
66+
67+
68+
4669
private ParameterAutowireUtils() {
4770
/* no-op */
4871
}
@@ -54,13 +77,18 @@ private ParameterAutowireUtils() {
5477
* {@link ApplicationContext} (or a sub-type thereof) or is annotated or
5578
* meta-annotated with {@link Autowired @Autowired},
5679
* {@link Qualifier @Qualifier}, or {@link Value @Value}.
80+
* @param parameter the parameter whose dependency should be autowired
81+
* @param parameterIndex the index of the parameter
5782
* @see #resolveDependency(Parameter, Class, ApplicationContext)
5883
*/
59-
static boolean isAutowirable(Parameter parameter) {
60-
return ApplicationContext.class.isAssignableFrom(parameter.getType())
61-
|| AnnotatedElementUtils.hasAnnotation(parameter, Autowired.class)
62-
|| AnnotatedElementUtils.hasAnnotation(parameter, Qualifier.class)
63-
|| AnnotatedElementUtils.hasAnnotation(parameter, Value.class);
84+
static boolean isAutowirable(Parameter parameter, int parameterIndex) {
85+
if (ApplicationContext.class.isAssignableFrom(parameter.getType())) {
86+
return true;
87+
}
88+
AnnotatedElement annotatedParameter = getEffectiveAnnotatedParameter(parameter, parameterIndex);
89+
return AnnotatedElementUtils.hasAnnotation(annotatedParameter, Autowired.class)
90+
|| AnnotatedElementUtils.hasAnnotation(annotatedParameter, Qualifier.class)
91+
|| AnnotatedElementUtils.hasAnnotation(annotatedParameter, Value.class);
6492
}
6593

6694
/**
@@ -77,6 +105,7 @@ static boolean isAutowirable(Parameter parameter) {
77105
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
78106
* will be used as the qualifier for resolving ambiguities.
79107
* @param parameter the parameter whose dependency should be resolved
108+
* @param parameterIndex the index of the parameter
80109
* @param containingClass the concrete class that contains the parameter; this may
81110
* differ from the class that declares the parameter in that it may be a subclass
82111
* thereof, potentially substituting type variables
@@ -90,8 +119,9 @@ static boolean isAutowirable(Parameter parameter) {
90119
* @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
91120
*/
92121
@Nullable
93-
static Object resolveDependency(Parameter parameter, Class<?> containingClass, ApplicationContext applicationContext) {
94-
boolean required = findMergedAnnotation(parameter, Autowired.class).map(Autowired::required).orElse(true);
122+
static Object resolveDependency(Parameter parameter, int parameterIndex, Class<?> containingClass, ApplicationContext applicationContext) {
123+
AnnotatedElement annotatedParameter = getEffectiveAnnotatedParameter(parameter, parameterIndex);
124+
boolean required = findMergedAnnotation(annotatedParameter, Autowired.class).map(Autowired::required).orElse(true);
95125
MethodParameter methodParameter = SynthesizingMethodParameter.forParameter(parameter);
96126
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
97127
descriptor.setContainingClass(containingClass);
@@ -102,4 +132,44 @@ private static <A extends Annotation> Optional<A> findMergedAnnotation(Annotated
102132
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
103133
}
104134

135+
/**
136+
* Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up
137+
* annotations directly on a {@link Parameter} will fail for inner class
138+
* constructors.
139+
*
140+
* <h4>Bug in javac in JDK &lt; 9</h4>
141+
* <p>The parameter annotations array in the compiled byte code excludes an entry
142+
* for the implicit <em>enclosing instance</em> parameter for an inner class
143+
* constructor.
144+
*
145+
* <h4>Workaround</h4>
146+
* <p>This method provides a workaround for this off-by-one error by allowing the
147+
* caller to access annotations on the preceding {@link Parameter} object (i.e.,
148+
* {@code index - 1}). If the supplied {@code index} is zero, this method returns
149+
* an empty {@code AnnotatedElement}.
150+
*
151+
* <h4>WARNING</h4>
152+
* <p>The {@code AnnotatedElement} returned by this method should never be cast and
153+
* treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()},
154+
* {@link Parameter#getType()}, etc.) will not match those for the declared parameter
155+
* at the given index in an inner class constructor.
156+
*
157+
* @return the supplied {@code parameter} or the <em>effective</em> {@code Parameter}
158+
* if the aforementioned bug is in effect
159+
*/
160+
private static AnnotatedElement getEffectiveAnnotatedParameter(Parameter parameter, int index) {
161+
Executable executable = parameter.getDeclaringExecutable();
162+
163+
if (executable instanceof Constructor &&
164+
ClassUtils.isInnerClass(executable.getDeclaringClass()) &&
165+
executable.getParameterAnnotations().length == executable.getParameterCount() - 1) {
166+
167+
// Bug in javac in JDK <9: annotation array excludes enclosing instance parameter
168+
// for inner classes, so access it with the actual parameter index lowered by 1
169+
return (index == 0) ? EMPTY_ANNOTATED_ELEMENT : executable.getParameters()[index - 1];
170+
}
171+
172+
return parameter;
173+
}
174+
105175
}

spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -155,10 +155,11 @@ public void afterEach(ExtensionContext context) throws Exception {
155155
@Override
156156
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
157157
Parameter parameter = parameterContext.getParameter();
158+
int index = parameterContext.getIndex();
158159
Executable executable = parameter.getDeclaringExecutable();
159160
return (executable instanceof Constructor &&
160161
AnnotatedElementUtils.hasAnnotation(executable, Autowired.class)) ||
161-
ParameterAutowireUtils.isAutowirable(parameter);
162+
ParameterAutowireUtils.isAutowirable(parameter, index);
162163
}
163164

164165
/**
@@ -172,9 +173,10 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
172173
@Nullable
173174
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
174175
Parameter parameter = parameterContext.getParameter();
176+
int index = parameterContext.getIndex();
175177
Class<?> testClass = extensionContext.getRequiredTestClass();
176178
ApplicationContext applicationContext = getApplicationContext(extensionContext);
177-
return ParameterAutowireUtils.resolveDependency(parameter, testClass, applicationContext);
179+
return ParameterAutowireUtils.resolveDependency(parameter, index, testClass, applicationContext);
178180
}
179181

180182

spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTestCase.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
import org.junit.jupiter.api.Nested;
2020
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.TestInfo;
2122

2223
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.beans.factory.annotation.Qualifier;
25+
import org.springframework.beans.factory.annotation.Value;
2326
import org.springframework.context.annotation.Bean;
2427
import org.springframework.context.annotation.Configuration;
2528
import org.springframework.test.context.junit.SpringJUnitJupiterTestSuite;
26-
import org.springframework.test.context.junit.jupiter.DisabledIf;
2729
import org.springframework.test.context.junit.jupiter.SpringExtension;
2830
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
2931
import org.springframework.test.context.junit.jupiter.nested.NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTestCase.TopLevelConfig;
@@ -33,7 +35,7 @@
3335
/**
3436
* Integration tests that verify support for {@code @Nested} test classes in conjunction
3537
* with the {@link SpringExtension} in a JUnit Jupiter environment ... when using
36-
* constructor injection as opposed to field injection.
38+
* constructor injection as opposed to field injection (see SPR-16653).
3739
*
3840
* <p>
3941
* To run these tests in an IDE that does not have built-in support for the JUnit
@@ -49,8 +51,7 @@ class NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTestCase {
4951

5052
final String foo;
5153

52-
@Autowired
53-
NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTestCase(String foo) {
54+
NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTestCase(TestInfo testInfo, @Autowired String foo) {
5455
this.foo = foo;
5556
}
5657

@@ -65,8 +66,6 @@ class AutowiredConstructor {
6566

6667
final String bar;
6768

68-
// Only fails on JDK 8 if the parameter is annotated with @Autowired.
69-
// Works if the constructor itself is annotated with @Autowired.
7069
@Autowired
7170
AutowiredConstructor(String bar) {
7271
this.bar = bar;
@@ -81,15 +80,10 @@ void nestedTest() throws Exception {
8180

8281
@Nested
8382
@SpringJUnitConfig(NestedConfig.class)
84-
@DisabledIf(expression = "#{systemProperties['java.version'].startsWith('1.8')}", //
85-
reason = "Disabled on Java 8 due to a bug in javac in JDK 8")
86-
// See https://github.com/junit-team/junit5/issues/1345
8783
class AutowiredConstructorParameter {
8884

8985
final String bar;
9086

91-
// Only fails on JDK 8 if the parameter is annotated with @Autowired.
92-
// Works if the constructor itself is annotated with @Autowired.
9387
AutowiredConstructorParameter(@Autowired String bar) {
9488
this.bar = bar;
9589
}
@@ -101,6 +95,43 @@ void nestedTest() throws Exception {
10195
}
10296
}
10397

98+
@Nested
99+
@SpringJUnitConfig(NestedConfig.class)
100+
class QualifiedConstructorParameter {
101+
102+
final String bar;
103+
104+
QualifiedConstructorParameter(TestInfo testInfo, @Qualifier("bar") String s) {
105+
this.bar = s;
106+
}
107+
108+
@Test
109+
void nestedTest() throws Exception {
110+
assertEquals("foo", foo);
111+
assertEquals("bar", bar);
112+
}
113+
}
114+
115+
@Nested
116+
@SpringJUnitConfig(NestedConfig.class)
117+
class SpelConstructorParameter {
118+
119+
final String bar;
120+
final int answer;
121+
122+
SpelConstructorParameter(@Autowired String bar, TestInfo testInfo, @Value("#{ 6 * 7 }") int answer) {
123+
this.bar = bar;
124+
this.answer = answer;
125+
}
126+
127+
@Test
128+
void nestedTest() throws Exception {
129+
assertEquals("foo", foo);
130+
assertEquals("bar", bar);
131+
assertEquals(42, answer);
132+
}
133+
}
134+
104135
// -------------------------------------------------------------------------
105136

106137
@Configuration

0 commit comments

Comments
 (0)