Java Jersey 实战指南:用 JAX-RS 注解写清晰的 REST API

0 阅读9分钟

简介

Jersey 是 Jakarta RESTful Web Services 规范的一种实现。

老名字常叫 JAX-RS,新包名是:

jakarta.ws.rs

Jersey 自己的核心包名通常是:

org.glassfish.jersey

简单理解:

Jakarta REST / JAX-RS 是规范
Jersey 是实现
Spring Boot 提供 Jersey 自动配置和 starter

Jersey 的开发方式是用注解把 Java 类声明成 HTTP 资源:

@Path("/users")
public class UserResource {

    @GET
    @Path("/{id}")
    public UserView getById(@PathParam("id") Long id) {
        return userService.getById(id);
    }
}

这种写法和 Spring MVC 的 Controller 很像,只是注解来自 Jakarta REST 规范。

Jersey 适合什么场景

Java 写 REST API 常见有几种方式:

方式说明
Servlet最底层,直接处理 request / response
Spring MVCSpring 生态里最常见的 Web 框架
JerseyJakarta REST / JAX-RS 实现
RESTEasy另一种 Jakarta REST / JAX-RS 实现
Apache CXF支持 JAX-RS、JAX-WS 等能力

Jersey 适合这些场景:

  • 已经在使用 JAX-RS / Jakarta REST 注解的项目
  • 传统 Java EE / Jakarta EE 项目
  • 需要兼容标准 REST 资源模型的服务
  • Spring Boot 项目里更偏好 JAX-RS 编程模型
  • 需要 Jersey Client、Filter、ExceptionMapper 等能力的项目

如果项目已经深度使用 Spring MVC,继续用 Spring MVC 通常更顺。

如果团队习惯 @Path@GET@Produces 这套标准注解,Jersey 会更自然。

Jersey、JAX-RS、Jakarta REST 的关系

这些名字容易混。

名称含义
JAX-RSJava RESTful Web Services 老称呼
Jakarta RESTful Web ServicesJAX-RS 迁到 Jakarta 后的新名字
JerseyJakarta REST / JAX-RS 的实现
RESTEasyJakarta REST / JAX-RS 的另一种实现
javax.ws.rs老包名,常见于 Java EE 8、Spring Boot 2 项目
jakarta.ws.rs新包名,常见于 Jakarta EE、Spring Boot 3 项目

Spring Boot 3 相关项目里通常使用:

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

老项目里可能会看到:

import javax.ws.rs.GET;
import javax.ws.rs.Path;

迁移到 Spring Boot 3 或 Jakarta EE 新版本时,包名变化是主要改动之一。

Jersey 和 Spring MVC 对比

同一个用户查询接口,用 Spring MVC 可能这样写:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public UserView getById(@PathVariable Long id) {
        return userService.getById(id);
    }
}

Jersey 写法:

@Path("/users")
public class UserResource {

    @GET
    @Path("/{id}")
    public UserView getById(@PathParam("id") Long id) {
        return userService.getById(id);
    }
}

对比:

维度Spring MVCJersey
路由类注解@RestController@RequestMapping@Path
GET@GetMapping@GET
POST@PostMapping@POST
路径参数@PathVariable@PathParam
查询参数@RequestParam@QueryParam
请求体@RequestBody方法参数直接接收实体
响应对象直接返回对象或 ResponseEntity直接返回对象或 Response

两者都能写 REST API。

区别不在“能不能写”,而在项目选择的编程模型和生态集成方式。

Spring Boot 集成 Jersey

Spring Boot 项目里使用 Jersey,直接引入 starter。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jersey</artifactId>
</dependency>

如果需要参数校验:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Spring Boot 会为 Jersey 做自动配置。

还需要提供一个 ResourceConfig Bean,用来注册资源类、过滤器、异常映射器等组件。

ResourceConfig 配置

package com.example.jersey.config;

import com.example.jersey.exception.ApiExceptionMapper;
import com.example.jersey.exception.ConstraintViolationMapper;
import com.example.jersey.filter.RequestLogFilter;
import com.example.jersey.resource.UserResource;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

@Component
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(UserResource.class);
        register(ApiExceptionMapper.class);
        register(ConstraintViolationMapper.class);
        register(RequestLogFilter.class);
    }
}

Spring Boot 官方文档建议在可执行 jar 场景里显式 register(...) 端点。

原因是 Jersey 对可执行 jar 的包扫描支持有限,显式注册更稳定。

如果要给 Jersey 统一加路径前缀,可以在 ResourceConfig 上加 @ApplicationPath

package com.example.jersey.config;

import com.example.jersey.resource.UserResource;
import jakarta.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

@Component
@ApplicationPath("/api")
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(UserResource.class);
    }
}

这样 @Path("/users") 最终访问路径就是:

/api/users

第一个 Resource

package com.example.jersey.resource;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.springframework.stereotype.Component;

@Component
@Path("/hello")
public class HelloResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello Jersey";
    }
}

注册:

register(HelloResource.class);

访问:

GET http://localhost:8080/api/hello

如果 Resource 类加了 @Component,它可以交给 Spring 管理,也可以注入 Spring Bean。

用户 CRUD Demo

下面用一个用户接口串起常用注解。

请求对象:

package com.example.jersey.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record CreateUserRequest(

        @NotBlank(message = "用户名不能为空")
        @Size(min = 2, max = 20, message = "用户名长度需要在2到20之间")
        String username,

        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        String email,

        @NotNull(message = "年龄不能为空")
        Integer age
) {
}

响应对象:

package com.example.jersey.user;

public record UserView(
        Long id,
        String username,
        String email,
        Integer age
) {
}

业务服务:

package com.example.jersey.user;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class UserService {

    private final AtomicLong idGenerator = new AtomicLong(1000);

    private final Map<Long, UserView> users = new ConcurrentHashMap<>();

    public List<UserView> findAll(String keyword) {
        return new ArrayList<>(users.values())
                .stream()
                .filter(user -> keyword == null || user.username().contains(keyword))
                .toList();
    }

    public UserView findById(Long id) {
        UserView user = users.get(id);
        if (user == null) {
            throw new ApiException(404, "用户不存在");
        }
        return user;
    }

    public UserView create(CreateUserRequest request) {
        Long id = idGenerator.incrementAndGet();
        UserView user = new UserView(id, request.username(), request.email(), request.age());
        users.put(id, user);
        return user;
    }

    public void delete(Long id) {
        users.remove(id);
    }
}

Resource:

package com.example.jersey.resource;

import com.example.jersey.user.CreateUserRequest;
import com.example.jersey.user.UserService;
import com.example.jersey.user.UserView;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.List;

@Component
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {

    private final UserService userService;

    public UserResource(UserService userService) {
        this.userService = userService;
    }

    @GET
    public List<UserView> list(@QueryParam("keyword") String keyword) {
        return userService.findAll(keyword);
    }

    @GET
    @Path("/{id}")
    public UserView getById(@PathParam("id") Long id) {
        return userService.findById(id);
    }

    @POST
    public Response create(@Valid CreateUserRequest request) {
        UserView user = userService.create(request);
        return Response.created(URI.create("/api/users/" + user.id()))
                .entity(user)
                .build();
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
        userService.delete(id);
        return Response.noContent().build();
    }
}

接口效果:

请求说明
GET /api/users查询全部用户
GET /api/users?keyword=tom按用户名关键字查询
GET /api/users/1001根据 ID 查询
POST /api/users创建用户
DELETE /api/users/1001删除用户

POST 请求示例:

{
  "username": "tom",
  "email": "tom@example.com",
  "age": 20
}

常用注解

Jersey 主要使用 Jakarta REST 注解。

注解作用
@Path定义类或方法路径
@GET处理 GET 请求
@POST处理 POST 请求
@PUT处理 PUT 请求
@PATCH处理 PATCH 请求
@DELETE处理 DELETE 请求
@Produces声明响应媒体类型
@Consumes声明请求体媒体类型
@PathParam获取路径参数
@QueryParam获取查询参数
@HeaderParam获取请求头
@CookieParam获取 Cookie
@FormParam获取表单字段
@BeanParam把多个参数封装到一个对象
@Context注入上下文对象

@Produces@Consumes 可以放在类上,也可以放在方法上。

方法上的配置优先级更高。

参数绑定

路径参数:

@GET
@Path("/{id}")
public UserView getById(@PathParam("id") Long id) {
    return userService.findById(id);
}

查询参数:

@GET
public List<UserView> list(@QueryParam("keyword") String keyword,
                           @QueryParam("page") @DefaultValue("1") Integer page) {
    return userService.findAll(keyword);
}

请求头:

@GET
@Path("/profile")
public UserView profile(@HeaderParam("X-User-Id") Long userId) {
    return userService.findById(userId);
}

表单参数:

@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response login(@FormParam("username") String username,
                      @FormParam("password") String password) {
    return Response.ok().build();
}

上下文对象:

@GET
@Path("/request-info")
public String requestInfo(@Context jakarta.ws.rs.core.UriInfo uriInfo) {
    return uriInfo.getRequestUri().toString();
}

BeanParam:封装查询条件

查询条件比较多时,可以用 @BeanParam

package com.example.jersey.user;

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.QueryParam;

public class UserQueryParam {

    @QueryParam("keyword")
    private String keyword;

    @QueryParam("page")
    @DefaultValue("1")
    private Integer page;

    @QueryParam("size")
    @DefaultValue("20")
    private Integer size;

    public String getKeyword() {
        return keyword;
    }

    public Integer getPage() {
        return page;
    }

    public Integer getSize() {
        return size;
    }
}

Resource:

@GET
@Path("/search")
public List<UserView> search(@BeanParam UserQueryParam queryParam) {
    return userService.findAll(queryParam.getKeyword());
}

这种写法适合列表查询、分页查询、筛选条件较多的接口。

Response:控制状态码和响应头

直接返回对象时,Jersey 会自动序列化。

需要精细控制状态码、响应头时,可以返回 Response

创建成功:

return Response.created(URI.create("/api/users/" + user.id()))
        .entity(user)
        .build();

无内容:

return Response.noContent().build();

自定义响应头:

return Response.ok(user)
        .header("X-Request-Source", "jersey")
        .build();

常见状态码:

场景状态码
查询成功200 OK
创建成功201 Created
删除成功且无响应体204 No Content
参数错误400 Bad Request
未登录401 Unauthorized
无权限403 Forbidden
资源不存在404 Not Found
服务端异常500 Internal Server Error

异常处理:ExceptionMapper

Jersey 用 ExceptionMapper 统一处理异常。

先定义业务异常:

package com.example.jersey.user;

public class ApiException extends RuntimeException {

    private final int status;

    public ApiException(int status, String message) {
        super(message);
        this.status = status;
    }

    public int getStatus() {
        return status;
    }
}

错误响应:

package com.example.jersey.exception;

public record ApiErrorResponse(
        String code,
        String message
) {
}

异常映射器:

package com.example.jersey.exception;

import com.example.jersey.user.ApiException;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Provider
public class ApiExceptionMapper implements ExceptionMapper<ApiException> {

    @Override
    public Response toResponse(ApiException exception) {
        ApiErrorResponse body = new ApiErrorResponse(
                "BUSINESS_ERROR",
                exception.getMessage()
        );

        return Response.status(exception.getStatus())
                .type(MediaType.APPLICATION_JSON)
                .entity(body)
                .build();
    }
}

注册:

register(ApiExceptionMapper.class);

这样 Resource 里可以直接抛业务异常,统一由 Mapper 转成 JSON 响应。

参数校验

Jersey 可以结合 Jakarta Validation。

请求对象上写约束:

public record CreateUserRequest(

        @NotBlank(message = "用户名不能为空")
        String username,

        @Email(message = "邮箱格式不正确")
        String email
) {
}

Resource 参数上加 @Valid

@POST
public Response create(@Valid CreateUserRequest request) {
    UserView user = userService.create(request);
    return Response.ok(user).build();
}

校验异常也可以通过 ExceptionMapper 统一处理。

package com.example.jersey.exception;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import java.util.List;

@Provider
public class ConstraintViolationMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        List<String> errors = exception.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .toList();

        return Response.status(Response.Status.BAD_REQUEST)
                .entity(errors)
                .build();
    }
}

Filter:请求日志和鉴权入口

Jersey 里常用过滤器处理请求日志、鉴权、TraceId 等逻辑。

package com.example.jersey.filter;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;

import java.io.IOException;

@Provider
public class RequestLogFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.printf(
                "Jersey 请求:%s %s%n",
                requestContext.getMethod(),
                requestContext.getUriInfo().getPath()
        );
    }
}

注册:

register(RequestLogFilter.class);

简单鉴权示例:

package com.example.jersey.filter;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

import java.io.IOException;

@Provider
public class TokenAuthFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String token = requestContext.getHeaderString("Authorization");
        if (token == null || token.isBlank()) {
            requestContext.abortWith(
                    Response.status(Response.Status.UNAUTHORIZED)
                            .entity("未登录")
                            .build()
            );
        }
    }
}

复杂安全逻辑更适合交给 Spring Security 或网关统一处理。

Jersey Client

Jersey 不只可以写服务端,也提供 Client API 调用 HTTP 服务。

Maven 依赖:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>${jersey.version}</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>${jersey.version}</version>
</dependency>

Client 示例:

package com.example.jersey.client;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

public class JerseyClientDemo {

    public static void main(String[] args) {
        Client client = ClientBuilder.newClient();

        CreateUserRequest request = new CreateUserRequest(
                "tom",
                "tom@example.com",
                20
        );

        Response response = client.target("http://localhost:8080/api")
                .path("/users")
                .request(MediaType.APPLICATION_JSON)
                .post(Entity.entity(request, MediaType.APPLICATION_JSON));

        System.out.println(response.getStatus());
        System.out.println(response.readEntity(String.class));

        response.close();
        client.close();
    }
}

Spring 项目里也可以使用 RestClientWebClient、OpenFeign 等客户端。

Jersey Client 更适合已经在 JAX-RS/Jersey 体系里的项目。

独立运行:Grizzly Demo

Jersey 也可以不依赖 Spring Boot,用 Grizzly 启动一个轻量 HTTP 服务。

依赖:

<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-grizzly2-http</artifactId>
    <version>${jersey.version}</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
    <version>${jersey.version}</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>${jersey.version}</version>
</dependency>

启动类:

package com.example.jersey.standalone;

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

import java.io.IOException;
import java.net.URI;

public class JerseyStandaloneApplication {

    public static void main(String[] args) throws IOException {
        ResourceConfig config = new ResourceConfig()
                .register(HelloResource.class);

        HttpServer server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create("http://localhost:8080/api/"),
                config
        );

        System.out.println("Jersey Grizzly 服务已启动");
        System.in.read();
        server.shutdownNow();
    }
}

这种方式适合 Demo、测试工具、轻量 HTTP 服务。

正式业务系统更常见的方式还是 Spring Boot 或应用服务器部署。

和 Spring MVC 共存

Spring Boot 项目里,Jersey 和 Spring MVC 可以共存,但需要明确请求由谁处理。

一种常见方式是让 Jersey 走 Filter 模式,并把 404 转发给其他 Web 框架。

application.yml

spring:
  jersey:
    type: filter

ResourceConfig

package com.example.jersey.config;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletProperties;
import org.springframework.stereotype.Component;

@Component
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(UserResource.class);
        property(ServletProperties.FILTER_FORWARD_ON_404, true);
    }
}

这样 Jersey 没匹配到的请求,可以继续交给 Spring MVC。

如果项目没有共存需求,Jersey 独立处理接口会更简单。

Spring Security 集成注意点

Jersey 和 Spring Security 结合时,如果使用方法级安全,Spring Boot 官方文档建议设置:

setProperties(Map.of(
        "jersey.config.server.response.setStatusOverSendError",
        true
));

完整示例:

import com.example.jersey.resource.UserResource;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(UserResource.class);
        setProperties(Map.of(
                "jersey.config.server.response.setStatusOverSendError",
                true
        ));
    }
}

这个配置可以避免 Jersey 过早提交错误响应,让 Spring Security 有机会正确处理认证和授权失败。

常见问题

Resource 没有生效

常见原因:

原因处理方式
没有注册 ResourceResourceConfigregister(...)
只使用 packages(...) 扫描可执行 jar 场景优先显式注册
缺少 @Component需要 Spring 管理时加 @Component
路径前缀理解错误检查 @ApplicationPath@Path

JSON 不能正常序列化

常见原因是缺少 Jackson 适配。

Spring Boot starter 通常会处理常见 JSON 场景。

独立 Jersey 项目可以加入:

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>${jersey.version}</version>
</dependency>

@RequestBody 为什么不能用

@RequestBody 是 Spring MVC 注解。

Jersey 使用 JAX-RS 模型,请求体通常直接放在方法参数里:

@POST
public Response create(CreateUserRequest request) {
    return Response.ok().build();
}

路径参数使用 @PathParam,查询参数使用 @QueryParam

javax.ws.rs 和 jakarta.ws.rs 能不能混用

不适合混用。

Spring Boot 2、老 Java EE 项目常见 javax.ws.rs

Spring Boot 3、Jakarta EE 新项目常见 jakarta.ws.rs

同一个项目里混用两套包名,容易出现注解不识别、类加载冲突、依赖版本不匹配等问题。

实践建议

场景建议
Spring Boot 3 项目使用 spring-boot-starter-jersey
Resource 注册优先在 ResourceConfig 显式 register
路径前缀使用 @ApplicationPath("/api")
JSON 接口类上统一 @Produces@Consumes
业务异常使用 ExceptionMapper
请求日志使用 ContainerRequestFilter
参数校验结合 Jakarta Validation
和 Spring MVC 共存使用 filter 模式和 404 转发
新项目包名使用 jakarta.ws.rs

小结

Jersey 的核心是 JAX-RS / Jakarta REST 编程模型。

@Path 定义资源路径,@GET@POST 等注解定义 HTTP 方法,@Produces@Consumes 控制媒体类型,@PathParam@QueryParam 等注解完成参数绑定。

Spring Boot 项目里,spring-boot-starter-jerseyResourceConfig 就能把 Jersey 接入应用。业务接口用 Resource 编写,异常交给 ExceptionMapper,横切逻辑交给 Filter,外部 HTTP 调用可以用 Jersey Client。

如果项目需要标准 JAX-RS 风格的 REST API,Jersey 是一套成熟、清晰、可落地的选择。