GraphQL Java 以及 Spring Boot: Schema


GraphQL Java 以及 Spring Boot: Schema

  • OpenJDK 11

  • Gradle 6.8

依赖

implementation 'com.graphql-java:graphql-java:16.2' // 新
implementation 'com.graphql-java:graphql-java-spring-boot-starter-webmvc:2.0' // 新
implementation 'com.google.guava:guava:30.1.1-jre' // 新(可选)

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

graphql dsl

创建 src/main/resources/starWarsSchema.graphqls :

schema {
    query: Query
}

type Query {
    hero(episode: Episode) : Character
    human(id: String) : Human
    droid(id: ID!): Droid
}

enum Episode {
    NEWHOPE
    EMPIRE
    JEDI
}

interface Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
}

type Human implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    homePlanet: String
}

type Droid implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    primaryFunction: String
}
  • 其中包括了 enuminterface

与 graphQL dsl 对应的 Java 类型

enum Episode

public enum Episode {
    NEWHOPE,
    EMPIRE,
    JEDI,
}

interface Character

public interface Character {
    String getId();
    String getName();
    List<String> getFriends();
    List<Episode> getAppearsIn();
}

type Human

public class Human implements Character{
    private String id;
    private String name;
    private List<String> friends;
    private List<Episode> appearsIn;
    private String homePlanet;

    // 省略 construct 和 getter
}

type Droid

public class Droid implements Character {
    private String id;
    private String name;
    private List<String> friends;
    private List<Episode> appearsIn;
    private String primaryFunction;

    // 省略 construct 和 getter
}

graphql dsl 与 java 类型建立联系

GraphQLProvider

@Component
public class GraphQLProvider {
    private GraphQL graphQL;
    private StarWarsWiring starWarsWiring;

    @Autowired
    public GraphQLProvider(StarWarsWiring starWarsWiring) {
        this.starWarsWiring = starWarsWiring;
    }

    @PostConstruct
    public void init() throws IOException {
        URL url = Resources.getResource("starWarsSchemaAnnotated.graphqls");
        String sdl = Resources.toString(url, Charsets.UTF_8);
        GraphQLSchema graphQLSchema = buildSchema(sdl);

        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    private GraphQLSchema buildSchema(String sdl) {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
        RuntimeWiring runtimeWiring = buildWiring();
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        // TypeRegistry 与 RuntimeWiring 共同构建 GraphQLSchema
        return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
    }

    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
            .type(newTypeWiring("Query")
                  .dataFetcher("hero", starWarsWiring.heroDataFetcher)
                  .dataFetcher("human", starWarsWiring.humanDataFetcher)
                  .dataFetcher("droid", starWarsWiring.droidDataFetcher))
            .type(newTypeWiring("Human")
                  // 默认使用 PropertyDataFetcher,如 id, name。 JavaBean 会调用 getter 方法
                  // graphql-java 提供一些 Scalar,如 String, Int, Boolean 等,所以这些基本类型自动处理
                  .dataFetcher("friends", starWarsWiring.friendsDataFetcher))
            .type(newTypeWiring("Droid")
                  .dataFetcher("friends", starWarsWiring.friendsDataFetcher))
            // interface 类型,需要 TypeResolver 决定值的真实类型
            .type(newTypeWiring("Character")
                  .typeResolver(starWarsWiring.characterTypeResolver))
            // enum 类型
            .type(newTypeWiring("Episode")
                  .enumValues(starWarsWiring.episodeResolver))
            .build();
    }

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }
}
  • GraphQL interface 需要定义 TypeResolver ,用于运行时判断值的具体类型

  • GraphQL enum 需要 EnumValuesProvider

data fetcher

@Component
public class StarWarsWiring {
    DataFetcher<Human> humanDataFetcher = environment -> {
        // 获取用户参数
        String id = environment.getArgument("id");
        return StarWarsData.humanData.get(id);
    };

    DataFetcher<Droid> droidDataFetcher = environment -> {
        String id = environment.getArgument("id");
        return StarWarsData.droidData.get(id);
    };

    DataFetcher<Character> heroDataFetcher = environment -> {
        return StarWarsData.getCharacterData("1002");
    };

    DataFetcher<List<Character>> friendsDataFetcher = environment -> {
        // 获取父节点的值
        Character character = environment.getSource();
        List<String> friendIds = character.getFriends();
        return friendIds.stream()
        .map(StarWarsData::getCharacterData) // N+1
        .collect(Collectors.toList());
    };

    // enum
    EnumValuesProvider episodeResolver = Episode::valueOf;

    // interface
    TypeResolver characterTypeResolver = env -> {
        // 需要解析 GraphQL 类型的对象
        Character character = env.getObject();
        if (character instanceof Human) {
            return (GraphQLObjectType) env.getSchema().getType("Human");
        } else {
            return (GraphQLObjectType) env.getSchema().getType("Droid");
        }
    };
}
  • 默认使用 PropertyDataFetcher ,基本类型的 data fetcher

  • friendsDataFetcher 存在 N+1 问题

data

public class StarWarsData {
    static Human luke = new Human(
            "1000",
            "Luke Skywalker",
            asList("1001", "1002", "2000", "2001"),
            asList(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI),
            "Tatooine"
    );

    static Human vader = new Human(
            "1001",
            "Darth Vader",
            asList("1000"),
            asList(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI),
            "Tatooine"
    );

    static Human han = new Human(
            "1002",
            "Han Solo",
            asList("1000", "2001"),
            asList(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI),
            null);


    public static Map<String, Human> humanData = new LinkedHashMap<>();

    static {
        humanData.put("1000", luke);
        humanData.put("1001", vader);
        humanData.put("1002", han);
    }

    static Droid threepio = new Droid(
            "2000",
            "C-3PO",
            asList("1000", "1002", "2001"),
            asList(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI),
            "Protocol"
    );

    static Droid artoo = new Droid(
            "2001",
            "R2-D2",
            asList("1000", "1002"),
            asList(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI),
            "Astromech"
    );

    public static Map<String, Droid> droidData = new LinkedHashMap<>();

    static {
        droidData.put("2000", threepio);
        droidData.put("2001", artoo);
    }

    public static Character getCharacterData(String id) {
        if (humanData.get(id) != null) {
            return humanData.get(id);
        } else if (droidData.get(id) != null) {
            return droidData.get(id);
        }
        return null;
    }
}