3.1 JAX-RS 基础
3.1.1 JAX-RS 概述
JAX-RS(Java API for RESTful Web Services)是 Jakarta EE 的标准规范,用于构建 RESTful Web 服务。Quarkus 使用 RESTEasy Reactive 作为 JAX-RS 的实现,提供了高性能的响应式 REST 服务支持。
graph TB
A[JAX-RS 架构] --> B[资源类]
A --> C[HTTP 方法]
A --> D[路径映射]
A --> E[内容协商]
A --> F[参数绑定]
A --> G[异常处理]
B --> B1[@Path]
B --> B2[@ApplicationPath]
C --> C1[@GET]
C --> C2[@POST]
C --> C3[@PUT]
C --> C4[@DELETE]
C --> C5[@PATCH]
D --> D1[路径参数]
D --> D2[查询参数]
D --> D3[矩阵参数]
E --> E1[@Produces]
E --> E2[@Consumes]
F --> F1[@PathParam]
F --> F2[@QueryParam]
F --> F3[@FormParam]
F --> F4[@HeaderParam]
G --> G1[ExceptionMapper]
G --> G2[WebApplicationException]
3.1.2 基本资源类
package com.example.resource;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.ArrayList;
@Path("/api/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
// 模拟数据存储
private static final List<Book> books = new ArrayList<>();
static {
books.add(new Book(1L, "Java 编程思想", "Bruce Eckel", "978-0131872486"));
books.add(new Book(2L, "Effective Java", "Joshua Bloch", "978-0134685991"));
books.add(new Book(3L, "Spring Boot 实战", "Craig Walls", "978-7115404671"));
}
@GET
public List<Book> getAllBooks() {
return books;
}
@GET
@Path("/{id}")
public Response getBook(@PathParam("id") Long id) {
Book book = findBookById(id);
if (book != null) {
return Response.ok(book).build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Book not found", 404))
.build();
}
}
@POST
public Response createBook(Book book) {
if (book.getTitle() == null || book.getTitle().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Title is required", 400))
.build();
}
book.setId(generateNextId());
books.add(book);
return Response.status(Response.Status.CREATED)
.entity(book)
.build();
}
@PUT
@Path("/{id}")
public Response updateBook(@PathParam("id") Long id, Book updatedBook) {
Book existingBook = findBookById(id);
if (existingBook == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Book not found", 404))
.build();
}
existingBook.setTitle(updatedBook.getTitle());
existingBook.setAuthor(updatedBook.getAuthor());
existingBook.setIsbn(updatedBook.getIsbn());
return Response.ok(existingBook).build();
}
@DELETE
@Path("/{id}")
public Response deleteBook(@PathParam("id") Long id) {
Book book = findBookById(id);
if (book == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Book not found", 404))
.build();
}
books.remove(book);
return Response.noContent().build();
}
// 辅助方法
private Book findBookById(Long id) {
return books.stream()
.filter(book -> book.getId().equals(id))
.findFirst()
.orElse(null);
}
private Long generateNextId() {
return books.stream()
.mapToLong(Book::getId)
.max()
.orElse(0L) + 1;
}
}
// 书籍实体类
public class Book {
private Long id;
private String title;
private String author;
private String isbn;
// 构造器
public Book() {}
public Book(Long id, String title, String author, String isbn) {
this.id = id;
this.title = title;
this.author = author;
this.isbn = isbn;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
}
// 错误响应类
public class ErrorResponse {
private String message;
private int code;
private long timestamp;
public ErrorResponse(String message, int code) {
this.message = message;
this.code = code;
this.timestamp = System.currentTimeMillis();
}
// Getters and Setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
3.2 HTTP 方法和路径映射
3.2.1 HTTP 方法注解
@Path("/api/users")
public class UserResource {
@Inject
UserService userService;
// GET - 获取资源
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> getAllUsers() {
return userService.findAll();
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public User getUser(@PathParam("id") Long id) {
return userService.findById(id);
}
// POST - 创建资源
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createUser(User user) {
User createdUser = userService.create(user);
return Response.status(Response.Status.CREATED)
.entity(createdUser)
.build();
}
// PUT - 完整更新资源
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User updateUser(@PathParam("id") Long id, User user) {
user.setId(id);
return userService.update(user);
}
// PATCH - 部分更新资源
@PATCH
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User patchUser(@PathParam("id") Long id, UserPatch patch) {
return userService.patch(id, patch);
}
// DELETE - 删除资源
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") Long id) {
userService.deleteById(id);
return Response.noContent().build();
}
// HEAD - 获取资源元信息
@HEAD
@Path("/{id}")
public Response checkUserExists(@PathParam("id") Long id) {
boolean exists = userService.existsById(id);
return exists ? Response.ok().build()
: Response.status(Response.Status.NOT_FOUND).build();
}
// OPTIONS - 获取支持的方法
@OPTIONS
public Response getOptions() {
return Response.ok()
.header("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
.build();
}
}
3.2.2 复杂路径映射
@Path("/api/organizations")
public class OrganizationResource {
@Inject
OrganizationService organizationService;
@Inject
UserService userService;
// 嵌套资源路径
@GET
@Path("/{orgId}/users")
@Produces(MediaType.APPLICATION_JSON)
public List<User> getOrganizationUsers(@PathParam("orgId") Long orgId) {
return userService.findByOrganizationId(orgId);
}
@POST
@Path("/{orgId}/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User addUserToOrganization(@PathParam("orgId") Long orgId, User user) {
user.setOrganizationId(orgId);
return userService.create(user);
}
// 多级路径参数
@GET
@Path("/{orgId}/departments/{deptId}/users")
@Produces(MediaType.APPLICATION_JSON)
public List<User> getDepartmentUsers(@PathParam("orgId") Long orgId,
@PathParam("deptId") Long deptId) {
return userService.findByOrganizationAndDepartment(orgId, deptId);
}
// 正则表达式路径
@GET
@Path("/code/{code: [A-Z]{2,3}[0-9]{3,6}}")
@Produces(MediaType.APPLICATION_JSON)
public Organization getByCode(@PathParam("code") String code) {
return organizationService.findByCode(code);
}
// 可选路径段
@GET
@Path("/search{path: (/.*)?}")
@Produces(MediaType.APPLICATION_JSON)
public List<Organization> search(@PathParam("path") String path,
@QueryParam("q") String query,
@QueryParam("type") String type) {
// 处理可选的路径段
String searchPath = path != null ? path.substring(1) : "";
return organizationService.search(query, type, searchPath);
}
}
3.3 参数绑定
3.3.1 路径参数(@PathParam)
@Path("/api/products")
public class ProductResource {
@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
return productService.findById(id);
}
@GET
@Path("/category/{category}/brand/{brand}")
public List<Product> getProductsByCategoryAndBrand(
@PathParam("category") String category,
@PathParam("brand") String brand) {
return productService.findByCategoryAndBrand(category, brand);
}
// 路径参数类型转换
@GET
@Path("/price/{min}/{max}")
public List<Product> getProductsByPriceRange(
@PathParam("min") @DefaultValue("0") BigDecimal minPrice,
@PathParam("max") @DefaultValue("999999") BigDecimal maxPrice) {
return productService.findByPriceRange(minPrice, maxPrice);
}
}
3.3.2 查询参数(@QueryParam)
@Path("/api/products")
public class ProductSearchResource {
@GET
@Path("/search")
@Produces(MediaType.APPLICATION_JSON)
public Response searchProducts(
@QueryParam("q") String query,
@QueryParam("category") String category,
@QueryParam("minPrice") @DefaultValue("0") BigDecimal minPrice,
@QueryParam("maxPrice") BigDecimal maxPrice,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("name") String sortBy,
@QueryParam("order") @DefaultValue("asc") String sortOrder,
@QueryParam("inStock") Boolean inStock) {
// 构建搜索条件
ProductSearchCriteria criteria = ProductSearchCriteria.builder()
.query(query)
.category(category)
.minPrice(minPrice)
.maxPrice(maxPrice)
.inStock(inStock)
.build();
// 构建分页信息
PageRequest pageRequest = PageRequest.of(page, size, sortBy, sortOrder);
// 执行搜索
PageResult<Product> result = productService.search(criteria, pageRequest);
return Response.ok(result)
.header("X-Total-Count", result.getTotalElements())
.header("X-Total-Pages", result.getTotalPages())
.build();
}
// 多值查询参数
@GET
@Path("/filter")
public List<Product> filterProducts(
@QueryParam("tags") List<String> tags,
@QueryParam("brands") Set<String> brands,
@QueryParam("colors") String[] colors) {
ProductFilter filter = new ProductFilter();
filter.setTags(tags);
filter.setBrands(brands);
filter.setColors(Arrays.asList(colors));
return productService.filter(filter);
}
}
3.3.3 表单参数(@FormParam)
@Path("/api/auth")
public class AuthResource {
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response login(@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("rememberMe") @DefaultValue("false") boolean rememberMe) {
try {
AuthResult result = authService.authenticate(username, password, rememberMe);
return Response.ok(result).build();
} catch (AuthenticationException e) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("Invalid credentials", 401))
.build();
}
}
@POST
@Path("/register")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response register(@FormParam("username") String username,
@FormParam("email") String email,
@FormParam("password") String password,
@FormParam("avatar") InputStream avatarStream,
@FormParam("avatar") FormDataContentDisposition avatarDetail) {
// 处理用户注册
User user = new User(username, email, password);
// 处理头像上传
if (avatarStream != null && avatarDetail != null) {
String avatarUrl = fileService.uploadAvatar(avatarStream, avatarDetail.getFileName());
user.setAvatarUrl(avatarUrl);
}
User createdUser = userService.register(user);
return Response.status(Response.Status.CREATED).entity(createdUser).build();
}
}
3.3.4 请求头参数(@HeaderParam)
@Path("/api/secure")
public class SecureResource {
@GET
@Path("/profile")
@Produces(MediaType.APPLICATION_JSON)
public Response getUserProfile(
@HeaderParam("Authorization") String authHeader,
@HeaderParam("User-Agent") String userAgent,
@HeaderParam("Accept-Language") @DefaultValue("en") String language,
@HeaderParam("X-Request-ID") String requestId) {
// 验证授权头
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("Missing or invalid authorization header", 401))
.build();
}
String token = authHeader.substring(7);
User user = authService.validateToken(token);
if (user == null) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("Invalid token", 401))
.build();
}
// 记录请求信息
auditService.logAccess(user.getId(), userAgent, requestId);
// 根据语言返回本地化的用户信息
UserProfile profile = userService.getLocalizedProfile(user.getId(), language);
return Response.ok(profile)
.header("X-Request-ID", requestId)
.build();
}
@POST
@Path("/upload")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
public Response uploadFile(
@HeaderParam("Content-Type") String contentType,
@HeaderParam("Content-Length") long contentLength,
@HeaderParam("X-File-Name") String fileName,
@HeaderParam("X-File-Hash") String fileHash,
InputStream fileStream) {
// 验证文件大小
if (contentLength > 10 * 1024 * 1024) { // 10MB 限制
return Response.status(Response.Status.REQUEST_ENTITY_TOO_LARGE)
.entity(new ErrorResponse("File too large", 413))
.build();
}
// 验证文件类型
if (!isAllowedContentType(contentType)) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity(new ErrorResponse("Unsupported file type", 415))
.build();
}
// 上传文件
FileUploadResult result = fileService.upload(fileStream, fileName, contentType, fileHash);
return Response.status(Response.Status.CREATED)
.entity(result)
.build();
}
private boolean isAllowedContentType(String contentType) {
return contentType != null && (
contentType.startsWith("image/") ||
contentType.equals("application/pdf") ||
contentType.startsWith("text/")
);
}
}
3.3.5 Cookie 参数(@CookieParam)
@Path("/api/preferences")
public class PreferencesResource {
@GET
@Path("/theme")
@Produces(MediaType.APPLICATION_JSON)
public Response getThemePreference(
@CookieParam("theme") @DefaultValue("light") String theme,
@CookieParam("language") @DefaultValue("en") String language,
@CookieParam("sessionId") String sessionId) {
// 验证会话
if (sessionId != null && sessionService.isValidSession(sessionId)) {
// 从数据库获取用户偏好
UserPreferences prefs = preferencesService.getBySessionId(sessionId);
return Response.ok(prefs).build();
} else {
// 使用 Cookie 中的偏好
UserPreferences prefs = new UserPreferences(theme, language);
return Response.ok(prefs).build();
}
}
@POST
@Path("/theme")
@Consumes(MediaType.APPLICATION_JSON)
public Response setThemePreference(
UserPreferences preferences,
@CookieParam("sessionId") String sessionId) {
if (sessionId != null && sessionService.isValidSession(sessionId)) {
// 保存到数据库
preferencesService.saveBySessionId(sessionId, preferences);
}
// 设置 Cookie
NewCookie themeCookie = new NewCookie.Builder("theme")
.value(preferences.getTheme())
.maxAge(30 * 24 * 60 * 60) // 30 天
.httpOnly(false)
.secure(false)
.build();
NewCookie languageCookie = new NewCookie.Builder("language")
.value(preferences.getLanguage())
.maxAge(30 * 24 * 60 * 60)
.httpOnly(false)
.secure(false)
.build();
return Response.ok(preferences)
.cookie(themeCookie, languageCookie)
.build();
}
}
3.4 内容协商
3.4.1 媒体类型处理
@Path("/api/content")
public class ContentNegotiationResource {
@Inject
BookService bookService;
// 支持多种输出格式
@GET
@Path("/books/{id}")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN})
public Response getBook(@PathParam("id") Long id, @Context HttpHeaders headers) {
Book book = bookService.findById(id);
if (book == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// 根据 Accept 头确定响应格式
List<MediaType> acceptableTypes = headers.getAcceptableMediaTypes();
for (MediaType mediaType : acceptableTypes) {
if (mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE)) {
return Response.ok(book, MediaType.APPLICATION_JSON).build();
} else if (mediaType.isCompatible(MediaType.APPLICATION_XML_TYPE)) {
return Response.ok(book, MediaType.APPLICATION_XML).build();
} else if (mediaType.isCompatible(MediaType.TEXT_PLAIN_TYPE)) {
String textRepresentation = String.format(
"Book: %s by %s (ISBN: %s)",
book.getTitle(), book.getAuthor(), book.getIsbn());
return Response.ok(textRepresentation, MediaType.TEXT_PLAIN).build();
}
}
// 默认返回 JSON
return Response.ok(book, MediaType.APPLICATION_JSON).build();
}
// 支持多种输入格式
@POST
@Path("/books")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Produces(MediaType.APPLICATION_JSON)
public Response createBook(Book book, @Context HttpHeaders headers) {
// 获取请求的内容类型
MediaType contentType = headers.getMediaType();
// 根据内容类型进行特殊处理
if (MediaType.APPLICATION_XML_TYPE.isCompatible(contentType)) {
// XML 特殊处理逻辑
book = preprocessXmlBook(book);
}
Book createdBook = bookService.create(book);
return Response.status(Response.Status.CREATED)
.entity(createdBook)
.build();
}
// 条件化内容协商
@GET
@Path("/books")
public Response getBooks(@QueryParam("format") String format,
@Context HttpHeaders headers) {
List<Book> books = bookService.findAll();
// 优先使用查询参数指定的格式
if ("xml".equalsIgnoreCase(format)) {
return Response.ok(books, MediaType.APPLICATION_XML).build();
} else if ("json".equalsIgnoreCase(format)) {
return Response.ok(books, MediaType.APPLICATION_JSON).build();
} else if ("csv".equalsIgnoreCase(format)) {
String csv = convertToCsv(books);
return Response.ok(csv, "text/csv")
.header("Content-Disposition", "attachment; filename=books.csv")
.build();
}
// 否则使用内容协商
List<MediaType> acceptableTypes = headers.getAcceptableMediaTypes();
if (acceptableTypes.contains(MediaType.APPLICATION_XML_TYPE)) {
return Response.ok(books, MediaType.APPLICATION_XML).build();
} else {
return Response.ok(books, MediaType.APPLICATION_JSON).build();
}
}
private Book preprocessXmlBook(Book book) {
// XML 特殊处理逻辑
if (book.getTitle() != null) {
book.setTitle(book.getTitle().trim());
}
return book;
}
private String convertToCsv(List<Book> books) {
StringBuilder csv = new StringBuilder();
csv.append("ID,Title,Author,ISBN\n");
for (Book book : books) {
csv.append(String.format("%d,\"%s\",\"%s\",\"%s\"\n",
book.getId(),
book.getTitle().replace("\"", "\"\""),
book.getAuthor().replace("\"", "\"\""),
book.getIsbn()));
}
return csv.toString();
}
}
3.4.2 自定义消息转换器
// 自定义 MessageBodyWriter
@Provider
@Produces("text/csv")
public class CsvMessageBodyWriter implements MessageBodyWriter<List<Book>> {
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return List.class.isAssignableFrom(type) &&
"text/csv".equals(mediaType.toString());
}
@Override
public void writeTo(List<Book> books, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8))) {
// 写入 CSV 头
writer.println("ID,Title,Author,ISBN");
// 写入数据行
for (Book book : books) {
writer.printf("%d,\"%s\",\"%s\",\"%s\"\n",
book.getId(),
escapeCsv(book.getTitle()),
escapeCsv(book.getAuthor()),
escapeCsv(book.getIsbn()));
}
}
}
private String escapeCsv(String value) {
if (value == null) return "";
return value.replace("\"", "\"\"");
}
}
// 自定义 MessageBodyReader
@Provider
@Consumes("text/csv")
public class CsvMessageBodyReader implements MessageBodyReader<List<Book>> {
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return List.class.isAssignableFrom(type) &&
"text/csv".equals(mediaType.toString());
}
@Override
public List<Book> readFrom(Class<List<Book>> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException {
List<Book> books = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(entityStream, StandardCharsets.UTF_8))) {
String line = reader.readLine(); // 跳过头行
while ((line = reader.readLine()) != null) {
String[] fields = parseCsvLine(line);
if (fields.length >= 4) {
Book book = new Book();
book.setId(Long.parseLong(fields[0]));
book.setTitle(fields[1]);
book.setAuthor(fields[2]);
book.setIsbn(fields[3]);
books.add(book);
}
}
}
return books;
}
private String[] parseCsvLine(String line) {
// 简单的 CSV 解析(生产环境建议使用专业的 CSV 库)
List<String> fields = new ArrayList<>();
boolean inQuotes = false;
StringBuilder field = new StringBuilder();
for (char c : line.toCharArray()) {
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
fields.add(field.toString());
field.setLength(0);
} else {
field.append(c);
}
}
fields.add(field.toString());
return fields.toArray(new String[0]);
}
}
3.5 异常处理
3.5.1 标准异常处理
// 自定义业务异常
public class BookNotFoundException extends RuntimeException {
private final Long bookId;
public BookNotFoundException(Long bookId) {
super("Book not found with id: " + bookId);
this.bookId = bookId;
}
public Long getBookId() {
return bookId;
}
}
public class ValidationException extends RuntimeException {
private final List<String> errors;
public ValidationException(List<String> errors) {
super("Validation failed: " + String.join(", ", errors));
this.errors = errors;
}
public List<String> getErrors() {
return errors;
}
}
// 全局异常映射器
@Provider
public class BookNotFoundExceptionMapper implements ExceptionMapper<BookNotFoundException> {
@Override
public Response toResponse(BookNotFoundException exception) {
ErrorResponse error = new ErrorResponse(
"Book not found",
404,
"BOOK_NOT_FOUND",
Map.of("bookId", exception.getBookId())
);
return Response.status(Response.Status.NOT_FOUND)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
@Override
public Response toResponse(ValidationException exception) {
ErrorResponse error = new ErrorResponse(
"Validation failed",
400,
"VALIDATION_ERROR",
Map.of("errors", exception.getErrors())
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// 通用异常映射器
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
private static final Logger logger = LoggerFactory.getLogger(GenericExceptionMapper.class);
@Override
public Response toResponse(Exception exception) {
logger.error("Unhandled exception", exception);
ErrorResponse error = new ErrorResponse(
"Internal server error",
500,
"INTERNAL_ERROR",
Map.of("timestamp", System.currentTimeMillis())
);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// 增强的错误响应类
public class ErrorResponse {
private String message;
private int status;
private String code;
private Map<String, Object> details;
private long timestamp;
public ErrorResponse(String message, int status) {
this(message, status, null, null);
}
public ErrorResponse(String message, int status, String code, Map<String, Object> details) {
this.message = message;
this.status = status;
this.code = code;
this.details = details;
this.timestamp = System.currentTimeMillis();
}
// Getters and Setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public Map<String, Object> getDetails() { return details; }
public void setDetails(Map<String, Object> details) { this.details = details; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
3.5.2 WebApplicationException 使用
@Path("/api/books")
public class BookResourceWithExceptions {
@Inject
BookService bookService;
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Book getBook(@PathParam("id") Long id) {
if (id == null || id <= 0) {
throw new WebApplicationException(
"Invalid book ID",
Response.Status.BAD_REQUEST
);
}
Book book = bookService.findById(id);
if (book == null) {
ErrorResponse error = new ErrorResponse("Book not found", 404);
throw new WebApplicationException(
Response.status(Response.Status.NOT_FOUND)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build()
);
}
return book;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Book createBook(Book book) {
// 验证输入
List<String> errors = validateBook(book);
if (!errors.isEmpty()) {
ErrorResponse error = new ErrorResponse(
"Validation failed",
400,
"VALIDATION_ERROR",
Map.of("errors", errors)
);
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build()
);
}
// 检查重复
if (bookService.existsByIsbn(book.getIsbn())) {
throw new WebApplicationException(
"Book with this ISBN already exists",
Response.Status.CONFLICT
);
}
return bookService.create(book);
}
@DELETE
@Path("/{id}")
public Response deleteBook(@PathParam("id") Long id) {
if (!bookService.existsById(id)) {
throw new WebApplicationException(
"Book not found",
Response.Status.NOT_FOUND
);
}
// 检查是否可以删除
if (bookService.hasActiveLoans(id)) {
throw new WebApplicationException(
"Cannot delete book with active loans",
Response.Status.CONFLICT
);
}
bookService.deleteById(id);
return Response.noContent().build();
}
private List<String> validateBook(Book book) {
List<String> errors = new ArrayList<>();
if (book.getTitle() == null || book.getTitle().trim().isEmpty()) {
errors.add("Title is required");
}
if (book.getAuthor() == null || book.getAuthor().trim().isEmpty()) {
errors.add("Author is required");
}
if (book.getIsbn() == null || !isValidIsbn(book.getIsbn())) {
errors.add("Valid ISBN is required");
}
return errors;
}
private boolean isValidIsbn(String isbn) {
// 简单的 ISBN 验证
return isbn != null && isbn.matches("^(978|979)[0-9]{10}$");
}
}
3.6 响应式编程支持
3.6.1 异步响应
@Path("/api/async")
public class AsyncResource {
@Inject
BookService bookService;
@Inject
@ConfigProperty(name = "app.async.timeout", defaultValue = "30")
int asyncTimeoutSeconds;
// 返回 CompletionStage
@GET
@Path("/books/{id}")
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<Response> getBookAsync(@PathParam("id") Long id) {
return CompletableFuture
.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
Book book = bookService.findById(id);
if (book == null) {
throw new BookNotFoundException(id);
}
return book;
})
.thenApply(book -> Response.ok(book).build())
.exceptionally(throwable -> {
if (throwable.getCause() instanceof BookNotFoundException) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Book not found", 404))
.build();
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Internal error", 500))
.build();
});
}
// 返回 Uni (Mutiny)
@GET
@Path("/books")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<Book>> getAllBooksAsync() {
return Uni.createFrom()
.completionStage(() -> CompletableFuture.supplyAsync(() -> {
// 模拟异步数据库查询
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
return bookService.findAll();
}))
.onFailure().transform(throwable ->
new WebApplicationException("Failed to fetch books",
Response.Status.INTERNAL_SERVER_ERROR));
}
// 流式响应
@GET
@Path("/books/stream")
@Produces(MediaType.APPLICATION_JSON)
public Multi<Book> streamBooks(@QueryParam("delay") @DefaultValue("100") int delayMs) {
List<Book> books = bookService.findAll();
return Multi.createFrom().iterable(books)
.onItem().delayIt().by(Duration.ofMillis(delayMs))
.onFailure().recoverWithItem(new Book(-1L, "Error", "Error", "Error"));
}
// 服务器发送事件 (SSE)
@GET
@Path("/books/events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<String> bookEvents() {
return Multi.createFrom().ticks().every(Duration.ofSeconds(2))
.onItem().transform(tick -> {
// 模拟实时书籍更新事件
Book randomBook = bookService.getRandomBook();
return String.format("data: {\"event\": \"book_updated\", \"book\": \"%s\"}\n\n",
randomBook.getTitle());
})
.select().first(10); // 限制为前 10 个事件
}
}
3.6.2 响应式数据处理
@Path("/api/reactive")
public class ReactiveDataResource {
@Inject
ReactiveBookService reactiveBookService;
// 批量处理
@POST
@Path("/books/batch")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<BatchResult> createBooksInBatch(List<Book> books) {
return Multi.createFrom().iterable(books)
.onItem().transformToUniAndMerge(book ->
reactiveBookService.createAsync(book)
.onFailure().recoverWithItem(new Book(-1L, "Failed", "Failed", "Failed"))
)
.collect().asList()
.onItem().transform(results -> {
long successCount = results.stream()
.filter(book -> book.getId() > 0)
.count();
long failureCount = results.size() - successCount;
return new BatchResult(successCount, failureCount, results);
});
}
// 分页异步查询
@GET
@Path("/books/page")
@Produces(MediaType.APPLICATION_JSON)
public Uni<PageResult<Book>> getBooksPage(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
return reactiveBookService.findAllAsync(page, size)
.onItem().transform(books -> {
long totalCount = reactiveBookService.countAll();
int totalPages = (int) Math.ceil((double) totalCount / size);
return new PageResult<>(books, page, size, totalCount, totalPages);
});
}
// 组合多个异步操作
@GET
@Path("/books/{id}/details")
@Produces(MediaType.APPLICATION_JSON)
public Uni<BookDetails> getBookDetails(@PathParam("id") Long id) {
Uni<Book> bookUni = reactiveBookService.findByIdAsync(id);
Uni<List<Review>> reviewsUni = reactiveBookService.getReviewsAsync(id);
Uni<BookStatistics> statsUni = reactiveBookService.getStatisticsAsync(id);
return Uni.combine().all().unis(bookUni, reviewsUni, statsUni)
.asTuple()
.onItem().transform(tuple -> {
Book book = tuple.getItem1();
List<Review> reviews = tuple.getItem2();
BookStatistics stats = tuple.getItem3();
return new BookDetails(book, reviews, stats);
})
.onFailure().transform(throwable ->
new WebApplicationException("Failed to fetch book details",
Response.Status.INTERNAL_SERVER_ERROR));
}
}
// 支持类
public class BatchResult {
private final long successCount;
private final long failureCount;
private final List<Book> results;
public BatchResult(long successCount, long failureCount, List<Book> results) {
this.successCount = successCount;
this.failureCount = failureCount;
this.results = results;
}
// Getters
public long getSuccessCount() { return successCount; }
public long getFailureCount() { return failureCount; }
public List<Book> getResults() { return results; }
}
public class PageResult<T> {
private final List<T> content;
private final int page;
private final int size;
private final long totalElements;
private final int totalPages;
public PageResult(List<T> content, int page, int size, long totalElements, int totalPages) {
this.content = content;
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
}
// Getters
public List<T> getContent() { return content; }
public int getPage() { return page; }
public int getSize() { return size; }
public long getTotalElements() { return totalElements; }
public int getTotalPages() { return totalPages; }
}
public class BookDetails {
private final Book book;
private final List<Review> reviews;
private final BookStatistics statistics;
public BookDetails(Book book, List<Review> reviews, BookStatistics statistics) {
this.book = book;
this.reviews = reviews;
this.statistics = statistics;
}
// Getters
public Book getBook() { return book; }
public List<Review> getReviews() { return reviews; }
public BookStatistics getStatistics() { return statistics; }
}
3.7 实践练习
3.7.1 练习1:构建完整的图书管理 API
创建一个完整的图书管理系统 REST API:
// 图书服务接口
public interface BookService {
List<Book> findAll();
Book findById(Long id);
List<Book> findByCategory(String category);
List<Book> search(String query);
Book create(Book book);
Book update(Book book);
void deleteById(Long id);
boolean existsById(Long id);
boolean existsByIsbn(String isbn);
boolean hasActiveLoans(Long id);
Book getRandomBook();
long countAll();
}
// 图书服务实现
@ApplicationScoped
public class BookServiceImpl implements BookService {
private final Map<Long, Book> books = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
@PostConstruct
void init() {
// 初始化示例数据
create(new Book(null, "Java 编程思想", "Bruce Eckel", "9780131872486", "Programming"));
create(new Book(null, "Effective Java", "Joshua Bloch", "9780134685991", "Programming"));
create(new Book(null, "Spring Boot 实战", "Craig Walls", "9787115404671", "Framework"));
create(new Book(null, "微服务架构设计模式", "Chris Richardson", "9787111627845", "Architecture"));
}
@Override
public List<Book> findAll() {
return new ArrayList<>(books.values());
}
@Override
public Book findById(Long id) {
return books.get(id);
}
@Override
public List<Book> findByCategory(String category) {
return books.values().stream()
.filter(book -> category.equalsIgnoreCase(book.getCategory()))
.collect(Collectors.toList());
}
@Override
public List<Book> search(String query) {
String lowerQuery = query.toLowerCase();
return books.values().stream()
.filter(book ->
book.getTitle().toLowerCase().contains(lowerQuery) ||
book.getAuthor().toLowerCase().contains(lowerQuery) ||
book.getIsbn().contains(query))
.collect(Collectors.toList());
}
@Override
public Book create(Book book) {
book.setId(idGenerator.getAndIncrement());
books.put(book.getId(), book);
return book;
}
@Override
public Book update(Book book) {
books.put(book.getId(), book);
return book;
}
@Override
public void deleteById(Long id) {
books.remove(id);
}
@Override
public boolean existsById(Long id) {
return books.containsKey(id);
}
@Override
public boolean existsByIsbn(String isbn) {
return books.values().stream()
.anyMatch(book -> isbn.equals(book.getIsbn()));
}
@Override
public boolean hasActiveLoans(Long id) {
// 模拟检查是否有活跃借阅
return false;
}
@Override
public Book getRandomBook() {
List<Book> allBooks = findAll();
if (allBooks.isEmpty()) {
return new Book(-1L, "No books available", "System", "N/A", "System");
}
return allBooks.get(new Random().nextInt(allBooks.size()));
}
@Override
public long countAll() {
return books.size();
}
}
// 扩展的 Book 实体
public class Book {
private Long id;
private String title;
private String author;
private String isbn;
private String category;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 构造器
public Book() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public Book(Long id, String title, String author, String isbn, String category) {
this();
this.id = id;
this.title = title;
this.author = author;
this.isbn = isbn;
this.category = category;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) {
this.id = id;
this.updatedAt = LocalDateTime.now();
}
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
this.updatedAt = LocalDateTime.now();
}
public String getAuthor() { return author; }
public void setAuthor(String author) {
this.author = author;
this.updatedAt = LocalDateTime.now();
}
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) {
this.isbn = isbn;
this.updatedAt = LocalDateTime.now();
}
public String getCategory() { return category; }
public void setCategory(String category) {
this.category = category;
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
3.7.2 练习2:实现搜索和过滤功能
@Path("/api/books")
public class BookSearchResource {
@Inject
BookService bookService;
@GET
@Path("/search")
@Produces(MediaType.APPLICATION_JSON)
public Response searchBooks(
@QueryParam("q") String query,
@QueryParam("category") String category,
@QueryParam("author") String author,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size,
@QueryParam("sort") @DefaultValue("title") String sortBy,
@QueryParam("order") @DefaultValue("asc") String sortOrder) {
List<Book> allBooks = bookService.findAll();
Stream<Book> bookStream = allBooks.stream();
// 应用过滤条件
if (query != null && !query.trim().isEmpty()) {
String lowerQuery = query.toLowerCase();
bookStream = bookStream.filter(book ->
book.getTitle().toLowerCase().contains(lowerQuery) ||
book.getAuthor().toLowerCase().contains(lowerQuery) ||
book.getIsbn().contains(query)
);
}
if (category != null && !category.trim().isEmpty()) {
bookStream = bookStream.filter(book ->
category.equalsIgnoreCase(book.getCategory())
);
}
if (author != null && !author.trim().isEmpty()) {
String lowerAuthor = author.toLowerCase();
bookStream = bookStream.filter(book ->
book.getAuthor().toLowerCase().contains(lowerAuthor)
);
}
// 排序
Comparator<Book> comparator = getComparator(sortBy, sortOrder);
bookStream = bookStream.sorted(comparator);
// 收集结果
List<Book> filteredBooks = bookStream.collect(Collectors.toList());
// 分页
int totalElements = filteredBooks.size();
int totalPages = (int) Math.ceil((double) totalElements / size);
int startIndex = page * size;
int endIndex = Math.min(startIndex + size, totalElements);
List<Book> pageContent = filteredBooks.subList(startIndex, endIndex);
SearchResult result = new SearchResult(
pageContent,
page,
size,
totalElements,
totalPages,
query,
category,
author
);
return Response.ok(result)
.header("X-Total-Count", totalElements)
.header("X-Total-Pages", totalPages)
.build();
}
private Comparator<Book> getComparator(String sortBy, String sortOrder) {
Comparator<Book> comparator;
switch (sortBy.toLowerCase()) {
case "title":
comparator = Comparator.comparing(Book::getTitle, String.CASE_INSENSITIVE_ORDER);
break;
case "author":
comparator = Comparator.comparing(Book::getAuthor, String.CASE_INSENSITIVE_ORDER);
break;
case "category":
comparator = Comparator.comparing(Book::getCategory, String.CASE_INSENSITIVE_ORDER);
break;
case "created":
comparator = Comparator.comparing(Book::getCreatedAt);
break;
default:
comparator = Comparator.comparing(Book::getTitle, String.CASE_INSENSITIVE_ORDER);
}
return "desc".equalsIgnoreCase(sortOrder) ? comparator.reversed() : comparator;
}
}
// 搜索结果类
public class SearchResult {
private final List<Book> content;
private final int page;
private final int size;
private final int totalElements;
private final int totalPages;
private final String query;
private final String category;
private final String author;
public SearchResult(List<Book> content, int page, int size, int totalElements,
int totalPages, String query, String category, String author) {
this.content = content;
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.query = query;
this.category = category;
this.author = author;
}
// Getters
public List<Book> getContent() { return content; }
public int getPage() { return page; }
public int getSize() { return size; }
public int getTotalElements() { return totalElements; }
public int getTotalPages() { return totalPages; }
public String getQuery() { return query; }
public String getCategory() { return category; }
public String getAuthor() { return author; }
}
3.7.3 练习3:实现文件上传和下载
@Path("/api/files")
public class FileResource {
@ConfigProperty(name = "app.upload.directory", defaultValue = "./uploads")
String uploadDirectory;
@ConfigProperty(name = "app.upload.max-size", defaultValue = "10485760") // 10MB
long maxFileSize;
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadFile(@MultipartForm FileUploadForm form) {
try {
// 验证文件
validateFile(form);
// 创建上传目录
Path uploadPath = Paths.get(uploadDirectory);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 生成唯一文件名
String originalFileName = form.fileName;
String fileExtension = getFileExtension(originalFileName);
String uniqueFileName = UUID.randomUUID().toString() + fileExtension;
// 保存文件
Path filePath = uploadPath.resolve(uniqueFileName);
Files.copy(form.file, filePath, StandardCopyOption.REPLACE_EXISTING);
// 创建文件记录
FileRecord fileRecord = new FileRecord(
uniqueFileName,
originalFileName,
Files.size(filePath),
form.description,
LocalDateTime.now()
);
return Response.status(Response.Status.CREATED)
.entity(fileRecord)
.build();
} catch (IOException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("File upload failed", 500))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage(), 400))
.build();
}
}
@GET
@Path("/download/{fileName}")
public Response downloadFile(@PathParam("fileName") String fileName) {
try {
Path filePath = Paths.get(uploadDirectory, fileName);
if (!Files.exists(filePath)) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("File not found", 404))
.build();
}
// 获取文件信息
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
// 返回文件流
StreamingOutput stream = output -> {
try (InputStream input = Files.newInputStream(filePath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
};
return Response.ok(stream, contentType)
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.header("Content-Length", Files.size(filePath))
.build();
} catch (IOException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("File download failed", 500))
.build();
}
}
private void validateFile(FileUploadForm form) {
if (form.file == null) {
throw new IllegalArgumentException("File is required");
}
if (form.fileName == null || form.fileName.trim().isEmpty()) {
throw new IllegalArgumentException("File name is required");
}
try {
long fileSize = form.file.available();
if (fileSize > maxFileSize) {
throw new IllegalArgumentException("File size exceeds maximum allowed size");
}
} catch (IOException e) {
throw new IllegalArgumentException("Unable to determine file size");
}
// 验证文件类型
String fileExtension = getFileExtension(form.fileName).toLowerCase();
if (!isAllowedFileType(fileExtension)) {
throw new IllegalArgumentException("File type not allowed");
}
}
private String getFileExtension(String fileName) {
int lastDotIndex = fileName.lastIndexOf('.');
return lastDotIndex > 0 ? fileName.substring(lastDotIndex) : "";
}
private boolean isAllowedFileType(String extension) {
Set<String> allowedTypes = Set.of(".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx");
return allowedTypes.contains(extension);
}
}
// 文件上传表单
public static class FileUploadForm {
@FormParam("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public InputStream file;
@FormParam("fileName")
@PartType(MediaType.TEXT_PLAIN)
public String fileName;
@FormParam("description")
@PartType(MediaType.TEXT_PLAIN)
public String description;
}
// 文件记录类
public class FileRecord {
private String id;
private String originalName;
private long size;
private String description;
private LocalDateTime uploadTime;
public FileRecord(String id, String originalName, long size, String description, LocalDateTime uploadTime) {
this.id = id;
this.originalName = originalName;
this.size = size;
this.description = description;
this.uploadTime = uploadTime;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getOriginalName() { return originalName; }
public void setOriginalName(String originalName) { this.originalName = originalName; }
public long getSize() { return size; }
public void setSize(long size) { this.size = size; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public LocalDateTime getUploadTime() { return uploadTime; }
public void setUploadTime(LocalDateTime uploadTime) { this.uploadTime = uploadTime; }
}
3.8 本章小结
3.8.1 核心概念回顾
本章深入探讨了 Quarkus 中 RESTful Web 服务开发的核心技术:
- JAX-RS 基础:掌握了资源类、HTTP 方法注解、路径映射等基本概念
- 参数绑定:学习了路径参数、查询参数、表单参数、请求头参数等多种参数绑定方式
- 内容协商:了解了媒体类型处理、自定义消息转换器等高级特性
- 异常处理:实现了全局异常映射器和标准异常处理机制
- 响应式编程:探索了异步响应、流式处理等现代化开发模式
3.8.2 技术要点总结
- 注解驱动:使用 JAX-RS 注解简化 REST 服务开发
- 类型安全:利用强类型参数绑定确保 API 的健壮性
- 内容协商:支持多种媒体类型,提供灵活的数据交换格式
- 异常处理:统一的异常处理机制,提供一致的错误响应
- 响应式支持:利用 Mutiny 实现高性能的异步处理
3.8.3 最佳实践
- API 设计:遵循 RESTful 设计原则,使用合适的 HTTP 方法和状态码
- 参数验证:在服务层进行输入验证,确保数据完整性
- 错误处理:提供详细的错误信息,帮助客户端处理异常情况
- 性能优化:合理使用异步处理,避免阻塞操作
- 安全考虑:验证输入参数,防止安全漏洞
3.8.4 下一章预告
下一章将学习 数据持久化与数据库集成,包括: - Hibernate ORM 与 Panache - 数据库连接配置 - 实体映射和关系管理 - 查询优化和事务处理 - 数据库迁移和版本控制
通过下一章的学习,你将能够构建完整的数据驱动应用程序。