在GraphQL(一):GraphQL介紹中講到目前已經有不少平臺完成了GraphQL實現,這裏以Java平臺爲例,介紹GraphQL服務的搭建。java
graphql-java是GraphQL的Java實現,它實現了GraphQL的執行,可是沒有任何關於HTTP或者JSON的處理,所以在接入SpringBoot時還須要graphql-java-spring的支持。官方的案例就是使用這兩個jar包完成的。git
在官方的案例中,咱們須要實例化一個GraphQL實例:github
@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;
}
}
複製代碼
這樣的實現須要咱們瞭解較多graphql-java的底層細節,好比:TypeDefinitionRegistry、RuntimeWiring、SchemaGenerator等,同時還須要硬編碼字符串。spring
一樣,在實現數據注入時,也須要硬編碼:springboot
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的方案。bash
graphql-java-tools可以從GraphQL的模式定義*.graphqls文件構建出對應的Java的POJO類型對象(graphql-java-tools將讀取classpath下全部以*.graphqls爲後綴名的文件,建立GraphQLSchema對象。),同時爲咱們屏蔽了graphql-java的底層細節,它自己依賴graphql-java。mybatis
graphql-spring-boot-starter是輔助SpringBoot接入GraphQL的庫,它自己依賴graphql-java和graphql-java-servlet(將GraphQL服務發佈爲經過HTTP可訪問的Web服務,封裝了一個GraphQLServlet接收GraphQL請求,並提供Servlet Listeners功能)。ide
接下來咱們將實現一個基於 graphql-spring-boot-starter + graphql-java-tools 搭建GraphQL服務的Demo。spring-boot
基於SpringBoot集成MyBatis提供GraphQL服務post
<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>
複製代碼
對應的SpringBoot版本是1.5.6
@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)
}
}
複製代碼
以及對應的Dao、Service、teacher.xml等
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中有多餘的字段,將被忽略,不會拋出異常。
這是公開API的地方,按照GraphQL的規範,Query、Mutation、Subscription三種查詢類型須要放在各自的節點下(這裏暫時不考慮訂閱):
type Query{
# 根據學校Id查詢學校,schoolId不能爲空,返回的School不能爲空
school(schoolId:String!):School!
}
type Mutation {
insertSchool(schoolId: String!,schoolName:String!,schoolAge:Int!,schoolAddress:String!) : School!
insertTeacher(teacher:TeacherInput!):Teacher!
}
複製代碼
graphql-java-tools爲咱們屏蔽了底層細節,咱們只須要繼承如下幾個類完成數據注入便可:
Resolver完成的是數據的注入,也就是對*.graphqls文件中的type的字段的數據進行注入,注入須要知足如下規則:
1. <field>
2. is<field> – only if the field is of type Boolean
3. get<field>
複製代碼
好比咱們咱們根據學校Id查詢學校的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中定義的類型有與之對應的Java Bean,這些Java Bean都提供了getField方法,所以不須要額外實現Resolver,有時候,在type中定義的類型的某個字段數據的獲取比較麻煩,不是簡單的getField能夠解決的,此時能夠爲此類型實現專門的字段值獲取的Resolver,假設School中的master字段邏輯獲取邏輯很複雜:
public class SchoolResolver implements GraphQLResolver<School> {
private SchoolDao schoolDao;
public School getMaster(School school) {
return schoolDao.getMasterById(school.getMasterId());
}
}
複製代碼
泛型中須要指定類型,字段數據獲取的方法名稱規則和常規接口的規則一致,只是須要把該類型做爲參數傳遞到方法內,值得注意的是,若是客戶端沒有請求Master字段,那麼getMaster方法將不會被執行。
實際上針對type中的每一個Field都須要有getField,使得Graphql可以獲取到數據注入到返回的結果中,若是針對此Field已經實現了Resolver,那麼會優先使用Resolver來注入數據,此時能夠省略掉getField(直接去掉School Bean中的master字段)不過仍是建議將Java Bean和type中的Field一一對應,便於維護。
以上是針對Query的Demo,關於Mutation請查看文本的源碼,這裏須要說明的是咱們的insertSchool和insertTeacher有些不一樣:
insertSchool(schoolId: String!,schoolName:String!,schoolAge:Int!,schoolAddress:String!) : School!
insertTeacher(teacher:TeacherInput!):Teacher!
複製代碼
insertTeacher引入了一個新類型TeacherInput,將須要傳遞到服務端的數據封裝起來,GraphQL的返回類型(Teacher)和輸入類型(TeacherInput)是不能共用的,因此加上Input後綴加以區分,一樣的,針對TeacherInput也須要有對應的Java Bean。
git@gitee.com:erdao123/springboot_graphql_mybatis_demo.git