Jersey 3 Hello World Example

September 24, 2021 |

Jersey is the reference implementation of the Jakarta RESTful Web Services (formerly JAX‑RS) specification. Jersey 3.x targets Jakarta EE 9+, which completed the rename of all javax.* packages to jakarta.*. This is a breaking change from Jersey 2.x – any code using javax.ws.rs must be updated to jakarta.ws.rs. This guide builds a working REST API from scratch using Jersey 3 with the JDK's built‑in HTTP server, requiring no application server.


The javaxjakarta Migration

Jakarta EE 9 (released 2020) renamed all javax.* packages to jakarta.*. The key imports that change for REST development are:

Jakarta EE 8 / Jersey 2 Jakarta EE 9+ / Jersey 3
javax.ws.rs.* jakarta.ws.rs.*
javax.ws.rs.core.* jakarta.ws.rs.core.*
javax.ws.rs.ext.* jakarta.ws.rs.ext.*
javax.inject.* jakarta.inject.*
javax.json.* jakarta.json.*

If you are migrating an existing Jersey 2 application, you'll need to:

  • Update all imports from javax.ws.rs to jakarta.ws.rs.
  • Update your Maven/Gradle dependencies to Jersey 3.x.
  • Update your application server to Jakarta EE 9+ if you deploy to one.

Maven Dependencies

Here's the complete pom.xml dependency set for Jersey 3 with the JDK HTTP container and Jackson JSON support:

<properties>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
  <jersey.version>3.1.5</jersey.version>
</properties>

<dependencies>
  <!-- Jersey core server -->
  <dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-server</artifactId>
    <version>${jersey.version}</version>
  </dependency>

  <!-- JDK HTTP server container -->
  <dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-jdk-http</artifactId>
    <version>${jersey.version}</version>
  </dependency>

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

  <!-- JSON support via Jackson -->
  <dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>${jersey.version}</version>
  </dependency>
</dependencies>

Note: The jersey-hk2 dependency provides HK2 (a lightweight dependency injection framework) that Jersey uses for internal wiring. You may also use CDI if you're in a Jakarta EE container, but for a standalone app, HK2 is the default.


Application Class

The Application subclass is the entry point for JAX‑RS configuration. It tells Jersey which resource classes and providers to register:

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.Set;

@ApplicationPath("/api")
public class MyApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(
            MenuResource.class
        );
    }
}

The @ApplicationPath annotation sets the base URI path for all resources. All endpoints defined in MenuResource will be available under /api/.... You can also use ResourceConfig (a Jersey subclass) for programmatic registration, which we'll see later.


Resource Class

The resource class defines the REST endpoints. Notice the use of jakarta.ws.rs.* annotations:

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Collection;

@Path("/menu")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MenuResource {

    private final MenuService service = new MenuService();

    @GET
    public Collection<MenuItem> getAll() {
        return service.findAll();
    }

    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") int id) {
        MenuItem item = service.findById(id);
        if (item == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(item).build();
    }

    @POST
    public Response create(MenuItem item) {
        MenuItem created = service.save(item);
        return Response.status(Response.Status.CREATED).entity(created).build();
    }

    @PUT
    @Path("/{id}")
    public Response update(@PathParam("id") int id, MenuItem item) {
        item.setId(id);
        MenuItem updated = service.update(item);
        return Response.ok(updated).build();
    }

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

Notice the use of Response for fine‑grained control over HTTP status codes and headers. The @Produces and @Consumes at the class level apply to all methods unless overridden.


Model and Service Classes

MenuItem.java

public class MenuItem {
    private int id;
    private String name;
    private double price;

    public MenuItem() {}

    public MenuItem(int id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // getters and setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}

MenuService.java – In‑memory store

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class MenuService {
    private final Map<Integer, MenuItem> store = new HashMap<>();
    private final AtomicInteger idGen = new AtomicInteger(1);

    public MenuService() {
        save(new MenuItem(0, "Burger", 8.99));
        save(new MenuItem(0, "Pizza",  11.49));
        save(new MenuItem(0, "Salad",  6.99));
    }

    public Collection<MenuItem> findAll() { return store.values(); }

    public MenuItem findById(int id) { return store.get(id); }

    public MenuItem save(MenuItem item) {
        int id = idGen.getAndIncrement();
        item.setId(id);
        store.put(id, item);
        return item;
    }

    public MenuItem update(MenuItem item) {
        store.put(item.getId(), item);
        return item;
    }

    public void delete(int id) { store.remove(id); }
}

The service is simple and not thread‑safe – for a real application, you'd use a proper database, but this serves the purpose of the example.


Main Class: Starting the Server

The main class uses JdkHttpServerFactory to create an HTTP server that delegates requests to Jersey:

import com.sun.net.httpserver.HttpServer;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import java.net.URI;

public class Main {

    private static final URI BASE_URI = URI.create("http://localhost:8080/");

    public static void main(String[] args) throws Exception {
        ResourceConfig config = new ResourceConfig()
            .register(MenuResource.class)
            .packages("com.example"); // or list resource classes explicitly

        HttpServer server = JdkHttpServerFactory.createHttpServer(BASE_URI, config);
        System.out.println("Server started at " + BASE_URI);
        System.out.println("API available at " + BASE_URI + "api/menu");
        System.out.println("Press Ctrl+C to stop.");

        // Keep running until interrupted
        Thread.currentThread().join();
    }
}

ResourceConfig is a Jersey‑specific subclass of Application that allows programmatic registration of resources and features. The packages() method scans the given package for JAX‑RS annotated classes, which is convenient when you have many resources.


Building and Running

Add the exec plugin to your pom.xml to run directly from Maven:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.1.0</version>
  <configuration>
    <mainClass>com.example.Main</mainClass>
  </configuration>
</plugin>

Then build and run:

mvn clean compile exec:java

Test the endpoints using curl:

# Get all menu items
curl http://localhost:8080/api/menu

# Get a specific item
curl http://localhost:8080/api/menu/1

# Create a new item
curl -X POST http://localhost:8080/api/menu \
  -H "Content-Type: application/json" \
  -d '{"name":"Pasta","price":9.99}'

# Update an item
curl -X PUT http://localhost:8080/api/menu/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Cheeseburger","price":10.99}'

# Delete an item
curl -X DELETE http://localhost:8080/api/menu/1

Exception Handling with ExceptionMapper

To handle exceptions globally and return meaningful HTTP responses, implement an ExceptionMapper. For example, handling NotFoundException:

import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {

    @Override
    public Response toResponse(NotFoundException exception) {
        return Response.status(Response.Status.NOT_FOUND)
                       .entity("Resource not found")
                       .build();
    }
}

Register it in your ResourceConfig:

ResourceConfig config = new ResourceConfig()
    .register(MenuResource.class)
    .register(NotFoundExceptionMapper.class)
    .packages("com.example");

Now, if any resource method throws NotFoundException, the mapper will produce a 404 response with the custom message.


Customising JSON with Jackson

Jersey’s Jackson integration allows you to use annotations to control serialization. For example, you can:

  • Rename a field using @JsonProperty.
  • Ignore fields with @JsonIgnore.
  • Use @JsonFormat for date formatting.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class MenuItem {
    @JsonIgnore
    private int internalId; // not serialized

    @JsonProperty("id")
    private int id;

    @JsonProperty("itemName")
    private String name;

    // ... getters/setters
}

The Jackson provider is automatically enabled when you include jersey-media-json-jackson in your dependencies.


Deployment Options

The JDK HTTP server approach is great for microservices, embedded applications, and demos. However, for production, you might consider:

  • Servlet containers – deploy your application as a WAR file to Tomcat, Jetty, or any Servlet 5+ container (Jakarta EE 9+). Use the jersey-container-servlet dependency.
  • Spring Boot – Spring Boot 3 uses Jakarta EE 9 and has built‑in support for Jersey (you can replace Spring MVC with Jersey).
  • Helidon SE – a microservices framework that also uses Jakarta REST and can be run with the JDK HTTP server.

For a simple, self‑contained application, the JDK HTTP server is a lightweight and sufficient choice.


Jersey 2 vs Jersey 3 Summary

Aspect Jersey 2 Jersey 3
Namespace javax.ws.rs jakarta.ws.rs
Spec version JAX‑RS 2.x Jakarta REST 3.x
Jakarta EE version EE 8 and earlier EE 9+
Java version required Java 8+ Java 11+
HK2 DI version 2.x 3.x

Summary

Jersey 3 implements the Jakarta REST 3 specification with the jakarta.ws.rs package namespace (replacing javax.ws.rs from Jersey 2). To build a REST API:

  • Add the Jersey 3 dependencies (core, JDK container, HK2, and Jackson).
  • Create an Application subclass (or ResourceConfig) to configure the app.
  • Annotate resource classes with @Path, @GET/@POST/etc.
  • Start the server using the JDK HTTP container with JdkHttpServerFactory.

The jersey-hk2 dependency provides dependency injection, and jersey-media-json-jackson handles JSON serialization automatically. For production, consider deploying to a Servlet container or using a framework like Spring Boot, but the JDK HTTP server is perfect for quick, standalone services.


Happy coding!