January 7, 2019
This article shows how to build a RESTful API using JavaEE. RESTful APIs are popular because they are easy to develop against and test. For some RESTful calls, you only need a browser to begin feeling your way through through an API. JavaEE 8 includes JAX-RS 2.1 and coupled with JSON processing and Bean Validation, can build out your API with a minimum of code.
This article targets the WildFly platform. The services will be built using Maven which to support Continuous Integration. The development environment is the IntelliJ IDE which allows for some optimizations to make development more rapid.
This article is on the IntelliJ Ultimate Edition which is the commercial (not free) edition. Ultimate is needed for the JBoss (former name of WildFly) connector.
The demo application is the start of a CRM, maintaining a single Contact entity that corresponds closely to a physical model. Stub operations will be provided for querying, adding, updating and deleting Contacts ("CRUD").
The project will be a single WAR file containing a RESTful web service.
Verify that your pom.xml matches the listing below.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.bekwam</groupId>
<artifactId>jaxrs-api-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.6.2.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
The design goals for the demo are that the interface be
WildFly 15 is a JavaEE 8-compliant app server. This means that it packages JAX-RS 2.1, JSON-B, and Bean Validation implementations. JAX-RS will will call Java methods in response to HTTP requests and return HTTP responses. JSON-B will serialize Java objects into JSON for transport. Bean Validation will validate incoming payloads.
The Contact object that implements Serializable and is marked up with several Bean Validation annotations. Add the following class to the apidemo package.
package apidemo;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
public class Contact implements Serializable {
private Long contactId;
@NotNull @Size(max=25)
private String firstName;
@NotNull
@Size(max=25)
private String lastName;
@Size(max=40)
private String companyName;
public Contact() {
}
public Contact(Long contactId, String firstName, String lastName, String companyName) {
this.contactId = contactId;
this.firstName = firstName;
this.lastName = lastName;
this.companyName = companyName;
}
public Long getContactId() {
return contactId;
}
public void setContactId(Long contactId) {
this.contactId = contactId;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
@Override
public String toString() {
return "Contact{" +
"contactId=" + contactId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", companyName='" + companyName + '\'' +
'}';
}
}
When run through Bean Validation, Contact must have the firstName and lastName fields set. This is the NotNull annotation. All fields are subject to a maximum size. firstName and lastName are invalid if either is longer than 25 characters. companyName, if specified, must be shorter than 40 characters.
ContactResource is the Java class that is invoked in response to API calls. It starts with a @Path annotation to set a base URL. Each method in the class will map to either that URL or a URL with additional qualifying segments. Although not required, this ContactResource uses different HTTP methods to distinguish between different operations. Some APIs might handle everything as only GET and POST, but I think the intent -- particularly surrounding the DELETE -- is clearer.
This is a summary of the mappings.
The HTTP GET methods are query operations. The single {id} query will select a specific record. The /contacts list query can be expanded to include parameters that map to selection criteria. By REST standards, neither of these operations change the Contact entity. The Contact query can be called repeatedly and cached if it has not changed since the last call.
The HTTP POST method is an add operation. A JSON payload is passed in the body.
The HTTP PUT method is an update operation. The entity must exist for a successful call.
The HTTP DELETE method is a delete operation. The entity must exist for a successful call.
package apidemo;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
@Path("/contacts")
public class ContactsResource {
private Logger logger = Logger.getLogger("ContactsResource");
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Contact> getContacts() {
List<Contact> retval = new ArrayList<>();
retval.add( new Contact(990L, "Carl", "Walker", "Bekwam, Inc"));
return retval;
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Contact getContact(@PathParam("id") Long id) {
return new Contact(990L, "Carl", "Walker", "Bekwam, Inc");
}
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateContact(@PathParam("id") Long id, Contact c) {
c.setContactId(id);
logger.info("updating id=" + id + " with c=" + c);
}
@DELETE
@Path("/{id}")
public void deleteContact(@PathParam("id") Long id) {}
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Contact addContact(@Valid Contact c) {
c.setContactId(10990L);
return c;
}
}
Completing the demo, the Application subclass is listed below. This tells the framework about the Resource classes that will respond to HTTP.
package apidemo;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
@ApplicationPath("/api")
public class APIApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> set = new HashSet<>();
set.add( ContactsResource.class );
return set;
}
}
To deploy the WAR file for testing, follow the WildFly Runtime setup in this article. You'll deploy the exploded WAR file as in that example.
The following screenshots are from ReadyAPI, a commercial tool for RESTful API testing.
The Get All operation is an HTTP GET to the /contacts URL. It returns a JSON array of JSON Contact objects. If no objects are found, an empty array should be returned. In JAX-RS, both query operations need the APPLICATION_JSON @Produces annotation set. The default Content-type is TEXT_PLAIN.
Getting a single Contact is performed by specifying an identifier after /contacts. The service should return a Not Found HTTP response if the specified identifier doesn't exist. (This is not shown in the demo.)
Adding a Contact uses an HTTP POST method. See the "POST" marking in the upper left of the screenshot below. The body of the payload contains a firstName, lastName, and (optional) companyName. The response is a 200 Ok that returns the added entity. I find it convenient to get the added entity back which can be supplemented with information created on the server side, most notable the ID.
Bean Validation is used to handle invalid input. In this test case, I exceeded the maximum length of the firstName field and got back a 400 Bad Request response containing text describing the validation error. Bean Validation is activated by outfitting the POJO with Bean Validation annotations like @NotNull and @Size and by specifying @Valid in the Java method arguments.
Update Contact is implemented with an HTTP PUT. Like Add Contact, it uses the @Valid annotation to trigger Bean Validation on the input payload. It also requires the @PathParam for the ID. In my coding style, I modify the incoming payload with the setContactId() call so that the complete Contact record can be handled with a single parameter in the business logic.
@BeanParam is an interesting construct if you're handling form data and need to merge in an identifier from the URL path.
For my update operations, I return 204 No Content. It would be equally acceptable to return a 200 Ok with a payload. Although the identifier is already established and a complete record is passed in, this might be of use if the server was supplementing the Contact with other fields such as "lastModifiedDate".
Delete Contact uses an HTTP DELETE method to invoke the Java code. It returns a 204 No Content message on a successful invocation.
This article demonstrates that a RESTful API can be created with WildFly using a minimal amount of code. Although the business logic surrounding the Contact management wasn't covered, it can easily be added using the stubs presented here. An EJB can be injected that will form JPA objects to be persisted. That EJB could also perform some beforehand entity tests for the {id} marked methods to return 404 if the entity no longer exists.
By Carl Walker
President and Principal Consultant of Bekwam, Inc