Skip to content

Warn at build when a resolver can't be called on its JDK dynamic proxy - #853

Open
oryan-block wants to merge 9 commits into
masterfrom
bugfix/822
Open

oryan-block wants to merge 9 commits into
masterfrom
bugfix/822

Conversation

@oryan-block

@oryan-block oryan-block commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #822

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

Description

A resolver behind a JDK dynamic proxy, like Spring AOP without proxyTargetClass, builds fine but fails on every field at query time. With the repro from the issue, { test } returns data={test=null} with:

object of type jdk.proxy2.$Proxy2 is not an instance of ProxiedQuery

Spring4AopProxyHandler makes the scanner match fields against the target class, but the proxy only implements the target's interfaces. CGLIB proxies work because they subclass the target.

The earlier commits here called the methods through the proxy's interface methods, so interceptors would still run. I dropped that:

Now SchemaClassScanner#warnIfNotCallableOnResolver checks the resolver picked for each field. If the resolver isn't an instance of the class that declares the method or field, and the member isn't static, it logs a warning and suggests class-based proxies:

Field 'Query.test' resolves to scenarios.ProxiedQuery.test, but the resolver is a jdk.proxy2.$Proxy2, which isn't a scenarios.ProxiedQuery, so every query of the field will fail. This happens with JDK dynamic proxies, which only implement interfaces. Use class-based proxies instead, e.g. proxyTargetClass = true in Spring.

It covers public fields too, so PropertyFieldResolver.property is now a val. For a field it also suggests adding a getter, since class-based proxies don't pass field reads to the target. A CGLIB proxy returned 0 for @JvmField val count = 1. Only the resolver that was picked gets checked, so a supertype resolver that a more specific resolver shadows doesn't warn. CGLIB, Guice and similar proxies subclass the target, so they don't warn either. Nothing else changes, and the field still fails at query time like before. Throwing instead has to wait for 15.0.0: #868.

No unit test checks the warning. The tests run on the module path and graphql.java.tools can't read logback, same as the warnings from #855. SchemaParserTest."parser handles spring AOP proxied resolvers by default" is back unchanged and still builds. mvn clean verify passes (317 tests), also with -Dgraphql-java.version=25.1.

I checked it through the installed jar in a separate consumer project with type Query { test: String, count: Int }, a resolver with fun test() = "ok" and @JvmField val count = 1, and { test count }:

  • JDK proxy: two warnings (Query.test, Query.count), and the query fails like before. On master it fails the same way, with no warning.
  • CGLIB proxy: no warning, test returns "ok" and a getter (open val total = 2) returns 2, but the field count returns 0
  • no proxy: no warning, {"test": "ok", "count": 1}

Release note: Resolvers behind a JDK dynamic proxy now log a warning at build when a field's method or field can't be called on the proxy. Use class-based proxies (proxyTargetClass = true).

oryan-block and others added 7 commits October 12, 2026 15:30
Resolvers behind a JDK dynamic proxy, e.g. Spring AOP without proxyTargetClass, are scanned as their target class, but the proxy only implements the target's interfaces, so every field failed at runtime with "object of type jdk.proxy2.$ProxyN is not an instance of ...". Each resolver method is now called through the interface method it implements, so the proxy's interceptors still run, and build() fails with a clear error if no interface declares it. Static methods are still called directly.

Only the first matching resolver of a non-root field is checked, so a supertype resolver's method that a more specific resolver takes precedence over doesn't fail the build.

Fixes #822

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A resolver method inherited from a generic superclass, e.g. getValue(T) of FooResolver<T>, is erased to getValue(Object), so it didn't match the interface method it implements and build() threw the new FieldResolverError for a supported resolver pattern. Its parameter types are now resolved against the target class too, like the interface's.

Also covers the guards that skip interfaces the proxy doesn't implement and static interface methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lver method

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the proxy's interfaces

Co-Authored-By: Claude <noreply@anthropic.com>
@oryan-block oryan-block changed the title Call JDK proxied resolver methods through the proxy's interfaces Warn at build when a resolver can't be called on its JDK dynamic proxy Oct 18, 2026
Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolvers behind a JDK dynamic proxy build fine but fail on every field at runtime

1 participant