当前位置: 代码迷 >> 综合 >> graphql-java(2)如何实现自定义标量类型
  详细解决方案

graphql-java(2)如何实现自定义标量类型

热度:88   发布时间:2023-09-23 10:25:39.0

https://dreamylost.cn/
文档 version = 1.4 仅供参考

Scalars

graphql类型系统的叶节点称为标量。一旦达到标量类型,就无法进一步下降到类型层次结构中。标量类型旨在表示不可分的值。这里是因为查询是document解析,也就是文档树。

graphql规范指出,所有实现都必须具有以下标量类型。

  • String 又名 GraphQLString - 一个UTF‐8字符序列
  • Boolean 又名 GraphQLBoolean - true 或 false
  • Int 又名 GraphQLInt - 一个有符号的32位整数
  • Float 又名 GraphQLFloat - 一个有符号双精度浮点数
  • ID 又名 GraphQLID - 唯一标识符,以与String相同的方式序列化;但是,将其定义为ID表示它不是人类可读的。

graphql-java添加了以下标量类型,这些标量类型在基于Java的系统中很有用

  • Long GraphQLLong - java.lang.Long
  • Short GraphQLShort - java.lang.Short
  • Byte GraphQLByte - java.lang.Byte
  • BigDecimal GraphQLBigDecimal - java.math.BigDecimal
  • BigInteger GraphQLBigInteger - java.math.BigInteger

graphql.Scalars类包含所提供标量类型的单例实例

Writing your Own Custom Scalars

您可以编写自己的自定义标量实现。这样,您将负责在运行时强制限制值,我们将在稍后解释。

想象我们决定需要一个电子邮件标量类型。它将电子邮件地址作为输入和输出。

我们将为此创建一个单例graphql.schema.GraphQLScalarType实例。

public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
    //接受一个Java对象并将其转换为该标量的输出形式@Overridepublic Object serialize(Object dataFetcherResult) {
    return serializeEmail(dataFetcherResult);}//接受变量输入对象并转换为Java运行时表示形式@Overridepublic Object parseValue(Object input) {
    return parseEmailFromVariable(input);}//将AST文字graphql.language.Value作为输入并转换为Java运行时表示形式@Overridepublic Object parseLiteral(Object input) {
    return parseEmailFromAstLiteral(input);}
});

Coercing values

任何自定义标量实现中的实际工作都是实现graphql.schema.Coercing。

您的自定义标量代码必须处理2种形式的输入(parseValue/parseLiteral)和1种形式的输出(序列化)。

想象一下这个查询,它使用变量,AST文字并输出我们的标量类型电子邮件。

mutation Contact($mainContact: Email!) {makeContact(mainContactEmail: $mainContact, backupContactEmail: "backup@company.com") {idmainContactEmail}
}

我们的自定义电子邮件标量将

  • 通过parseValue调用以将$mainContact变量值转换为运行时对象
  • 通过parseLiteral调用以将AST graphql.language.StringValue “backup@company.com” 转换为运行时对象
  • 通过序列化调用,以将mainContactEmail的运行时表示形式转换为可用于输出的表单

Validation of input and output

该方法可以验证接收到的输入是否有意义。例如,我们的电子邮件标量将尝试验证输入和输出确实是电子邮件地址。

graphql.schema.Coercing的JavaDoc方法协定如下

  • 只允许从序列化抛出graphql.schema.CoercingSerializeException。这表明该值不能序列化为适当的形式。您不得允许其他运行时异常转义此方法以获取正常的graphql行为进行验证。您必须返回非null值。
  • 仅允许从parseValue引发graphql.schema.CoercingParseValueException。这表明该值不能解析为适当形式的输入。您不得允许其他运行时异常转义此方法以获取正常的graphql行为进行验证。您必须返回非null值。
  • 仅允许从parseLiteral引发graphql.schema.CoercingParseLiteralException。这表明AST值不能解析为适当形式的输入。您不得允许任何运行时异常转义此方法,以获取正常的graphql行为进行验证。

有些人试图依靠运行时异常进行验证,并希望它们以graphql错误的形式出现。然而并发如此,您必须遵循Coercing方法协定,以允许graphql-java引擎根据有关标量类型的graphql规范工作。

Example implementation

以下是我们想象中的email标量类型的一个非常粗略的实现,以向您展示如何实现这种标量的Coercing方法。

public static class EmailScalar {
    public static final GraphQLScalarType Email = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
    @Overridepublic Object serialize(Object dataFetcherResult) {
    return serializeEmail(dataFetcherResult);}@Overridepublic Object parseValue(Object input) {
    return parseEmailFromVariable(input);}@Overridepublic Object parseLiteral(Object input) {
    return parseEmailFromAstLiteral(input);}});private static boolean looksLikeAnEmailAddress(String possibleEmailValue) {
    // 这个正则不是标准的验证邮件地址的那个return Pattern.matches("^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", possibleEmailValue);}private static Object serializeEmail(Object dataFetcherResult) {
    String possibleEmailValue = String.valueOf(dataFetcherResult);if (looksLikeAnEmailAddress(possibleEmailValue)) {
    return possibleEmailValue;} else {
    throw new CoercingSerializeException("Unable to serialize " + possibleEmailValue + " as an email address");}}private static Object parseEmailFromVariable(Object input) {
    if (input instanceof String) {
    String possibleEmailValue = input.toString();if (looksLikeAnEmailAddress(possibleEmailValue)) {
    return possibleEmailValue;}}throw new CoercingParseValueException("Unable to parse variable value " + input + " as an email address");}private static Object parseEmailFromAstLiteral(Object input) {
    if (input instanceof StringValue) {
    String possibleEmailValue = ((StringValue) input).getValue();if (looksLikeAnEmailAddress(possibleEmailValue)) {
    return possibleEmailValue;}}throw new CoercingParseLiteralException("Value is not any email address : '" + String.valueOf(input) + "'");}
}

使用自定义标量

使用上面定义的Email类型完成请求与相应

//自定义标量,......表示省略
......//在StarWarsWiring中定义(或直接使用,主要为了使用依赖注入)
GraphQLScalarType Email = EmailScalar.Email;......//在织入时绑定
private GraphQLSchema buildSchema(String sdl) {
    //读取从resources下加载解析的gql schema文件的字符串TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);//创建运行时类型织入RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();//动态映射//运行时织入包含DataFetcher、TypeResolvers和自定义Scalar,它们是制作完全可执行的schema所必需的。builder.type(newTypeWiring("Query").dataFetcher("hero", starWarsWiring.heroDataFetcher).dataFetcher("human", starWarsWiring.humanDataFetcher).dataFetcher("humans", starWarsWiring.humansDataFetcher).dataFetcher("droid", starWarsWiring.droidDataFetcher));builder.type(newTypeWiring("Human").dataFetcher("friends", starWarsWiring.friendsDataFetcher));builder.type(newTypeWiring("Droid").dataFetcher("friends", starWarsWiring.friendsDataFetcher));builder.type(newTypeWiring("Character").typeResolver(starWarsWiring.characterTypeResolver));builder.type(newTypeWiring("Episode").enumValues(starWarsWiring.episodeResolver));builder.scalar(starWarsWiring.Email);//注入StarWarsWiring中实现的自定义的标量SchemaGenerator schemaGenerator = new SchemaGenerator();//创建gql schema对象,构造可执行请求的gqlreturn schemaGenerator.makeExecutableSchema(typeRegistry, builder.build());
}

在starWarsSchemaAnnotated.graphqls文件修改为

scalar Email......
type Human implements Character {id: ID!name: String!friends: [Character]appearsIn: [Episode]!homePlanet: StringsecretBackstory : String @deprecated(reason : "We have decided that this is not canon")# 添加一个属性,使用了自定义标量email: Email!
}
......

修改Java POJO,为Human类增加String类型 email字段

使用如下查询

{humans {nameemail}
}

返回信息(开了追踪所以有extensions属性)

{
    "data": {
    "humans": [{
    "name": "Luke Skywalker","email": "dreamylost@outlook.com"},{
    "name": "Darth Vader","email": "dreamylost@outlook.com"},{
    "name": "Han Solo","email": "dreamylost@outlook.com"},{
    "name": "Leia Organa","email": "dreamylost@outlook.com"}]},"extensions": {
    "tracing": {
    "version": 1,"startTime": "2020-04-03T09:25:00.753Z","endTime": "2020-04-03T09:25:00.757Z","duration": 3874865,"parsing": {
    "startOffset": 491647,"duration": 404205},"validation": {
    "startOffset": 754108,"duration": 231721},"execution": {
    "resolvers": [{
    "path": ["humans"],"parentType": "Query","returnType": "[Human]","fieldName": "humans","startOffset": 1317556,"duration": 506334},{
    "path": ["humans",0,"name"],"parentType": "Human","returnType": "String!","fieldName": "name","startOffset": 2007874,"duration": 58120},{
    "path": ["humans",0,"email"],"parentType": "Human","returnType": "Email!","fieldName": "email","startOffset": 2199262,"duration": 58219},{
    "path": ["humans",1,"name"],"parentType": "Human","returnType": "String!","fieldName": "name","startOffset": 2507644,"duration": 36967},{
    "path": ["humans",1,"email"],"parentType": "Human","returnType": "Email!","fieldName": "email","startOffset": 2643613,"duration": 27507},{
    "path": ["humans",2,"name"],"parentType": "Human","returnType": "String!","fieldName": "name","startOffset": 2880688,"duration": 34660},{
    "path": ["humans",2,"email"],"parentType": "Human","returnType": "Email!","fieldName": "email","startOffset": 3097616,"duration": 47421},{
    "path": ["humans",3,"name"],"parentType": "Human","returnType": "String!","fieldName": "name","startOffset": 3369605,"duration": 30026},{
    "path": ["humans",3,"email"],"parentType": "Human","returnType": "Email!","fieldName": "email","startOffset": 3505375,"duration": 45639}]}},"dataloader": {
    "overall-statistics": {
    "loadCount": 8,"loadErrorCount": 0,"loadErrorRatio": 0.0,"batchInvokeCount": 1,"batchLoadCount": 4,"batchLoadRatio": 0.5,"batchLoadExceptionCount": 0,"batchLoadExceptionRatio": 0.0,"cacheHitCount": 4,"cacheHitRatio": 0.5},"individual-statistics": {
    "characters": {
    "loadCount": 8,"loadErrorCount": 0,"loadErrorRatio": 0.0,"batchInvokeCount": 1,"batchLoadCount": 4,"batchLoadRatio": 0.5,"batchLoadExceptionCount": 0,"batchLoadExceptionRatio": 0.0,"cacheHitCount": 4,"cacheHitRatio": 0.5}}}}
}

完整代码 https://github.com/jxnu-liguobin/springboot-examples

  相关解决方案