GraphQL实战-第四篇-构建开发框架

宗晟
2023-12-01

GraphQL实战-第四篇-构建开发框架

前三篇是关于GraphQL的介绍和练手用的demo实现,从此篇开始,分享真正在实战中对GraphQL的应用。

目的

  • 利用graphql的特性改变现有开发形式,提升开发效率
  • 在实际开发中,将graphql的部分沉淀出一个框架,便于新项目的敏捷开发

在此构建一个engine的项目,实现GraphQL的常用功能及问题解决方案,此项目即可直接应用于web项目的开发,也可以打成jar寄存于其他项目中。

接下来直接上手

首先还是基于Maven构建的Spring Boot项目 pom

        <!--graphql-java-->
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java</artifactId>
            <version>15.0</version>
        </dependency>

接下来需要加载schema,并初始化grahql

@Component
@Slf4j
public class GraphQLManagerProvider {

    @Value("${engine.schema.file_path:classpath*:schema/*.graphql*}")
    public String file_path;

    @Autowired
    private ApplicationContext ctx;

    @Autowired
    private QueryCommonProvider queryCommonProvider;

    @Autowired
    private MutationCommonProvider mutationCommonProvider;

    private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    public GraphQL createGraphQL() throws Exception {
        TypeDefinitionRegistry sdl = getSDLFormLocal();
        if (sdl == null) {
            log.error("没有要加载的schema文件");
            return null;
        }
        //刷新bean
        initBean(sdl);
        return GraphQL.newGraphQL(buildSchema(sdl)).build();
    }

    public void initBean(TypeDefinitionRegistry sdl) {
        //获取BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) ctx.getAutowireCapableBeanFactory();
        //创建bean信息.
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(GraphQL.class);
        beanDefinitionBuilder.addConstructorArgValue(buildSchema(sdl));
        //动态注册bean.
        defaultListableBeanFactory.registerBeanDefinition("graphQL", beanDefinitionBuilder.getBeanDefinition());
        //获取动态注册的bean
        GraphQL graphQL = ctx.getBean(GraphQL.class);
        log.info("graphql refresh :{}", graphQL);
    }

    private GraphQLSchema buildSchema(TypeDefinitionRegistry typeDefinitionRegistry) {
        RuntimeWiring runtimeWiring = buildWiring(typeDefinitionRegistry);
        return new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
    }

    private RuntimeWiring buildWiring(TypeDefinitionRegistry typeDefinitionRegistry) {
        Optional<SchemaDefinition> optionalSchemaDefinition = typeDefinitionRegistry.schemaDefinition();
        SchemaDefinition schemaDefinition = optionalSchemaDefinition.get();
        Map<String, Type> typeNameMap = schemaDefinition.getOperationTypeDefinitions()
                .stream()
                .collect(Collectors.toMap(OperationTypeDefinition::getName, OperationTypeDefinition::getTypeName));
        //动态加载schema中定义的query的typeName
        TypeRuntimeWiring.Builder queryBuilder = TypeRuntimeWiring.newTypeWiring(((TypeName) typeNameMap.get("query")).getName());
        //指定业务动态处理策略
        queryBuilder.defaultDataFetcher(this.queryCommonProvider);
        TypeRuntimeWiring.Builder mutationBuilder = TypeRuntimeWiring.newTypeWiring(((TypeName) typeNameMap.get("mutation")).getName());
        mutationBuilder.defaultDataFetcher(this.mutationCommonProvider);
        return RuntimeWiring.newRuntimeWiring()
                .type(queryBuilder)
                .type(mutationBuilder)
                .build();
    }

    /**
     * 加载Schema
     *
     * @return
     * @throws Exception
     */
    private TypeDefinitionRegistry getSDLFormLocal() throws Exception {
        List<Resource> pathList = new ArrayList<>();
        Resource[] resources = findResource();
        if (resources != null && resources.length > 0) {
            for (Resource resource : resources) {
                if (resource.getFilename().indexOf("graphql") > 0) {
                    log.info("load schema file name: {}", resource.getFilename());
                    pathList.add(resource);
                }
            }
        } else {
            log.error("模型文件不存在");
            return null;
        }
        return typeDefinitionRegistry(pathList);
    }

    /**
     * 整合各个schema文件路径
     *
     * @param pathList
     * @return
     * @throws Exception
     */
    public TypeDefinitionRegistry typeDefinitionRegistry(List<Resource> pathList) throws Exception {
        SchemaParser schemaParser = new SchemaParser();
        TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
        for (Resource resource : pathList) {
            InputStream inputStream = resource.getInputStream();
            String sdl = StringUtil.readStreamString(inputStream, "UTF-8");
            typeRegistry.merge(schemaParser.parse(sdl));
        }
        return typeRegistry;
    }


    private Resource[] findResource() throws IOException {
        return resourcePatternResolver.getResources(file_path);
    }

}

要点介绍

  • file_path:schema的文件路径,这里通过配置来获取,并指定了默认值
  • ctx:用于将bean注册到spring单例池
  • queryCommonProvider 处理query类型的GraphQL策略,为graphql提供数据支持
  • mutationCommonProvider 处理mutation类型的GraphQL策略,为graphql提供数据支持
  • createGraphQL() 用于构建一个graphql,并将graphql注册到spring
  • initBean 将graphql注册到Spring
  • buildWiring 这里指定了graphql数据获取的策略,即queryCommonProvider,实现了动态解析graphql返回业务数据的功能

然后是需要在项目启动的时候加载GraphQL,即触发createGraphQL方法

@Component
@Slf4j
public class GraphQLManager {
    @Autowired
    private GraphQLManagerProvider graphQLManagerProvider;

    @PostConstruct
    public void init() {
        try {
            log.info("graphql init");
            graphQLManagerProvider.createGraphQL();
        } catch (Exception e) {
            log.error("GraphQLManager#init", e);
        }
    }
}

接下来是DataFetcher的实现

@Slf4j
@Component
public class QueryCommonProvider implements DataFetcher {

    @Autowired
    private List<IQueryResolver> queryResolverList;

    @Autowired
    private CommonQueryResolver commonQueryResolver;


    /**
     * graphql执行业务实现
     *
     * @param environment
     * @return
     * @throws Exception
     */
    @Override
    public Object get(DataFetchingEnvironment environment) throws Exception {
        GraphQLFieldDefinition fieldDefinition = environment.getFieldDefinition();
        String name = fieldDefinition.getName();
        Object[] traslatedArgs = new Object[0];
        Method currentMethord = null;
        IQueryResolver curResolver = null;
        for (IQueryResolver resolver : this.queryResolverList) {
            Method[] methods = resolver.getClass().getDeclaredMethods();
            for (Method method : methods) {
                if (method.getName().equals(name)) {
                    currentMethord = method;
                    curResolver = resolver;
                    break;
                }
            }
        }
        if (currentMethord == null) {
            return doExcute(name, traslatedArgs);
        }
        Method real = AopUtils.getMostSpecificMethod(currentMethord,
                AopUtils.getTargetClass(curResolver));
        try {
            traslatedArgs = ValueTypeTranslator
                    .translateArgs(real, environment.getArguments(),
                            fieldDefinition.getArguments());
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return doExcute(name, traslatedArgs);
    }

    /**
     * 遍历service和method寻找匹配的serviceMethod
     *
     * @param functionName
     * @param args
     * @return
     */
    private Object doExcute(String functionName, Object[] args) {
        for (IQueryResolver resolver : this.queryResolverList) {
            Method[] methods = resolver.getClass().getDeclaredMethods();
            for (Method method : methods) {
                if (method.getName().equals(functionName)) {
                    try {
                        return method.invoke(resolver, args);
                    } catch (IllegalAccessException e) {
                        throw new EngineException(e.getMessage());
                    } catch (InvocationTargetException e) {
                        Throwable cause = e.getTargetException();
                        if (cause instanceof EngineException) {
                            throw (EngineException) cause;
                        }
                        throw new EngineException(e.getCause().getMessage());
                    }
                }
            }
        }
        //找不到 走common
        this.commonQueryResolver.excute(functionName, args);
        return null;
    }

}

这里讲一下graphql和java的对应规则

  1. 定义一个接口IResolver标志此类是graphql的业务解析类
  2. IQueryResolver和IMutationResolver继承与IResolver,用于区分query和Mutation的操作类型
  3. 所有在graphql中定义的方法,都需要在IResolver的实现类中有同名同参的java方法用于执行业务

按此思路,QueryCommonProvider类实现了以上的规则

要点解析:

  • queryResolverList 是系统中所有 IQueryResolver的实现类
  • get() 实现DataFetcher的get方法,这里通过遍历IQueryResolver的实现类和每个类的method,寻找名称匹配的method方法,用于执行业务

接下来是IQueryResolver和IResolver

public interface IResolver {
}
public interface IQueryResolver extends IResolver {
}

到此,graphql用于实战开发的架子已经基本完成了,相比于之前的demo,这里实现了动态解析graphql的功能。

接下来是测试效果

首先在项目中建一个schema.graphqls 的文件

#as super schema
#三大根类型(root type: query mutation subscription)定义的时候本身或者继承其的子类必须有方法,否则就会报错
schema {
    # 查询
    query: Query
    # 变更
    mutation: Mutation
    # 暂不使用subscription
    # subscription: Subscription

}

# 查询
type Query{
    queryProductById(id:ID!):Product
}
# 变更
type Mutation {
    updateProduct:Int
}

type Product {
    id:ID
    name:String
    sppu:String
    price:Int
}

这里定义了一个queryProductById的查询方法

则对应的java应该是这样

@Data
@Builder
public class Product {
    private Long id;
    private String sppu;
    private String name;
    private Long price;
}
@Service
public class ProductService implements IQueryResolver {

    public Product queryProductById(Long id) {
        return Product.builder()
                .id(id)
                .name("product")
                .price(592L)
                .sppu("post")
                .build();
    }
}

这里实现IQueryResolver,表示这是一个query的实现

然后需要一个web访问入口

@RequestMapping("graphql")
@RestController
public class GraphQLController {

    @Autowired
    private GraphQLManager graphQLManager;

    @Autowired
    private GraphQL graphQL;

    @RequestMapping("query")
    @ResponseBody
    public Object query(@RequestParam("query") String query) {
        ExecutionResult result = this.graphQL.execute(query);
        return result.toSpecification();
    }
}

启动测试

http://127.0.0.1:8080/graphql/query?query={queryProductById(id:99562){id name sppu}}

响应为

{
    "data": {
        "queryProductById": {
            "id": "99562",
            "name": "product",
            "sppu": "post"
        }
    }
}

源码地址:https://gitee.com/chengluchao/graphql-clc/tree/dev-20200928/graphql-engine/src/main/java/com/xlpan/engine

在后续还会做持续优化,如统一业务返回格式,request的传递,分页策略和sql生成等。

 类似资料: