leangen / graphql-spqr

Build a GraphQL service in seconds
Apache License 2.0
1.1k stars 182 forks source link

Error: "object is not an instance of declaring class" when extending Connection or Page #259

Closed darrikonn closed 5 years ago

darrikonn commented 5 years ago

Using the Java Play framework (sbt).

Keep getting the following error:

Exception while fetching data (/foo/pageInfo) : object is not an instance of declaring class
...
Exception while fetching data (/foo/edges) : object is not an instance of declaring class

when extending Page or Connection with Graphl-SPQR. This doesn't usually show up until after I've saved and hot reloading kicks in, so the first queries, that I make in the GraphiQL editor, are successful. This only happens after I save, that kicks in a hot reload (even if there are no changes). This does not happen if I use the Page directly (without extending it).

Has anyone encountered the same problem?

Code

I'm generating the schema with

GraphQLSchema schema = new GraphQLSchemaGenerator()
            .withResolverBuilders(
                    //Resolve by annotations
                    new AnnotatedResolverBuilder(),
                    //Resolve public methods inside root package
                    new PublicResolverBuilder("gql"))
            .withOperationsFromSingletons(Query.resolvers())
            .withValueMapperFactory(new JacksonValueMapperFactory())
            .generate();

and tried the example from RelayTests, both public ExtendedConnection<ExtendedEdge<Book>> getExtended and public ExtendedPage<Book> getExtended

Stacktrace

graphql.execution.SimpleDataFetcherExceptionHandler - Exception while fetching data (/foo/pageInfo) : object is not an instance of declaring class
java.lang.IllegalArgumentException: object is not an instance of declaring class
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at graphql.schema.PropertyDataFetcher.getPropertyViaGetterUsingPrefix(PropertyDataFetcher.java:178)
    at graphql.schema.PropertyDataFetcher.getPropertyViaGetterMethod(PropertyDataFetcher.java:167)
    at graphql.schema.PropertyDataFetcher.getPropertyViaGetter(PropertyDataFetcher.java:144)
    at graphql.schema.PropertyDataFetcher.get(PropertyDataFetcher.java:139)
kaqqao commented 5 years ago

So everything works until a hot reload occurs? And then only extended pages break? It almost 100% has to do with classloaders chaging and an instance is then seen as being of a different class. Classloader issue are a nightmare to troubleshoot.

The puzzing part is that it happens only for extended pages... No clue what makes them special.

Any chance you create a runnable example that I can poke with a debugger?

darrikonn commented 5 years ago

Yeah exactly! Everything that's extended breaks, i.e. extended pages and extended connections.

For the runnable example; running the following code from the relay tests breaks after hot reloading kicks in (https://github.com/leangen/graphql-spqr/blob/master/src/test/java/io/leangen/graphql/RelayTest.java)

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import graphql.relay.ConnectionCursor;
import graphql.relay.DefaultEdge;
import graphql.relay.Edge;
import graphql.relay.PageInfo;
import graphql.relay.Relay;
import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLId;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.execution.relay.Connection;
import io.leangen.graphql.execution.relay.Page;
import io.leangen.graphql.execution.relay.generic.PageFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public class TestQuery {

    public static class Book {
        private String title;
        private String isbn;

        @JsonCreator
        Book(@JsonProperty("title") String title, @JsonProperty("id") String isbn) {
            this.title = title;
            this.isbn = isbn;
        }

        public String getTitle() {
            return title;
        }

        @GraphQLQuery(name = "id")
        public @GraphQLId(relayId = true) String getIsbn() {
            return isbn;
        }
    }

    public static class ExtendedEdge<T> extends DefaultEdge<T> {

        private final COLOR color;

        ExtendedEdge(T node, ConnectionCursor cursor, COLOR color) {
            super(node, cursor);
            this.color = color;
        }

        public COLOR getColor() {
            return color;
        }

        public enum COLOR {
            BLACK, WHITE
        }
    }

    public static class ExtendedPage<N> implements Page<N> {

        private final List<Edge<N>> edges;
        private final PageInfo pageInfo;
        private final long totalCount;

        ExtendedPage(List<Edge<N>> edges, PageInfo pageInfo, long totalCount) {
            this.edges = edges;
            this.pageInfo = pageInfo;
            this.totalCount = totalCount;
        }

        @Override
        public List<Edge<N>> getEdges() {
            return edges;
        }

        @Override
        public PageInfo getPageInfo() {
            return pageInfo;
        }

        public long getTotalCount() {
            return totalCount;
        }
    }

    public static class ExtendedConnection<E extends Edge> implements Connection<E> {

        private final List<E> edges;
        private final PageInfo pageInfo;
        private final long totalCount;

        ExtendedConnection(List<E> edges, PageInfo pageInfo, long count) {
            this.edges = edges;
            this.pageInfo = pageInfo;
            this.totalCount = count;
        }

        @Override
        public List<E> getEdges() {
            return edges;
        }

        @Override
        public PageInfo getPageInfo() {
            return pageInfo;
        }

        public long getTotalCount() {
            return totalCount;
        }
    }

    @GraphQLQuery(name = "test1")
    public ExtendedPage<Book> test1(@GraphQLArgument(name = "first") int first, @GraphQLArgument(name = "after") String after) {
        List<Book> books = new ArrayList<>();
        books.add(new Book("Tesseract", "x123"));
        long count = 100L;
        long offset = Long.parseLong(after);
        return PageFactory.createOffsetBasedPage(books, count, offset, (edges, info) -> new ExtendedPage<>(edges, info, count));
    }

    @GraphQLQuery(name = "test2")
    public ExtendedConnection<ExtendedEdge<Book>> test2(@GraphQLArgument(name = "first") int first, @GraphQLArgument(name = "after") String after) {
        List<Book> books = new ArrayList<>();
        books.add(new Book("Tesseract", "x123"));
        long count = 100L;
        long offset = Long.parseLong(after);
        Iterator<ExtendedEdge.COLOR> colors = Arrays.asList(ExtendedEdge.COLOR.WHITE, ExtendedEdge.COLOR.BLACK).iterator();
        return PageFactory.createOffsetBasedConnection(books, count, offset,
                (node, cursor) -> new ExtendedEdge<>(node, cursor, colors.next()), (edges, info) -> new ExtendedConnection<>(edges, info, count));
    }
}

And then I build the schema with

GraphQLSchema schema = new GraphQLSchemaGenerator()
            .withResolverBuilders(
                    new AnnotatedResolverBuilder(),
                    new PublicResolverBuilder("gql"))
            .withOperationsFromSingletons([new TestQuery()])
            .withValueMapperFactory(new JacksonValueMapperFactory())
            .generate();
GraphQL graphQL = schema.build();

// execute the input
String query = json.get("query").asText();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).build();
ExecutionResult result = graphQL.execute(executionInput);

return ok(Json.toJson(result.toSpecification());
darrikonn commented 5 years ago

Any news on this?

darrikonn commented 5 years ago

My solution was to create my own relay page with the fields that I needed (same for edge/pageinfo if that were the case). I extended the PageFactory to use my custom Page instead of the extended/default one.

I'm gonna close this for now, since I'm fine with my solution. If anyone else encounters this problem, feel free to reopen 👍