REST services with JAX-RS

JAX-RS is a standard way to declaratively define REST services in Java. Mu Server has a built-in sub-set of JAX-RS 3.0 which allows a simple way to declare and expose REST APIs.

A basic example

In this example, there is an in-memory data structure holding user information. There is a single URL defined that allows a lookup for a user based on their ID and returns the data as JSON:

public class JaxRSExample {

    public static void main(String[] args) {

        Map<Integer, String> users = new HashMap<>();
        users.put(1, "Mike");
        users.put(2, "Sam");
        users.put(3, "Dan");
        UserResource userResource = new UserResource(users);

        MuServer server = MuServerBuilder.httpServer()
            .addHandler(RestHandlerBuilder.restHandler(userResource))
            .start();

        System.out.println("API example: " + server.uri().resolve("/jaxrsexample/users/1"));

    }

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

        private final Map<Integer, String> users;

        public UserResource(Map<Integer, String> users) {
            this.users = users;
        }

        @GET
        @Path("/{id}")
        @Produces("application/json")
        public String get(@PathParam("id") int id) {
            String name = users.get(id);
            if (name == null) {
                throw new NotFoundException("No user with id " + id);
            }
            return new JSONObject()
                .put("id", id)
                .put("name", name)
                .toString(4);
        }

    }
}
(see full file)

You can try out some of the URLs here:

Note that the RestHandlerBuilder.restHandler(resources) method takes an array of JAX-RS objects. It is recommended that if you have multiple resources to expose that you create a single RestHandler with multiple resources attached.

Note that Mu Server 2.0 switched from the javax.ws.rs to the jakarta.ws.rs package for JAX-RS classes.

Differences with common implementations

The JAX-RS implementation that comes with Mu Server is intentionally only a subset of the full spec. The main way it differs from common implementations is in how it handles the construction of your resource classes.

Specifically, when creating a rest server, you pass instantiated instances of your resources to the Mu Server REST builder, which means Mu Server will not instantiate any of your objects. This allows you to create your objects in whatever way you desire, and makes it very easy to use constructor-based dependency injection, as in the example above.

See the Mu Server spec implementation page for full details of what is and isn't implemented.

Serialisation with Jackson JAX-RS

To support automatic JSON serialisation of response bodies and deserialisation of request bodies, you can add the Jackson JAX-RS provider dependency:

<dependency>
    <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
    <artifactId>jackson-jakarta-rs-json-provider</artifactId>
    <version>2.17.0</version>
</dependency>

You can then register a body reader and/or writer on the RestHandlerBuilder. In the following example the User class is automatically converted to JSON:

public class JacksonJaxRSExample {

    public static void main(String[] args) {
        UserResource userResource = new UserResource();
        JacksonJsonProvider jacksonJsonProvider = new JacksonJsonProvider();
        MuServer server = MuServerBuilder.httpServer()
            .addHandler(
                RestHandlerBuilder.restHandler(userResource)
                    .addCustomWriter(jacksonJsonProvider)
                    .addCustomReader(jacksonJsonProvider)
            )
            .start();

        System.out.println("API example: " + server.uri().resolve("/users"));
    }

    public static class User {
        public boolean isActive;
        public String name;
        public int age;
    }

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

        @GET
        @Produces("application/json")
        public User getUser() {
            User user = new User();
            user.isActive = true;
            user.name = "John Smith";
            user.age = 40;
            return user;
        }

        @POST
        @Consumes("application/json")
        @Produces("text/plain")
        public String postUser(User user) {
            return "I got a user with isActive=" + user.isActive
                + " and name " + user.name + " and age " + user.age;
        }

    }

}
(see full file)

CORS Configuration

If browsers are connecting to your REST API from another domain, you will need to grant access to the URLs that the UI runs from. This can be achieved with the CORSConfigBuilder class. The following example allows UIs to access the REST URL from the swagger petstore site or from any localhost URL:

RestHandlerBuilder.restHandler(resources)
    .withCORS(
        CORSConfigBuilder.corsConfig()
            .withAllowedOrigins("https://petstore.swagger.io")
            .withAllowedOriginRegex("http(s)?://localhost:[0-9]+")
    )

The javadoc has information on more advanced options such as allowing credentials, specifying allowed headers, and settings for preflight OPTIONS requests.

Documentation

Mu Server has built-in support for generating OpenAPI specifications for your REST APIs. This means a JSON file describing your API can be optionally exposed on your web server and then UIs such as Swagger can be used to visualise and test your API.

While the Swagger UI is not built in, a very simple HTML documentation page can also be automatically exposed.

To expose documentation, use the documentation-related methods on the RestHandlerBuilder class:

RestHandlerBuilder.restHandler(resources)
    .withOpenApiJsonUrl("/openapi.json")
    .withOpenApiHtmlUrl("/api.html")

You can see what those look like at the following links:

If hosting a swagger UI at a different endpoint, you will need to add CORS configuration as described above. To test this, copy the OpenAPI JSON URL and paste it into the text box at the Swagger Petstore Example.

In order to provide more detail in your documentation, you can apply the Description annotation to your JAX RS classes, methods, and parameters. Methods can also describe return values by specifying an ApiResponse annotation.

To give a general introduction to your API, you can also specify extra API Info via the withOpenApiDocument on the RestHandlerBuilder and set various settings by using the OpenAPIObjectBuilder's openAPIObject() method. The following example demonstrates some of these features:

public class JaxRSDocumentationExample {

    public static void main(String[] args) {

        MuServer server = MuServerBuilder.httpServer()
            .addHandler(createRestHandler())
            .start();

        System.out.println("API HTML: " + server.uri().resolve("/api.html"));
        System.out.println("API JSON: " + server.uri().resolve("/openapi.json"));

    }

    public static RestHandlerBuilder createRestHandler() {
        return RestHandlerBuilder.restHandler(new UserResource())
            .withCORS(corsConfig().withAllowedOriginRegex(".*"))
            .withOpenApiHtmlUrl("/api.html")
            .withOpenApiJsonUrl("/openapi.json")
            .withOpenApiDocument(
                OpenAPIObjectBuilder.openAPIObject()
                    .withInfo(
                        infoObject()
                            .withTitle("User API Documentation")
                            .withDescription("This is just a demo API that doesn't actually work!")
                            .withVersion("1.0")
                            .build())
                    .withExternalDocs(
                        externalDocumentationObject()
                            .withDescription("Documentation docs")
                            .withUrl(URI.create("https://muserver.io/jaxrs"))
                            .build()
                    )
            );
    }

    @Path("/users")
    @Description(value = "A human user", details = "Use this API to get and create users")
    public static class UserResource {

        @GET
        @Path("/{id}")
        @Produces("application/json")
        @Description("Gets a single user")
        @ApiResponse(code = "200", message = "Success")
        @ApiResponse(code = "404", message = "No user with that ID found")
        public String get(
            @Description("The ID of the user")
            @PathParam("id") int id) {
            return new JSONObject()
                .put("id", id)
                .toString(4);
        }

        @POST
        @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
        @Description("Creates a new user")
        @ApiResponse(code = "201", message = "The user was created")
        @ApiResponse(code = "400", message = "The ID or name was not specified")
        public Response create(
            @Description("A unique ID for the new user")
            @Required @FormParam("id") int id,
            @Description("The name of the user")
            @FormParam("name") String name) {
            return Response.status(201).build();
        }

    }
}
(see full file)

This results in this simple HTML view and this OpenAPI JSON.

Note: Mu Server does not support the Swagger annotations defined in the io.swagger.annotations package.

Generating schema and examples of complex objects

OpenAPI using JSON schema notation to describe the objects used in requests and responses. These schema object are generated by MuServer using the SchemaObjectBuilder class. MuServer allows you to register fixed schema objects to be used for your custom classes, or to specify them at runtime.

In the following example, a method returns a Product object which is converted to JSON using a custom message body writer. The ProductResource class describes the schema of the returned data along with an example generated at runtime:

public class JaxRSSchemaExample {

    /**
     * A data class that is able to serialise to a JSONObject
     */
    static class Product {
        public final String name;
        public final double price;

        Product(String name, double price) {
            this.name = name;
            this.price = price;
        }

        public JSONObject toJSON() {
            return new JSONObject()
                .put("name", name)
                .put("price", price);
        }
    }

    /**
     * A JAX-RS message body writer that converts a Product object to JSON
     */
    @Produces("application/json")
    private static class ProductWriter implements MessageBodyWriter<Product> {
        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
            return Product.class.isAssignableFrom(type);
        }

        public void writeTo(Product product, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
            product.toJSON().write(new OutputStreamWriter(entityStream));
        }
    }

    /**
     * The resource class that returns a product. Because it implements SchemaObjectCustomizer
     * the customize method is called whenever the OpenAPI document is requested.
     */
    @Path("/api/products")
    static class ProductResource implements SchemaObjectCustomizer {
        @GET
        @Produces("application/json")
        public Product randomProduct() {
            return new Product("Mu Product", 99.99);
        }

        @Override
        public SchemaObjectBuilder customize(SchemaObjectBuilder builder, SchemaObjectCustomizerContext context) {
            if (context.resource() == this && context.type().equals(Product.class)) {
                Map<String, SchemaObject> props = new HashMap<>();
                props.put("name", SchemaObjectBuilder.schemaObjectFrom(String.class).build());
                props.put("price", SchemaObjectBuilder.schemaObjectFrom(double.class).build());
                builder.withProperties(props)
                    .withRequired(asList("name", "price"))
                    // If a map is given as an example object, then the properties are listed separately.
                    // Alternatively, a string can be given as the example.
                    .withExample(new Product("Example Product", 9.99).toJSON().toMap());
            }
            return builder;
        }
    }

    public static void main(String[] args) {

        MuServer server = MuServerBuilder.muServer()
            .withHttpsPort(13571)
            .addHandler(
                RestHandlerBuilder.restHandler(new ProductResource())
                    .withOpenApiHtmlUrl("/api.html")
                    .withOpenApiJsonUrl("/openapi.json")
                    .addCustomWriter(new ProductWriter())
                    .withCORS(corsConfig().withAllowedOrigins("https://petstore.swagger.io"))
            )
            .start();

        System.out.println("API HTML: " + server.uri().resolve("/api.html"));
        System.out.println("API JSON: " + server.uri().resolve("/openapi.json"));

    }

}
(see full file)

To register methods that can customize a schema and it's example, you can call the RestHandlerBuilder.addSchemaObjectCustomizer(SchemaObjectCustomizer) method. However, as in the example above, if the resource class implements SchemaObjectCustomizer then it will be automatically registered.

An alternative to runtime customization is to map schema objects to Java classes. For example, the example above could have specified the schema for a product as follows:

restHandlerBuilder(new ProductResource())
    .addCustomSchema(Product.class, SchemaObjectBuilder.schemaObjectFrom(Product.class)
         .withProperties(props)
         .withExample(example)
         .build())

When viewed in a UI such as swagger, the schema and example can be shown. For example:

Screenshot of Swagger showing Product information

Filters and Security

Request and response filters can be added with the addRequestFilter and addResponseFilter methods on the RestHandlerBuilder.

One use-case for this is for security. Mu-Server comes with a simple BasicAuth filter which can be controlled by implementing Authenticator and Authorizer interfaces. See JaxRSBasicAuth.java for a full example.

You can access a ResourceInfo or the current MuRequest by looking up the properties on the context:

@Override
public void filter(ContainerRequestContext requestContext) {
    ResourceInfo resourceInfo = (ResourceInfo)
            requestContext.getProperty(MuRuntimeDelegate.RESOURCE_INFO_PROPERTY);
    MuRequest muRequest = (MuRequest)
            requestContext.getProperty(MuRuntimeDelegate.MU_REQUEST_PROPERTY);
    // do stuff
}

Reader and Writer interceptors

Use the addReaderInterceptor and addWriterInterceptor in order to intercept request and response bodies respectively. To get ResourceInfo or the MuRequest you can access the properties on the interceptor context in the same way as for filters described above.

Custom readers, writers, and exception handlers

The following methods on the RestHandlerBuilder can be used:

See the RestHandlerBuilder javadoc for more details.

File uploads on Form parameters

For multipart/form-data requests, the @FormParam method parameter annotation can be used with UploadedFile objects, including List<UploadedFile> for multiple uploads of the same name.