graphql-java: 使用文档首页

欢迎使用 graphql-java

这是一个用Java实现的GraphQL。基于 GraphQL规范JavaScript参考实现.

Status: Version 6.0 is released.

强烈推荐你关注一下 基于 graphql-java 开发的 相关项目 .

贡献或编码的行为规范

请注意,这个项目的发行条款,包括了 `贡献或编码的行为规范 <https://github.com/graphql-java/graphql-java/blob/master/CODE_OF_CONDUCT.md>`_为本项目作贡献(提交代码和问题等【译注原文:commenting or opening PR/Issues etc】)的同时,你即同意遵守这个行为规范,所以请你花时间认真阅读它.

问题与讨论

如果你有问题或想讨论与本项目相关的事:

发行授权

graphql-java is licensed under the MIT License.

文档

 

英文原文:https://github.com/graphql-java/graphql-java

翻译:GraphQL中文网 http://blog.mygraphql.com

graphql-java:贡献

贡献

所有的贡献都是欢迎的。谢谢!

为了让大家快乐贡献,以下是一些提示:

  • Respect the [Code of Conduct](#code-of-conduct)
  • Before opening an Issue to report a bug, please try the latest development version. It can happen that the problem is already solved.
  • Please use Markdown to format your comments properly. If you are not familiar with that: Getting started with writing and formatting on GitHub
  • For Pull Requests:
    * Here are some general tips

    • Please be a as focused and clear as possible and don’t mix concerns. This includes refactorings mixed with bug-fixes/features, see [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html)
    • It would be good to add a automatic test. All tests are written in Spock.

本地构建和测试

Just clone the repo and type

./gradlew build

In build/libs you will find the jar file.

Running the tests:

./gradlew test

Installing in the local Maven repository:

./gradlew install

graphql-java:如何落地应用(Application concerns)

如何落地应用(Application concerns)

graphql-java 引擎主要的关注点是按 GraphQL 规范来执行查询。

它本身不关注应用的其它方面,如:

  • 数据库访问
  • 缓存数据
  • 数据权限控制
  • 数据分页
  • HTTP 转换
  • JSON 编码
  • 依赖注入的编程方法

你需要在自己的业务逻辑层中实现这些。

下面是一些相关方案的介绍:

上下文对象(Context Objects)

为更方便的业务调用,你可以在查询执行中加入Context Object。

例如,你的应用的边界模块可能会做用户识别,然后 GraphQL 查询执行时,你可以想做数据权限控制。

下面例子演示怎么向你的查询传递信息:

//
// this could be code that authorises the user in some way and sets up enough context
// that can be used later inside data fetchers allowing them
// to do their job
//
UserContext contextForUser = YourGraphqlContextBuilder.getContextForUser(getCurrentUser());

ExecutionInput executionInput = ExecutionInput.newExecutionInput()
        .context(contextForUser)
        .build();

ExecutionResult executionResult = graphQL.execute(executionInput);

// ...
//
// later you are able to use this context object when a data fetcher is invoked
//

DataFetcher dataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        UserContext userCtx = environment.getContext();
        Long businessObjId = environment.getArgument("businessObjId");

        return invokeBusinessLayerMethod(userCtx, businessObjId);
    }
};

graphql-java:关于 Relay 支持

关于 Relay 支持

包含了一些基础的 Relay 特性的支持。

注意: 这里的 Relay 指 “Relay Classic”, 暂不支持 “Relay Modern”.

完整的例子,见 https://github.com/graphql-java/todomvc-relay-java

Relay 以 JSON 格式,向服务器发送 queryvariables 两个字段。query 字段是一个 JSON 格式的字符串, variables 字段是一个 变量定义( variable definitions) 的 map。relay 兼容的服务器,需要解释 JSON 然后传 query 字符串到本框架。包括 variables map 作为 execute 方法的第3个参数。如下:

@RequestMapping(value = "/graphql", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Object executeOperation(@RequestBody Map body) {
    String query = (String) body.get("query");
    Map<String, Object> variables = (Map<String, Object>) body.get("variables");
    if (variables == null) {
        variables = new LinkedHashMap<>();
    }
    ExecutionResult executionResult = graphql.execute(query, (Object) null, variables);
    Map<String, Object> result = new LinkedHashMap<>();
    if (executionResult.getErrors().size() > 0) {
        result.put("errors", executionResult.getErrors());
        log.error("Errors: {}", executionResult.getErrors());
    }
    result.put("data", executionResult.getData());
    return result;
}

Apollo 支持

没有为对接 Apollo 客户端做什么。因它兼容所有schema。

上面的 Controller 例子一样可以与 Apollo 客户端交互。

graphql-java:拦截器Instrumentation

拦截器Instrumentation

通过实现 graphql.execution.instrumentation.Instrumentation 接口,你可以在执行查询的过程中注入定制代码。并可以修改运行期的行为。

它的主要用途是性能监控和定制日志,但也可以完成其它任务。

当创建 `Graphql 对象时,可以绑定相关的 Instrumentation

GraphQL.newGraphQL(schema)
        .instrumentation(new TracingInstrumentation())
        .build();

定制拦截器(Custom Instrumentation)

要实现 Instrumentation ,需要实现多个 “begin” 开头的方法。这方法会在查询执行过程中,每一步骤开始前被调用。

所有回调方法,都应该返回 graphql.execution.instrumentation.InstrumentationContext 对象,这个对象会在本步骤完成时被回调用,回调用时会告知数据的获取结果,如果出错,可以获取 Throwable 对象。.

下面是一个定制的 Instrumentation 。作用是测量执行时间。

class CustomInstrumentationState implements InstrumentationState {
    private Map<String, Object> anyStateYouLike = new HashMap<>();

    void recordTiming(String key, long time) {
        anyStateYouLike.put(key, time);
    }
}

class CustomInstrumentation implements Instrumentation {
    @Override
    public InstrumentationState createState() {
        //
        // instrumentation state is passed during each invocation of an Instrumentation method
        // and allows you to put stateful data away and reference it during the query execution
        //
        return new CustomInstrumentationState();
    }

    @Override
    public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
        long startNanos = System.nanoTime();
        return (result, throwable) -> {

            CustomInstrumentationState state = parameters.getInstrumentationState();
            state.recordTiming(parameters.getQuery(), System.nanoTime() - startNanos);
        };
    }

    @Override
    public InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters) {
        //
        // You MUST return a non null object but it does not have to do anything and hence
        // you use this class to return a no-op object
        //
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters) {
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public InstrumentationContext<ExecutionResult> beginDataFetch(InstrumentationDataFetchParameters parameters) {
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public InstrumentationContext<CompletableFuture<ExecutionResult>> beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) {
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public InstrumentationContext<ExecutionResult> beginField(InstrumentationFieldParameters parameters) {
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
        return new NoOpInstrumentation.NoOpInstrumentationContext<>();
    }

    @Override
    public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) {
        //
        // this allows you to intercept the data fetcher used ot fetch a field and provide another one, perhaps
        // that enforces certain behaviours or has certain side effects on the data
        //
        return dataFetcher;
    }

    @Override
    public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) {
        //
        // this allows you to instrument the execution result some how.  For example the Tracing support uses this to put
        // the `extensions` map of data in place
        //
        return CompletableFuture.completedFuture(executionResult);
    }
}

链式拦截(Chaining Instrumentation)

你可以用 graphql.execution.instrumentation.ChainedInstrumentation 把多个 Instrumentation 连接起来。这些 Instrumentation 对象会按顺序被调用。

List<Instrumentation> chainedList = new ArrayList<>();
chainedList.add(new FooInstrumentation());
chainedList.add(new BarInstrumentation());
ChainedInstrumentation chainedInstrumentation = new ChainedInstrumentation(chainedList);

GraphQL.newGraphQL(schema)
        .instrumentation(chainedInstrumentation)
        .build();

Apollo跟踪与拦截( Tracing Instrumentation)

graphql.execution.instrumentation.tracing.TracingInstrumentation 是一个可以收集跟踪信息的拦截器。

它按照 Apollo 跟踪格式 https://github.com/apollographql/apollo-tracing 来收集跟踪信息。

详细的跟踪信息( tracing map)会放在查询结果的 extensions(扩展) 部分。

如以下的查询:

query {
  hero {
    name
    friends {
      name
    }
  }
}

会返回如下的结果:

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  },
  "extensions": {
    "tracing": {
      "version": 1,
      "startTime": "2017-08-14T23:13:39.362Z",
      "endTime": "2017-08-14T23:13:39.497Z",
      "duration": 135589186,
      "execution": {
        "resolvers": [
          {
            "path": [
              "hero"
            ],
            "parentType": "Query",
            "returnType": "Character",
            "fieldName": "hero",
            "startOffset": 105697585,
            "duration": 79111240
          },
          {
            "path": [
              "hero",
              "name"
            ],
            "parentType": "Droid",
            "returnType": "String",
            "fieldName": "name",
            "startOffset": 125010028,
            "duration": 20213
          },
          {
            "path": [
              "hero",
              "friends"
            ],
            "parentType": "Droid",
            "returnType": "[Character]",
            "fieldName": "friends",
            "startOffset": 133352819,
            "duration": 7927560
          },
          {
            "path": [
              "hero",
              "friends",
              0,
              "name"
            ],
            "parentType": "Human",
            "returnType": "String",
            "fieldName": "name",
            "startOffset": 134105887,
            "duration": 6783
          },
          {
            "path": [
              "hero",
              "friends",
              1,
              "name"
            ],
            "parentType": "Human",
            "returnType": "String",
            "fieldName": "name",
            "startOffset": 134725922,
            "duration": 7016
          },
          {
            "path": [
              "hero",
              "friends",
              2,
              "name"
            ],
            "parentType": "Human",
            "returnType": "String",
            "fieldName": "name",
            "startOffset": 134875089,
            "duration": 6342
          }
        ]
      }
    }
  }
}

字段校验拦截器(Field Validation Instrumentation)

graphql.execution.instrumentation.fieldvalidation.FieldValidationInstrumentation 拦截器,可以在执行查询前校验字段和字段参数。如果校验失败,查询将停止,并返回错误信息。

你可以编写自己的``FieldValidation`` 实现,或者直接用 SimpleFieldValidation 去为每个field定义校验逻辑。

ExecutionPath fieldPath = ExecutionPath.parse("/user");
FieldValidation fieldValidation = new SimpleFieldValidation()
        .addRule(fieldPath, new BiFunction<FieldAndArguments, FieldValidationEnvironment, Optional<GraphQLError>>() {
            @Override
            public Optional<GraphQLError> apply(FieldAndArguments fieldAndArguments, FieldValidationEnvironment environment) {
                String nameArg = fieldAndArguments.getFieldArgument("name");
                if (nameArg.length() > 255) {
                    return Optional.of(environment.mkError("Invalid user name", fieldAndArguments));
                }
                return Optional.empty();
            }
        });

FieldValidationInstrumentation instrumentation = new FieldValidationInstrumentation(
        fieldValidation
);

GraphQL.newGraphQL(schema)
        .instrumentation(instrumentation)
        .build();

graphql-java:使用 Dataloader

使用 Dataloader

使用 graphql, 你很可能会去查询图结构的数据(graph of data ) (这可能是句废话). 如果用简单直接的方法去获取每个field的数据,可能会效率很低。

使用 java-dataloader 可以帮助你更有效地缓存和批量化数据加载操作。 ``dataloader``会缓存所有加载过的数据,使再次使用相同数据时,不需要再加载。

假设我们用以下的 StarWars 查询。这查询了一个英雄( hero)和他朋友的名字,和他朋友的朋友的名字。很多时候,他们有共同的朋友。

{
    hero {
        name
        friends {
            name
            friends {
               name
            }
        }
    }
}

下面是查询的结果。你可以看到,Han, Leia, Luke 和 R2-D2 是一群紧密的朋友。他们有很多共同的朋友。

[hero: [name: 'R2-D2', friends: [
        [name: 'Luke Skywalker', friends: [
                [name: 'Han Solo'], [name: 'Leia Organa'], [name: 'C-3PO'], [name: 'R2-D2']]],
        [name: 'Han Solo', friends: [
                [name: 'Luke Skywalker'], [name: 'Leia Organa'], [name: 'R2-D2']]],
        [name: 'Leia Organa', friends: [
                [name: 'Luke Skywalker'], [name: 'Han Solo'], [name: 'C-3PO'], [name: 'R2-D2']]]]]
]

一个直接的实现是为每个人物对象(person object)调用一次 DataFetcher 去获取数据。

这样你需要 15 次网络调用 。即使这群有有很多相同的朋友。使用 dataloader 可以让 graphql 查询更高效。

graphql 会批量化每个层级的查询。 ( 如先是 hero 之后是 friends 之后是他们的 friends), Data loader 返回了一个 ” 期约(promise)”,期约会返回一个 person object.(人物对象)。在查询的每个层级, dataloader.dispatch() 方法均会被调用一次,以获取真实的数据。当开启了缓存功能时 (默认开启),将直接返回之前加载过的 person,而不会再发起一次查询。

上例中,共有 5 个独立的 people。但当缓存和批量化开启后,只发起了 3 次调用 batch loader 方法的查询操作。3 次网络或DB访问,当然比 15 次牛B多了。【译者补】

如果你使用了如 java.util.concurrent.CompletableFuture.supplyAsync() 的异步程序方式 。多个field的远程加载数据就可以并发进行了。. 这可以让查询更快,因一次并发了多个远程调用。

下面就是示例代码:

// a batch loader function that will be called with N or more keys for batch loading
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
    @Override
    public CompletionStage<List<Object>> load(List<String> keys) {
        //
        // we use supplyAsync() of values here for maximum parellisation
        //
        return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
    }
};

// a data loader for characters that points to the character batch loader
DataLoader<String, Object> characterDataLoader = new DataLoader<>(characterBatchLoader);

//
// use this data loader in the data fetchers associated with characters and put them into
// the graphql schema (not shown)
//
DataFetcher heroDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        return characterDataLoader.load("2001"); // R2D2
    }
};

DataFetcher friendsDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        StarWarsCharacter starWarsCharacter = environment.getSource();
        List<String> friendIds = starWarsCharacter.getFriendIds();
        return characterDataLoader.loadMany(friendIds);
    }
};

//
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
// in this case there is 1 but you can have many
//
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("character", characterDataLoader);

//
// this instrumentation implementation will dispatch all the dataloaders
// as each level fo the graphql query is executed and hence make batched objects
// available to the query and the associated DataFetchers
//
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
        = new DataLoaderDispatcherInstrumentation(registry);

//
// now build your graphql object and execute queries on it.
// the data loader will be invoked via the data fetchers on the
// schema fields
//
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
        .instrumentation(dispatcherInstrumentation)
        .build();

```

需要注意的是,只有你使用了 DataLoaderDispatcherInstrumentation ,上面说的才会生效。由它来调用 dataLoader.dispatch() 。不然,期约( promises ) 将不会被执行,就更不会有数据获取了。

查询范围的 Data Loaders

对于 Web 请求,请求的结果可能会因不同用户而不同的。如果是特定用户的数据,你一定不希望用户A的数据,被用户B查询到。

所以 DataLoader 实例的范围很重要。这时,你需要对每个 Request 创建一个新的 DataLoader,来保证它只在当前请求中生效。

如果你需要的是不同请求间共享数据,所以你会希望 DataLoader 的生命周期更长。

但如用你用请求级的 data loaders ,为每个请求创建 GraphQL and DataLoader 是花费很少资源的。Its the GraphQLSchema creation that can be expensive, especially if you are using graphql SDL parsing.

i在代码中静态引用 schema 。可以是静态变量或 IoC 单件组件。但每次处理请求时,都需要创建 GraphQL 对象。

GraphQLSchema staticSchema = staticSchema_Or_MayBeFrom_IoC_Injection();

DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("character", getCharacterDataLoader());

DataLoaderDispatcherInstrumentation dispatcherInstrumentation
        = new DataLoaderDispatcherInstrumentation(registry);

GraphQL graphQL = GraphQL.newGraphQL(staticSchema)
        .instrumentation(dispatcherInstrumentation)
        .build();

graphQL.execute("{ helloworld }");

// you can now throw away the GraphQL and hence DataLoaderDispatcherInstrumentation
// and DataLoaderRegistry objects since they are really cheap to build per request

graphql-java:运行期异常(Runtime Exceptions )

运行期异常(Runtime Exceptions )

在一些异常的情况下, graphql engine 有可能抛出 Runtime exceptions。下面是调用 graphql.execute(...) 期间可能出现的异常。

他们不是执行 graphql 查询中发生的异常。 但还是不能被忽略。

  • graphql.schema.CoercingSerializeException

发生这类型的异常,原因是序列化Scalar时出错。如 Int 字段获取到 String 值。

  • graphql.schema.CoercingParseValueException

发生这类型的异常,原因是解释输入的 Scalar 时出错 ,如 int 参数的实际输入值是一个 String。

  • graphql.execution.UnresolvedTypeException

当 graphql.schema.TypeResolver` 不能判断抽象对象( interface or union type) 的实际类型( concrete object type) 时发生这个异常。

  • graphql.execution.NonNullableValueCoercedAsNullException

如果 个不允许为 null 的参数,被赋值为 null。会抛出上面异常。.

  • graphql.execution.InputMapDefinesTooManyFieldsException

可以输入对象( input type object )包含了未在Schema中定义的field。就会发生上面异常。

  • graphql.schema.validation.InvalidSchemaException
表示在运行下面函数时, Schema 校验失败。
graphql.schema.GraphQLSchema.Builder#build()`
  • graphql.GraphQLException

这个是通用的异常。如不能访问 POJO 的 field。这可以等同于 RuntimeException。

  • graphql.AssertException

这是内部断言的预计外异常

事实上不应该发生【译注:如果抛出了,可能是框架有BUG了。】

graphql-java:订阅(Subscriptions)

订阅(Subscriptions)

订阅查询(Subscription Queries)

Graphql 订阅(subscriptions)使你可以让你订阅响应式数据源(reactive source) 。当有新数据时,会发送给订阅者。

可以阅读 http://graphql.org/blog/subscriptions-in-graphql-and-relay/ 来了解订阅的背景知识。

假设你有一个股票服务。可以用这个 graphql 语句来订阅它的数据:

subscription StockCodeSubscription {
    stockQuotes(stockCode:"IBM') {
        dateTime
        stockCode
        stockPrice
        stockPriceChange
    }
}

股票价格变化时,graphql 订阅 可以把 ExecutionResult 对象以流的方式传送给订阅者。和其它 graphql 查询一样,只会发送指定的字段 。

不同的是,一开始的查询结果是一个响应式流(reactive-streams) Publisher(流发布者) 对象。通过对象可以获取未来的数据。

你需要使用 SubscriptionExecutionStrategy 策略作为执行策略(execution strategy)。因为它支持 reactive-streams APIs.

GraphQL graphQL = GraphQL
        .newGraphQL(schema)
        .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy())
        .build();

ExecutionResult executionResult = graphQL.execute(query);

Publisher<ExecutionResult> stockPriceStream = executionResult.getData();

这里的 Publisher<ExecutionResult> 就是流事件的发布者【译注:原文 publisher of a stream of events】。你需要编写你自己的流处理代码,如:

GraphQL graphQL = GraphQL
        .newGraphQL(schema)
        .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy())
        .build();

String query = "" +
        "    subscription StockCodeSubscription {\n" +
        "        stockQuotes(stockCode:\"IBM') {\n" +
        "            dateTime\n" +
        "            stockCode\n" +
        "            stockPrice\n" +
        "            stockPriceChange\n" +
        "        }\n" +
        "    }\n";

ExecutionResult executionResult = graphQL.execute(query);

Publisher<ExecutionResult> stockPriceStream = executionResult.getData();

AtomicReference<Subscription> subscriptionRef = new AtomicReference<>();
stockPriceStream.subscribe(new Subscriber<ExecutionResult>() {

    @Override
    public void onSubscribe(Subscription s) {
        subscriptionRef.set(s);
        s.request(1);
    }

    @Override
    public void onNext(ExecutionResult er) {
        //
        // process the next stock price
        //
        processStockPriceChange(er.getData());

        //
        // ask the publisher for one more item please
        //
        subscriptionRef.get().request(1);
    }

    @Override
    public void onError(Throwable t) {
        //
        // The upstream publishing data source has encountered an error
        // and the subscription is now terminated.  Real production code needs
        // to decide on a error handling strategy.
        //
    }

    @Override
    public void onComplete() {
        //
        // the subscription has completed.  There is not more data
        //
    }
});

需要编写 reactive-streams 代码去消费一源源不断的 ExecutionResults。你可以在 http://www.reactive-streams.org/ 中看到更 reactive-streams 代码的编写细节。

``RxJava``是这个流行的 reactive-streams 实现。在 http://reactivex.io/intro.html 中可以看到更多创建Publishers 数据 和 Subscriptions 数据的细节。

graphql-java 只是产出一个流对象。它不关心如何在网络上用 web sockets 或其它手段发送流数据 。虽然这很重要,但不是作为基础 graphql-java 库应该做的。

我们编写了一个 websockets 的(基于 Jetty) 模拟股票报价的示例应用。它使用了 RxJava。

详见 https://github.com/graphql-java/graphql-java-subscription-example

关于订阅服务的 Data Fetchers

订阅字段的 DataFetcher 的职责是生成一个 Publisher。这个 Publisher 输出的每一个对象,将会通过 graphql 查询来映射。然后作为执行结果返回。

你会像这样子去编写Data Fetcher:

DataFetcher<Publisher<StockInfo>> publisherDataFetcher = new DataFetcher<Publisher<StockInfo>>() {
    @Override
    public Publisher<StockInfo> get(DataFetchingEnvironment environment) {
        String stockCodeArg = environment.getArgument("stockCode");
        return buildPublisherForStockCode(stockCodeArg);
    }
};

如何获取流事件,就由你的 reactive code 来决定 了。graphql-java 会帮助你从流对象中获取 graphql 字段(fields)。像一般的 graphql 查询一样。

graphql-java:执行(Execution)

执行(Execution)

查询(Queries)

为了对 一个Schema 执行查询。需要先构造一个 GraphQL 对象,并带着一些参数去调用 execute() 方法.

查询将返回一个 ExecutionResult 对象,其中包含查询的结果数据 (或出错时的错误信息集合).

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(queryType)
        .build();

GraphQL graphQL = GraphQL.newGraphQL(schema)
        .build();

ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
        .build();

ExecutionResult executionResult = graphQL.execute(executionInput);

Object data = executionResult.getData();
List<GraphQLError> errors = executionResult.getErrors();

更复杂的示例,可以看 StarWars 查询测试用例

Data Fetchers

每个graphql schema 中的field,都需要绑定相应的 graphql.schema.DataFetcher 以获取数据. 其它GraphQL的实现把这叫 resolvers*.

很多时候,你可以用默认的 graphql.schema.PropertyDataFetcher 去从 Java POJO 中自动提取数据到对应的 field. 如果你未为 field 指定 data fetcher 那么就默认使用它.

但你最少需要为顶层的领域对象(domain objects) 编写 data fetchers. 其中可以会与database交互,或用HTTP与其它系统交互.

graphql-java 不关心你如何获取你的业务数据,这是你的自己. 它也不关心你如果授权你的业务数据. 你应该在自己的业务逻辑层,去实现这些逻辑.

简单 Data fetcher 示例:

DataFetcher userDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        return fetchUserFromDatabase(environment.getArgument("userId"));
    }
};

框架在执行查询时。会调用上面的方法,其中的 graphql.schema.DataFetchingEnvironment 参数包括以下信息:被查询的 field、查询这个field时可能带上的查询参数、这个field的父数据对象(Source Object)、 查询的ROOT数据对象、查询执行上下文环境对象(query context object).

上面是同步获取数据的例子,执行引擎需要等待一个 data fetcher 返回数据才能继续下一个. 也可以通过编写异步的 DataFetcher ,异步地返回 CompletionStage 对象,在下文中将会说明使用方法.

当获取数据出现异常时

如果异步是出现在调用 data fetcher 时, 默认的执行策略(execution strategy) 将生成一个 graphql.ExceptionWhileDataFetching 错误,并将其加入到查询结果的错误列表中. 请留意,GraphQL 在发生异常时,允许返回部分成功的数据,并将带上异常信息.

下面是默认的异常行为处理逻辑.

public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(SimpleDataFetcherExceptionHandler.class);

    @Override
    public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
        Throwable exception = handlerParameters.getException();
        SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
        ExecutionPath path = handlerParameters.getPath();

        ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
        handlerParameters.getExecutionContext().addError(error);
        log.warn(error.getMessage(), exception);
    }
}

如果你抛出的异常本身是 GraphqlError 类型,框架会把其中的消息 和 自定义扩展属性(custom extensions attributes)转换到 ExceptionWhileDataFetching 对象中. 这可以方便你把自己的错误信息,放到返回给调用者的 GraphQL 错误列表中.

例如,你在 DataFetcher 中抛出了这个异常. 那么 foo and fizz 属性将会包含在返回给调用者的graphql查询错误中.

class CustomRuntimeException extends RuntimeException implements GraphQLError {
    @Override
    public Map<String, Object> getExtensions() {
        Map<String, Object> customAttributes = new LinkedHashMap<>();
        customAttributes.put("foo", "bar");
        customAttributes.put("fizz", "whizz");
        return customAttributes;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }

    @Override
    public ErrorType getErrorType() {
        return ErrorType.DataFetchingException;
    }
}

你可以编写自己的 graphql.execution.DataFetcherExceptionHandler 来改变这些逻辑。只需要在执行策略(execution strategy)注册一下.

例如,上面的代码记录了底层的异常和堆栈. 如果你不希望这些出现在输出的错误列表中。你可以用以下的方法去实现.

DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() {
    @Override
    public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
        //
        // do your custom handling here.  The parameters have all you need
    }
};
ExecutionStrategy executionStrategy = new AsyncExecutionStrategy(handler);

序列化成 JSON

通常,用 HTTP 方法去调用 graphql ,用 JSON 格式作为返回结果. 返回,需要把 graphql.ExecutionResult 对象转换为 JSON 格式包.

一般用 Jackson or GSON 去做 JSON 序列化. 但他们对结果数据的转换方法有一些不同点. 例如 JSON 的`nulls` 在 graphql 结果中的是有用的。所以必须在 json mappers 中设置需要它

为保证你返回的 JSON 结果 100% 合符 graphql 规范, 应该调用result对象的 toSpecification 方法,然后以 JSON格式 发送响应.

这样就可以确保返回数据合符在 http://facebook.github.io/graphql/#sec-Response 中的规范

ExecutionResult executionResult = graphQL.execute(executionInput);

Map<String, Object> toSpecificationResult = executionResult.toSpecification();

sendAsJson(toSpecificationResult);

更新(Mutations)

如果你不了解什么叫更新(Mutations),建议先阅读规范 http://graphql.org/learn/queries/#mutations.

首先,你需要定义一个支持输入参数的 GraphQLObjectType . 在更新数据时,框架会带上这些参数去调用 data fetcher.

下面是,GraphQL 更新语句的例子 :

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

修改操作是需要带输入参数的,上例中对应变量 $ep and $review

对应地,Schema 应该这么写【译注:以下是 Java 写法,你也可以用SDL写法】 :

GraphQLInputObjectType episodeType = GraphQLInputObjectType.newInputObject()
        .name("Episode")
        .field(newInputObjectField()
                .name("episodeNumber")
                .type(Scalars.GraphQLInt))
        .build();

GraphQLInputObjectType reviewInputType = GraphQLInputObjectType.newInputObject()
        .name("ReviewInput")
        .field(newInputObjectField()
                .name("stars")
                .type(Scalars.GraphQLString))
        .field(newInputObjectField()
                .name("commentary")
                .type(Scalars.GraphQLString))
        .build();

GraphQLObjectType reviewType = newObject()
        .name("Review")
        .field(newFieldDefinition()
                .name("stars")
                .type(GraphQLString))
        .field(newFieldDefinition()
                .name("commentary")
                .type(GraphQLString))
        .build();

GraphQLObjectType createReviewForEpisodeMutation = newObject()
        .name("CreateReviewForEpisodeMutation")
        .field(newFieldDefinition()
                .name("createReview")
                .type(reviewType)
                .argument(newArgument()
                        .name("episode")
                        .type(episodeType)
                )
                .argument(newArgument()
                        .name("review")
                        .type(reviewInputType)
                )
                .dataFetcher(mutationDataFetcher())
        )
        .build();

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(queryType)
        .mutation(createReviewForEpisodeMutation)
        .build();

注意,输入参数应该是 GraphQLInputObjectType 类型. 请留意. 对于修改操作,输入参数只能用这个类型(type),而不能用如 ``GraphQLObjectType``之类的输出类型(type). Scalars 类型(type) 可以用于输入和输出.

对于更新操作,DataFetcher的职责是执行数据更新行返回执行结果.

private DataFetcher mutationDataFetcher() {
    return new DataFetcher() {
        @Override
        public Review get(DataFetchingEnvironment environment) {
            //
            // The graphql specification dictates that input object arguments MUST
            // be maps.  You can convert them to POJOs inside the data fetcher if that
            // suits your code better
            //
            // See http://facebook.github.io/graphql/October2016/#sec-Input-Objects
            //
            Map<String, Object> episodeInputMap = environment.getArgument("episode");
            Map<String, Object> reviewInputMap = environment.getArgument("review");

            //
            // in this case we have type safe Java objects to call our backing code with
            //
            EpisodeInput episodeInput = EpisodeInput.fromMap(episodeInputMap);
            ReviewInput reviewInput = ReviewInput.fromMap(reviewInputMap);

            // make a call to your store to mutate your database
            Review updatedReview = reviewStore().update(episodeInput, reviewInput);

            // this returns a new view of the data
            return updatedReview;
        }
    };
}

上面代码,先更新业务数据,然后返回 Review 对象给调用方.

异步执行(Asynchronous Execution)

graphql-java 是个全异步的执行引擎. 如下,调用 executeAsync() 后,返回 CompleteableFuture

GraphQL graphQL = buildSchema();

ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
        .build();

CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);

promise.thenAccept(executionResult -> {
    // here you might send back the results as JSON over HTTP
    encodeResultToJsonAndSendResponse(executionResult);
});

promise.join();

使用 CompletableFuture 对象,你可以指定,在查询完成后,组合其它操作(action)或函数你的函数. 需要你需要同步等待执行结果 ,可以调用 .join() 方法.

graphql-java引擎内部是异步执行的,但你可以通过调用 join 方法变为同步等待. 下面是等效的代码:

ExecutionResult executionResult = graphQL.execute(executionInput);

// the above is equivalent to the following code (in long hand)

CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
ExecutionResult executionResult2 = promise.join();

如果你编写的 graphql.schema.DataFetcher 返回 CompletableFuture<T> 对象,那么它会被糅合到整个异步查询中. 这样,你可以同时发起我个数据获取操作,让它们并行运行. 而由DataFetcher控制具体的线程并发策略.

下面示例使用 java.util.concurrent.ForkJoinPool.commonPool() 并行执行器,用其它线程完成数据获取.

DataFetcher userDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        CompletableFuture<User> userPromise = CompletableFuture.supplyAsync(() -> {
            return fetchUserViaHttp(environment.getArgument("userId"));
        });
        return userPromise;
    }
};

上面是旧的写法,也可以用Java 8 lambdas 的写法:

DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(
        () -> fetchUserViaHttp(environment.getArgument("userId")));

graphql-java 保证所有 CompletableFuture 对象组合,最后生成合符 graphql 规范的执行结果.

还有一个方法可以简化异步 data fetchers 的编写. 使用 graphql.schema.AsyncDataFetcher.async(DataFetcher<T>) 去包装``DataFetcher``. 这样可以使用 static imports 来提高代码可读性.

DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));

关于执行策略(Execution Strategies)

在执行查询或更新数据时,引擎会使用实现了 ``graphql.execution.ExecutionStrategy``接口 的对象,来决定执行策略. graphql-java 中已经有几个现成的策略,但如果你需要,你可以写自己的。.

你可以这样给 GraphQL 对象绑定执行策略。

GraphQL.newGraphQL(schema)
        .queryExecutionStrategy(new AsyncExecutionStrategy())
        .mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
        .build();

实际上,上面就是引擎默认的策略了。大部分情况下用它就够了。

异步执行策略(AsyncExecutionStrategy)

默认的查询 执行策略是 graphql.execution.AsyncExecutionStrategy ,它会把每个 field 返回视为 CompleteableFuture 。它并不会控制 filed 的获取顺序. 这个策略可以优化查询执行的性能.

Data fetchers 返回 CompletionStage` 对象,就可以全异步执行整个查询了。

例如以下的查询:

query {
  hero {
    enemies {
      name
    }
    friends {
      name
    }
  }
}

The AsyncExecutionStrategy is free to dispatch the enemies field at the same time as the friends field. It does not
have to do enemies first followed by friends, which would be less efficient.

这个策略不会按顺序来集成结果数据。但查询结果会按GraphQL规范顺序来返回。只是数据获取的顺序不确定。

对于查询,这个策略是 graphql 规范 http://facebook.github.io/graphql/#sec-Query 允许和推荐的。

详细见 规范 .

异步顺序执行策略(AsyncSerialExecutionStrategy)

Graphql 规范指出,修改操作(mutations)“必须”按照 field 的顺序来执行。

所以,为了确保一个 field 一个 field 顺序地执行更新,更新操作(mutations)默认使用 graphql.execution.AsyncSerialExecutionStrategy 策略。你的 mutation Data Fetcher 仍然可以返回 CompletionStage 对象, 但它和其它 field 的是串行执行的。

基于执行器的执行策略:ExecutorServiceExecutionStrategy

The graphql.execution.ExecutorServiceExecutionStrategy execution strategy will always dispatch each field
fetch in an asynchronous manner, using the executor you give it. It differs from AsyncExecutionStrategy in that
it does not rely on the data fetchers to be asynchronous but rather makes the field fetch invocation asynchronous by
submitting each field to the provided java.util.concurrent.ExecutorService.

因为这样,所以它不能用于更新(mutation)操作。

ExecutorService  executorService = new ThreadPoolExecutor(
        2, /* core pool size 2 thread */
        2, /* max pool size 2 thread */
        30, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        new ThreadPoolExecutor.CallerRunsPolicy());

GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
        .queryExecutionStrategy(new ExecutorServiceExecutionStrategy(executorService))
        .mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
        .build();

订阅执行策略(SubscriptionExecutionStrategy)

Graphql 订阅(subscriptions) 使你可以对GraphQL 数据进行为状态的订阅。你可以使用 SubscriptionExecutionStrategy 执行策略,它支持 reactive-streams APIs。

阅读 http://www.reactive-streams.org/ 可以得到关于 PublisherSubscriber 接口的更多信息。

也可以阅读subscriptions的文档,以了解如何编写基于支持订阅的 graphql 服务。

批量化执行器(BatchedExecutionStrategy)

对于有数组(list)field 的 schemas, 我们提供了 graphql.execution.batched.BatchedExecutionStrategy 策略。它可以批量化地调用标注了@Batched 的 DataFetchers 的 get() 方法。

关于 BatchedExecutionStrategy 是如何工作的。它是如此的特别,让我不知道如何解释【译注:原文:Its a pretty special case that I don’t know how to explain properly】

控制字段的可见性

所有 GraphqlSchema 的字段(field)默认都是可以访问的。但有时候,你可能想不同用户看到不同部分的字段。

你可以在schema 上绑定一个 graphql.schema.visibility.GraphqlFieldVisibility 对象。.

框架提供了一个可以指定字段(field)名的实现,叫 graphql.schema.visibility.BlockedFields..

GraphqlFieldVisibility blockedFields = BlockedFields.newBlock()
        .addPattern("Character.id")
        .addPattern("Droid.appearsIn")
        .addPattern(".*\\.hero") // it uses regular expressions
        .build();

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(StarWarsSchema.queryType)
        .fieldVisibility(blockedFields)
        .build();

如果你需要,还有一个实现可以防止 instrumentation 拦截你的 schema。

请注意,这会使您的服务器违反graphql规范和大多数客户端的预期,因此请谨慎使用.

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(StarWarsSchema.queryType)
        .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
        .build();

你可以编写自己的 GraphqlFieldVisibility 来控制字段的可见性。

class CustomFieldVisibility implements GraphqlFieldVisibility {

    final YourUserAccessService userAccessService;

    CustomFieldVisibility(YourUserAccessService userAccessService) {
        this.userAccessService = userAccessService;
    }

    @Override
    public List<GraphQLFieldDefinition> getFieldDefinitions(GraphQLFieldsContainer fieldsContainer) {
        if ("AdminType".equals(fieldsContainer.getName())) {
            if (!userAccessService.isAdminUser()) {
                return Collections.emptyList();
            }
        }
        return fieldsContainer.getFieldDefinitions();
    }

    @Override
    public GraphQLFieldDefinition getFieldDefinition(GraphQLFieldsContainer fieldsContainer, String fieldName) {
        if ("AdminType".equals(fieldsContainer.getName())) {
            if (!userAccessService.isAdminUser()) {
                return null;
            }
        }
        return fieldsContainer.getFieldDefinition(fieldName);
    }
}

查询缓存(Query Caching)

Before the graphql-java engine executes a query it must be parsed and validated, and this process can be somewhat time consuming.

为了避免重复的解释和校验。 GraphQL.Builder 可以使用``PreparsedDocumentProvider``去重用 Document 实例。

它不是缓存 查询结果,只是缓存解释过的文档( Document )。

Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build(); (1)
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
        .preparsedDocumentProvider(cache::get) (2)
        .build();
  1. 创建你需要的缓存实例,本例子是使用的是 Caffeine 。它是个高质量的缓存解决方案。缓存实例应该是线程安全和可以线程间共享的。
  2. PreparsedDocumentProvider 是一个函式接口( functional interface),方法名是get。.

为提高缓存命中率,GraphQL 语句中的 field 参数(arguments)建议使用变量( variables)来表达,而不是直接把值写在语句中。

下面的查询 :

query HelloTo {
     sayHello(to: "Me") {
        greeting
     }
}

应该写成:

query HelloTo($to: String!) {
     sayHello(to: $to) {
        greeting
     }
}

带上参数( variables):

{
   "to": "Me"
}

这样,这不管查询的变量(variable)如何变化 ,查询解释也就可以重用。