Skip to content

Commit 8d32dc9

Browse files
prdoylecbuescher
authored andcommitted
New injector (elastic#111722)
* Initial new injector * Allow createComponents to return classes * Downsample injection * Remove more vestiges of subtype handling * Lowercase logger * Respond to code review comments * Only one object per class * Some additional cleanup incl spotless * PR feedback * Missed one * Rename workQueue * Remove Injector.addRecordContents * TelemetryProvider requires us to inject an object using a supertype * Address Simon's comments * Clarify the reason for SuppressForbidden * Make log indentation code less intrusive
1 parent 352e148 commit 8d32dc9

File tree

19 files changed

+1025
-25
lines changed

19 files changed

+1025
-25
lines changed

server/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
exports org.elasticsearch.common.file;
191191
exports org.elasticsearch.common.geo;
192192
exports org.elasticsearch.common.hash;
193+
exports org.elasticsearch.injection.api;
193194
exports org.elasticsearch.injection.guice;
194195
exports org.elasticsearch.injection.guice.binder;
195196
exports org.elasticsearch.injection.guice.internal;
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.injection;
10+
11+
import org.elasticsearch.injection.api.Inject;
12+
import org.elasticsearch.injection.spec.ExistingInstanceSpec;
13+
import org.elasticsearch.injection.spec.InjectionSpec;
14+
import org.elasticsearch.injection.spec.MethodHandleSpec;
15+
import org.elasticsearch.injection.spec.ParameterSpec;
16+
import org.elasticsearch.injection.step.InjectionStep;
17+
import org.elasticsearch.logging.LogManager;
18+
import org.elasticsearch.logging.Logger;
19+
20+
import java.lang.invoke.MethodHandle;
21+
import java.lang.invoke.MethodHandles;
22+
import java.lang.reflect.Constructor;
23+
import java.util.ArrayDeque;
24+
import java.util.Collection;
25+
import java.util.HashSet;
26+
import java.util.LinkedHashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Queue;
30+
import java.util.Set;
31+
import java.util.stream.Stream;
32+
33+
import static java.util.function.Predicate.not;
34+
import static java.util.stream.Collectors.joining;
35+
import static java.util.stream.Collectors.toCollection;
36+
import static java.util.stream.Collectors.toMap;
37+
38+
/**
39+
* The main object for dependency injection.
40+
* <p>
41+
* Allows the user to specify the requirements, then call {@link #inject} to create an object plus all its dependencies.
42+
* <p>
43+
* <em>Implementation note</em>: this class itself contains logic for <em>specifying</em> the injection requirements;
44+
* the actual injection operations are performed in other classes like {@link Planner} and {@link PlanInterpreter},
45+
*/
46+
public final class Injector {
47+
private static final Logger logger = LogManager.getLogger(Injector.class);
48+
49+
/**
50+
* The specifications supplied by the user, as opposed to those inferred by the injector.
51+
*/
52+
private final Map<Class<?>, InjectionSpec> seedSpecs;
53+
54+
Injector(Map<Class<?>, InjectionSpec> seedSpecs) {
55+
this.seedSpecs = seedSpecs;
56+
}
57+
58+
public static Injector create() {
59+
return new Injector(new LinkedHashMap<>());
60+
}
61+
62+
/**
63+
* Instructs the injector to instantiate <code>classToProcess</code>
64+
* in accordance with whatever annotations may be present on that class.
65+
* <p>
66+
* There are only three ways the injector can find out that it must instantiate some class:
67+
* <ol>
68+
* <li>
69+
* This method
70+
* </li>
71+
* <li>
72+
* The parameter passed to {@link #inject}
73+
* </li>
74+
* <li>
75+
* A constructor parameter of some other class being instantiated,
76+
* having exactly the right class (not a supertype)
77+
* </li>
78+
* </ol>
79+
*
80+
* @return <code>this</code>
81+
*/
82+
public Injector addClass(Class<?> classToProcess) {
83+
MethodHandleSpec methodHandleSpec = methodHandleSpecFor(classToProcess);
84+
var existing = seedSpecs.put(classToProcess, methodHandleSpec);
85+
if (existing != null) {
86+
throw new IllegalArgumentException("class " + classToProcess.getSimpleName() + " has already been added");
87+
}
88+
return this;
89+
}
90+
91+
/**
92+
* Equivalent to multiple chained calls to {@link #addClass}.
93+
*/
94+
public Injector addClasses(Collection<Class<?>> classesToProcess) {
95+
classesToProcess.forEach(this::addClass);
96+
return this;
97+
}
98+
99+
/**
100+
* Equivalent to {@link #addInstance addInstance(object.getClass(), object)}.
101+
*/
102+
public <T> Injector addInstance(Object object) {
103+
@SuppressWarnings("unchecked")
104+
Class<T> actualClass = (Class<T>) object.getClass(); // Whatever the runtime type is, it's represented by T
105+
return addInstance(actualClass, actualClass.cast(object));
106+
}
107+
108+
/**
109+
* Equivalent to multiple calls to {@link #addInstance(Object)}.
110+
*/
111+
public Injector addInstances(Collection<?> objects) {
112+
for (var x : objects) {
113+
addInstance(x);
114+
}
115+
return this;
116+
}
117+
118+
/**
119+
* Indicates that <code>object</code> is to be injected for parameters of type <code>type</code>.
120+
* The given object is treated as though it had been instantiated by the injector.
121+
*/
122+
public <T> Injector addInstance(Class<? super T> type, T object) {
123+
assert type.isInstance(object); // No unchecked casting shenanigans allowed
124+
var existing = seedSpecs.put(type, new ExistingInstanceSpec(type, object));
125+
if (existing != null) {
126+
throw new IllegalStateException("There's already an object for " + type);
127+
}
128+
return this;
129+
}
130+
131+
/**
132+
* Main entry point. Causes objects to be constructed.
133+
* @return {@link Map} whose keys are all the requested <code>resultTypes</code> and whose values are all the instances of those types.
134+
*/
135+
public Map<Class<?>, Object> inject(Collection<? extends Class<?>> resultTypes) {
136+
resultTypes.forEach(this::ensureClassIsSpecified);
137+
PlanInterpreter i = doInjection();
138+
return resultTypes.stream().collect(toMap(c -> c, i::theInstanceOf));
139+
}
140+
141+
private <T> void ensureClassIsSpecified(Class<T> resultType) {
142+
if (seedSpecs.containsKey(resultType) == false) {
143+
addClass(resultType);
144+
}
145+
}
146+
147+
private PlanInterpreter doInjection() {
148+
logger.debug("Starting injection");
149+
Map<Class<?>, InjectionSpec> specMap = specClosure(seedSpecs);
150+
Map<Class<?>, Object> existingInstances = new LinkedHashMap<>();
151+
specMap.values().forEach((spec) -> {
152+
if (spec instanceof ExistingInstanceSpec e) {
153+
existingInstances.put(e.requestedType(), e.instance());
154+
}
155+
});
156+
PlanInterpreter interpreter = new PlanInterpreter(existingInstances);
157+
interpreter.executePlan(injectionPlan(seedSpecs.keySet(), specMap));
158+
logger.debug("Done injection");
159+
return interpreter;
160+
}
161+
162+
/**
163+
* Finds an {@link InjectionSpec} for every class the injector is capable of injecting.
164+
* <p>
165+
* We do this once the injector is fully configured, with all calls to {@link #addClass} and {@link #addInstance} finished,
166+
* so that we can easily build the complete picture of how injection should occur.
167+
* <p>
168+
* This is not part of the planning process; it's just discovering all the things
169+
* the injector needs to know about. This logic isn't concerned with ordering or dependency cycles.
170+
*
171+
* @param seedMap the injections the user explicitly asked for
172+
* @return an {@link InjectionSpec} for every class the injector is capable of injecting.
173+
*/
174+
private static Map<Class<?>, InjectionSpec> specClosure(Map<Class<?>, InjectionSpec> seedMap) {
175+
assert seedMapIsValid(seedMap);
176+
177+
// For convenience, we pretend there's a gigantic method out there that takes
178+
// all the seed types as parameters.
179+
Queue<ParameterSpec> workQueue = seedMap.values()
180+
.stream()
181+
.map(InjectionSpec::requestedType)
182+
.map(Injector::syntheticParameterSpec)
183+
.collect(toCollection(ArrayDeque::new));
184+
185+
// This map doubles as a checklist of classes we're already finished processing
186+
Map<Class<?>, InjectionSpec> result = new LinkedHashMap<>();
187+
188+
ParameterSpec p;
189+
while ((p = workQueue.poll()) != null) {
190+
Class<?> c = p.injectableType();
191+
InjectionSpec existingResult = result.get(c);
192+
if (existingResult != null) {
193+
logger.trace("Spec for {} already exists", c.getSimpleName());
194+
continue;
195+
}
196+
197+
InjectionSpec spec = seedMap.get(c);
198+
if (spec instanceof ExistingInstanceSpec) {
199+
// simple!
200+
result.put(c, spec);
201+
continue;
202+
}
203+
204+
// At this point, we know we'll need a MethodHandleSpec
205+
MethodHandleSpec methodHandleSpec;
206+
if (spec == null) {
207+
// The user didn't specify this class; we must infer it now
208+
spec = methodHandleSpec = methodHandleSpecFor(c);
209+
} else if (spec instanceof MethodHandleSpec m) {
210+
methodHandleSpec = m;
211+
} else {
212+
throw new AssertionError("Unexpected spec: " + spec);
213+
}
214+
215+
logger.trace("Inspecting parameters for constructor of {}", c);
216+
for (var ps : methodHandleSpec.parameters()) {
217+
logger.trace("Enqueue {}", ps);
218+
workQueue.add(ps);
219+
}
220+
221+
registerSpec(spec, result);
222+
}
223+
224+
if (logger.isTraceEnabled()) {
225+
logger.trace("Specs: {}", result.values().stream().map(Object::toString).collect(joining("\n\t", "\n\t", "")));
226+
}
227+
return result;
228+
}
229+
230+
private static MethodHandleSpec methodHandleSpecFor(Class<?> c) {
231+
Constructor<?> constructor = getSuitableConstructorIfAny(c);
232+
if (constructor == null) {
233+
throw new IllegalStateException("No suitable constructor for " + c);
234+
}
235+
236+
MethodHandle ctorHandle;
237+
try {
238+
ctorHandle = lookup().unreflectConstructor(constructor);
239+
} catch (IllegalAccessException e) {
240+
throw new IllegalStateException(e);
241+
}
242+
243+
List<ParameterSpec> parameters = Stream.of(constructor.getParameters()).map(ParameterSpec::from).toList();
244+
245+
return new MethodHandleSpec(c, ctorHandle, parameters);
246+
}
247+
248+
/**
249+
* @return true (unless an assertion fails). Never returns false.
250+
*/
251+
private static boolean seedMapIsValid(Map<Class<?>, InjectionSpec> seed) {
252+
seed.forEach(
253+
(c, s) -> { assert s.requestedType().equals(c) : "Spec must be associated with its requestedType, not " + c + ": " + s; }
254+
);
255+
return true;
256+
}
257+
258+
/**
259+
* For the classes we've been explicitly asked to inject,
260+
* pretend there's some massive method taking all of them as parameters
261+
*/
262+
private static ParameterSpec syntheticParameterSpec(Class<?> c) {
263+
return new ParameterSpec("synthetic_" + c.getSimpleName(), c, c);
264+
}
265+
266+
private static Constructor<?> getSuitableConstructorIfAny(Class<?> type) {
267+
var constructors = Stream.of(type.getConstructors()).filter(not(Constructor::isSynthetic)).toList();
268+
if (constructors.size() == 1) {
269+
return constructors.get(0);
270+
}
271+
var injectConstructors = constructors.stream().filter(c -> c.isAnnotationPresent(Inject.class)).toList();
272+
if (injectConstructors.size() == 1) {
273+
return injectConstructors.get(0);
274+
}
275+
logger.trace("No suitable constructor for {}", type);
276+
return null;
277+
}
278+
279+
private static void registerSpec(InjectionSpec spec, Map<Class<?>, InjectionSpec> specsByClass) {
280+
Class<?> requestedType = spec.requestedType();
281+
var existing = specsByClass.put(requestedType, spec);
282+
if (existing == null || existing.equals(spec)) {
283+
logger.trace("Register spec: {}", spec);
284+
} else {
285+
throw new IllegalStateException("Ambiguous specifications for " + requestedType + ": " + existing + " and " + spec);
286+
}
287+
}
288+
289+
private List<InjectionStep> injectionPlan(Set<Class<?>> requiredClasses, Map<Class<?>, InjectionSpec> specsByClass) {
290+
logger.trace("Constructing instantiation plan");
291+
Set<Class<?>> allParameterTypes = new HashSet<>();
292+
specsByClass.values().forEach(spec -> {
293+
if (spec instanceof MethodHandleSpec m) {
294+
m.parameters().stream().map(ParameterSpec::injectableType).forEachOrdered(allParameterTypes::add);
295+
}
296+
});
297+
298+
var plan = new Planner(specsByClass, requiredClasses, allParameterTypes).injectionPlan();
299+
if (logger.isDebugEnabled()) {
300+
logger.debug("Injection plan: {}", plan.stream().map(Object::toString).collect(joining("\n\t", "\n\t", "")));
301+
}
302+
return plan;
303+
}
304+
305+
/**
306+
* <em>Evolution note</em>: there may be cases in the where we allow the user to
307+
* supply a {@link java.lang.invoke.MethodHandles.Lookup} for convenience,
308+
* so that they aren't required to make things public just to participate in injection.
309+
*/
310+
private static MethodHandles.Lookup lookup() {
311+
return MethodHandles.publicLookup();
312+
}
313+
314+
}

0 commit comments

Comments
 (0)