在GraphQL(一):GraphQL介紹中講到目前已經(jīng)有很多平臺完成了GraphQL實現(xiàn)勤揩,這里以Java平臺為例泉蝌,介紹GraphQL服務(wù)的搭建。
graphql-java + graphql-java-spring
graphql-java是GraphQL的Java實現(xiàn)笔咽,它實現(xiàn)了GraphQL的執(zhí)行茬暇,但是沒有任何關(guān)于HTTP或者JSON的處理,因此在接入SpringBoot時還需要graphql-java-spring的支持敢课。官方的案例就是使用這兩個jar包完成的阶祭。
在官方的案例中绷杜,我們需要實例化一個GraphQL實例:
@Component
public class GraphQLProvider {
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.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();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
@Bean
public GraphQL graphQL() {
return graphQL;
}
}
這樣的實現(xiàn)需要我們了解較多graphql-java的底層細(xì)節(jié),比如:TypeDefinitionRegistry濒募、RuntimeWiring鞭盟、SchemaGenerator等,同時還需要硬編碼字符串瑰剃。
同樣齿诉,在實現(xiàn)數(shù)據(jù)注入時,也需要硬編碼:
public DataFetcher getBookByIdDataFetcher() {
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
return dataFetchingEnvironment -> {
Map<String, String> book = dataFetchingEnvironment.getSource();
String authorId = book.get("authorId");
return authors
.stream()
.filter(author -> author.get("id").equals(authorId))
.findFirst()
.orElse(null);
};
}
于是就有了 graphql-spring-boot-starter + graphql-java-tools 搭建GraphQL的方案晌姚。
graphql-spring-boot-starter + graphql-java-tools
graphql-java-tools
graphql-java-tools 能夠從GraphQL的模式定義 *.graphqls 文件構(gòu)建出對應(yīng)的Java的POJO類型對象(graphql-java-tools將讀取classpath下所有以*.graphqls為后綴名的文件粤剧,創(chuàng)建GraphQLSchema對象),同時為我們屏蔽了graphql-java的底層細(xì)節(jié)挥唠,它本身依賴graphql-java抵恋。
graphql-spring-boot-starter
graphql-spring-boot-starter是輔助SpringBoot接入GraphQL的庫,它本身依賴graphql-java和graphql-java-servlet(將GraphQL服務(wù)發(fā)布為通過HTTP可訪問的Web服務(wù)宝磨,封裝了一個GraphQLServlet接收GraphQL請求弧关,并提供Servlet Listeners功能)。
接下來我們將實現(xiàn)一個基于 graphql-spring-boot-starter + graphql-java-tools 搭建GraphQL服務(wù)的Demo唤锉。
Demo
1. 在pom中增加以下依賴
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>3.6.0</version>
</dependency>
對應(yīng)的SpringBoot版本是1.5.6
2. 增加Teacher實體
@Serialization
data class Teacher(
val id: String = "commonId",
var teacherId: String = "",
var teacherName: String = "",
var teacherPhone: String = "",
var schoolId: String = ""
) {
override fun toString(): String {
return JSON.toJSONString(this)
}
}
以及對應(yīng)的Dao梯醒、Service、teacher.xml等
3. 在classpath下新建schema.graphqls
type School {
id: ID!
schoolId: String
schoolName: String
schoolAge: Int
schoolAddress: String
teachers: [Teacher]
master: String
}
type Teacher{
teacherId: String
teacherName: String
teacherPhone: String
schoolId: String
}
input TeacherInput{
teacherId: String
teacherName: String
teacherPhone: String
schoolId: String
}
這里的模型最好和Java Bean一致腌紧,如果Java bean中有多余的字段,將被忽略畜隶,不會拋出異常壁肋。
4. 在classpath下新建root.graphqls
這是公開API的地方,按照GraphQL的規(guī)范籽慢,Query浸遗、Mutation、Subscription三種查詢類型需要放在各自的節(jié)點下(這里暫時不考慮訂閱):
type Query{
# 根據(jù)學(xué)校Id查詢學(xué)校箱亿,schoolId不能為空跛锌,返回的School不能為空
school(schoolId:String!):School!
}
type Mutation {
insertSchool(schoolId: String!,schoolName:String!,schoolAge:Int!,schoolAddress:String!) : School!
insertTeacher(teacher:TeacherInput!):Teacher!
}
5. 實現(xiàn)Resolver
graphql-java-tools為我們屏蔽了底層細(xì)節(jié),我們只需要繼承以下幾個類完成數(shù)據(jù)注入即可:
- GraphQLQueryResolver
- GraphQLMutationResolver
- GraphQLSubscriptionResolver
Resolver完成的是數(shù)據(jù)的注入届惋,也就是對*.graphqls文件中的type的字段的數(shù)據(jù)進(jìn)行注入髓帽,注入需要滿足以下規(guī)則:
1. <field>
2. is<field> – only if the field is of type Boolean
3. get<field>
比如我們我們根據(jù)學(xué)校Id查詢學(xué)校的API:
@Component
class SchoolQueryResolver : GraphQLQueryResolver {
@Autowired
private lateinit var schoolService: SchoolService
fun school(schoolId: String): School {
return schoolService.getSchoolBySchoolId(schoolId)
}
//或者
fun getSchool(schoolId: String): School {
return schoolService.getSchoolBySchoolId(schoolId)
}
}
我們在schema.graphqls中定義的類型有與之對應(yīng)的Java Bean,這些Java Bean都提供了getField方法脑豹,因此不需要額外實現(xiàn)Resolver郑藏,有時候,在type中定義的類型的某個字段數(shù)據(jù)的獲取比較麻煩瘩欺,不是簡單的getField可以解決的必盖,此時可以為此類型實現(xiàn)專門的字段值獲取的Resolver拌牲,假設(shè)School中的master字段邏輯獲取邏輯很復(fù)雜:
public class SchoolResolver implements GraphQLResolver<School> {
private SchoolDao schoolDao;
public School getMaster(School school) {
return schoolDao.getMasterById(school.getMasterId());
}
}
泛型中需要指定類型,字段數(shù)據(jù)獲取的方法名稱規(guī)則和常規(guī)接口的規(guī)則一致歌粥,只是需要把該類型作為參數(shù)傳遞到方法內(nèi)塌忽,值得注意的是,如果客戶端沒有請求Master字段失驶,那么getMaster方法將不會被執(zhí)行土居。
實際上針對type中的每個Field都需要有g(shù)etField,使得Graphql能夠獲取到數(shù)據(jù)注入到返回的結(jié)果中突勇,如果針對此Field已經(jīng)實現(xiàn)了Resolver装盯,那么會優(yōu)先使用Resolver來注入數(shù)據(jù),此時可以省略掉getField(直接去掉School Bean中的master字段)不過還是建議將Java Bean和type中的Field一一對應(yīng)甲馋,便于維護(hù)埂奈。
以上是針對Query的Demo,關(guān)于Mutation請查看文本的源碼定躏,這里需要說明的是我們的insertSchool和insertTeacher有些不同:
insertSchool(schoolId: String!,schoolName:String!,schoolAge:Int!,schoolAddress:String!) : School!
insertTeacher(teacher:TeacherInput!):Teacher!
insertTeacher引入了一個新類型TeacherInput账磺,將需要傳遞到服務(wù)端的數(shù)據(jù)封裝起來,GraphQL的返回類型(Teacher)和輸入類型(TeacherInput)是不能共用的痊远,所以加上Input后綴加以區(qū)分垮抗,同樣的,針對TeacherInput也需要有對應(yīng)的Java Bean碧聪。