Spring中如何通过REST URI执行GraphQL查询?报错排查
需求说明
我需要实现一个REST URI(如/books),使其仅执行与书籍相关的GraphQL查询,查询逻辑依赖?id、?name等请求参数。
遇到的错误
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Circular view path [books]: would dispatch back to the current handler URL [/books] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)] with root cause
jakarta.servlet.ServletException: Circular view path [books]: would dispatch back to the current handler URL [/books] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
请问该需求是否可以实现?若可以,我在代码中哪里出现了错误?
相关代码
控制器代码
package com.example.graphqlserver; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.graphql.data.method.annotation.SchemaMapping; // import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class BookController { @GetMapping("/books") public Book getBook(@RequestParam(name = "id", defaultValue = "") String id) { return this.bookById(id); } @QueryMapping public Book bookById(@Argument String id) { return Book.getById(id); } @SchemaMapping public Author author(Book book) { return Author.getById(book.authorId()); } }
Book类代码
package com.example.graphqlserver; import java.util.Arrays; import java.util.List; public record Book(String id, String name, int pageCount, String authorId) { private static List<Book> books = Arrays.asList( new Book("book-1", "Effective Java", 416, "author-1"), new Book("book-2", "Hitchhiker's Guide to the Galaxy", 208, "author-2"), new Book("book-3", "Down Under", 436, "author-3")); public static Book getById(String id) { return books.stream() .filter(book -> book.id().equals(id)) .findFirst() .orElse(null); } }
Author类代码
package com.example.graphqlserver; import java.util.Arrays; import java.util.List; public record Author(String id, String firstName, String lastName) { private static List<Author> authors = Arrays.asList( new Author("author-1", "Joshua", "Bloch"), new Author("author-2", "Douglas", "Adams"), new Author("author-3", "Bill", "Bryson")); public static Author getById(String id) { return authors.stream() .filter(author -> author.id().equals(id)) .findFirst() .orElse(null); } }
GraphQL Schema
type Query { bookById(id: ID): Book } type Book { id: ID name: String pageCount: Int author: Author } type Author { id: ID firstName: String lastName: String }
该需求完全可以实现,你代码中的问题出在**@RestController注解的返回值处理逻辑**上,具体原因及修复方案如下:
错误原因
虽然你使用了@RestController(本质是@Controller + @ResponseBody),正常情况下会将返回对象序列化为JSON,但当Book.getById()返回null时(比如传入不存在的id,或未传id时使用默认空字符串查询),Spring MVC会认为没有指定视图,自动生成与请求路径同名的视图名books,随后试图解析这个视图时,又匹配到了@GetMapping("/books")的处理器,最终导致循环调用,抛出错误。
修复方案
方案1:用ResponseEntity控制响应,处理null情况
修改getBook方法,通过ResponseEntity灵活控制HTTP状态码和响应体,避免返回null:
@GetMapping("/books") public ResponseEntity<Book> getBook(@RequestParam(name = "id", defaultValue = "") String id) { Book book = this.bookById(id); if (book == null) { return ResponseEntity.notFound().build(); // 查询不到时返回404 } return ResponseEntity.ok(book); }
方案2:确保查询结果非空
修改Book.getById方法,当查询不到书籍时返回一个默认空对象(语义上不如返回404清晰,按需选择):
public static Book getById(String id) { return books.stream() .filter(book -> book.id().equals(id)) .findFirst() .orElse(new Book("", "", 0, "")); }
额外优化:支持name参数查询
根据需求扩展getBook方法,增加对name参数的处理:
@GetMapping("/books") public ResponseEntity<List<Book>> getBooks( @RequestParam(name = "id", required = false) String id, @RequestParam(name = "name", required = false) String name) { List<Book> result = new ArrayList<>(); if (id != null && !id.isEmpty()) { Book book = Book.getById(id); if (book != null) { result.add(book); } } else if (name != null && !name.isEmpty()) { result = Book.books.stream() .filter(book -> book.name().contains(name)) .collect(Collectors.toList()); } else { // 无参数时返回所有书籍,或根据需求返回400 result = Book.books; } return result.isEmpty() ? ResponseEntity.notFound().build() : ResponseEntity.ok(result); }
注意需要将Book类中的books列表改为public,或添加对应的公共查询方法。
补充说明
你代码中的@QueryMapping和@SchemaMapping是用于GraphQL接口的,与这个REST接口互不冲突:GraphQL默认通过/graphql端点访问,你的REST接口是/books,两者可以共存。
内容的提问来源于stack exchange,提问作者Brother Eye

