Skip to content
JackSparrow414
Go back

Getting Started with RESTEasy

Table of contents

Open Table of contents

Getting Started with RESTEasy

Spring Boot and Spring MVC are probably the frameworks we use most often for RESTful applications. Another approach uses frameworks that implement the Java EE standards, now renamed Jakarta EE. This post discusses frameworks that implement the relevant Java EE JSR specifications, without introducing Spring.

Background

How did Java EE (Java Enterprise Edition) applications handle HTTP requests in the past?

They used Servlets: the controller layer handled requests by extending HttpServlet. For example:

Traditional Servlets

public class indexServlet extends HttpServlet {

  @Override
    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
      // handle request
      req.getParameter("parameterName");
      ......
       // return response
       resp.getWriter().write("something");
    }
}

web.xml configuration:

<?xml version="1.0" encoding="UTF-8" ?>
<web-app
        xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
        version="4.0">
  <servlet>
        <servlet-name>indexServlet</servlet-name>
        <servlet-class>IndexServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>indexServlet</servlet-name>
        <url-pattern>index/*</url-pattern>
    </servlet-mapping>
</web-app>

Later, @WebServlet could provide the same behavior.

As the RESTful architectural style became mainstream, however, the Java industry had not yet standardized how to build RESTful applications in Java EE.

JAX-RS and JSRs

JAX-RS is part of Java EE. Its latest corresponding JSR (Java Specification Request) is JSR 370, which specifies JAX-RS 2.1.

Read JSR 370 online here.

You can also read the specification on the Jakarta EE website. I recommend this approach, since Java EE has moved to Jakarta EE.

Why Read the Specification?

A Brief Reading of JSR 370

Applications, Resources, and Providers chapters in the JAX-RS specification contents

The JSR 370 table of contents has roughly 12 chapters. To get started, a basic understanding of Chapters 2, 3, and 4 is sufficient.

Applications

A JAX-RS application consists of one or more resources (see Chapter 3) and zero or more providers (see Chapter 4). This chapter describes aspects of JAX-RS that apply to an application as a whole, subsequent chapters describe particular aspects of a JAX-RS application and requirements on JAX-RS implementations

JAX-RS positions an Application as primarily comprising Resources and Providers. We can think of it as a global servlet that serves as the HTTP entry point.

Usage: Application configuration and examples in the JAX-RS specification

There is only one servlet here. In a traditional Servlet project, each controller requires a separate servlet configuration in web.xml.

Resources

Resources in one sentence:

A resource class is a Java class that uses JAX-RS annotations to implement a corresponding Web resource. Resource classes are POJOs that have at least one method annotated with @Path or a request method designator

A resource class contains methods annotated with @Path. If you have used Spring MVC, this should be familiar; the annotations are broadly similar.

For example, @GET, @POST, @PUT, @DELETE, @PATCH, @HEAD, and @OPTIONS identify the HTTP method.

@QueryParam and @PathParam receive URL query parameters and path parameters.

@Produces and @Consumes specify the response type and the accepted request type, respectively.

These annotations are enough to get started. For more details, see Sections 3.2 and 3.5 of the online JSR 370 PDF.

Providers

Providers in JAX-RS are responsible for various cross-cutting concerns such as filtering requests, converting representations into Java objects, mapping exceptions to responses, etc. A provider can be either pre-packaged in the JAX-RS runtime or supplied by an application. All application-supplied providers implement interfaces in the JAX-RS API and MAY be annotated with @Provider for automatic discovery purposes; the integration of pre-packaged providers into the JAX-RS runtime is implementation dependent. This chapter introduces some of the basic JAX-RS providers; other providers are introduced in Chapter 5 and Chapter 6

Providers handle shared application functionality: filters and request/response processing, such as converting JSON into an entity. How would a traditional Servlet application handle different Content-Types? We would typically add a filter to intercept requests or responses, then manually process them for each scenario using methods such as HttpServletRequest.getParameters. JAX-RS provides the basic interfaces for this. To implement JAX-RS ourselves, we would implement those interfaces and handle the different cases in this way to meet application requirements. That would also qualify as an implementation of the JAX-RS standard.

We can implement JAX-RS interfaces ourselves and annotate them with @Provider to create custom providers.

Basic RESTEasy Usage

As mentioned above, JAX-RS provides the fundamental interfaces: they express an agreed contract, but must be implemented before applications can use them. If every developer implemented them independently, each RESTful application would require a great deal of code. With a basic Servlet, for example, we have only request and response objects; supporting different requests and responses means parsing the requests and adapting the response output ourselves. RESTEasy reduces this work so developers can focus on business logic. It fully implements JSR 370, handling request conversion and response processing so application developers can build the functionality they need on top of it.

Create a RESTful Java EE Application

This example deploys the Java EE application on Tomcat 9. Tomcat is a Servlet container rather than a full Java EE container, but with a few additions it can deploy Java EE applications too.

Tomcat 9 supports Servlet 4, and RESTEasy is easy to configure on Servlet 3 and later. That is why this example uses Tomcat 9.

@ApplicationPath("/auth")
public class AuthorizationServerApplication extends Application {
}

The single line above lets RESTEasy automatically initialize the RESTful application. A traditional Servlet application might still require web.xml configuration.

ApplicationPath specifies the web application’s base path.

Note: some introductory articles tell readers to configure the fully qualified name of the class annotated with ApplicationPath in web.xml, as in the Application example above. Those instructions target web applications before Servlet 3. When using RESTEasy in a container supporting Servlet 3 or later, this configuration is unnecessary. For details, see the official Servlet container configuration documentation.

How Does RESTEasy Take Over the Application?

RESTEasy implements the ServletContainerInitializer interface. RESTEasy documentation describing application loading through ServletContainerInitializer

This interface was added in Servlet 3.0. See Chapter 4, Servlet Context, in the latest Servlet 4.0 specification. Servlet specification methods for dynamically adding Servlets, Filters, and Listeners during startup

It can be understood as a hook for work during Servlet container initialization. See the detailed Javadoc.

Here are a few screenshots showing what ResteasyServletInitializer does:

ResteasyServletInitializer.onStartup initializing application, resource, and provider collections ResteasyServletInitializer iterating over application classes and calling register RESTEasy register method source registering resources, providers, and a Servlet

They show that ResteasyServletInitializer registers Resources and Providers and adds a Servlet.

For more details, read the ResteasyServletInitializer source. It is straightforward and under 200 lines. RESTEasy registers it through SPI. SPI configuration registering ResteasyServletInitializer under META-INF/services

By the time container startup finishes, Resources and Providers have been registered, along with one servlet that handles HTTP requests globally.

Further exploration:

Handle RESTful HTTP Requests

In JAX-RS, methods annotated with @Path are defined as Resources. An HTTP request made through a browser or another tool, such as curl, is treated as access to a resource.

The following basic RESTEasy examples demonstrate the annotations. They should be easy to follow if you have used Spring MVC.

Basic Usage

The basic examples cover ordinary GET, POST, and DELETE requests.

/**
 * basic use for RestEasy
 */
@Log
@Path("hello")
public class HelloResource {

    private static List<UserVO> users = new ArrayList<>();

    /**
     * curl http:localhost:8080/auth/hello/jack
     */
    @GET
    @Path("{name}")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@PathParam("name") String name) {
        return "Hello " + name.toUpperCase();
    }

    /**
     * RESTEasy provides an advanced @PathParam annotation that does not require an explicit path value if the variable name matches the path variable.
     * <a href="https://docs.jboss.org/resteasy/docs/3.8.1.Final/userguide/html/_NewParam.html">Usage documentation</a>
     * Use it with the Maven Compiler Plugin.
     *  curl http:localhost:8080/auth/hello/advanced/jack
     */
    @GET
    @Path("advanced/{name}")
    @Produces(MediaType.TEXT_PLAIN)
    @DenyAll
    public String helloRestEasy(@org.jboss.resteasy.annotations.jaxrs.PathParam String name) {
        return "Hi,there " + name;
    }

    /**
     * curl -X POST -H 'Content-Type: application/json' -d '{"userName":"jack", "age":18"}' http://localhost:8080/auth/hello/users
     */
    @POST
    @Path("users")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createUser(@Valid UserVO userVO) {
        users.add(userVO);
        return Response.ok().entity(userVO).build();
    }

    /**
     * curl http:localhost:8080/auth/hello/users/0
     */
    @GET
    @Path("users/{index}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response queryUser(@PathParam("index") Integer index) {
        checkParameter(index);
        UserVO targetUser = users.get(index);
        return Response.ok().entity(targetUser).build();
    }

    /**
     * curl -X DELETE http://localhost:8080/auth/hello/users/0
     */
    @DELETE
    @Path("users/{index}")
    public Response deleteUser(@PathParam("index") Integer index) {
        checkParameter(index);
        users.remove(index);
        return Response.ok().build();
    }

    private void checkParameter(Integer index) {
        if (users.size() == 0 || Objects.isNull(index) ||  index < 0 || index >= users.size()) {
            throw new WebApplicationException("user Not Found, please enter valid index", Response.Status.NOT_FOUND);
        }
    }
}

Advanced Usage

The advanced examples cover form handling and file uploads and downloads.


    private final String UPLOADED_FILE_PATH = "D:\\tmp\\";

    /**
     * Receive form data.
     * curl -X POST -d 'username=jack&age=18' http://localhost/auth/hello/users
     */
    @POST
    @Path("users")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response createFromFrom(@Form UserVO userVO) {
        if (Objects.isNull(userVO)) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
        users.add(userVO);
        return Response.ok().build();
    }

    /**
     * Receive one or more files.
     * curl -F 'fileName=@pictureLocation/upload.png' http://localhost:8080/auth/hello/file
     */
    @POST
    @Path("file")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response uploadImage(MultipartFormDataInput input) {
        //Get API input data
        Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
        List<InputPart> inputParts = uploadForm.get("fileName");
        if (Objects.isNull(inputParts)) {
            throw new WebApplicationException("no upload file, please upload file", Status.BAD_REQUEST);
        }
        StringBuilder uploadFileName = new StringBuilder();
        for (InputPart inputPart : inputParts) {
            // convert the uploaded file to inputstream
            try(InputStream inputStream = inputPart.getBody(InputStream.class, null)) {
                //Use this header for extra processing if required
                MultivaluedMap<String, String> header = inputPart.getHeaders();
                String fileName = getFileName(header);
                uploadFileName.append(fileName).append(",");
                // Risks running out of memory
                byte[] bytes = IOUtils.toByteArray(inputStream);
                // constructs upload file path
                fileName = UPLOADED_FILE_PATH + fileName;
                writeFile(bytes, fileName);
                log.info("Success !!!!!");
            } catch (Exception e) {
                log.log(Level.WARNING, "upload file failed", e);
                throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR);
            }
        }
        return Response.status(200)
            .entity("File uploaded successfully.Uploaded file name : "+ uploadFileName.substring(0, uploadFileName.length()-1)).build();
    }

    /**
     * header sample
     * {
     * 	Content-Type=[image/png],
     * 	Content-Disposition=[form-data; name="file"; filename="filename.extension"]
     * }
     **/
    private String getFileName(MultivaluedMap<String, String> header) {
        String[] contentDisposition = header.getFirst("Content-Disposition").split(";");
        for (String filename : contentDisposition) {
            if ((filename.trim().startsWith("filename"))) {
                String[] name = filename.split("=");
                return name[1].trim().replaceAll("\"", "");
            }
        }
        return "unknown";
    }

    private void writeFile(byte[] content, String filename) throws IOException {
        File file = new File(filename);
        if (!file.exists()) {
            file.createNewFile();
        }
        FileOutputStream fop = new FileOutputStream(file);
        fop.write(content);
        fop.flush();
        fop.close();
    }

    /**
     * curl -o download.png http://localhost:8080/auth/hello/file/upload.png
     * @param fileName
     * @return
     */
    @GET
    @Path("file/{fileName}")
    @Produces("image/png")
    public Response downLoadImage(@org.jboss.resteasy.annotations.jaxrs.PathParam String fileName) {
        if(fileName == null || fileName.isEmpty()) {
            ResponseBuilder response = Response.status(Status.BAD_REQUEST);
            return response.build();
        }
        //Prepare a file object with file to return
        File file = new File(UPLOADED_FILE_PATH + fileName);
        ResponseBuilder response = Response.ok(file);
        response.header("Content-Disposition", "attachment; filename="+fileName);
        return response.build();
    }

What if we return a stream directly? Traditional Servlets write data through ServletOutputStream, while JAX-RS specifies StreamingOutput. Here is a simple example that writes a Properties object to the client through ObjectOutputStream.

    @POST
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    @Path("getStream")
    public Response getStream() {
        Properties props = new Properties();
        props.put("test", "test");
        // Example
        StreamingOutput streamingOutput = output -> {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(output);
            objectOutputStream.writeObject(props);
        };
        // When returning a PrintWriter, use this approach and set the content encoding
        StreamingOutput streamingOutput = output -> {
            PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)));
            writer.wtrite("<h1>test</h1>")
        };
        // Traditional Servlet approach; also set the content encoding
        response.setContentType("text/plain; charset=UTF-8")
        PrintWriter writer = response.getWriter();

        return Response.ok().entity(streamingOutput).build();
    }

Test:

public class StreamControllerTest {

    @SneakyThrows
    @Test
    public void assetGetStream() {
        Properties propsRet;
        URL url = new URL("http://localhost:8080/mvc-demo/mvc/stream/getStream");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        InputStream in = connection.getInputStream();
        // Receive it with ObjectInputStream
        ObjectInputStream result = new ObjectInputStream(in);
        propsRet = (Properties) result.readObject();
        System.out.println(propsRet);
    }

}

After these examples, basic application development should be straightforward.

Global Exception Handling

In the examples above, we simply throw an exception for an invalid request. To give the client a useful response after an exception, we need an exception-handling mechanism. Spring Boot uses @ExceptionHandler and @RestControllerAdvice for global handling. JAX-RS offers a similar approach: implement ExceptionMapper and annotate it with @Provider. This example should also clarify the role of Providers in JAX-RS.

@Provider
public class GlobalWebExceptionMapper implements ExceptionMapper<WebApplicationException> {

    @Override
    public Response toResponse(WebApplicationException e) {
        return Response.status(e.getResponse().getStatus())
                .type(MediaType.APPLICATION_JSON)
                .entity(new ExceptionData(e.getMessage()))
                .build();
    }
}

Notes

Change Log


Share this post:

Previous Post
Getting Started with JWT and nimbus-jose-jwt
Next Post
Implementing the OAuth 2.0 Authorization Code Flow

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.