1. 簡介
在本教程中,我們將使用 Apache Camel 構建一個小型應用程序,以公開 GraphQL 和 REST API。 Apache Camel 是一種強大的集成框架,它簡化了連接不同系統,包括 API、數據庫和消息服務。
2. 項目設置
首先,在我們的 pom.xml 文件中,我們添加了 Camel core、Jetty、GraphQL 和 Jackson 的依賴項:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>23.1</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jetty</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-graphql</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.0</version>
</dependency>
這些依賴項提供了構建應用程序所需的組件。 camel-jetty 允許我們使用 Jetty 作為嵌入式 Web 服務器來暴露 HTTP 端點,而 camel-graphql 用於服務 GraphQL 查詢。
最後,Jackson 用於處理 JSON 序列化和反序列化。
3. 創建 Book 模型
我們將創建一個簡單的模型類,名為 Book。這個類代表我們希望在 API 中返回的數據:public class Book {
private String id;
private String title;
private String author;
// 構造函數、getter 和 setter 方法
}
4. 創建服務類
接下來,我們創建一個 BookService 類,該類返回書籍列表。為了簡化,我們模擬數據:
public class BookService {
private final List<Book> books = new ArrayList<>();
public BookService() {
books.add(new Book("1", "Clean Code", "Robert"));
books.add(new Book("2", "Effective Java", "Joshua"));
}
public List<Book> getBooks() {
return books;
}
public Book getBookById(String id) {
return books.stream().filter(b -> b.getId().equals(id)).findFirst().orElse(null);
}
public Book addBook(Book book) {
books.add(book);
return book;
}
}
此服務提供三種主要操作:檢索所有書籍、按 ID 獲取書籍以及添加新書籍。
5. 創建 REST 端點與 Camel
我們將使用 Apache Camel 與 Jetty 作為嵌入式 Web 服務器來暴露 RESTful 端點。Camel 簡化了定義 HTTP 端點和路由邏輯,通過 RouteBuilder。
讓我們從在一個名為 BookRoute 的類中定義一個路由開始:
public class BookRoute extends RouteBuilder {
private final BookService bookService = new BookService();
@Override
public void configure() {
onException(Exception.class)
.handled(true)
.setHeader("Content-Type", constant("application/json"))
.setBody(simple("{\"error\": \"${exception.message}\"}"));
restConfiguration()
.component("jetty")
.host("localhost")
.port(8080)
.contextPath("/api")
.bindingMode(RestBindingMode.json);
//...
}
}
configure() 方法是所有路由定義的地方。Camel 在初始化期間調用它來構建處理管道。 我們使用 onException() 方法來創建一個全局異常處理程序,它捕獲在請求處理期間拋出的任何未處理的異常。
restConfiguration() 定義服務器設置。我們使用 Jetty 在端口 8080,並將綁定模式設置為 JSON,以便 Camel 自動將 Java 對象轉換為 JSON 響應。
主機和端口設置確定我們的 API 將何處可訪問,而上下文路徑建立所有端點的基本 URL 前綴。
接下來,我們將創建三個用於管理 Book 資源的端點:
- GET /api/books: 一個 GET 端點用於檢索所有書籍
- GET /api/books/{id}: 一個 GET 端點用於根據 ID 檢索書籍
- POST /api/books: 一個 POST 端點用於添加新書籍
讓我們使用 rest() 定義我們的實際 REST 端點:
rest("/books")
.get().to("direct:getAllBooks")
.get("/{id}").to("direct:getBookById")
.post().type(Book.class).to("direct:addBook");
我們現在定義每個操作的內部 Camel 路由:
- from(“direct:getAllBooks”): 當請求到達 /api/books 時,此路由被觸發。它調用 bookService.getBooks() 以返回書籍列表。
- from(“direct:getBookById”): 此路由在客户端請求書籍 ID 時觸發。路徑變量 id 自動映射到 Camel 標頭 id,該標頭傳遞給 bookService.getBookById()。
- from(“direct:addBook”): 當接收到帶有 JSON 身體的 POST 請求時,Camel 將其反序列化為 Book 對象,並調用 bookService.addBook() 與它。
這些連接 direct:* 端點到 BookService 方法:
from("direct:getAllBooks")
.bean(bookService, "getBooks");
from("direct:getBookById")
.bean(bookService, "getBookById(${header.id})");
from("direct:addBook")
.bean(bookService, "addBook");
通過利用 Apache Camel 的流暢 DSL,我們清晰地將 HTTP 路由與業務邏輯分開,並提供了一種可維護且可擴展的方式來暴露 REST API。
6. 創建 GraphQL 模式
為了向我們的應用程序添加 GraphQL 支持,我們首先在一個名為 books.graphqls 的單獨文件中定義模式。此文件使用 GraphQL 模式定義語言 (SDL),允許我們以一種簡單、聲明性的格式描述 API 的結構:
type Book {
id: String!
title: String!
author: String
}
type Query {
books: [Book]
bookById(id: String!): Book
}
type Mutation {
addBook(id: String!, title: String!, author: String): Book
}
模式從一個 Book 類型開始,它代表我們系統中的主要實體。它包含三個字段:id、title 和 author。id 和 title 字段已用感嘆號 (!) 註釋,指示這些字段是不可空的。
在類型定義之後,Query 類型概述了客户端可以執行的數據檢索操作。具體來説,它允許客户端使用 books 查詢或使用 bookById 查詢檢索單個圖書。
為了允許客户端創建新的圖書條目,我們添加一個 Mutation 類型。 addBook 變體接受兩個參數——title (必需) 和 author (可選),並返回新創建的 Book 對象。
現在,我們需要創建一個將 GraphQL 查詢連接到 Java 服務的模式加載器類。我們創建一個名為 CustomSchemaLoader 的類,該類加載模式、將其解析為註冊表,並定義如何使用數據獲取器解決每個 GraphQL 操作:
public class CustomSchemaLoader{
private final BookService bookService = new BookService();
public GraphQLSchema loadSchema() {
try (InputStream schemaStream = getClass().getClassLoader().getResourceAsStream("books.graphqls")) {
if (schemaStream == null) {
throw new RuntimeException("GraphQL schema file 'books.graphqls' not found in classpath");
}
TypeDefinitionRegistry registry = new SchemaParser()
.parse(new InputStreamReader(schemaStream));
RuntimeWiring wiring = buildRuntimeWiring();
return new SchemaGenerator().makeExecutableSchema(registry, wiring);
} catch (Exception e) {
logger.error("Failed to load GraphQL schema", e);
throw new RuntimeException("GraphQL schema initialization failed", e);
}
}
public RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("books", env -> bookService.getBooks())
.dataFetcher("bookById", env -> bookService.getBookById(env.getArgument("id"))))
.type("Mutation", builder -> builder
.dataFetcher("addBook", env -> {
String id = env.getArgument("id");
String title = env.getArgument("title");
String author = env.getArgument("author");
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("Title cannot be empty");
}
return bookService.addBook(new Book(id, title, author));
}))
.build();
}
}
dataFetcher() 作為 GraphQL 查詢或變體和我們服務層中實際方法之間的連接器。 例如,當客户端查詢 books 時,系統內部調用 bookService.getBooks()。
對於 addBook 變體,解析器提取 title 和 author 參數,並將它們傳遞給 bookService.addBook(title, author)。
7. 添加 GraphQL 路由
有了我們的 schema 和服務邏輯,下一步是使用 Apache Camel 暴露我們的 GraphQL 端點。 為了實現這一點,我們定義一條 Camel 路由,該路由監聽傳入的 HTTP POST 請求並將它們委託給 GraphQL 引擎進行處理。
我們使用 Jetty 組件配置路由,監聽 HTTP 請求,端口為 8080,具體路徑為 /graphql:
from("jetty:http://localhost:8088/graphql?matchOnUriPrefix=true")
.log("Received GraphQL request: ${body}")
.convertBodyTo(String.class)
.process(exchange -> {
String body = exchange.getIn().getBody(String.class);
try {
Map<String, Object> payload = new ObjectMapper().readValue(body, Map.class);
String query = (String) payload.get("query");
if (query == null || query.trim().isEmpty()) {
throw new IllegalArgumentException("Missing 'query' field in request body");
}
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.build();
ExecutionResult result = graphQL.execute(executionInput);
Map<String, Object> response = result.toSpecification();
exchange.getIn().setBody(response);
} catch (Exception e) {
throw new RuntimeException("GraphQL processing error", e);
}
})
.marshal().json(JsonLibrary.Jackson)
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"));
該路由監聽發送到 /graphql 的 HTTP POST 請求。 它從傳入的 payload 中提取“query”字段並將其執行到加載的 GraphQL schema 中。 查詢的結果被轉換成標準 GraphQL 響應格式,然後被 marshaled 回 JSON。
8. 主應用程序類
現在路線已定義,我們需要一個主類來啓動應用程序。此類負責初始化 Camel 文檔上下文、註冊模式加載器、添加路線以及保持服務器運行。
我們創建一個名為 CamelRestGraphQLApp 的類,其中包含一個主方法,用於執行必要的設置:
public class CamelRestGraphQLApp {
public static void main(String[] args) throws Exception {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new BookRoute());
context.start();
logger.info("服務器運行在 http://localhost:8080");
Thread.sleep(Long.MAX_VALUE);
context.stop();
}
}
此類註冊模式加載器作為 Bean,添加路線並啓動服務器。
Thread.sleep(Long.MAX_VALUE) 調用是一種簡單的方法,用於保持應用程序運行。在生產級應用程序中,這將被更健壯的機制用於管理應用程序生命週期替換,但僅用於演示目的,它保持服務器運行以處理傳入請求。
9. 測試
我們可以使用 Camel 的 CamelContext 和 ProducerTemplate 來模擬發送 HTTP 請求:@Test
void whenCallingRestGetAllBooks_thenShouldReturnBookList() {
String response = template.requestBodyAndHeader(
"http://localhost:8080/api/books",
null,
Exchange.CONTENT_TYPE,
"application/json",
String.class
);
assertNotNull(response);
assertTrue(response.contains("Clean Code"));
assertTrue(response.contains("Effective Java"));
}
@Test
void whenCallingBooksQuery_thenShouldReturnAllBooks() {
String query = """
{
"query": "{ books { id title author } }"
}""";
String response = template.requestBodyAndHeader(
"http://localhost:8080/graphql",
query,
Exchange.CONTENT_TYPE,
"application/json",
String.class
);
assertNotNull(response);
assertTrue(response.contains("Clean Code"));
assertTrue(response.contains("Effective Java"));
}
10. 結論
在本文中,我們成功地將 REST 和 GraphQL 端點集成到我們的 Camel 應用程序中,從而通過查詢式和修改式 API 有效地管理圖書數據。