GraphQL實戰-第四篇-構建開發框架
https://blog.csdn.net/xplan5/article/details/108846012
前三篇是關於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的對應規則
- 定義一個接口IResolver標志此類是graphql的業務解析類
- IQueryResolver和IMutationResolver繼承與IResolver,用於區分query和Mutation的操作類型
- 所有在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"
}
}
}
在后續還會做持續優化,如統一業務返回格式,request的傳遞,分頁策略和sql生成等。