ExerciseChallenge
Challenge
Make the cycle impossible
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- A cycle is an ordering impossibility, not a framework limitation
- Field injection satisfies it only because wiring is a second pass
- Converting to constructor injection surfaces the design problem
- The fix is a third type, not a workaround or a configuration flag
- allow-circular-references silences the message and keeps the problem
Starter
Starter.java
import java.lang.reflect.*;
import java.util.*;
/**
* Challenge: this graph works. Make it impossible for it to have been written.
*
* ReportService and ArchiveService need each other, and field injection wires
* them without complaint. Run it and see. Then convert both to constructor
* injection, watch it fail, and fix the design rather than the wiring.
*
* The failure is the point. A cycle that a container can satisfy is still two
* classes that cannot be built, understood or tested independently.
*/
public class Starter {
static class TemplateEngine {
String render(String id) {
return "<report id=" + id + ">";
}
}
static class ReportService {
TemplateEngine templates; // @Autowired
ArchiveService archive; // @Autowired
String publish(String id) {
String body = templates.render(id);
archive.store(id, body);
return body;
}
}
static class ArchiveService {
ReportService reports; // @Autowired — and here is the cycle
private final Map<String, String> stored = new LinkedHashMap<>();
void store(String id, String body) {
stored.put(id, body);
}
/** Re-renders a report that was never stored. */
String restore(String id) {
String body = stored.get(id);
return body != null ? body : reports.publish(id);
}
}
/** Field injection: instantiate everything, then wire. Two passes. */
static Map<Class<?>, Object> byField(Class<?>... types) throws Exception {
Map<Class<?>, Object> beans = new LinkedHashMap<>();
for (Class<?> t : types) {
beans.put(t, t.getDeclaredConstructor().newInstance());
}
for (Object bean : beans.values()) {
for (Field f : bean.getClass().getDeclaredFields()) {
if (Modifier.isFinal(f.getModifiers())) continue;
f.setAccessible(true);
f.set(bean, beans.get(f.getType()));
}
}
return beans;
}
// TODO 2: write the constructor-injection container.
//
// static Object byConstructor(Class<?> type, LinkedHashSet<Class<?>> path)
//
// Resolve each constructor parameter by calling yourself. Track the path
// so you can report the cycle instead of overflowing the stack — a
// LinkedHashSet that refuses a duplicate gives you both the detection and
// the chain to print.
public static void main(String[] args) throws Exception {
Map<Class<?>, Object> beans = byField(TemplateEngine.class, ReportService.class, ArchiveService.class);
ReportService reports = (ReportService) beans.get(ReportService.class);
ArchiveService archive = (ArchiveService) beans.get(ArchiveService.class);
System.out.println("field injection wired the cycle: " + reports.publish("A1"));
System.out.println("and it round-trips: " + archive.restore("A1"));
System.out.println("archive.reports == reports ? " + (archive.reports == reports));
// TODO 1: write down the order in which the two constructors would
// have to run for constructor injection to succeed. Do it before
// touching any code.
// TODO 3: convert ReportService and ArchiveService to constructor
// injection, with final fields, and build them with byConstructor.
// It fails. Print the chain.
// TODO 4: fix the design. Ask what job both classes are reaching for
// when they call each other, extract exactly that, and let both depend
// on it. Your container should then build the graph with no special
// cases and no cycle.
// TODO 5: two things people reach for instead. Explain why each is
// worse than the fix you just made:
//
// a) @Lazy on one of the two injection points
// b) spring.main.allow-circular-references=true
}
}Run it locally:
cd exercises/java/spring-core/constructor-vs-field-injection/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Write the order in which the two constructors would have to run. There isn't one — that is the answer, not a bug.
Hint 2
Which of the two classes actually needs the other, and for what? Name the one job they are both reaching for.
Hint 3
@Lazy breaks the cycle by injecting a proxy that resolves later. Ask yourself what that does to a startup-time failure.
Hint 4
If the extracted type has one method and no state, you have probably found the right seam.
Done when
- The graph is fully constructor-injected and every field is final
- No cycle remains — your container builds the graph without special cases
- A comment names the third type you extracted and what it is responsible for
- You can explain why @Lazy and allow-circular-references are not fixes
← Back to Why is constructor injection preferred over field injection?