From 7c79b0a8add05ea063f77d9de3b5bf283ce5c5d7 Mon Sep 17 00:00:00 2001 From: Kamil Krzywanski Date: Sat, 14 Mar 2026 23:45:37 +0100 Subject: [PATCH] Issue #2176 - Skip to lookup unwanted Querydsl properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kamil Krzywański --- .../data/core/ClassTypeInformation.java | 26 ++++++-- .../data/core/PropertyPath.java | 28 ++++++++- .../data/core/SimplePropertyPath.java | 24 ++++++- .../data/core/TypeDiscoverer.java | 45 ++++++++++--- .../data/core/TypeInformation.java | 38 ++++++++++- .../binding/PropertyPathInformation.java | 3 +- .../querydsl/binding/QuerydslBindings.java | 63 ++++++++++++++++++- .../data/core/TypeDiscovererUnitTests.java | 2 +- .../data/querydsl/Example.java | 20 ++++++ .../QuerydslPredicateBuilderUnitTests.java | 31 +++++++++ 10 files changed, 255 insertions(+), 25 deletions(-) create mode 100644 src/test/java/org/springframework/data/querydsl/Example.java diff --git a/src/main/java/org/springframework/data/core/ClassTypeInformation.java b/src/main/java/org/springframework/data/core/ClassTypeInformation.java index 7610db9124..e0cb05d279 100644 --- a/src/main/java/org/springframework/data/core/ClassTypeInformation.java +++ b/src/main/java/org/springframework/data/core/ClassTypeInformation.java @@ -15,10 +15,12 @@ */ package org.springframework.data.core; +import java.lang.reflect.Field; import java.util.List; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; @@ -29,11 +31,12 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch + * @author Kamil Krzywański */ @SuppressWarnings({ "rawtypes", "unchecked" }) class ClassTypeInformation extends TypeDiscoverer { - private static final ConcurrentLruCache> cache = new ConcurrentLruCache<>(128, + private static final ConcurrentLruCache> cache = new ConcurrentLruCache<>(128, ClassTypeInformation::new); private static final ConcurrentLruCache, ResolvableType> resolvableTypeCache = new ConcurrentLruCache<>(128, @@ -41,12 +44,16 @@ class ClassTypeInformation extends TypeDiscoverer { private final Class type; + ClassTypeInformation(ResolvableTypeHolder resolvableTypeHolder) { + this(resolvableTypeHolder.type, resolvableTypeHolder.field); + } + ClassTypeInformation(Class type) { - this(ResolvableType.forType(type)); + this(ResolvableType.forType(type), null); } - ClassTypeInformation(ResolvableType type) { - super(type); + ClassTypeInformation(ResolvableType type, @Nullable Field field) { + super(type, field); this.type = (Class) type.resolve(Object.class); } @@ -70,7 +77,7 @@ public static ClassTypeInformation from(Class type) { return from(resolvableTypeCache.get(type)); } - static ClassTypeInformation from(ResolvableType type) { + static ClassTypeInformation from(ResolvableType type, @Nullable Field field) { Assert.notNull(type, "Type must not be null"); @@ -84,7 +91,11 @@ static ClassTypeInformation from(ResolvableType type) { return (ClassTypeInformation) TypeInformation.MAP; } - return (ClassTypeInformation) cache.get(type); + return (ClassTypeInformation) cache.get(new ResolvableTypeHolder(type, field)); + } + + static ClassTypeInformation from(ResolvableType type) { + return from(type, null); } @Override @@ -111,4 +122,7 @@ public TypeInformation specialize(TypeInformation type) { public String toString() { return type.getName(); } + + private record ResolvableTypeHolder(ResolvableType type, @Nullable Field field){} + } diff --git a/src/main/java/org/springframework/data/core/PropertyPath.java b/src/main/java/org/springframework/data/core/PropertyPath.java index 3410c9f0d1..93663b3f08 100644 --- a/src/main/java/org/springframework/data/core/PropertyPath.java +++ b/src/main/java/org/springframework/data/core/PropertyPath.java @@ -15,6 +15,7 @@ */ package org.springframework.data.core; +import java.lang.annotation.Annotation; import java.util.Iterator; import java.util.regex.Pattern; @@ -43,12 +44,13 @@ * @author Mark Paluch * @author Mariusz Mączkowski * @author Johannes Englmeier + * @author Kamil Krzywański * @see PropertyReference * @see TypedPropertyPath * @see java.beans.PropertyDescriptor */ public interface PropertyPath extends Streamable { - + Annotation[] NO_ANNOTATIONS = new Annotation[0]; /** * Syntax sugar to create a {@link TypedPropertyPath} from a method reference to a Java beans property. *

@@ -211,6 +213,30 @@ default PropertyPath nested(String path) { return SimplePropertyPath.from(lookup, getOwningType()); } + /** + * Returns the annotations declared on the underlying field. + *

+ * This exposes annotations present directly on the represented {@link java.lang.reflect.Field} + * (field-level annotations), for example {@code @Id} or {@code @Column}. + * + * @return annotations declared on the underlying field; never {@literal null}. + */ + default Annotation[] getPropertyAnnotations(){ + return NO_ANNOTATIONS; + } + + /** + * Returns the {@link java.lang.reflect.Field#getModifiers() modifiers} of the underlying field. + *

+ * This exposes the field modifier bitmask such as {@code public}, {@code protected}, {@code private}, + * {@code static}, {@code final}, {@code transient}, {@code volatile}, etc. + * + * @return modifier bitmask as defined by {@link java.lang.reflect.Modifier}. + */ + default int getPropertyModifiers(){ + return 0; + } + /** * Returns an {@link Iterator Iterator of PropertyPath} that iterates over all property path segments. For example: * diff --git a/src/main/java/org/springframework/data/core/SimplePropertyPath.java b/src/main/java/org/springframework/data/core/SimplePropertyPath.java index e71bd0265d..923ab4574f 100644 --- a/src/main/java/org/springframework/data/core/SimplePropertyPath.java +++ b/src/main/java/org/springframework/data/core/SimplePropertyPath.java @@ -16,6 +16,7 @@ package org.springframework.data.core; import java.beans.Introspector; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -40,6 +41,7 @@ * @author Mark Paluch * @author Mariusz Mączkowski * @author Johannes Englmeier + * @author Kamil Krzywański */ class SimplePropertyPath implements PropertyPath { @@ -56,6 +58,8 @@ class SimplePropertyPath implements PropertyPath { private final TypeInformation typeInformation; private final TypeInformation actualTypeInformation; private final boolean isCollection; + private final int fieldModifiers; + private final Annotation[] fieldAnnotations; private @Nullable SimplePropertyPath next; @@ -99,6 +103,8 @@ class SimplePropertyPath implements PropertyPath { this.isCollection = this.typeInformation.isCollectionLike(); this.actualTypeInformation = this.typeInformation.getActualType() == null ? this.typeInformation : this.typeInformation.getRequiredActualType(); + this.fieldAnnotations = property.annotations; + this.fieldModifiers = property.modifiers; } private static @Nullable Property lookupProperty(TypeInformation owningType, String name) { @@ -361,14 +367,29 @@ public String toString() { return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath()); } + + @Override + public int getPropertyModifiers() { + return fieldModifiers; + } + + @Override + public Annotation[] getPropertyAnnotations() { + return fieldAnnotations; + } + private static final class Property { private final TypeInformation type; private final String path; + private final int modifiers; + private final Annotation[] annotations; private Property(TypeInformation type, String path) { this.type = type; this.path = path; + this.annotations = type.getFieldAnnotations(); + this.modifiers = type.getFieldModifiers(); } @Override @@ -392,8 +413,7 @@ public int hashCode() { @Override public String toString() { - - return "Key[" + "type=" + type + ", " + "path=" + path + ']'; + return "Property[type=" + type + ", path=" + path + ']'; } } } diff --git a/src/main/java/org/springframework/data/core/TypeDiscoverer.java b/src/main/java/org/springframework/data/core/TypeDiscoverer.java index d3ed09bd08..91bd80bec7 100644 --- a/src/main/java/org/springframework/data/core/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/core/TypeDiscoverer.java @@ -16,7 +16,9 @@ package org.springframework.data.core; import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -50,11 +52,13 @@ * @author Jürgen Diez * @author Alessandro Nistico * @author Johannes Englmeier + * @author Kamil Krzywański */ class TypeDiscoverer implements TypeInformation { - private static final ConcurrentLruCache> CACHE = new ConcurrentLruCache<>(64, + private static final ConcurrentLruCache> CACHE = new ConcurrentLruCache<>(64, TypeDiscoverer::new); + private static final Annotation[] NO_ANNOTATIONS = new Annotation[0]; private final ResolvableType resolvableType; private final Map>> fields = new ConcurrentHashMap<>(); @@ -74,11 +78,20 @@ class TypeDiscoverer implements TypeInformation { private final Lazy>> typeArguments; private final Lazy>> resolvedGenerics; + private final Annotation[] annotations; + private final int modifier; + + private TypeDiscoverer(TypeHolder type) { + this(type.type, type.field); + } protected TypeDiscoverer(ResolvableType type) { + this(type, null); + } - Assert.notNull(type, "Type must not be null"); + protected TypeDiscoverer(ResolvableType type, @Nullable Field field) { + Assert.notNull(type, "Type must not be null"); this.resolvableType = type; this.componentType = Lazy.of(this::doGetComponentType); this.valueType = Lazy.of(this::doGetMapValueType); @@ -87,13 +100,17 @@ protected TypeDiscoverer(ResolvableType type) { .map(TypeInformation::of) // use TypeInformation comparison to remove any attachments to variableResolver // holding the type source .collect(Collectors.toList())); + this.modifier = field == null ? 0 : field.getModifiers(); + this.annotations = field == null ? NO_ANNOTATIONS: field.getAnnotations(); } - static TypeDiscoverer ofCached(ResolvableType type) { + static TypeDiscoverer ofCached(ResolvableType type, @Nullable Field field) { Assert.notNull(type, "Type must not be null"); - return (TypeDiscoverer) CACHE.get(type); + var typeHolder = new TypeHolder(type, field); + + return (TypeDiscoverer) CACHE.get(typeHolder); } @Override @@ -209,7 +226,7 @@ public ResolvableType toResolvableType() { @Override public TypeInformation getRawTypeInformation() { - return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.toClass())); + return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.toClass()), null); } @Override @@ -271,7 +288,7 @@ public List> getParameterTypes(Method method) { var noGenericsResolvable = !Arrays.stream(resolvableSuperType.resolveGenerics()).filter(it -> it != null).findAny() .isPresent(); - return noGenericsResolvable ? new ClassTypeInformation<>(ResolvableType.forRawClass(superType)) + return noGenericsResolvable ? new ClassTypeInformation<>(ResolvableType.forRawClass(superType), null) : TypeInformation.of(resolvableSuperType); } @@ -322,6 +339,16 @@ public TypeInformation specialize(TypeInformation type) { return TypeInformation.of((Class) type.getType()); } + @Override + public Annotation[] getFieldAnnotations() { + return annotations; + } + + @Override + public int getFieldModifiers() { + return modifier; + } + @Override public boolean equals(@Nullable Object o) { @@ -385,9 +412,9 @@ private Optional> getPropertyInformation(String fieldname) { var rawType = getType(); var field = ReflectionUtils.findField(rawType, fieldname); - return field != null ? Optional.of(TypeInformation.of(ResolvableType.forField(field, resolvableType))) + return field != null ? Optional.of(TypeInformation.of(ResolvableType.forField(field, resolvableType), field)) : Optional.ofNullable(BeanUtils.getPropertyDescriptor(rawType, fieldname)) - .filter(it -> it.getName().equals(fieldname)).map(it -> from(it, rawType)).map(TypeInformation::of); + .filter(it -> it.getName().equals(fieldname)).map(it -> from(it, rawType)).map(type -> TypeInformation.of(type, field)); } private ResolvableType from(PropertyDescriptor descriptor, Class rawType) { @@ -410,4 +437,6 @@ private ResolvableType from(PropertyDescriptor descriptor, Class rawType) { private boolean isNullableWrapper() { return NullableWrapperConverters.supports(getType()); } + + private record TypeHolder(ResolvableType type,@Nullable Field field){} } diff --git a/src/main/java/org/springframework/data/core/TypeInformation.java b/src/main/java/org/springframework/data/core/TypeInformation.java index a192c9571f..fa3a57e724 100644 --- a/src/main/java/org/springframework/data/core/TypeInformation.java +++ b/src/main/java/org/springframework/data/core/TypeInformation.java @@ -15,7 +15,9 @@ */ package org.springframework.data.core; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.TypeVariable; import java.util.Collection; @@ -40,9 +42,10 @@ * @author Alessandro Nistico * @author Johannes Englmeier * @author Christoph Strobl + * @author Kamil Krzywański */ public interface TypeInformation { - + Annotation[] NO_ANNOTATIONS = new Annotation[0]; @SuppressWarnings("rawtypes") TypeInformation COLLECTION = new ClassTypeInformation<>(Collection.class); @SuppressWarnings("rawtypes") TypeInformation LIST = new ClassTypeInformation<>(List.class); @SuppressWarnings("rawtypes") TypeInformation SET = new ClassTypeInformation<>(Set.class); @@ -56,12 +59,16 @@ public interface TypeInformation { * @return will never be {@literal null}. * @since 3.0 */ - static TypeInformation of(ResolvableType type) { + static TypeInformation of(ResolvableType type, @Nullable Field field) { Assert.notNull(type, "Type must not be null"); return type.hasGenerics() || (type.isArray() && type.getComponentType().hasGenerics()) // - || (type.getType() instanceof TypeVariable) ? TypeDiscoverer.ofCached(type) : ClassTypeInformation.from(type); + || (type.getType() instanceof TypeVariable) ? TypeDiscoverer.ofCached(type, field) : ClassTypeInformation.from(type, field); + } + + static TypeInformation of(ResolvableType type) { + return of(type, null); } /** @@ -376,6 +383,31 @@ default boolean isSubTypeOf(Class type) { return !type.equals(getType()) && type.isAssignableFrom(getType()); } + /** + * Returns the annotations declared on the underlying field. + *

+ * This exposes annotations present directly on the represented {@link java.lang.reflect.Field} + * (field-level annotations), for example {@code @Id} or {@code @Column}. + * + * @return annotations declared on the underlying field; never {@literal null}. + */ + default Annotation[] getFieldAnnotations(){ + return NO_ANNOTATIONS; + } + + /** + * Returns the {@link java.lang.reflect.Field#getModifiers() modifiers} of the underlying field. + *

+ * This exposes the field modifier bitmask such as {@code public}, {@code protected}, {@code private}, + * {@code static}, {@code final}, {@code transient}, {@code volatile}, etc. + * + * @return modifier bitmask as defined by {@link java.lang.reflect.Modifier}. + */ + default int getFieldModifiers(){ + return 0; + } + + /** * Returns the {@link TypeDescriptor} equivalent of this {@link TypeInformation}. * diff --git a/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java b/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java index 499a7acbdd..5a6f3f6f86 100644 --- a/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java +++ b/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java @@ -36,6 +36,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch + * @author Kamil Krzywański * @since 1.13 */ record PropertyPathInformation(PropertyPath path) implements PathInformation { @@ -62,7 +63,7 @@ public static PropertyPathInformation of(String path, TypeInformation type) { return PropertyPathInformation.of(PropertyPath.from(path, type)); } - private static PropertyPathInformation of(PropertyPath path) { + public static PropertyPathInformation of(PropertyPath path) { return new PropertyPathInformation(path); } diff --git a/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java b/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java index 9cdf320b05..f58a5d25a6 100644 --- a/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java +++ b/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java @@ -15,6 +15,8 @@ */ package org.springframework.data.querydsl.binding; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; @@ -23,6 +25,9 @@ import java.util.Optional; import java.util.Set; +import com.querydsl.core.annotations.PropertyType; +import com.querydsl.core.annotations.QueryTransient; +import com.querydsl.core.annotations.QueryType; import org.jspecify.annotations.Nullable; import org.springframework.data.core.PropertyPath; @@ -61,6 +66,7 @@ * @author Oliver Gierke * @author Mark Paluch * @author Johannes Englmeier + * @author Kamil Krzywański * @since 1.11 * @see QuerydslBinderCustomizer */ @@ -267,8 +273,12 @@ PathInformation getPropertyPath(String path, TypeInformation type) { } try { - PathInformation propertyPath = PropertyPathInformation.of(path, type); - return isPathVisible(propertyPath) ? propertyPath : null; + var propertyPath = PropertyPath.from(path, type); + if (!isPathAccessible(propertyPath)) + return null; + + PathInformation propertyPathInformation = PropertyPathInformation.of(propertyPath); + return isPathVisible(propertyPathInformation) ? propertyPathInformation : null; } catch (PropertyReferenceException o_O) { return null; } @@ -322,6 +332,53 @@ private boolean isPathVisible(PathInformation path) { return true; } + private boolean isPathAccessible(PropertyPath propertyPath) { + return !isTransient(propertyPath) && !isStatic(propertyPath) && !isExcluded(propertyPath); + } + + private static boolean isTransient(PropertyPath propertyPath) { + return Modifier.isTransient(propertyPath.getPropertyModifiers()); + } + + private static boolean isStatic(PropertyPath propertyPath) { + return Modifier.isStatic(propertyPath.getPropertyModifiers()); + } + + private boolean isExcluded(PropertyPath propertyPath) { + return isPropertyExcluded(propertyPath.getPropertyAnnotations()); + } + + /** + * Determines whether a property should be excluded from Querydsl binding based on its annotations. + *

+ * A property is considered excluded if any of the following conditions match: + *

    + *
  • it is annotated with {@link QueryType} and its {@link QueryType#value()} equals {@link PropertyType#NONE},
  • + *
  • it is annotated with {@link QueryTransient},
  • + *
  • it is annotated with {@code jakarta.persistence.Transient} (detected by fully-qualified name to avoid + * a hard dependency on Jakarta Persistence).
  • + *
+ * + * @param annotations the annotations declared for the inspected element (must not be {@literal null}). + * @return {@literal true} if the property should be excluded; {@literal false} otherwise. + */ + private boolean isPropertyExcluded(Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof QueryType queryType) { + if (queryType.value() == PropertyType.NONE) { + return true; + } + } + if (annotation instanceof QueryTransient) { + return true; + } + if (annotation.annotationType().getName().equals("jakarta.persistence.Transient")) { + return true; + } + } + return false; + } + /** * Returns whether the given path is visible, which means either an alias and not explicitly denied, explicitly * allowed or not on the denylist if no allowlist configured. @@ -434,7 +491,7 @@ public class AliasingPathBinder

, T> extends PathBind /** * Creates a new {@link AliasingPathBinder} for the given {@link Path}. * - * @param paths must not be {@literal null}. + * @param path must not be {@literal null}. */ AliasingPathBinder(P path) { this(null, path); diff --git a/src/test/java/org/springframework/data/core/TypeDiscovererUnitTests.java b/src/test/java/org/springframework/data/core/TypeDiscovererUnitTests.java index 7ab688eeeb..a2383796ef 100755 --- a/src/test/java/org/springframework/data/core/TypeDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/core/TypeDiscovererUnitTests.java @@ -344,7 +344,7 @@ void differentEqualsAndHashCodeForTypeDiscovererAndClassTypeInformation() { ResolvableType type = ResolvableType.forClass(Object.class); var discoverer = new TypeDiscoverer<>(type); - var classTypeInformation = new ClassTypeInformation<>(type); + var classTypeInformation = new ClassTypeInformation<>(type, null); assertThat(discoverer).isNotEqualTo(classTypeInformation); assertThat(classTypeInformation).isNotEqualTo(type); diff --git a/src/test/java/org/springframework/data/querydsl/Example.java b/src/test/java/org/springframework/data/querydsl/Example.java new file mode 100644 index 0000000000..f136043b41 --- /dev/null +++ b/src/test/java/org/springframework/data/querydsl/Example.java @@ -0,0 +1,20 @@ +package org.springframework.data.querydsl; + +import com.querydsl.core.annotations.PropertyType; +import com.querydsl.core.annotations.QueryEntity; +import com.querydsl.core.annotations.QueryType; + +/** + * Test domain type for GH-2176. + * @author Kamil Krzywański + */ +@QueryEntity +public class Example { + + public String one; + + @QueryType(PropertyType.NONE) + public String two; + + public transient String three; +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java b/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java index fd238c2362..0b585bb4f8 100755 --- a/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java @@ -26,6 +26,8 @@ import org.springframework.data.core.TypeInformation; import org.springframework.data.querydsl.Address; +import org.springframework.data.querydsl.Example; +import org.springframework.data.querydsl.QExample; import org.springframework.data.querydsl.QSpecialUser; import org.springframework.data.querydsl.QUser; import org.springframework.data.querydsl.QUserWrapper; @@ -48,6 +50,7 @@ * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch + * @author Kamil Krzywański */ class QuerydslPredicateBuilderUnitTests { @@ -255,4 +258,32 @@ void dropsValuesContainingAnEmptyString() { assertThat(QuerydslPredicateBuilder.isEmpty(builder.getPredicate(USER_TYPE, values, DEFAULT_BINDINGS))).isTrue(); } + + @Test // GH-2176 + void shouldIgnorePropertiesPresentOnDomainTypeButNotOnQType() { + + var type = TypeInformation.of(Example.class); + +// // queryable + values.add("four", "foo"); + values.add("one", "foo"); + values.add("two", "bar"); + values.add("three", "baz"); + + var predicate = builder.getPredicate(type, values, DEFAULT_BINDINGS); + + assertThat(predicate).isEqualTo(QExample.example.one.eq("foo")); + } + + @Test // GH-2176 + void shouldReturnEmptyPredicateIfOnlyNonQueryablePropertiesAreProvided() { + + var type = TypeInformation.of(Example.class); + values.add("two", "bar"); + values.add("three", "baz"); + var predicate = builder.getPredicate(type, values, DEFAULT_BINDINGS); + + assertThat(QuerydslPredicateBuilder.isEmpty(predicate)).isTrue(); + } + }