1. 引言
在本教程中,我們將使用 Apache Camel 構建一個小型應用程序,以暴露 GraphQL 和 REST API。Apache Camel 是一種強大的集成框架,它簡化了連接不同系統,包括 API、數據庫和消息服務。
2. 項目設置
首先,在我們的 <em >pom.xml</em> 文件中,添加以下依賴項:對 Apache Camel 的核心組件 <a href="https://mvnrepository.com/artifact/org.apache.camel/camel-core">Camel core</a>, Jetty 組件 <a href="https://mvnrepository.com/artifact/org.apache.camel/camel-jetty">Jetty</a>, GraphQL 支持 <a href="https://mvnrepository.com/artifact/org.apache.camel/camel-graphql">GraphQL</a>, 以及 Jackson 序列化器 <a href="https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind">Jackson</a>。
<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;
// constructor, getter and setter methods
}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. 使用 Camel 創建 REST 端點
我們將使用 Apache Camel 和 Jetty 作為嵌入式 Web 服務器來暴露 RESTful 端點。Camel 通過 RouteBuilder 簡化了定義 HTTP 端點和路由邏輯的過程。
讓我們首先在一個名為 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}: 一個用於根據 ID 檢索書籍的 GET 端點
- 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 header id,該值會傳遞給 bookService.getBookById()。
- from(“direct:addBook”): 當收到帶有 JSON 身體的 POST 請求時,Camel 將其反序列化為 Book 對象,並使用它調用 bookService.addBook()。
這些連接了 * 端點到 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 查詢檢索單個書籍的 ID。
為了允許客户端創建新的書籍條目,我們添加一個 Mutation 類型。 addBook mutation 接受兩個參數——title(必需)和 author(可選),並返回新創建的 Book 對象。
現在我們需要創建一個將 GraphQL 查詢連接到 Java 服務的 schema 加載器類。我們創建一個名為 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 查詢或變更操作與我們服務層中實際方法的連接器。例如,當客户端查詢 書籍 時,系統內部會調用 。
同樣,對於 addBook 變更操作,解析器從請求中提取 標題 和 作者 參數,並將其傳遞給 。
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 文檔上下文、註冊 Schema 加載器、添加路由以及保持服務器運行狀態。
我們創建一個名為 CamelRestGraphQLApp 的類,其中包含一個主方法,用於執行必要的設置:
public class CamelRestGraphQLApp {
public static void main(String[] args) throws Exception {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new BookRoute());
context.start();
logger.info("Server running at 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 應用程序中,從而通過查詢式和 Mutation 驅動的 API 有效地管理圖書數據。