diff --git a/querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/AbstractQuerydslProcessor.java b/querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/AbstractQuerydslProcessor.java index 2784068232..4d19718821 100644 --- a/querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/AbstractQuerydslProcessor.java +++ b/querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/AbstractQuerydslProcessor.java @@ -212,6 +212,14 @@ protected void processAnnotations() { // extend entity types typeFactory.extendTypes(); + if (!context.entityTypes.isEmpty()) { + var serializerConfig = + conf.getSerializerConfig(context.entityTypes.values().iterator().next()); + if (serializerConfig.createDefaultVariable()) { + detectCircularQClassReferences(); + } + } + context.clean(); } @@ -682,6 +690,71 @@ private void serialize(Serializer serializer, Collection models) { } } + private void detectCircularQClassReferences() { + Map typeMap = context.entityTypes; + + List detectedCycles = new ArrayList<>(); + Set globalVisited = new HashSet<>(); + + for (EntityType start : typeMap.values()) { + if (globalVisited.contains(start.getFullName())) continue; + + Deque path = new ArrayDeque<>(); + Set inStack = new HashSet<>(); + dfs(start, typeMap, path, inStack, globalVisited, detectedCycles); + } + + if (!detectedCycles.isEmpty()) { + var message = new StringBuilder(); + message.append("[QueryDSL] Circular Q-class references detected.\n"); + message.append( + "This may cause class initialization deadlock in multi-threaded environments.\n\n"); + message.append("Detected cycles:\n"); + for (int i = 0; i < detectedCycles.size(); i++) { + message.append(" (").append(i + 1).append(") ").append(detectedCycles.get(i)).append("\n"); + } + message.append("\nTo avoid deadlock, consider:\n"); + message.append(" (1) Removing the bidirectional association on one side.\n"); + message.append( + " (2) Pre-initializing Q-classes in a single thread before handling requests (e.g. via @PostConstruct).\n"); + message.append( + " (3) Using 'new QClass(\"alias\")' instead of static field access in your repositories."); + + processingEnv.getMessager().printMessage(Kind.WARNING, message.toString()); + } + } + + private void dfs( + EntityType current, + Map typeMap, + Deque path, + Set inStack, + Set globalVisited, + List detectedCycles) { + + String currentName = current.getFullName(); + globalVisited.add(currentName); + inStack.add(currentName); + path.addLast(current.getSimpleName()); + + for (Property property : current.getProperties()) { + String neighborName = property.getType().getFullName(); + EntityType neighbor = typeMap.get(neighborName); + if (neighbor == null) continue; + + if (inStack.contains(neighborName)) { + List cycle = new ArrayList<>(path); + cycle.add(neighbor.getSimpleName()); + detectedCycles.add(String.join(" → ", cycle)); + } else if (!globalVisited.contains(neighborName)) { + dfs(neighbor, typeMap, path, inStack, globalVisited, detectedCycles); + } + } + + path.removeLast(); + inStack.remove(currentName); + } + protected String getClassName(EntityType model) { var type = conf.getTypeMappings().getPathType(model, model, true); var packageName = type.getPackageName(); diff --git a/querydsl-tooling/querydsl-apt/src/test/java/com/querydsl/apt/QuerydslAnnotationProcessorCompileTest.java b/querydsl-tooling/querydsl-apt/src/test/java/com/querydsl/apt/QuerydslAnnotationProcessorCompileTest.java index 7d63a84645..846bedf562 100644 --- a/querydsl-tooling/querydsl-apt/src/test/java/com/querydsl/apt/QuerydslAnnotationProcessorCompileTest.java +++ b/querydsl-tooling/querydsl-apt/src/test/java/com/querydsl/apt/QuerydslAnnotationProcessorCompileTest.java @@ -129,6 +129,179 @@ public class PlainClass { assertThat(compilation.generatedSourceFile("test.QPlainClass")).isEmpty(); } + @Test + void circularQClassReference_producesWarning() { + JavaFileObject orderSource = + JavaFileObjects.forSourceString( + "test.Order", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class Order { + public Customer customer; + } + """); + + JavaFileObject customerSource = + JavaFileObjects.forSourceString( + "test.Customer", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class Customer { + public Order lastOrder; + } + """); + + Compilation compilation = + javac() + .withProcessors(new QuerydslAnnotationProcessor()) + .compile(orderSource, customerSource); + + CompilationSubject.assertThat(compilation).succeeded(); + CompilationSubject.assertThat(compilation) + .hadWarningContaining("Circular Q-class references detected"); + } + + @Test + void unidirectionalReference_noWarning() { + JavaFileObject orderSource = + JavaFileObjects.forSourceString( + "test.Order", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class Order { + public Customer customer; + } + """); + + JavaFileObject customerSource = + JavaFileObjects.forSourceString( + "test.Customer", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class Customer { + public String name; + } + """); + + Compilation compilation = + javac() + .withProcessors(new QuerydslAnnotationProcessor()) + .compile(orderSource, customerSource); + + CompilationSubject.assertThat(compilation).succeeded(); + CompilationSubject.assertThat(compilation).hadWarningCount(0); + } + + @Test + void collectionReference_noWarning() { + JavaFileObject orderSource = + JavaFileObjects.forSourceString( + "test.Order", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + import java.util.List; + + @QueryEntity + public class Order { + public List items; + } + """); + + JavaFileObject orderItemSource = + JavaFileObjects.forSourceString( + "test.OrderItem", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class OrderItem { + public Order order; + } + """); + + Compilation compilation = + javac() + .withProcessors(new QuerydslAnnotationProcessor()) + .compile(orderSource, orderItemSource); + + CompilationSubject.assertThat(compilation).succeeded(); + CompilationSubject.assertThat(compilation).hadWarningCount(0); + } + + @Test + void indirectCircularReference_producesWarning() { + JavaFileObject aSource = + JavaFileObjects.forSourceString( + "test.EntityA", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class EntityA { + public EntityB b; + } + """); + + JavaFileObject bSource = + JavaFileObjects.forSourceString( + "test.EntityB", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class EntityB { + public EntityC c; + } + """); + + JavaFileObject cSource = + JavaFileObjects.forSourceString( + "test.EntityC", + """ + package test; + + import com.querydsl.core.annotations.QueryEntity; + + @QueryEntity + public class EntityC { + public EntityA a; + } + """); + + Compilation compilation = + javac() + .withProcessors(new QuerydslAnnotationProcessor()) + .compile(aSource, bSource, cSource); + + CompilationSubject.assertThat(compilation).succeeded(); + CompilationSubject.assertThat(compilation) + .hadWarningContaining("Circular Q-class references detected"); + } + @Test void entityWithInheritance_generatesQClasses() { JavaFileObject superSource =