- Blog
- How to Build a Lightweight RESTful Client with Java
- Introduction
- What You’ll Build
- What You’ll Need
- Project Structure
- Project Dependencies
- pom.xml
- Creating DTOs
- LoginRequest.java
- JwtAuthenticationResponse.java
- ApiResponse.java
- Product.java
- ProductRequest.java
- Creating Client API
- ProductManagerConfig.java
- ProductManager.java
- ProductManagerImpl.java
- Creating HTTP Request Utils
- HttpRequestUtils.java
- Testing Time
- ProductManagerTest.java
- Source Code
- What’s Next?
Blog
How to Build a Lightweight RESTful Client with Java
- Post author: Chinna
- Post published: August 14, 2022
- Post category: Java
- Post comments: 0 Comments
In our previous tutorial, we implemented RESTful CRUD API. Now, we are going to implement a REST client to consume those APIs with JWT authentication in Java without using any framework.
Introduction
When you want to build a lightweight REST client with very few external libraries, then using a framework like Spring to develop the consumer application is not an option. Hence, in this tutorial, we are gonna implement a client in Java with very few third-party libraries.
What You’ll Build
A REST client to consume REST APIs with JWT authentication in order to perform CRUD operations.
What You’ll Need
- Spring Tool Suite or any IDE of your choice
- JDK 11
- MySQL Server
- Apache Maven
Project Structure
This is how our project will look like once created
Project Dependencies
Let’s create a simple maven project and add a few basic dependencies like Jackson , Lombok & Junit in the pom.xml . We will also add the jackson-datatype-jsr310 module which is required to support JSR-310 (Java 8 Date & Time API) data types.
pom.xml
4.0.0 com.javachinna product-api-client 0.0.1-SNAPSHOT product-api-client Demo project for Java REST Client 11 com.fasterxml.jackson.core jackson-databind 2.13.2 com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.13.3 org.projectlombok lombok 1.18.22 org.junit.jupiter junit-jupiter 5.9.0 test
Creating DTOs
Let’s create some classes for mapping the request and response of the REST APIs.
LoginRequest.java
LoginRequest class holds the credentials required for the authentication.
package com.javachinna.rest.client.model; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class LoginRequest
JwtAuthenticationResponse.java
JwtAuthenticationResponse holds the access token returned by the authentication API.
package com.javachinna.rest.client.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; @Data @JsonIgnoreProperties(ignoreUnknown = true) public class JwtAuthenticationResponse
ApiResponse.java
ApiResponse maps the response of Create/Update/Delete API.
package com.javachinna.rest.client.model; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class ApiResponse
Product.java
Product maps the response of GET API.
/** * @author Chinna */ package com.javachinna.rest.client.model; import java.time.LocalDate; import lombok.Data; /** * Product */ @Data public class Product
ProductRequest.java
ProductRequest class maps the request of POST API which creates a new product with the product details passed in the request.
package com.javachinna.rest.client.model; import java.time.LocalDate; import lombok.Data; @Data public class ProductRequest
Creating Client API
ProductManagerConfig.java
ProductManagerConfig class holds the REST API base URL and credentials required for authentication.
package com.javachinna.rest.client.config; import lombok.Value; @Value public class ProductManagerConfig
ProductManager.java
ProductManager is an interface in which we have defined all the public methods required for obtaining access tokens and consuming CRUD REST APIs.
package com.javachinna.rest.client; import java.util.HashMap; import java.util.List; import com.javachinna.rest.client.model.ApiResponse; import com.javachinna.rest.client.model.Product; import com.javachinna.rest.client.model.ProductRequest; public interface ProductManager < Product getProduct(Integer l) throws Exception; List> getAllProducts() throws Exception; ApiResponse createProduct(ProductRequest request) throws Exception; ApiResponse updateProduct(Integer productId, ProductRequest request) throws Exception; ApiResponse deleteProduct(Integer productId) throws Exception; String getAccessToken() throws Exception; >
ProductManagerImpl.java
ProductManagerImpl provides implementation to the ProductManager interface. We have already secured the CRUD APIs with token based authentication. Hence, we need to invoke the authentication API to obtain an access token first. Then, we need to send this token in the Authorization header of the request to the CRUD REST API.
package com.javachinna.rest.client; import java.util.HashMap; import java.util.List; import java.util.Map; import com.javachinna.rest.client.config.ProductManagerConfig; import com.javachinna.rest.client.model.ApiResponse; import com.javachinna.rest.client.model.JwtAuthenticationResponse; import com.javachinna.rest.client.model.LoginRequest; import com.javachinna.rest.client.model.Product; import com.javachinna.rest.client.model.ProductRequest; import com.javachinna.rest.client.util.HttpRequestUtils; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class ProductManagerImpl implements ProductManager < private static final String AUTH_API = "auth/signin"; private static final String PRODUCT_API = "products"; private final ProductManagerConfig productManagerConfig; public Product getProduct(Integer productId) throws Exception < String url = productManagerConfig.getBaseUrl() + PRODUCT_API + "/" + productId; return HttpRequestUtils.get(url, getAccessToken(), Product.class); >@SuppressWarnings("unchecked") public List> getAllProducts() throws Exception < String url = productManagerConfig.getBaseUrl() + PRODUCT_API; Mapresponse = HttpRequestUtils.get(url, getAccessToken(), Map.class); return (List) response.get("content"); > public ApiResponse createProduct(ProductRequest request) throws Exception < return HttpRequestUtils.post(productManagerConfig.getBaseUrl() + PRODUCT_API, request, ApiResponse.class, getAccessToken()); >public ApiResponse updateProduct(Integer productId, ProductRequest request) throws Exception < return HttpRequestUtils.put(productManagerConfig.getBaseUrl() + PRODUCT_API + "/" + productId, request, ApiResponse.class, getAccessToken()); >public ApiResponse deleteProduct(Integer productId) throws Exception < return HttpRequestUtils.delete(productManagerConfig.getBaseUrl() + PRODUCT_API + "/" + productId, ApiResponse.class, getAccessToken()); >public String getAccessToken() throws Exception < LoginRequest loginRequest = new LoginRequest(productManagerConfig.getUsername(), productManagerConfig.getPassword()); JwtAuthenticationResponse jwtResponse = HttpRequestUtils.post(productManagerConfig.getBaseUrl() + AUTH_API, loginRequest, JwtAuthenticationResponse.class); return jwtResponse.getAccessToken(); >>
Creating HTTP Request Utils
HttpRequestUtils.java
HttpRequestUtils contains generic methods used to make HTTP GET, POST, PUT and DELETE requests.
package com.javachinna.rest.client.util; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.Builder; import java.net.http.HttpResponse; import java.time.Duration; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * * @author Chinna [javachinna.com] * */ public class HttpRequestUtils < private static final String CONTENT_TYPE = "Content-Type"; private static final String AUTHORIZATION = "Authorization"; private static final String BEARER = "Bearer "; private static final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); private static final HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(10)).build(); private HttpRequestUtils() < >public static T get(String url, String token, Class valueType) throws IOException, InterruptedException, JsonProcessingException, JsonMappingException < HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create(url)) .setHeader(AUTHORIZATION, BEARER + token).build(); HttpResponseresponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); return objectMapper.readValue(response.body(), valueType); > public static T post(String uri, Object request, Class valueType) throws JsonProcessingException, IOException, InterruptedException, JsonMappingException < return post(uri, request, valueType, null); >public static T post(String uri, Object request, Class valueType, String token) throws JsonProcessingException, IOException, InterruptedException, JsonMappingException < Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).POST(getBodyPublisher(request)).header(CONTENT_TYPE, "application/json"); return send(valueType, token, builder); >public static T put(String uri, Object request, Class valueType, String token) throws JsonProcessingException, IOException, InterruptedException, JsonMappingException < Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).PUT(getBodyPublisher(request)).header(CONTENT_TYPE, "application/json"); return send(valueType, token, builder); >public static T delete(String uri, Class valueType, String token) throws JsonProcessingException, IOException, InterruptedException, JsonMappingException < Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).DELETE(); return send(valueType, token, builder); >private static BodyPublisher getBodyPublisher(Object request) throws JsonProcessingException < return HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(request)); >private static T send(Class valueType, String token, Builder builder) throws IOException, InterruptedException, JsonProcessingException, JsonMappingException < builder.header(CONTENT_TYPE, "application/json"); if (token != null) < builder.header(AUTHORIZATION, BEARER + token); >HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) < throw new RuntimeException(response.statusCode() + " : " + response.body()); >return objectMapper.readValue(response.body(), valueType); > >
Note: The ObjectMapper().findAndRegisterModules() is used to find and register the modules like jackson-datatype-jsr310 present in the classpath. So that the jackson library will be able to handle the conversion of java.time.LocalDate from JSON to string and vice versa.
Testing Time
ProductManagerTest.java
@Order annotation is used to configure the order in which the annotated element (i.e., field, method, or class) should be evaluated or executed relative to other elements of the same category.
If this annotation is used with @RegisterExtension or @ExtendWith , the category applies to extension fields.
Likewise, when it is used with MethodOrderer.OrderAnnotation , the category applies to test methods.
Also, if it is used with ClassOrderer.OrderAnnotation , the category applies to test classes.
If @Order is not explicitly declared on an element, the DEFAULT order value will be assigned to the element.
We have used this annotation in order to make sure that testCreateProduct() is executed first since there will not be any records in the database.
package com.javachinna.rest.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.time.LocalDate; import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import com.javachinna.rest.client.config.ProductManagerConfig; import com.javachinna.rest.client.model.ApiResponse; import com.javachinna.rest.client.model.Product; import com.javachinna.rest.client.model.ProductRequest; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class ProductManagerTest < ProductManagerConfig productManagerServiceConfig = new ProductManagerConfig( "http://localhost:8080/api/", "[email protected]", "admin@"); ProductManager productManager = new ProductManagerImpl(productManagerServiceConfig); @Test @Order(1) void testCreateProduct() throws Exception < ProductRequest request = new ProductRequest(); request.setName("Dell Inspiron"); request.setDescription("Inspiron 16 plus"); request.setVersion("7620"); request.setEdition("2021"); request.setValidFrom(LocalDate.now()); ApiResponse response = productManager.createProduct(request); assertNotNull(response); assertEquals(true, response.getSuccess()); >@Test @Order(2) void testGetAllProducts() throws Exception < List> list = productManager.getAllProducts(); assertNotNull(list); assertEquals("7620", list.get(0).get("version")); > @Test @Order(3) void testGetProduct() throws Exception < Integer productId = getProductId(); Product product = productManager.getProduct(productId); assertNotNull(product); assertEquals("7620", product.getVersion()); >@Test @Order(4) void testUpdateProduct() throws Exception < Integer productId = getProductId(); Product product = productManager.getProduct(productId); ProductRequest request = new ProductRequest(); request.setName(product.getName()); request.setDescription(product.getDescription()); request.setVersion(product.getVersion()); request.setValidFrom(product.getValidFrom()); request.setEdition("2022"); ApiResponse response = productManager.updateProduct(productId, request); assertNotNull(response); assertEquals(true, response.getSuccess()); product = productManager.getProduct(productId); assertEquals("2022", product.getEdition()); >private Integer getProductId() throws Exception < List> list = productManager.getAllProducts(); Integer productId = (Integer) list.get(0).get("id"); return productId; > @Test @Order(5) void testDeleteProduct() throws Exception < Integer productId = getProductId(); ApiResponse response = productManager.deleteProduct(productId); assertNotNull(response); assertEquals(true, response.getSuccess()); >>
Output
Source Code
What’s Next?
In this article, we have implemented a thin REST API client application to consume secured CRUD REST APIs in Java. Also, we have written a Junit test to verify the successful execution of REST API Calls.