這篇文要來紀錄一下 Java 的 GraphQL 純 Spring MVC 實作練習,
因為網路上的許多範例都是使用
SpringBoot 來實作,
所以在這裡紀錄一下使用純 Spring MVC 、不使用
SpringBoot 的方法。
在這裡想要實作的情境是:
我們有 Author、和 Book 兩種資料,
Author 和
Book 都有 id、name 屬性,
Author 還有 bookIdList 屬性,是一個
Array,紀錄著此 Author 撰寫的 Book 的 id。
我們要實作兩個 query
和一個 mutation,分別是:
Author getAuthorById(id) :
用 Author id 查 Author。
回傳的 bookList 可以給一個 prefix
參數,它可以為回傳的 book name 前面加上 prefix
(這只是用來練習如何在 Java
GraphQL server 端取得 Query 的參數)。
Book getBookById(id) :
用 Book id 查 Book,
Author updateAuthorName(id, name):
修改特定 id 的 Author 的 name。
GraphQL 的 Schema 設計如下,其中 Date 是我們自訂的 type,之後會建立 GraphQLScalarType 類別來解析它:
scalar Date type Book { id: ID! name: String publishDate: Date } type Author { id: ID! name: String bookList(prefix: String): [Book] } type Query { getAuthorById(id: ID!): Author getBookById(id: ID!): Book } type Mutation { updateAuthorName(id: ID!, name: String!): Author }
Query Request 的例子如下:
query { getAuthorById(id: 3) { id name bookList(prefix: "test") { id name publishDate } } getBookById(id: 33) { id name publishDate } }
Mutatio Request 的例子如下:
mutation { updateAuthorName(id: 3, name: "Author 333") { id name } }
以下開始實作,這裡使用 Opoen JDK 11,
先建立一個 archetype 為 maven-archetype-webapp
的 Java Dynamic Web Application Maven 專案,可以參考這裡,
首先先來看一下專案的結構設計,如下圖所示:
接著開始說明,
因為我們要使用 Spring MVC 和 GraphQL 的 library,所以在 pom.xml 加上以下 dependency:
pom.xml (部份內容):
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency> <!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java --> <dependency> <groupId>com.graphql-java</groupId> <artifactId>graphql-java</artifactId> <version>20.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.graphql-java-kickstart/graphql-java-tools --> <dependency> <groupId>com.graphql-java-kickstart</groupId> <artifactId>graphql-java-tools</artifactId> <version>13.0.3</version> </dependency>
接著在 web.xml 裡設定把流量導向 Spring MVC:
/src/main/webapp/WEB-INF/web.xml:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/mvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
設定 Spring MVC 的設定檔:
src/main/webapp/WEB-INF/mvc-config.xml:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:util="http://www.springframework.org/schema/util" xmlns:oxm="http://www.springframework.org/schema/oxm" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd" > <context:component-scan base-package="com.*"/> <aop:aspectj-autoproxy/> <mvc:annotation-driven/> </beans>
說明:把 GraphQL 的 Schema 設定在 schema.graphqls 中。
/src/main/resources/schema.graphqls:
scalar Date type Book { id: ID! name: String publishDate: Date } type Author { id: ID! name: String bookList(prefix: String): [Book] } type Query { getAuthorById(id: ID!): Author } type Mutation { updateAuthorName(id: ID!, name: String!): Author }
再來建好需要的 Bean 。
/src/java/com/bean/Author.java:
package com.bean; import java.util.List; public class Author { private int id; private String name; private List<Integer> bookIdList; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Integer> getBookIdList() { return bookIdList; } public void setBookIdList(List<Integer> bookIdList) { this.bookIdList = bookIdList; } }
/src/java/com/bean/Book.java:
package com.bean; import java.util.Calendar; public class Book { private int id; private String name; Calendar publishDate; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Calendar getPublishDate() { return publishDate; } public void setPublishDate(Calendar publishDate) { this.publishDate = publishDate; } }
建好查詢資料和修改資料的 DAO (這邊沒有使用真正的資料庫,只是建假的資料來模擬)。
/src/main/java/com/dao/AuthorDAO.java:
package com.dao; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Repository; import com.bean.Author; @Repository public class AuthorDAO { private static List<Author> authorList; public AuthorDAO() { authorList = new ArrayList<Author>(); //Set mock data to simulate data in database. Author author = new Author(); author.setId(1); author.setName("Author 1"); author.setBookIdList(List.of(1, 11)); authorList.add(author); author = new Author(); author.setId(2); author.setName("Author 2"); author.setBookIdList(List.of(2, 22)); authorList.add(author); author = new Author(); author.setId(3); author.setName("Author 3"); author.setBookIdList(List.of(3, 33)); authorList.add(author); } public Author getAuthorById(int id) { return authorList.stream().filter((Author author) -> { return author.getId() == id; }).findAny().orElse(null); } public Author updateAuthorName(int id, String name) { Author author = getAuthorById(id); if (author != null) { author.setName(name); } return author; } }
/src/main/java/com/dao/BookDAO.java:
package com.dao; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Repository; import com.bean.Author; import com.bean.Book; @Repository public class BookDAO { private static List<Book> bookList; public BookDAO() { bookList = new ArrayList<Book>(); //Set mock data to simulate data in database. Book book = new Book(); book.setId(1); book.setName("Book 1"); book.setPublishDate(getCalendarByStr("2019-01-01T01:00:00+0800")); bookList.add(book); book = new Book(); book.setId(11); book.setName("Book 11"); book.setPublishDate(getCalendarByStr("2019-01-01T01:01:00+0800")); bookList.add(book); book = new Book(); book.setId(2); book.setName("Book 2"); book.setPublishDate(getCalendarByStr("2019-01-01T02:00:00+0800")); bookList.add(book); book = new Book(); book.setId(22); book.setName("Book 22"); book.setPublishDate(getCalendarByStr("2019-01-01T02:02:00+0800")); bookList.add(book); book = new Book(); book.setId(3); book.setName("Book 3"); book.setPublishDate(getCalendarByStr("2019-01-01T03:00:00+0800")); bookList.add(book); book = new Book(); book.setId(33); book.setName("Book 33"); book.setPublishDate(getCalendarByStr("2019-01-01T03:03:00+0800")); bookList.add(book); } public Book getBookById(int id) { return bookList.stream().filter((Book book) -> { return book.getId() == id; }).findAny().orElse(null); } public List<Book> getBookListByAuthor(Author author) { if (author == null || author.getBookIdList() == null || author.getBookIdList().size() == 0) { return new ArrayList<Book>(); } return author.getBookIdList().stream().map((Integer bookId) -> { return bookList.stream().filter((Book book) -> { return book.getId() == bookId; }).findAny().orElse(null); }).filter((Book book) -> { return book != null; }) .collect(Collectors.toList()); } Calendar getCalendarByStr(String dateStr) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); Calendar date = Calendar.getInstance(); try { date.setTime(sdf.parse(dateStr)); } catch (ParseException e) { e.printStackTrace(); } return date; } }
我們這邊建一個 GraphqlController.java 來對應至GraphQL server 會收到的
request body JSON,
等一下在 Spring MVC 的 controller 可以直接把 GraphQL
request 的值用 GraphqlController.java 來接,
這邊說明一下,Client 端要向
GraphQL 發起請求的時候,可以使用 HTTP GET 及 HTTP POST 的方式:
-
HTTP GET:
將參數直接串在網址後面,例如以下例子,不過要注意有些限制,像是無法使用 variables、mutation (雖然官方這樣說,不過其實如果 GraphQL server 是像這篇例子一樣自己實做的話,還是可以自己去設計取得 variables, mutation 實作行為,例如 query=mutation {...}) 等,可參考 Making a GraphQL requests using the GET method:https://xxx/xxx?query=...&operationName=...
-
HTTP POST:
將參數 (query、operationName, variables 等) 直接放在 HTTP Body 中 (相當於 Postman 的 raw 格式),例如:{ "query": "...", "operationName": "...", "variables": { "myVariable": "someValue", ... } }
詳細可以參考這裡的說明: HTTP Methods, Headers, and Body。
而 GraphqlController.java 我們把它設計成可對應 HTTP POST Body 的 JSON (有 query, operationName, variables 等屬性)。
/src/main/java/com/query/GraphqlRequestQuery.java:
package com.query; import java.util.Map; public class GraphqlRequestQuery { String query; String operationName; Map<String, Object> variables = new HashMap<>(); public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } public String getOperationName() { return operationName; } public void setOperationName(String operationName) { this.operationName = operationName; } public Map<String, Object> getVariables() { return variables; } public void setVariables(Map<String, Object> variables) { this.variables = variables; } }
建立各個 Resolver 和 Mutation,這會分別對應到 GraphQL 的 query 和
mutation,
說明都寫在注釋裡。
QueryResolver.java 對應到 GraphQL query,裡面會實作 getAuthorById() 和 getBookById() 方法
/src/main/java/com/resolver/QueryResolver.java:
package com.resolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.bean.Author; import com.bean.Book; import com.dao.AuthorDAO; import com.dao.BookDAO; import graphql.kickstart.tools.GraphQLQueryResolver; @Component //對應至 GraphQL request 的 query public class QueryResolver implements GraphQLQueryResolver { @Autowired AuthorDAO authorDAO; @Autowired BookDAO bookDAO; //對應至 GraphQL request query 的 getAuthorById public Author getAuthorById(int id) { return authorDAO.getAuthorById(id); } //對應至 GraphQL request query 的 getBookById public Book getBookById(int id) { return bookDAO.getBookById(id); } }
AuthorResolver.java 對應到 GraphQL query 回傳 Author 的 bookList
屬性實作方法,
因為在 QueryResolver.java 中 getAuthorById() 回傳的 Author
裡面找不到 bookList 屬性 (Author.java 只有 bookIdList 和 getBookIdList() 的
getter ,並且 bookIdList 是 int[],也不是 Book[] ),
所以我們必須為
Author 的 bookList 屬性實作值的取得方法。
/src/main/java/com/resolver/AuthorResolver.java:
package com.resolver; import java.util.List; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.bean.Author; import com.bean.Book; import com.dao.BookDAO; import graphql.kickstart.tools.GraphQLResolver; @Component //對應至 GraphQL request query 中,Author 屬性值的 Resolver 獲取方式 public class AuthorResolver implements GraphQLResolver<Author> { @Autowired BookDAO bookDAO; //對應至 GraphQL request query 中,Author 屬性 bookList 值的 Resolver 獲取方式 List<Book> bookList(Author author, String prefix) { //可獲取 Parent (也就是 Author) 的資料和傳入參數 (此例為 prefix) List<Book> bookList = bookDAO.getBookListByAuthor(author); if (StringUtils.isNotBlank(prefix)) { bookList = bookList.stream().map((Book book) -> { Book renamedBook = new Book(); renamedBook.setId(book.getId()); renamedBook.setName(prefix + "-" + book.getName()); renamedBook.setPublishDate(book.getPublishDate()); return renamedBook; }).collect(Collectors.toList()); } return bookList; } }
MutationResolver.java 對應至 GraphQL mutation,裡面會實作 updateAuthorName
/src/main/java/com/resolver/MutationResolver.java:
package com.resolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.bean.Author; import com.dao.AuthorDAO; import graphql.kickstart.tools.GraphQLMutationResolver; @Component //對應至 GraphQL request 的 mutation public class MutationResolver implements GraphQLMutationResolver{ @Autowired AuthorDAO authorDAO; //對應至 GraphQL request mutation 的 updateAuthorName public Author updateAuthorName(int id, String name) { return authorDAO.updateAuthorName(id, name); } }
接下來我們要來處理自訂的 Date Scalar , 我們這邊建立一個 DateScalarBuilder.java 來回傳自訂實作的 GraphQLScalarType,關於 parseValue() 和 parseLiteral() 的差異可以參考官方這篇 Scalars in GraphQL 和 Stackoverflow 這篇 what's the difference between parseValue and parseLiteral in GraphQLScalarType 別人的回答
DateScalarBuilder.java:
package com.scalar; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import graphql.language.StringValue; import graphql.schema.Coercing; import graphql.schema.GraphQLScalarType; public class DateScalarBuilder { private DateScalarBuilder() { } public static GraphQLScalarType getDateScalar() { String dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; return GraphQLScalarType.newScalar() .name("Date") .description("A custom scalar that handles date.") .coercing(new Coercing<Calendar, String>() { @Override public String serialize(Object dataFetcherResult) { //實作從 Java 物件 (此例是 Calendar) 轉成要回傳 graphql response 的 String Calendar date = (Calendar) dataFetcherResult; SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); return sdf.format(date.getTime()) ; } @Override public Calendar parseValue(Object input) { //實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar) //如果 input 使用了 inline input 的方式,就會使用 parseValue() 來解析 //例如: /* * Request: * query myQuery { * * getBookById(id: 1) { * id * name * } * } */ String dateStr = (String) input; SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); Calendar date = Calendar.getInstance(); try { date.setTime(sdf.parse(dateStr)); } catch (ParseException e) { e.printStackTrace(); } return date; } @Override public Calendar parseLiteral(Object input) { //實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar) //如果 input 使用了 variables 的方式,就會使用 parseLiteral() 來解析 //例如: /* * Request: * query myQuery($id: ID) { * * getBookById(id: $id) { * id * name * } * } * * Variables: * { * "id": 1 * } */ String dateStr = ((StringValue) input).getValue(); SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); Calendar date = Calendar.getInstance(); try { date.setTime(sdf.parse(dateStr)); } catch (ParseException e) { e.printStackTrace(); } return date; } }) .build(); } }
GraphqlController.java 是 SpringMVC 的 Controller 設定,
這裡我們設定了
/graphql 為處理 GraphQL request 的 url,
限制只接收 HTTP POST 且 Mime
type 為 application/json 格式的 request。
接著讀取 GraphQL Schema 、設定要用的 Resolver 、 Scalar,並配合接收到的 GraphQL
request
產生並回傳查詢結果。
src/java/com/controller/GraphqlController.java:
package com.controller; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.kickstart.tools.SchemaParser; import graphql.schema.GraphQLSchema; import com.scalar.DateScalarBuilder; import com.query.GraphqlRequestQuery; import com.resolver.AuthorResolver; import com.resolver.MutationResolver; import com.resolver.QueryResolver; @Controller public class GraphqlController { @Autowired private QueryResolver queryResolver; @Autowired private AuthorResolver authorResolver; @Autowired private MutationResolver mutationResolver; @RequestMapping(value="/graphql", method = {RequestMethod.POST}, produces = "application/json") public @ResponseBody Map<String, Object> myGraphql(@RequestBody GraphqlRequestQuery requestQuery) { //讀取 GraphQL Schema、設定各 Resolver GraphQLSchema graphQLSchema = SchemaParser.newParser() .file("schema.graphqls") .scalars(DateScalarBuilder.getDateScalar()) .resolvers(queryResolver , authorResolver , mutationResolver) .build() .makeExecutableSchema(); GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); //將 GraphQL request 的 query (也可視需要取出 operationName, variables) 值取出,準備進行查詢 ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(requestQuery.getQuery()) .operationName(requestQuery.getOperationName()) .variables(requestQuery.getVariables()) .build(); ExecutionResult executionResult = graphQL.execute(executionInput); //executionResult.toSpecification() 會回傳一個 Map, //裡面包含了查詢結果 data 、 errors、 extensions 等資料 return executionResult.toSpecification(); } }
最後展示一下查詢結果:
- 查詢:
query { getAuthorById(id: 3) { id name bookList(prefix: "test") { id name publishDate } }, getBookById(id: 33) { id name publishDate } }
結果:{ "data": { "getAuthorById": { "id": "3", "name": "Author 3", "bookList": [ { "id": "3", "name": "test-Book 3" "publishDate": "2019-01-01T03:00:00+0800" }, { "id": "33", "name": "test-Book 33" "publishDate": "2019-01-01T03:03:00+0800" } ] }, "getBookById": { "id": "33", "name": "Book 33" "publishDate": "2019-01-01T03:03:00+0800" } } }
- 查詢:
mutation { updateAuthorName(id: 3, name: "Author 3 - changed") { id name bookList(prefix: "test") { id name publishDate } } }
結果:{ "data": { "updateAuthorName": { "id": "3", "name": "Author 3 - changed", "bookList": [ { "id": "3", "name": "test-Book 3" "publishDate": "2019-01-01T03:00:00+0800" }, { "id": "33", "name": "test-Book 33" "publishDate": "2019-01-01T03:03:00+0800" } ] } } }
下載分享:
參考資料:
沒有留言 :
張貼留言