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
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
Get All Products
Request Parameter Validation
POST API
Create Product
Request Body Validation
PUT API
Update Product
DELETE API
Delete Product
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.