背景
本文是《JavaEE 后端从小白到大神》修仙系列第七篇,正式进入JavaEE后端世界。若想详细学习请点击首篇博文,我们开始吧。
第七篇:JAX-RS(RESTful Web Service)实战
- JAX-RS 核心注解(@Path、@GET、@POST、@Produces、@Consumes);
- JSON 绑定(JSON-B、Jackson);
- 异常处理与过滤器;
- 参数绑定与路径解析;
- 客户端调用;
- 与 Spring MVC REST 的差异。
一、JAX-RS 概述
1. 什么是 JAX-RS
JAX-RS(Java API for RESTful Web Services)是 Jakarta EE(原 Java EE)中定义的RESTful Web 服务标准规范,提供了一套 Java 注解和 API,用于快速开发和部署遵循 REST 架构风格的 Web 服务。
从 Jakarta EE 9 开始,JAX-RS 已更名为 Jakarta RESTful Web Services,包名从 javax.ws.rs 迁移到 jakarta.ws.rs。
一句话总结:JAX-RS = Java 平台开发 RESTful API 的标准规范。
2. JAX-RS 的核心演进
| 版本 |
发布时间 |
核心变化 |
| JAX-RS 1.0(JSR 311) |
2008 |
初始规范,定义核心注解 |
| JAX-RS 1.1(JSR 311) |
2011 |
小幅修订 |
| JAX-RS 2.0(JSR 339) |
2013 |
新增 Client API、过滤器和拦截器、异步处理 |
| JAX-RS 2.1(JSR 370) |
2017 |
新增反应式客户端、SSE(Server-Sent Events)支持 |
| Jakarta REST 3.0 |
2020 |
包名从 javax 迁移到 jakarta |
| Jakarta REST 3.1 |
2022 |
适配 Jakarta EE 10 |
| Jakarta REST 4.0 |
2024 |
适配 Jakarta EE 11 |
| Jakarta REST 5.0 |
2026(开发中) |
适配 Jakarta EE 12,进一步与 CDI 集成 |
3. 核心概念
| 概念 |
说明 |
| 资源(Resource) |
通过 REST API 暴露的实体或服务,用 Java 类表示 |
| 资源方法(Resource Method) |
资源类中处理 HTTP 请求的方法 |
| 表示(Representation) |
资源的某种表现形式(JSON、XML、文本等) |
| 提供者(Provider) |
JAX-RS 扩展组件(如 ExceptionMapper、MessageBodyReader/Writer) |
4. JAX-RS 与实现的关系
JAX-RS 只是一个规范(Specification) ,不是具体实现。要使用 JAX-RS 开发应用,需要选择一个兼容的实现(Implementation) 。
| JAX-RS 实现 |
说明 |
| Jersey |
参考实现,由 Eclipse 基金会维护,最常用 |
| RESTEasy |
Red Hat 出品,与 WildFly 深度集成 |
| Apache CXF |
Apache 开源,功能全面 |
| Restlet |
轻量级 REST 框架 |
二、核心注解
JAX-RS 通过一组注解来简化 RESTful 服务的开发。
1. @Path——定义资源路径
@Path 用于标注资源类或资源方法的相对路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import jakarta.ws.rs.Path;
// 类级别:基础路径
@Path("/api/users")
public class UserResource {
// 方法级别:子路径
@Path("/{id}")
@GET
public User getUser(@PathParam("id") Long id) {
// ...
}
}
|
访问路径:/api/users/123
2. HTTP 方法注解
JAX-RS 为每种 HTTP 方法提供了对应的注解。
| 注解 |
HTTP 方法 |
用途 |
@GET |
GET |
查询资源 |
@POST |
POST |
创建资源 |
@PUT |
PUT |
全量更新资源 |
@DELETE |
DELETE |
删除资源 |
@HEAD |
HEAD |
获取响应头(不返回 body) |
@OPTIONS |
OPTIONS |
查询资源支持的方法 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Path("/api/users")
public class UserResource {
@GET
public List<User> listUsers() {
// 查询所有用户
}
@GET
@Path("/{id}")
public User getUser(@PathParam("id") Long id) {
// 查询单个用户
}
@POST
public User createUser(User user) {
// 创建用户
}
@PUT
@Path("/{id}")
public User updateUser(@PathParam("id") Long id, User user) {
// 全量更新用户
}
@DELETE
@Path("/{id}")
public void deleteUser(@PathParam("id") Long id) {
// 删除用户
}
}
|
3. @Produces——指定响应格式
@Produces 用于指定资源方法可以产生(返回) 的 MIME 媒体类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
// 类级别:所有方法默认返回 JSON
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
// 返回 JSON
@GET
public List<User> listUsers() { ... }
// 指定返回 XML(覆盖类级别)
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_XML)
public User getUserXml(@PathParam("id") Long id) { ... }
// 支持多种格式,根据请求头 Accept 选择
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public User getUser(@PathParam("id") Long id) { ... }
}
|
4. @Consumes——指定请求格式
@Consumes 用于指定资源方法可以消费(接收) 的 MIME 媒体类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.core.MediaType;
@Path("/api/users")
@Consumes(MediaType.APPLICATION_JSON) // 类级别:默认接收 JSON
public class UserResource {
@POST
public User createUser(User user) {
// 接收 JSON 请求体
}
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_XML) // 方法级别覆盖
public User updateUser(@PathParam("id") Long id, User user) {
// 接收 XML 请求体
}
}
|
5. Hello World 示例
一个完整的 JAX-RS Hello World 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "Hello, World!";
}
}
|
访问 http://localhost:8080/app/hello,返回 Hello, World!。
三、参数绑定与路径解析
JAX-RS 提供了多种注解来从 HTTP 请求中提取参数。
1. @PathParam——路径参数
从 URL 路径中提取参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Path("/api/users")
public class UserResource {
// 路径:/api/users/123
@GET
@Path("/{id}")
public User getUser(@PathParam("id") Long id) {
// id = 123
}
// 多段路径参数
// 路径:/api/users/123/orders/456
@GET
@Path("/{userId}/orders/{orderId}")
public Order getOrder(
@PathParam("userId") Long userId,
@PathParam("orderId") Long orderId) {
// userId = 123, orderId = 456
}
}
|
2. @QueryParam——查询参数
从 URL 查询字符串中提取参数。
1
2
3
4
5
6
7
8
9
10
11
|
@Path("/api/users")
public class UserResource {
// 请求:GET /api/users?page=1&size=20
@GET
public List<User> listUsers(
@QueryParam("page") @DefaultValue("1") int page,
@QueryParam("size") @DefaultValue("10") int size) {
// page = 1, size = 20
}
}
|
@DefaultValue 用于指定参数默认值,避免 null 或 0。
从 application/x-www-form-urlencoded 格式的请求体中提取参数。
1
2
3
4
5
6
7
8
9
10
11
12
|
@Path("/api/auth")
public class AuthResource {
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response login(
@FormParam("username") String username,
@FormParam("password") String password) {
// username = 表单提交的用户名
}
}
|
从 HTTP 请求头中提取参数。
1
2
3
4
5
6
7
8
9
10
|
@Path("/api")
public class ApiResource {
@GET
public String getData(
@HeaderParam("Authorization") String authToken,
@HeaderParam("User-Agent") String userAgent) {
// authToken = 请求头中的 Authorization 值
}
}
|
5. @CookieParam——Cookie 参数
从 Cookie 中提取参数。
1
2
3
4
5
6
7
8
9
|
@Path("/api")
public class SessionResource {
@GET
public String getSession(
@CookieParam("JSESSIONID") String sessionId) {
// sessionId = Cookie 中的 JSESSIONID 值
}
}
|
6. @BeanParam——参数聚合
将多个参数封装到一个对象中,简化代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 定义参数聚合类
public class UserQueryParams {
@QueryParam("page")
private int page;
@QueryParam("size")
private int size;
@QueryParam("keyword")
private String keyword;
// getter/setter
}
// 使用 @BeanParam
@Path("/api/users")
public class UserResource {
@GET
public List<User> listUsers(@BeanParam UserQueryParams params) {
// 所有查询参数自动注入到 params 对象
}
}
|
7. 参数注解汇总
| 注解 |
参数来源 |
示例 |
@PathParam |
URL 路径 |
/users/{id} |
@QueryParam |
URL 查询字符串 |
?page=1 |
@FormParam |
表单请求体 |
username=admin |
@HeaderParam |
HTTP 请求头 |
Authorization: Bearer xxx |
@CookieParam |
Cookie |
JSESSIONID=xxx |
@BeanParam |
多个参数聚合 |
封装为对象 |
四、JSON 绑定
1. JSON-B(Jakarta JSON Binding)
JSON-B 是 Jakarta EE 的标准 JSON 绑定规范,用于在 Java 对象和 JSON 之间自动转换。
Maven 依赖:
1
2
3
4
5
|
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
<version>3.0.0</version>
</dependency>
|
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
public class JsonbExample {
public static void main(String[] args) {
Jsonb jsonb = JsonbBuilder.create();
// Java 对象 → JSON
User user = new User(1L, "张三", 25);
String json = jsonb.toJson(user);
System.out.println(json); // {"id":1,"name":"张三","age":25}
// JSON → Java 对象
String jsonStr = "{\"id\":1,\"name\":\"张三\",\"age\":25}";
User parsed = jsonb.fromJson(jsonStr, User.class);
}
}
|
2. JAX-RS 中的自动 JSON 绑定
当资源方法的 @Consumes 或 @Produces 指定为 application/json 时,JAX-RS 实现(如 Jersey)会自动使用 JSON-B 或 Jackson 完成 Java 对象与 JSON 的相互转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
// 返回 User 对象 → 自动转为 JSON
@GET
@Path("/{id}")
public User getUser(@PathParam("id") Long id) {
return userService.findById(id);
}
// 接收 JSON → 自动转为 User 对象
@POST
public User createUser(User user) {
return userService.create(user);
}
}
|
3. 自定义 JSON 序列化
通过 @JsonbProperty、@JsonbTransient 等注解控制 JSON 序列化行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbTransient;
public class User {
private Long id;
@JsonbProperty("userName") // JSON 字段名改为 userName
private String name;
@JsonbTransient // 不序列化到 JSON
private String password;
private Integer age;
// getter/setter
}
|
五、异常处理
1. WebApplicationException
JAX-RS 提供了 WebApplicationException,可以在资源方法中直接抛出,并指定 HTTP 状态码和响应内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Path("/api/users")
public class UserResource {
@GET
@Path("/{id}")
public User getUser(@PathParam("id") Long id) {
User user = userService.findById(id);
if (user == null) {
// 返回 404 Not Found
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return user;
}
@POST
public User createUser(User user) {
if (user.getName() == null || user.getName().isEmpty()) {
// 返回 400 Bad Request,带错误信息
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST)
.entity("用户名不能为空")
.type(MediaType.TEXT_PLAIN)
.build()
);
}
return userService.create(user);
}
}
|
2. ExceptionMapper——统一异常处理
ExceptionMapper<E> 是一个接口,用于将特定类型的异常映射为 HTTP 响应。
核心步骤:
- 创建一个类实现
ExceptionMapper<E> 接口;
- 使用
@Provider 注解标记;
- 实现
toResponse() 方法,返回 Response 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
// 1. 自定义业务异常
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("用户不存在:" + id);
}
}
// 2. 创建 ExceptionMapper
@Provider
public class UserNotFoundMapper implements ExceptionMapper<UserNotFoundException> {
@Override
public Response toResponse(UserNotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"error\":\"" + exception.getMessage() + "\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// 3. 在资源方法中使用
@Path("/api/users")
public class UserResource {
@GET
@Path("/{id}")
public User getUser(@PathParam("id") Long id) {
User user = userService.findById(id);
if (user == null) {
throw new UserNotFoundException(id); // 由 Mapper 处理
}
return user;
}
}
|
3. 全局异常捕获
可以创建一个 GenericExceptionMapper 捕获所有未处理的异常,作为兜底方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable exception) {
// 记录日志
exception.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"error\":\"服务器内部错误\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
}
|
4. 异常处理最佳实践
- 业务异常:自定义异常类,配合
ExceptionMapper 返回有意义的 HTTP 状态码;
- 参数校验:使用
WebApplicationException 返回 400 错误;
- 资源不存在:返回 404;
- 权限不足:返回 403;
- 兜底方案:提供
GenericExceptionMapper 捕获所有未处理异常,避免泄露敏感信息。
六、过滤器与拦截器
JAX-RS 2.0 引入了过滤器(Filter) 和拦截器(Interceptor) 的标准 API,用于在请求/响应的处理管道中插入横切逻辑。
1. Filter vs Interceptor
| 对比维度 |
Filter(过滤器) |
Interceptor(拦截器) |
| 操作对象 |
消息元数据(HTTP 头、查询参数、媒体类型等) |
消息体(Entity)的输入/输出流 |
| 主要用途 |
日志、认证、授权、CORS |
压缩/解压、加密/解密 |
| 能否中止请求 |
可以(如认证失败时中止) |
不可以 |
| 接口 |
ContainerRequestFilter、ContainerResponseFilter |
ReaderInterceptor、WriterInterceptor |
2. ContainerRequestFilter——请求过滤器
在服务器端拦截请求,可在资源方法执行前进行预处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
@Provider
@Priority(1000) // 优先级数值越小越早执行
public class AuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// 获取 Authorization 头
String authHeader = requestContext.getHeaderString("Authorization");
// 简单认证示例(实际应使用 JWT 或 Session)
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// 中止请求,返回 401 Unauthorized
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.entity("{\"error\":\"未授权访问\"}")
.type(MediaType.APPLICATION_JSON)
.build()
);
}
}
}
|
3. ContainerResponseFilter——响应过滤器
在服务器端拦截响应,可在资源方法执行后进行后处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
@Provider
public class CorsFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
// 添加 CORS 响应头
responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
responseContext.getHeaders().add("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
responseContext.getHeaders().add("Access-Control-Allow-Headers",
"Content-Type, Authorization");
}
}
|
4. 服务器端过滤器执行顺序
JAX-RS 服务器端有 4 个过滤器/拦截器的扩展点,按以下顺序执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
请求到达
↓
1. PreMatchContainerRequest 过滤器(资源匹配前执行)
↓
2. ContainerRequest 过滤器(资源匹配后执行)
↓
3. ReadInterceptor(读取请求体时执行)
↓
4. 资源方法执行
↓
5. WriteInterceptor(写入响应体时执行)
↓
6. ContainerResponse 过滤器
↓
响应返回
|
5. @NameBinding——选择性绑定
默认情况下,Filter 会应用到所有资源方法。如果只想拦截特定方法,可以使用 @NameBinding:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 1. 定义名称绑定注解
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.*;
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Secured {}
// 2. 在 Filter 上使用 @Secured
@Secured
@Provider
public class AuthenticationFilter implements ContainerRequestFilter {
// ...
}
// 3. 在资源方法或类上使用 @Secured
@Path("/api/admin")
@Secured // 该类的所有方法都需要认证
public class AdminResource {
@GET
@Secured // 仅该方法需要认证
public String getAdminData() { ... }
}
|
6. @PreMatching——资源匹配前拦截
@PreMatching 标记的 Filter 在资源匹配之前执行,此时无法获取具体的资源方法信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.container.ContainerRequestFilter;
@Provider
@PreMatching
public class PreMatchFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) {
// 在 JAX-RS 将请求匹配到具体资源方法之前执行
// 可用于请求路由、日志等
System.out.println("请求路径:" + requestContext.getUriInfo().getPath());
}
}
|
七、JAX-RS 客户端 API
JAX-RS 2.0 引入了标准化的客户端 API,用于调用 RESTful 服务。
1. 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
public class RestClientExample {
public static void main(String[] args) {
// 1. 创建 Client 实例
Client client = ClientBuilder.newClient();
try {
// 2. 设置目标 URL
WebTarget target = client.target("http://localhost:8080/api");
// 3. 发起 GET 请求
String response = target.path("/users/1")
.request(MediaType.APPLICATION_JSON)
.get(String.class);
System.out.println("响应:" + response);
// 4. 获取完整 Response 对象(可获取状态码、响应头)
Response resp = target.path("/users")
.queryParam("page", 1)
.queryParam("size", 10)
.request(MediaType.APPLICATION_JSON)
.get();
System.out.println("状态码:" + resp.getStatus());
String body = resp.readEntity(String.class);
System.out.println("响应体:" + body);
resp.close(); // 关闭响应
} finally {
// 5. 关闭 Client(释放资源)
client.close();
}
}
}
|
2. POST 请求(发送 JSON)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class PostExample {
public static void main(String[] args) {
Client client = ClientBuilder.newClient();
try {
// 创建 User 对象
User user = new User("张三", 25);
// 发送 POST 请求
User created = client.target("http://localhost:8080/api/users")
.request(MediaType.APPLICATION_JSON)
.post(Entity.json(user), User.class);
System.out.println("创建成功:" + created.getId());
} finally {
client.close();
}
}
}
|
3. 路径参数与查询参数
1
2
3
4
5
6
7
8
|
// 路径参数
WebTarget target = client.target("http://localhost:8080/api/users/{id}")
.resolveTemplate("id", 123); // 替换路径参数
// 查询参数
WebTarget target = client.target("http://localhost:8080/api/users")
.queryParam("page", 1)
.queryParam("size", 20);
|
4. Client 实例管理
Client 是重量级对象,创建和销毁开销较大。
最佳实践:
- 整个应用只创建一个
Client 实例;
- 使用连接池(如 Apache HttpClient)提升性能;
- 应用关闭时统一关闭
Client。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 应用启动时创建
@ApplicationScoped
public class RestClientProducer {
private Client client;
@PostConstruct
public void init() {
client = ClientBuilder.newClient();
}
@PreDestroy
public void destroy() {
if (client != null) {
client.close();
}
}
public Client getClient() {
return client;
}
}
|
八、JAX-RS 与 Spring MVC REST 对比
1. 本质区别
| 对比维度 |
JAX-RS |
Spring MVC REST |
| 类型 |
规范(Specification) |
框架(Framework) |
| 实现 |
需要第三方实现(Jersey、RESTEasy) |
自带完整实现 |
| 定位 |
专注于 RESTful Web Services |
完整的 Web 应用框架(包含 REST 支持) |
2. 注解对比
| 功能 |
JAX-RS |
Spring MVC |
| 资源路径 |
@Path |
@RequestMapping / @GetMapping |
| HTTP 方法 |
@GET、@POST、@PUT、@DELETE |
@GetMapping、@PostMapping 等 |
| 路径参数 |
@PathParam |
@PathVariable |
| 查询参数 |
@QueryParam |
@RequestParam |
| 请求体 |
自动绑定(JSON → 对象) |
@RequestBody |
| 响应体 |
自动绑定(对象 → JSON) |
@ResponseBody / @RestController |
| 依赖注入 |
需配合 CDI |
@Autowired 原生支持 |
3. 代码对比
JAX-RS(使用 Jersey) :
1
2
3
4
5
6
7
8
9
|
@Path("/hello")
public class HelloController {
@GET
@Path("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public Response hello(@PathParam("name") String name) {
return Response.ok("Hello, " + name).build();
}
}
|
Spring MVC:
1
2
3
4
5
6
7
8
|
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping(value = "/{name}", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> hello(@PathVariable String name) {
return new ResponseEntity<>("Hello, " + name, HttpStatus.OK);
}
}
|
4. 选型建议
| 场景 |
推荐 |
| 追求轻量、标准化、可移植 |
JAX-RS(规范统一,换实现不换代码) |
| Spring 生态下开发 |
Spring MVC(天然集成 Spring Security、Spring Data 等) |
| 微服务架构 |
两者均可,Spring Cloud 生态更成熟 |
| 需要 HTML 视图渲染 |
Spring MVC(JSP/Thymeleaf 支持更好) |
| 已有 Spring 经验 |
Spring MVC(上手更快) |
核心观点:JAX-RS 是标准规范,适合需要跨实现移植的场景;Spring MVC 是完整框架,适合深度使用 Spring 生态的项目。两者没有绝对优劣,根据项目需求选择。
九、实战案例:用户管理 RESTful API
1. 业务需求
实现一个完整的用户管理 RESTful API:
| 接口 |
方法 |
路径 |
功能 |
| 查询所有用户 |
GET |
/api/users |
分页查询用户列表 |
| 查询单个用户 |
GET |
/api/users/{id} |
根据 ID 查询用户 |
| 创建用户 |
POST |
/api/users |
新增用户 |
| 更新用户 |
PUT |
/api/users/{id} |
全量更新用户 |
| 删除用户 |
DELETE |
/api/users/{id} |
删除用户 |
2. 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbTransient;
import java.time.LocalDateTime;
public class User {
private Long id;
private String username;
private String email;
@JsonbTransient // 不序列化到 JSON(密码不返回)
private String password;
private Integer age;
private LocalDateTime createTime;
// 构造器、getter、setter
public User() {}
public User(String username, String email, String password, Integer age) {
this.username = username;
this.email = email;
this.password = password;
this.age = age;
this.createTime = LocalDateTime.now();
}
// getter/setter 省略
}
|
3. Service 层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@Stateless
public class UserService {
@PersistenceContext
private EntityManager em;
public List<User> findAll(int page, int size) {
return em.createQuery("SELECT u FROM User u ORDER BY u.id", User.class)
.setFirstResult((page - 1) * size)
.setMaxResults(size)
.getResultList();
}
public User findById(Long id) {
User user = em.find(User.class, id);
if (user == null) {
throw new UserNotFoundException(id);
}
return user;
}
public User create(User user) {
user.setCreateTime(LocalDateTime.now());
em.persist(user);
return user;
}
public User update(Long id, User userData) {
User user = findById(id);
user.setUsername(userData.getUsername());
user.setEmail(userData.getEmail());
user.setAge(userData.getAge());
if (userData.getPassword() != null && !userData.getPassword().isEmpty()) {
user.setPassword(userData.getPassword());
}
return em.merge(user);
}
public void delete(Long id) {
User user = findById(id);
em.remove(user);
}
}
|
4. 自定义异常
1
2
3
4
5
6
7
8
9
10
11
|
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("用户不存在:" + id);
}
}
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
|
5. ExceptionMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Provider
public class UserNotFoundMapper implements ExceptionMapper<UserNotFoundException> {
@Override
public Response toResponse(UserNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"error\":\"" + e.getMessage() + "\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
}
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
@Override
public Response toResponse(ValidationException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\":\"" + e.getMessage() + "\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
}
|
6. 资源类(REST Controller)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
import jakarta.ejb.EJB;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@EJB
private UserService userService;
// 1. 查询所有用户(分页)
@GET
public Response listUsers(
@QueryParam("page") @DefaultValue("1") int page,
@QueryParam("size") @DefaultValue("10") int size) {
if (page < 1) page = 1;
if (size < 1 || size > 100) size = 10;
List<User> users = userService.findAll(page, size);
return Response.ok(users).build();
}
// 2. 查询单个用户
@GET
@Path("/{id}")
public Response getUser(@PathParam("id") Long id) {
User user = userService.findById(id);
return Response.ok(user).build();
}
// 3. 创建用户
@POST
public Response createUser(User user) {
// 简单校验
if (user.getUsername() == null || user.getUsername().isEmpty()) {
throw new ValidationException("用户名不能为空");
}
if (user.getEmail() == null || user.getEmail().isEmpty()) {
throw new ValidationException("邮箱不能为空");
}
User created = userService.create(user);
return Response.status(Response.Status.CREATED)
.entity(created)
.build();
}
// 4. 更新用户
@PUT
@Path("/{id}")
public Response updateUser(@PathParam("id") Long id, User user) {
User updated = userService.update(id, user);
return Response.ok(updated).build();
}
// 5. 删除用户
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") Long id) {
userService.delete(id);
return Response.noContent().build();
}
}
|
7. 应用配置(Application 子类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
@ApplicationPath("/api") // 所有 JAX-RS 资源的基础路径
public class RestApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>();
// 注册资源类和 Provider
classes.add(UserResource.class);
classes.add(UserNotFoundMapper.class);
classes.add(ValidationExceptionMapper.class);
classes.add(AuthenticationFilter.class);
classes.add(CorsFilter.class);
return classes;
}
}
|
访问路径:http://localhost:8080/app/api/users
8. 测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class UserResourceTest {
public static void main(String[] args) {
Client client = ClientBuilder.newClient();
try {
// 1. 创建用户
User newUser = new User("张三", "[email protected]", "123456", 25);
User created = client.target("http://localhost:8080/app/api/users")
.request(MediaType.APPLICATION_JSON)
.post(Entity.json(newUser), User.class);
System.out.println("创建成功,ID:" + created.getId());
// 2. 查询用户
User user = client.target("http://localhost:8080/app/api/users/{id}")
.resolveTemplate("id", created.getId())
.request(MediaType.APPLICATION_JSON)
.get(User.class);
System.out.println("查询结果:" + user.getUsername());
// 3. 删除用户
Response resp = client.target("http://localhost:8080/app/api/users/{id}")
.resolveTemplate("id", created.getId())
.request()
.delete();
System.out.println("删除状态码:" + resp.getStatus());
resp.close();
} finally {
client.close();
}
}
}
|
十、总结
1. 核心知识脉络
| 层级 |
技术 |
核心概念 |
| 核心注解 |
@Path、@GET、@POST、@Produces、@Consumes |
定义资源和 HTTP 方法映射 |
| 参数绑定 |
@PathParam、@QueryParam、@FormParam、@HeaderParam |
从 HTTP 请求中提取参数 |
| 数据绑定 |
JSON-B / Jackson |
Java 对象 ↔ JSON 自动转换 |
| 异常处理 |
ExceptionMapper、WebApplicationException |
统一异常 → HTTP 响应映射 |
| 横切逻辑 |
Filter、Interceptor |
请求/响应拦截、认证、日志 |
| 客户端调用 |
Client API |
调用远程 RESTful 服务 |
2. 关键理解
- JAX-RS 是规范,不是实现:需要选择一个兼容的实现(Jersey 或 RESTEasy);
- 注解驱动:通过
@Path、@GET 等注解声明式定义 RESTful 服务,代码简洁;
- 自动数据绑定:配合 JSON-B,Java 对象与 JSON 自动转换,无需手动序列化;
- 统一异常处理:
ExceptionMapper 可将业务异常统一转换为规范的 HTTP 响应;
- Filter 处理元数据,Interceptor 处理消息体:两者分工明确,各司其职;
- 标准化客户端 API:JAX-RS 2.0 提供了统一的客户端 API,可调用任意 REST 服务。
3. 与后续学习的衔接
理解 JAX-RS 后,你将能更好地理解:
- MicroProfile REST Client:声明式 REST 客户端,简化微服务调用;
- OpenAPI / Swagger:自动生成 REST API 文档;
- Spring Web MVC 的 REST 支持:对比两种 REST 开发方式的异同;
- 云原生微服务:RESTful API 是微服务间通信的基础。