How to Create REST API in Java without Spring

Earlier, we have seen how to Build Spring Boot 2.X RESTful CRUD API. However, sometimes we might need to implement REST services without using the Spring framework at all. Hence, we are gonna create REST CRUD APIs using the Jersey framework in this article.

What You’ll Build

  • Create REST APIs to perform CRUD operations
  • Add support for Connection Pooling, Request validation, Exception Handling, and Logback Logging

What You’ll Need

  • Spring Tool Suite 4
  • JDK 11
  • MySQL Server 8
  • Apache Maven 3
  • Apache Tomcat 9

Tech Stack

  • Jersey 2.x – Implementation of JAX-RS 2.1 API Specification
  • Jersey-hk2 – A light-weight and dynamic dependency injection framework
  • JPA 2.1 – Java Persistence API Specification
  • Hibernate 5.x – Implementation of JPA 2.1 Specification
  • Hibernate-c3p0 – Connection Pool for Hibernate
  • Lombok – Java library tool that is used to minimize boilerplate code
  • Logback Classic – Logging Framework which implements SLF4J API  Specification

Jersey 2.x Vs Jersey 3.x

Jersey 3.x is no longer compatible with JAX-RS 2.1 API (JSR 370), instead, it is compatible with Jakarta RESTful WebServices 3.x API. Therefore, Jersey 2.x, which remains compatible with JAX-RS 2.1 API is still being continued. That is why I have chosen Jersey 2.x for this tutorial.

Project Structure

This is how our project will look like once created

Jersey REST API Project Structure

Create Maven Project

We can execute the following maven command to create a Servlet container deployable Jersey 2.34 web application:

mvn archetype:generate -DarchetypeGroupId=org.glassfish.jersey.archetypes -DarchetypeArtifactId=jersey-quickstart-webapp -DarchetypeVersion=2.34

It will ask you to provide your input for group Id, artifiact Id, name and package name details. Provide these details or enter to go with the default one. Once the project is generated, we can import that into our IDE.

Add Dependencies

Let’s add some other required dependencies like hibernate, Mysql-connector, Lombok, Logback, etc., After adding, our pom.xml will be as shown below:

pom.xml

<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/maven-v4_0_0.xsd">

	<modelVersion>4.0.0</modelVersion>

	<groupId>com.javachinna</groupId>
	<artifactId>jersey-rest</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>jersey-rest</name>

	<build>
		<finalName>jersey-rest</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<inherited>true</inherited>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<configuration>
					<failOnMissingWebXml>false</failOnMissingWebXml>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.glassfish.jersey</groupId>
				<artifactId>jersey-bom</artifactId>
				<version>${jersey.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		<dependency>
			<groupId>org.glassfish.jersey.containers</groupId>
			<artifactId>jersey-container-servlet-core</artifactId>
			<!-- use the following artifactId if you don't need servlet 2.x compatibility -->
			<!-- artifactId>jersey-container-servlet</artifactId -->
		</dependency>
		<!-- A light-weight and dynamic dependency injection framework -->
		<dependency>
			<groupId>org.glassfish.jersey.inject</groupId>
			<artifactId>jersey-hk2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.glassfish.jersey.media</groupId>
			<artifactId>jersey-media-json-binding</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.ext/jersey-bean-validation -->
		<dependency>
			<groupId>org.glassfish.jersey.ext</groupId>
			<artifactId>jersey-bean-validation</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>${hibernate.version}</version>
		</dependency>
		<!-- c3p0 connection pool -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-c3p0</artifactId>
			<version>${hibernate.version}</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.26</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.20</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.2.4</version>
		</dependency>
	</dependencies>
	<properties>
		<hibernate.version>5.5.4.Final</hibernate.version>
		<jersey.version>2.34</jersey.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
</project>

Create JPA Entity

Product.java

This is our entity which maps to the Product table in the database

package com.javachinna.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotBlank;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
public class Product {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@NotBlank(message = "Product name is required")
	private String name;

	@NotBlank(message = "Product price is required")
	private String price;
}

Create JPA Repository

ProductRepository.java

package com.javachinna.repo;

import java.util.List;
import java.util.Optional;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import com.javachinna.model.Product;

public class ProductRepository {
	private EntityManagerFactory emf = Persistence.createEntityManagerFactory("JavaChinna");
	private EntityManager em;

	public ProductRepository() {
		em = emf.createEntityManager();
	}

	public Product save(Product product) {
		em.getTransaction().begin();
		em.persist(product);
		em.getTransaction().commit();
		return product;
	}

	public Optional<Product> findById(Long id) {
		em.getTransaction().begin();
		Product product = em.find(Product.class, id);
		em.getTransaction().commit();
		return product != null ? Optional.of(product) : Optional.empty();
	}

	@SuppressWarnings("unchecked")
	public List<Product> findAll() {
		return em.createQuery("from Product").getResultList();
	}

	public Product update(Product product) {
		em.getTransaction().begin();
		product = em.merge(product);
		em.getTransaction().commit();
		return product;
	}

	public void deleteById(Long id) {
		em.getTransaction().begin();
		em.remove(em.find(Product.class, id));
		em.getTransaction().commit();
	}

	public void close() {
		emf.close();
	}
}

Create Service Interface and Implementation

ProductService.java

package com.javachinna.service;

import java.util.List;
import java.util.Optional;

import com.javachinna.model.Product;

public interface ProductService {
	Product save(Product product);

	Product update(Product product);

	void deleteById(Long id);

	Optional<Product> findById(Long id);

	List<Product> findAll();
}

ProductServiceImpl.java

@Inject annotation is used to inject the dependency just like the spring-specific @Autowired annotation. Here, we are using constructor injection. We can use it for field injection as well. However, I prefer to use the field injection only for optional dependencies. Otherwise, it is always good to use constructor injection.

package com.javachinna.service.impl;

import java.util.List;
import java.util.Optional;

import javax.inject.Inject;

import com.javachinna.model.Product;
import com.javachinna.repo.ProductRepository;
import com.javachinna.service.ProductService;;

public class ProductServiceImpl implements ProductService {

	private ProductRepository productRepository;

	@Inject
	public ProductServiceImpl(ProductRepository productRepository) {
		this.productRepository = productRepository;
	}

	@Override
	public Product save(Product product) {
		return productRepository.save(product);
	}

	@Override
	public void deleteById(Long id) {
		productRepository.deleteById(id);
	}

	@Override
	public Optional<Product> findById(Long id) {
		return productRepository.findById(id);
	}

	@Override
	public List<Product> findAll() {
		return productRepository.findAll();
	}

	@Override
	public Product update(Product product) {
		return productRepository.update(product);
	}

}

Create REST Resource

Let’s create a ProductResource class to expose REST endpoints for performing CRUD operations on the Product entity.

ProductResource.java

@Path annotation identifies the URI path that a resource class or class method will serve requests for. Paths are relative. For an annotated class the base URI is the application path, see ApplicationPath. For an annotated method the base URI is the effective URI of the containing class. For the purposes of absolutizing a path against the base URI, a leading ‘/’ in a path is ignored and base URIs are treated as if they ended in ‘/’. That is why we haven’t specified any leading ‘/’ in the paths.

@QueryParam annotation binds the value(s) of an HTTP query parameter to a resource method parameter, resource class field, or resource class bean property. Values are URL decoded unless this is disabled using the @Encoded annotation. A default value can be specified using the @DefaultValue annotation.

@PathParam annotation binds the value of a URI template parameter or a path segment containing the template parameter to a resource method parameter, resource class field, or resource class bean property. The value is URL decoded unless this is disabled using the @Encoded annotation. A default value can be specified using the @DefaultValue annotation

@GET annotation indicates that the annotated method responds to HTTP GET requests.

@POST annotation indicates that the annotated method responds to HTTP POST requests.

@PUT annotation indicates that the annotated method responds to HTTP PUT requests.

@DELETE annotation indicates that the annotated method responds to HTTP DELETE requests.

@Valid annotation triggers the validation of the method parameter. It marks a property, method parameter, or method return type for validation cascading.

package com.javachinna.controller;

import java.util.List;

import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import com.javachinna.exception.ResourceNotFoundException;
import com.javachinna.model.Product;
import com.javachinna.service.ProductService;

import lombok.extern.slf4j.Slf4j;

/**
 * Root resource (exposed at "products" path)
 */
@Slf4j
@Path("products")
public class ProductResource {

	private ProductService productService;

	/**
	 * @param productService
	 */
	@Inject
	public ProductResource(ProductService productService) {
		this.productService = productService;
	}

	/**
	 * Method handling HTTP GET requests. The returned object will be sent to
	 * the client as "application/json" media type.
	 * 
	 * @param consumerKey
	 * @return
	 */
	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public List<Product> getProductList(@NotBlank(message = "Consumerkey is required") @QueryParam(value = "consumerKey") String consumerKey) {
		log.info("Consumer: {}", consumerKey);
		return productService.findAll();
	}

	@GET
	@Path("{productId}")
	@Produces(MediaType.APPLICATION_JSON)
	public Product getProduct(@PathParam(value = "productId") Long productId) {
		return productService.findById(productId).orElseThrow(() -> new ResourceNotFoundException("productId " + productId + " not found"));
	}

	@POST
	public String createProduct(@Valid Product product) {
		productService.save(product);
		return "Product added";
	}

	@PUT
	@Path("{productId}")
	public String updateProduct(@PathParam(value = "productId") Long productId, @Valid Product product) {
		return productService.findById(productId).map(p -> {
			p.setName(product.getName());
			p.setPrice(product.getPrice());
			productService.update(p);
			return "Product updated";
		}).orElseThrow(() -> new ResourceNotFoundException("productId " + productId + " not found"));
	}

	@DELETE
	@Path("{productId}")
	public String deleteProduct(@PathParam(value = "productId") Long productId) {
		return productService.findById(productId).map(p -> {
			productService.deleteById(productId);
			return "Product deleted";
		}).orElseThrow(() -> new ResourceNotFoundException("productId " + productId + " not found"));
	}
}

Exception Handling with Exception Mapper

Create Custom Exception

ResourceNotFoundException.java

This custom exception will be thrown whenever an entity is not found by the id in the database.

package com.javachinna.exception;

public class ResourceNotFoundException extends RuntimeException {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public ResourceNotFoundException() {
		super();
	}

	public ResourceNotFoundException(String message) {
		super(message);
	}

	public ResourceNotFoundException(String message, Throwable cause) {
		super(message, cause);
	}
}

Create Exception Mapper

ResourceNotFoundMapper.java

ExceptionMapper interface is a contract for a provider that maps Java exceptions to javax.ws.rs.core.Response.

Providers implementing ExceptionMapper contract must be either programmatically registered in an API runtime or must be annotated with @Provider annotation to be automatically discovered by the runtime during a provider scanning phase.

@Provider annotation marks the implementation of an extension interface that should be discoverable by the runtime during a provider scanning phase.

ResourceNotFoundMapper is responsible for returning an HTTP Status 404 with the exception message in the response when ResourceNotFoundException is thrown.

package com.javachinna.exception;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class ResourceNotFoundMapper implements ExceptionMapper<ResourceNotFoundException> {
	@Override
	public Response toResponse(ResourceNotFoundException ex) {
		return Response.status(404).entity(ex.getMessage()).type(MediaType.TEXT_PLAIN_TYPE).build();
	}
}

Configure Resources, Binders and Properties

ResourceConfig is used for configuring a web application. Hence we have extended this class to register our ProductResource, Binder, and set properties.

AbstractBinder is a skeleton implementation of an injection binder with convenience methods for binding definitions. Hence we have created an anonymous binder and overridden the configure method in order to bind our ProductServiceImpl to ProductService interface and ProductRepository to ProductRepository class since we haven’t defined a DAO interface.

AppConfig.java

package com.javachinna.config;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;

import com.javachinna.controller.ProductResource;
import com.javachinna.repo.ProductRepository;
import com.javachinna.service.ProductService;
import com.javachinna.service.impl.ProductServiceImpl;

public class AppConfig extends ResourceConfig {

	public AppConfig() {
		register(ProductResource.class);
		register(new AbstractBinder() {
			@Override
			protected void configure() {
				bind(ProductServiceImpl.class).to(ProductService.class);
				bind(ProductRepository.class).to(ProductRepository.class);
			}
		});
		// Now you can expect validation errors to be sent to the
		// client.
		property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
	}
}

Configure Hibernate

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
          http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
    version="2.1">
    <persistence-unit name="JavaChinna">
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/rest?createDatabaseIfNotExist=true" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="chinna44" />
            <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

Note: We have set the hibernate.hbm2ddl.auto=create in order to create the tables based on the entities during application startup. So that, we don’t need to set up the database manually.

Configure Logback Logging

logback.xml

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Note: To enable debug logging, you can change the root level to debug.

Configure Deployment Descriptor

web.xml

The web.xml file should have the two init-param which are below. The first init-param’s value is the root package name that contains your JAX-RS resources. And the second init-param value is the complete package name of our AppConfig.java file. Please note that it is a package name + class name.

<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container, see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<servlet>
		<servlet-name>Jersey Web Application</servlet-name>
		<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>jersey.config.server.provider.packages</param-name>
			<param-value>com.javachinna</param-value>
		</init-param>
		<init-param>
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>com.javachinna.config.AppConfig</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/api/*</url-pattern>
	</servlet-mapping>
</web-app>

Build Application

Run mvn clean install command to clean and build the war file.

Deploy Application

Deploy the generated war file in a server like tomcat.

Test REST Services

GET API

Get by Product ID

HTTP GET Request and Response

Get All Products

HTTP GET All Products Request and Response

Request Parameter Validation

Query Param Validation

POST API

Create Product

HTTP POST Request and Response

Request Body Validation

POST API Request Body Validation

PUT API

Update Product

PUT API Request and Response

DELETE API

Delete Product

DELETE API Request and Response

References

https://www.appsdeveloperblog.com/dependency-injection-hk2-jersey-jax-rs/

Source Code

As always, you can get the source code from Github below

https://github.com/JavaChinna/jersey-rest-crud

Conclusion

That’s all folks! In this article, you’ve learned how to implement Spring Boot RESTful services for CRUD operations.

I hope you enjoyed this article. Thank you for reading.

Leave a Reply