Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -682,6 +690,71 @@ private void serialize(Serializer serializer, Collection<EntityType> models) {
}
}

private void detectCircularQClassReferences() {
Map<String, EntityType> typeMap = context.entityTypes;

List<String> detectedCycles = new ArrayList<>();
Set<String> globalVisited = new HashSet<>();

for (EntityType start : typeMap.values()) {
if (globalVisited.contains(start.getFullName())) continue;

Deque<String> path = new ArrayDeque<>();
Set<String> 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<String, EntityType> typeMap,
Deque<String> path,
Set<String> inStack,
Set<String> globalVisited,
List<String> 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<String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderItem> 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 =
Expand Down
Loading