Skip to content

Fix #3236: AsyncPredicate and GatewayPredicate do not override equals…#4122

Open
stbattula-research wants to merge 1 commit intospring-cloud:mainfrom
stbattula-research:fix/async-predicate-equals-hashcode
Open

Fix #3236: AsyncPredicate and GatewayPredicate do not override equals…#4122
stbattula-research wants to merge 1 commit intospring-cloud:mainfrom
stbattula-research:fix/async-predicate-equals-hashcode

Conversation

@stbattula-research
Copy link
Copy Markdown

Fix: AsyncPredicate and GatewayPredicate do not override equals/hashCode

Fixes #3236 — Identical Routes are not considered equal: AsyncPredicate does not override equals()


Problem

Spring Cloud Gateway builds route predicates as trees of AsyncPredicate and GatewayPredicate objects. When two routes are configured identically—same predicates, same filters—they should be considered equal so the gateway can deduplicate, cache, or diff route definitions efficiently.

However, none of the inner classes in AsyncPredicate or GatewayPredicate overrode equals() or hashCode(). They all fell back to java.lang.Object.equals(), which uses reference/identity comparison. Two separately constructed but logically identical predicates were therefore never equal:

AsyncPredicate<ServerWebExchange> p1 = AsyncPredicate.from(pathPredicate("/api"));
AsyncPredicate<ServerWebExchange> p2 = AsyncPredicate.from(pathPredicate("/api"));

p1.equals(p2); // false — should be true

This meant that two Route objects built from identical configuration could never satisfy a value-equality check even when every field was the same.


Root Cause

The following eight inner classes were missing equals() and hashCode():

AsyncPredicate

  • DefaultAsyncPredicate
  • NegateAsyncPredicate
  • AndAsyncPredicate
  • OrAsyncPredicate

GatewayPredicate

  • GatewayPredicateWrapper
  • NegateGatewayPredicate
  • AndGatewayPredicate
  • OrGatewayPredicate

Fix

Added equals() and hashCode() to all eight inner classes following the standard Java contract:

  • Wrapper/Delegate classes (DefaultAsyncPredicate, GatewayPredicateWrapper): equality delegates to the wrapped predicate.
  • Negate classes (NegateAsyncPredicate, NegateGatewayPredicate): equality compares the inner predicate.
  • Composite classes (AndAsyncPredicate, OrAsyncPredicate, AndGatewayPredicate, OrGatewayPredicate): equality compares both left and right components.

All implementations use java.util.Objects.equals() / Objects.hash() for null-safe delegation.

Example — DefaultAsyncPredicate (before → after)

Before: inherits Object.equals() — identity comparison only.

After:

@Override
public boolean equals(Object o) {
    if (this == o) { return true; }
    if (!(o instanceof DefaultAsyncPredicate)) { return false; }
    DefaultAsyncPredicate<?> that = (DefaultAsyncPredicate<?>) o;
    return Objects.equals(this.delegate, that.delegate);
}

@Override
public int hashCode() {
return Objects.hashCode(this.delegate);
}

The same pattern is applied consistently to all eight classes.


Files Changed

File Change
spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java Added equals() + hashCode() to DefaultAsyncPredicate, NegateAsyncPredicate, AndAsyncPredicate, OrAsyncPredicate
spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/GatewayPredicate.java Added equals() + hashCode() to GatewayPredicateWrapper, NegateGatewayPredicate, AndGatewayPredicate, OrGatewayPredicate
spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateEqualsTests.java New test class — 15 test cases covering all equality scenarios

Tests Added

AsyncPredicateEqualsTests covers:

  • DefaultAsyncPredicate: same instance, same delegate, different delegate, null
  • NegateAsyncPredicate: same inner predicate, different inner predicate
  • AndAsyncPredicate: same components, different left, different right
  • OrAsyncPredicate: same components, different components
  • GatewayPredicate inner classes: NegateGatewayPredicate, AndGatewayPredicate, OrGatewayPredicate equality
  • Composite predicates built via .and(), .or(), .negate() builder methods

Note on End-to-End Route Equality

This fix is a necessary prerequisite for full route equality. For two routes built from separate factory invocations to be equal end-to-end, individual predicate factory implementations (e.g. PathRoutePredicateFactory) must also implement equals() and hashCode() based on their configuration objects. That is a follow-on concern tracked separately; this PR addresses the predicate composition layer which was the immediate bug reported.


Checklist

  • equals() / hashCode() contract is consistent (a.equals(b)a.hashCode() == b.hashCode())
  • All eight affected inner classes updated
  • Unit tests added and passing
  • No changes to runtime predicate evaluation logic
  • Backward-compatible: existing code that does not rely on predicate equality is unaffected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identical Routes are not considered equal: AsyncPredicate does not override equals()

2 participants