1. 概述
GraphQL是一种用于Web API的查询和操作语言。为了使与GraphQL的集成更加流畅,SPQR(Schema Publisher & Query Resolver)库应运而生。
在这个教程中,我们将学习GraphQL SPQR的基本知识,并在一个简单的Spring Boot项目中看到它的应用。
2. 什么是GraphQL SPQR?
GraphQL是由Facebook创建的知名查询语言。核心是模式定义文件,其中我们定义自定义类型和函数。
传统方法是,要在项目中添加GraphQL,通常需要两步:首先,我们需要在项目中添加GraphQL模式文件;其次,需要为模式文件中的每个类型编写相应的Java持久化对象(POJO)。这意味着我们在模式文件和Java类中维护着相同的信息,这既容易出错又增加了项目的维护负担。
GraphQL Schema Publisher & Query Resolver(简称SPQR)是为了减少上述问题而诞生的,它能从注解的Java类自动生成GraphQL模式。
3. 使用Spring Boot引入GraphQL SPQR
接下来,我们将通过一个简单服务来展示SPQR的使用。我们将使用graphql-spring-boot-starter
和GraphQL SPQR。
3.1. 设置
首先,我们在pom.xml
中添加SPQR和Spring Boot的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>spqr</artifactId>
<version>0.12.4</version>
</dependency>
3.2. 创建模型类Book
添加了必要的依赖后,我们创建一个简单的Book
类:
public class Book {
private Integer id;
private String author;
private String title;
}
如上所示,这个类并未包含任何SPQR注解。如果对源代码没有所有权,但希望利用此库,这非常有用。
3.3. 创建BookService
为了管理书籍集合,我们将创建一个IBookService
接口:
public interface IBookService {
Book getBookWithTitle(String title);
List<Book> getAllBooks();
Book addBook(Book book);
Book updateBook(Book book);
boolean deleteBook(Book book);
}
然后提供接口的实现:
@Service
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@Override
public Book getBookWithTitle(String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@Override
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@Override
public Book addBook(Book book) {
BOOKS_DATA.add(book);
return book;
}
@Override
public Book updateBook(Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@Override
public boolean deleteBook(Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkien", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}
3.4. 通过graphql-spqr暴露服务
最后一步是创建解析器,以暴露GraphQL的查询和突变。为此,我们将使用两个重要的SPQR注解:@GraphQLMutation
和@GraphQLQuery
:
@Service
public class BookResolver {
@Autowired
IBookService bookService;
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return bookService.getBookWithTitle(title);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
return bookService.addBook(book);
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
return bookService.updateBook(book);
}
@GraphQLMutation(name = "deleteBook")
public void deleteBook(@GraphQLArgument(name = "book") Book book) {
bookService.deleteBook(book);
}
}
如果我们不想在每个方法中都写@GraphQLArgument
,并且满足于将GraphQL参数命名为输入参数,可以使用-parameters
编译选项。
3.5. REST控制器
最后,我们将定义一个Spring的@RestController
。为了通过SPQR暴露服务,我们需要配置GraphQLSchema
和GraphQL
对象:
@RestController
public class GraphqlController {
private final GraphQL graphQL;
@Autowired
public GraphqlController(BookResolver bookResolver) {
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withBasePackages("com.baeldung")
.withOperationsFromSingleton(bookResolver)
.generate();
this.graphQL = new GraphQL.Builder(schema)
.build();
}
重要的是,我们必须将BookResolver
注册为单例。
在SPQR旅程的最后一步,我们需要创建一个/graphql
端点,作为与服务交互的单一入口点,执行请求的查询和突变:
@PostMapping(value = "/graphql")
public Map<String, Object> execute(@RequestBody Map<String, String> request, HttpServletRequest raw)
throws GraphQLException {
ExecutionResult result = graphQL.execute(request.get("query"));
return result.getData();
}
}
3.6. 结果
我们可以通过检查/graphql
端点来查看结果。例如,执行以下cURL命令获取所有Book
记录:
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"{getAllBooks {id author title }}"}' \
http://localhost:8080/graphql
3.7. 测试
配置完成后,我们可以测试我们的项目。我们将使用@SpringBootTest
来测试新端点并验证响应。让我们定义JUnit测试并注入所需的WebTestClient
:
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = SpqrApp.class)
class SpqrAppIntegrationTest {
private static final String GRAPHQL_PATH = "/graphql";
@Autowired
private WebTestClient webTestClient;
@Test
void whenGetAllBooks_thenValidResponseReturned() {
String getAllBooksQuery = "{getAllBooks{ id title author }}";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(getAllBooksQuery)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.getAllBooks").isNotEmpty();
}
@Test
void whenAddBook_thenValidResponseReturned() {
String addBookMutation = "mutation { addBook(newBook: {id: 123, author: \"J. K. Rowling\", "
+ "title: \"Harry Potter and Philosopher's Stone\"}) { id author title } }";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(addBookMutation)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.addBook.id").isEqualTo("123")
.jsonPath("$.addBook.title").isEqualTo("Harry Potter and Philosopher's Stone")
.jsonPath("$.addBook.author").isEqualTo("J. K. Rowling");
}
private static String toJSON(String query) {
try {
return new JSONObject().put("query", query).toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
4. 使用GraphQL SPQR Spring Boot Starter
SPQR团队还创建了一个Spring Boot启动器,使得使用起来更加方便。让我们了解一下!
4.1. 设置
首先,在pom.xml
中添加spqr-spring-boot-starter
:
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
4.2. BookService
然后,我们需要对BookService
进行两个修改。首先,它需要被@GraphQLApi
注解。此外,我们想要在API中公开的方法也需要相应的注解:
@Service
@GraphQLApi
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "deleteBook")
public boolean deleteBook(@GraphQLArgument(name = "book") Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkein", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}
如你所见,我们基本上将BookResolver
中的代码移到了BookService
中。另外,我们不需要GraphqlController
类 - /graphql
端点会自动添加。
5. 总结
GraphQL是一个激动人心的框架,也是传统RESTful端点的替代方案。尽管提供了大量灵活性,但它也可能增加一些繁琐的任务,如维护模式文件。SPQR的目标是让与GraphQL的协作变得更简单、更少出错。
在这篇文章中,我们看到了如何将SPQR添加到现有的POJO中,并配置它来处理查询和突变。然后,我们看到了在GraphQL中实际运行的新端点。最后,我们使用Spring的测试支持验证了应用程序的行为。
如往常一样,本文使用的示例代码可在GitHub上找到。SPQR Spring Boot启动套件的代码可以在GitHub上找到。