Repository navigation
Call JDK proxied resolver methods through the proxy's interfaces - #853
Open
oryan-block wants to merge 2 commits into
Open
oryan-block wants to merge 2 commits into
oryan-block wants to merge 2 commits into
Conversation
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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #822
Checklist
Description
A resolver behind a JDK dynamic proxy, like Spring AOP without
proxyTargetClass, builds fine but fails on every field at query time. The repro from the issue is aProxiedQuery : GraphQLQueryResolverwithfun test() = "ok", wrapped withProxyFactory(ProxiedQuery()).proxy.{ test }returnsdata={test=null}with this error:The same resolver behind a CGLIB proxy works.
Spring4AopProxyHandler#getTargetClassreturns the target class, soFieldResolverScannerfinds the resolver methods onProxiedQuery.MethodFieldResolverthen invokes that class'sMethodon the proxy, which only implements the target's interfaces. Calling the method on the unwrapped target instead would skip the proxy's interceptors (@PreAuthorizeand friends), so I didn't do that.Now
FieldResolverScanner#findInvocableMethodpicks theMethodto invoke. It returns the scanned method unchanged when there's no source, when the method is static, or when the source is an instance of the method's declaring class. That covers plain resolvers, data classes, and CGLIB, Guice, Javassist and Weld proxies. Otherwise it looks at the target class's interfaces that the proxy actually implements, and takes the first non-static interface method that the scanned method implements. The call goes through the proxy, so interceptors still run. The interface method is made accessible, since non-public interfaces need it.MethodFieldResolvergets the result as a newinvocableMethodconstructor parameter and passes it to both data fetchers. Everything type-related still uses the scanned method on the target class.Matching is done by
Method.implements: same name, same parameter count, and parameter types that agree once both sides are resolved against the target class withTypeUtils.getRawType(falling back to the erasure). That's needed for generic interfaces, whereEchoApi<String>.echo(T)is implemented byecho(String), and for methods inherited from a generic superclass, whereFooResolver<T>.getValue(T)is erased togetValue(Object). When the interface parameter is a type variable, primitives are boxed first, because Kotlin compilesApi<Int>.incrementtoincrement(int).If no interface method matches,
build()throws aFieldResolverErrorthat names the method, the field, the schema location and the proxy class, and suggests declaring the method on an interface or using class-based proxies. That's the issue repro:(Package names trimmed, and the proxy number varies.) The issue also suggested calling through
Proxy.getInvocationHandler(proxy). Going through the interface method gets the same result with plain reflection and doesn't depend on what the handler is.findFieldResolvernow evaluates the searches lazily for non-root fields and stops at the first match. Without that, a JDK-proxied supertype resolver with a class-only method would fail the build even when a more specific resolver takes precedence for that field. Root fields still evaluate every search for the duplicate check. The source location formatting moved into a sharedgetSourceLocation, and the missing-resolver message is unchanged.The new
ProxiedResolverTestbuilds real proxies with Spring'sProxyFactoryand a recordingMethodInterceptor, and runs queries. It covers root methods on the light and regular data fetcher paths, a suspend function, an interceptor that rejects the call, field resolvers on theNormalResolverInfoandMultiResolverInfopaths, a more specific resolver shadowing a proxied supertype resolver, generic interfaces including KotlinInt, field resolver methods inherited from a generic superclass (through a generic and a non-generic interface), a non-public interface, static methods and a CGLIB proxy. Three tests check the build error: a class-only method (exact message), a method only on an interface the proxy doesn't expose (setInterfaces), and a method only matched by a static interface method. On master the runtime cases fail with the "is not an instance of" error, the rejecting interceptor never runs, and the class-only case builds without throwing. The generic superclass cases fail on the first commit with the newFieldResolverError. The static method, shadowed supertype and CGLIB tests pass on master too and are there as guards. I also checked that putting back the eager search, or dropping thetrySetAccessiblecall, theisInstancefilter or the static guard on interface methods, each makes its test fail. I removed the build-onlySchemaParserTesttest "parser handles spring AOP proxied resolvers by default" and its fixture, since the new tests replace it.clean verifypasses.Left out:
class Q<T : Number> : Api<T>, scanned as rawQ) can't be resolved byTypeUtils, so it still gets the build error.abstract class Base<T> { fun echo(value: T) }) fail at runtime with aClassCastExceptionfromTypeVariableImpltoClasseven without a proxy. That's an existing argument conversion limitation, not a proxy one.foo(int)/foo(Integer)overload pair. Which one the scanner picks depends ongetDeclaredMethodsorder, so the test wouldn't be deterministic.#849 touches
FieldResolverScannerandMethodFieldResolvertoo, butgit merge-treeshows no conflicts with it or with the other open PRs.Behaviour change: only resolvers behind JDK dynamic proxies with a
ProxyHandlerthat reports a target class the proxy isn't an instance of (in practiceSpring4AopProxyHandler) are affected. Fields whose method is declared on an interface the proxy implements now work and run the interceptors. Non-static methods that aren't declared on any interface the proxy implements now makebuild()throw aFieldResolverErrorinstead of failing on every call. So an app with such a field that's never queried will now fail at startup, which probably needs a release note and a minor version. That error is thrown even withallowUnimplementedResolversormissingResolverDataFetcherset, because the resolver exists but can't be called. Static methods on JDK proxies are still called directly and skip interceptors, as before. On JDK-proxied resolvers, "can't wait on" errors now name the interface method, e.g.QueryApi.test. For non-root fields, data class methods shadowed by a resolver are no longer made accessible. The chosen resolver is the same. Nothing changes for plain resolvers, data classes, class-based proxies or JDK proxies without aProxyHandler.MethodFieldResolver,FieldResolverScannerandFieldResolverErrorare internal, so there's no public API change.🤖 Generated with Claude Code