如何使用 Apache Camel 設定 GraphQL/REST API
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;
// 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()
定義伺服器設定。我們在連接埠8080
上使用 Jetty,並將綁定模式設為 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 標頭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 Schema
為了向我們的應用程式添加 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
突變接受兩個參數 - 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 路由
有了我們的模式和服務邏輯,下一步就是使用 Apache Camel 來公開我們的 GraphQL 端點。為了實現這一點,我們定義了一個 Camel 路由,用於監聽傳入的 HTTP POST 請求並將其委託給 GraphQL 引擎進行處理。
我們使用 Jetty 元件配置路由來監聽連接埠8080
上的 HTTP 請求,具體來說是在/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.
它從傳入的有效負載中提取查詢欄位並根據載入的 GraphQL 模式執行它。查詢結果被轉換為標準的 GraphQL 回應格式,然後被編組回 JSON。
8. 主應用程式類
現在路由已經定義,我們需要一個主類別來引導應用程式。此類別負責初始化 Camel 上下文、註冊模式載入器、新增路由以及保持伺服器處於活動狀態。
我們建立一個名為CamelRestGraphQLApp
的類,它有一個執行必要設定的 main 方法:
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 應用程式中,從而能夠透過基於查詢和基於變異的 API 高效地管理書籍資料。
與往常一樣,原始碼可在 GitHub 上取得。