Restful API using Jersey 3 Jakarta EE 9

This article walks through a sample REST application built using Jersey3 and then we will see how to test the rest endpoints. Last not but least how to deploy it on Server.

Project Structure



Technologies Used
  • Java 15
  • Mvn 3.5.0
  • Jersey 3.0.2
  • JUnit 5.3.1

Java Classes

JakartaRestfulApp.java
Main Jersey Application Class, registers MenuResource class.
public class JakartaRestfulApp extends Application {
	private final Set> classes;

	public JakartaRestfulApp() {
		HashSet> c = new HashSet>();
		c.add(MenuResource.class);
		classes = Collections.unmodifiableSet(c);
	}

	@Override
	public Set> getClasses() {
		return classes;
	}
}
MenuResource.java
Our Resourse class which exposes Restful endpoints for the Menu object. It uses MenuService to store and retrieve Menu items.
@Path("/rest")
public class MenuResource {
	MenuService service = MenuService.getInstance();

	@GET
	@Path("/menus")
	@Produces("application/json")
	public List getMenus() {
		return service.getAll();
	}

	@GET
	@Path("/menus/{menu_ID}")
	@Produces("application/json")
	public Response getMenu(@PathParam("menu_ID") Long menuId) {
		try {
			return Response.ok(service.get(menuId)).build();
		} catch (Exception e) {
			return Response.serverError().status(HttpStatus.BAD_REQUEST_400.getStatusCode(), e.getMessage()).build();
		}
	}

	@POST
	@Path("/menus")
	@Produces("application/json")
	public Response add(Menu c) {
		service.add(c);
		return Response.ok(c).status(HttpStatus.CREATED_201.getStatusCode(), "Created").build();
	}

	@PUT
	@Path("/menus/{menu_ID}")
	@Produces("application/json")
	public Response update(@PathParam("menu_ID") Long menuId, Menu c) {
		try {
			return Response.ok(service.update(menuId,c.getName())).build();
		} catch (Exception e) {
			return Response.serverError().status(HttpStatus.BAD_REQUEST_400.getStatusCode(), e.getMessage()).build();
		}
	}

	@PATCH
	@Path("/menus/{menu_ID}")
	@Produces("application/json")
	public Response update2(@PathParam("menu_ID") Long menuId, @QueryParam("name") String name) {
		try {
			return Response.ok(service.update(menuId,name)).build();
		} catch (Exception e) {
			return Response.serverError().status(HttpStatus.BAD_REQUEST_400.getStatusCode(), e.getMessage()).build();
		}
	}

	@DELETE
	@Path("/menus/{menu_ID}")
	@Produces("application/json")
	public Response delete(@PathParam("menu_ID") Long menuId) {
		try {
			service.delete(menuId);
			return Response.ok().status(HttpStatus.OK_200.getStatusCode()).build();
		} catch (Exception e) {
			return Response.serverError().status(HttpStatus.BAD_REQUEST_400.getStatusCode(), e.getMessage()).build();
		}
	}

	@OPTIONS
	@Path("/menus")
	@Produces("text/plain")
	public String touch() {
		return "options";
	}

	@HEAD
	@Path("/menus")
	@Produces("text/plain")
	public String head() {
		return "head";
	}
}
MenuService.java
This class is used to store and retrieve Menu objects. It uses a HashSet to store objects created.
public class MenuService {

	private static MenuService instance = new MenuService();
	private static HashSet menus;

	private MenuService() {
		menus = new HashSet<>();
		menus.add(new Menu(1L, "Menu One"));
	}

	public static MenuService getInstance() {
		return instance;
	}

	public void add(Menu menu) {
		menus.add(menu);
	}

	public List getAll() {
		return new ArrayList(menus);
	}

	public Menu get(Long id) throws Exception {
		Iterator it = menus.iterator();
		while (it.hasNext()) {
			Menu curr = (Menu) it.next();
			if (curr.getId() == id)
				return curr;
		}
		throw new Exception("Object not found");
	}

	public boolean delete(Long id) throws Exception {
		Iterator it = menus.iterator();
		while (it.hasNext()) {
			Menu curr = (Menu) it.next();
			if (curr.getId() == id) {
				it.remove();
				return true;
			}
		}
		throw new Exception("Object not found");
	}
	
	public Menu update(Long id, String update) throws Exception {
		Iterator it = menus.iterator();
		while (it.hasNext()) {
			Menu curr = (Menu) it.next();
			if (curr.getId() == id) {
				curr.setName(update);
				return curr;
			}
				
		}
		throw new Exception("Object not found");
	}
}
Menu.java
Menu object representing a restaurant menu containing some basic information like id and name.
public class Menu {

	private Long id;

	private String name;

	public Menu() {
	}

	public Menu(Long id, String name) {
		this.id = id;
		this.name = name;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
JDKSEServer.java
Jersey supports a number of deployment options. For this example, we will use a lightweight JDK server that comes with JDK.
public class JDKSEServer {
	private static final Logger LOGGER = Logger.getLogger(JDKSEServer.class.getName());
	/**
	 * Starts the JDK HTTP server serving the JAX-RS application.
	 */
	static HttpServer startServer() throws IOException {
		ResourceConfig config = new ResourceConfig(MenuResource.class);
		HttpServer server = JdkHttpServerFactory.createHttpServer(getBaseURI(), config);
		return server;
	}

	public static void main(String[] args) {
		try {
			final HttpServer httpServer = startServer();
			Runtime.getRuntime().addShutdownHook(new Thread(() -> {
				try {
					LOGGER.info("Shutting down the application...");
					httpServer.stop(0);
				} catch (Exception e) {
					LOGGER.log(Level.SEVERE, null, e);
				}
			}));

			LOGGER.info("Application started.%nStop the application using CTRL+C");
			Thread.currentThread().join();
		} catch (Exception ex) {
			LOGGER.log(Level.SEVERE, null, ex);
		}
	}

	/**
	 * Gets base {@link URI}.
	 */
	public static URI getBaseURI() {
		return UriBuilder.fromUri("http://localhost/").port(8080).build();
	}
}

Unit Testing

TestMenuResource.java
We will use JerseyTest to test some of the methods that MenuResource supports.
public class TestMenuResource extends JerseyTest {

	@BeforeEach
	void init() throws Exception {
		super.setUp();
	}

	@Override
	protected Application configure() {
                 //to start a new container in s different port
		forceSet(TestProperties.CONTAINER_PORT, "0");
		return new ResourceConfig(MenuResource.class);
	}

	@Test
	public void get_one() throws Exception {
		Response response = target("/rest/menus/1").request().get();
		System.out.println("sd " + response.getStatus());
		assertEquals(response.getStatus(), 200);
		assertEquals(response.readEntity(Menu.class).getName(), "Menu One");
	}

	protected TestContainerFactory getTestContainerFactory() {
		return new JdkHttpServerTestContainerFactory();
	}

}

Executing Code

mvn clean compile exec:java
Sep 24, 2021 4:57:57 PM org.glassfish.jersey.server.wadl.WadlFeature configure WARNING: JAX-B API not found . WADL feature is disabled. Sep 24, 2021 4:57:57 PM jersey.server.JDKSEServer main INFO: Application started.%nStop the application using CTRL+C

References

Summary
In this article, we saw how to build and test Jakarta Restful API with Jersey 3.

Introduction

In 2000, Roy Fielding proposed Representational State Transfer (REST) as an architectural approach to designing web services. REST is an architectural style for building distributed systems based on hypermedia. REST is independent of any underlying protocol and is not necessarily tied to HTTP.

What is REST

REST stands for representational state transfer. It is a set of constraints that set out how an API (application programming interface) should work.

REST API Design Principles

  • Uniform Interface: REST APIs are designed around resources, which are any kind of object, data, or service that can be accessed by the client. A resource has an identifier, which is a URI that uniquely identifies that resource.
    Use Noun and not verb to represent a resource
    In general, it helps to use plural nouns

  • Client-Server: Client and Server are independent of each others and the only way the client interacts is through the URI of the resources.
    Client and server can evolve independently.
    Client and server can be implemented in different tech stack.

  • Stateless : REST APIs use a stateless request model. HTTP requests should be independent and may occur in any order. The only place where information is stored is in the resources themselves, and each request should be an atomic operation.
    Being stateless means REST API can scale easily.

  • Cacheable: response to a request be implicitly or explicitly labeled as cacheable or non-cacheable.

  • Layered System: Client and Server should not be tightly coupled and if required and added any intermediate layer the API still should work.

Spring Boot REST API example

REST is an acronym for REpresentational State Transfer. In this article, we will walk through a sample REST application built using Spring Boot. This application does not use any database to store data, all the data is in memory. We will expose a few REST endpoints. Main purpose of this article is to demonstrate how to build REST API's using Spring boot.

Application Details

This application is about building a backend for a Blog software and exposing two REST API endpoints.
Following two endpoints both returns response in JSON format.
/blogapi/blogs : Returns list of blogs
/blogapi/blogs/ID : Returns details of a specific blog
Technology Used
  • Spring Boot 2.2.6.RELEASE
  • Logback 1.2.3
  • Maven 3
  • Java 11
  • Jackson 2.10.3

Project Structure


Code Reference

Maven pom.xml
<?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>
	<artifactId>com.bootng.rest</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>Spring Boot Rest</name>
	<description>Springboot Rest App</description>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath />
	</parent>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- junit 5 -->
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-engine</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
			<version>2.19.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<addResources>true</addResources>
				</configuration>
				<executions>
					<execution>
						<goals>
							<goal>repackage</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>
BlogStory Model
BlogStory model class which represent a blog entry.
package com.bootng.model;

public class BlogStory {
  private String id;
  private String name;
  private String summary;
  private String description;
  private String category;
  public BlogStory() {
  }
  public BlogStory (String name, String category, String summary) {
    this.id = name.replaceAll(" ","_");
    this.name = name;
    this.summary = summary;
    this.category = category;
    this.description = summary + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt "
        + "ut labore et dolore magna aliqua. "
        + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
        + " Duis aute irure dolor in reprehenderit in voluptate velit esse cillum "
        + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, "
        + "sunt in culpa qui officia deserunt mollit anim id est laborum" ;   
  }
// Setters
 
}
BlogService Class
Service class which returns list of available blogs. Blog with an id etc.
@Service
public class BlogService {
  List category = Arrays.asList("Technical", "Travel", "Food", "Finance", "Entertainment");
  List stories = new ArrayList();
  {
    stories.add(new BlogStory("Java 11", "Technical", "Java 11 Blog"));
    stories.add(new BlogStory("Java 14", "Technical", "Java 14 Blog"));
    stories.add(new BlogStory("Asia Travel", "Travel", "Places to visit in Asia"));
    stories.add(new BlogStory("Europe Travel", "Travel", "Places to visit in Europe"));
    stories.add(new BlogStory("Japan Travel", "Travel", "Places to visit in Japan"));
    stories.add(new BlogStory("Asian Food", "Food", "Asian Food......"));
  }

  public BlogStory getBlogStory(String id) throws AppException {
    return stories.stream().filter(story -> id.equals(story.getId())).findAny().orElse(null);
  }

  public List getBlogStory() throws AppException {
    return stories;
  }

  public void addStory(BlogStory newStory) throws AppException {
    this.stories.add(newStory);
  }

  public List getBlogTags() throws AppException {
    return category;
  }
}
Main Application Class
Spring Boot's main application class.
@ComponentScan({"com.bootng"})
@SpringBootApplication
public class RestApplication {
  public static void main(String args[]) {
    SpringApplication.run(RestApplication.class, args);
  }
}
BlogAPIController Controller Class
Controller class which exposes the two endpoints
GET /blogapi/blogs
and GET /blogapi/blogs/ID
@Controller annotation is used to mark this class as a Controller.
@RequestMapping is used to map the request paths "/blogs" to getBlogSotries method and /blog/ID to getBlogStory method.
In both cases we are mapping both the paths to HTTP GET verb by using method = RequestMethod.GET
@ResponseBody is used to convert the result to JSON (as we specified by produces=MediaType.APPLICATION_JSON_VALUE)
@Controller
@RequestMapping("/blogapi")
public class BlogAPIController {
private static final Logger log = LoggerFactory.getLogger(BlogAPIController.class);
@Autowired
BlogService blogService;
ResponseEntity apiResponse = new ResponseEntity(newStory, HttpStatus.OK);
    return apiResponse;
}

@RequestMapping(value = {"/blogs"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public @ResponseBody ResponseEntity> getBlogStories() {
    log.info("inside getBlogStories GET method");
    List blogStory = null;
    ResponseEntity> apiResponse;
    try {
      blogStory = blogService.getBlogStory();
      apiResponse = new ResponseEntity>(blogStory, HttpStatus.OK);
    } catch (AppException e) {
      apiResponse =
          new ResponseEntity>(blogStory, HttpStatus.INTERNAL_SERVER_ERROR);
      e.printStackTrace();
    }
    return apiResponse;
  }

@RequestMapping(value = {"/blogs"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public @ResponseBody ResponseEntity getBlogStory(@PathParam(value = "") String id) {
    log.info("inside blog GET method");
    BlogStory blogStory = null;
    ResponseEntity apiResponse;
    try {
      blogStory = blogService.getBlogStory(id);
      if (blogStory == null)
        apiResponse = new ResponseEntity(blogStory, HttpStatus.NOT_FOUND);
      else
        apiResponse = new ResponseEntity(blogStory, HttpStatus.OK);
    } catch (AppException e) {
      apiResponse = new ResponseEntity(blogStory, HttpStatus.INTERNAL_SERVER_ERROR);
      e.printStackTrace();
    }
    return apiResponse;
  }
}

Start Application

Run Application
Build the project using mvn clean install and then run it with mvn spring-boot:run
mvn clean install
mvn spring-boot:run

Call REST API's using CURL

CURL: Get list of Blogs
Using curl we can get list of blogs from the /blogs endpoint
curl -X GET  http://localhost:8080/blogapi/blogs 
CURL: Get specific blog details
Get a specific blog with id "Java_14" by calling /blogs/Java_14
curl -X GET localhost:8080/blogapi/blogs/Java_14
Git Source Code
  • git clone https://github.com/siddharthagit/spring-boot-references
  • cd springboot-rest
  • mvn clean install
  • mvn spring-boot:run

    Conclusion

    In this article, we used Springs RestController annotation to build the API. With Spring boot we can use other frameworks like Jersey, Restlet, etc also to build API.