Spring Boot RESTful Web Services Versioning Example
Welcome, in this tutorial, we will show a RESTful API by using the Accept Header versioning technique in a Spring Boot app.
1. Introduction
Before going further in this tutorial, we will look at the common terminology such as introduction to Spring Boot and Lombok.
1.1 Spring Boot
- Spring boot is a module that provides rapid application development feature to the spring framework including auto-configuration, standalone-code, and production-ready code
- It creates applications that are packaged as jar and are directly started using embedded servlet container (such as Tomcat, Jetty or, Undertow). Thus, no need to deploy the war files
- It simplifies the maven configuration by providing the starter template and helps to resolve the dependency conflicts. It automatically identifies the required dependencies and imports them into the application
- It helps in removing the boilerplate code, extra annotations, and XML configurations
- It provides powerful batch processing and manages the rest endpoints
- It provides an efficient JPA-starter library to effectively connect the application with the relational databases
- It offers a Microservice architecture and cloud configuration that manages all the application related configuration properties in a centralized manner
1.2 Lombok
- Lombok is nothing but a small library that reduces the amount of boilerplate Java code from the project
- Automatically generates the getters and setters for the object by using the Lombok annotations
- Hooks in via the Annotation processor API
- Raw source code is passed to Lombok for code generation before the Java Compiler continues. Thus, produces properly compiled Java code in conjunction with the Java Compiler
- Under the
target/classes
folder you can view the compiled class files - Can be used with Maven, Gradle IDE, etc.
1.2.1 Lombok features
Feature | Details |
---|---|
val | Local variables are declared as final |
var | Mutable local variables |
@Slf4J | Creates an SLF4J logger |
@Cleanup | Will call close() on the resource in the finally block |
@Getter | Creates getter methods for all properties |
@Setter | Creates setter for all non-final properties |
@EqualsAndHashCode |
|
@ToString |
|
@NoArgsConstructor |
|
@RequiredArgsContructor |
|
@AllArgsConstructor |
|
@Data |
|
@Builder |
|
@Value |
|
Let us go ahead with the tutorial implementation but before going any further I’m assuming that you’re aware of the Spring boot basics.
2. Spring Boot RESTful Web Services Versioning Example
2.1 Tools Used for Spring boot application and Project Structure
We are using Eclipse Kepler SR2, JDK 8, and Maven. In case you’re confused about where you should create the corresponding files or folder, let us review the project structure of the spring boot application.
Let us start building the application!
3. Creating a Spring Boot application
Below are the steps involved in developing the application.
3.1 Maven Dependency
Here, we specify the dependency for the Spring boot (Web and JPA, Spring doc Open API (for swagger interface)), H2 database, Java Faker (to generate the dummy data), and Lombok. The updated file will have the following code.
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent fromV1 repository --> </parent> <groupId>com.versioning</groupId> <artifactId>VersioningRestfulServices</artifactId> <version>0.0.1-SNAPSHOT</version> <name>VersioningRestfulServices</name> <description>Versioning restful services in spring boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
3.2 Application yml file
Create a new YML file at the location: VersioningRestfulServices/src/main/resources/
and add the following code where we will define –
- The h2 database connection and hibernate details
- The h2 console details will be accessible at the following URL –
http://localhost:9800/h2-console
in the browser - The Swagger UI path will be accessible at the following URL –
http://localhost:9800/swagger-ui-custom.html
in the browser
You’re free to change the application or the database details as per your wish.
application.yml
server: error: include-stacktrace: never port: 9800 spring: application: name: versioning-restful-services datasource: driverClassName: org.h2.Driver password: '' url: 'jdbc:h2:mem:testdb' username: sa h2: console: enabled: true path: /h2-console jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop properties: hibernate: show_sql: true springdoc: swagger-ui: path: /swagger-ui-custom.html
3.3 Java Classes
Let us write the important java class(es) involved in this application. For brevity, we will skip the following classes –
Employee.java
– Entity class that will be persisted in the databaseEmployeeRepository.java
– Repository interface that extends theJpaRepository
interface to perform the SQL operations. The interface also provides a custom method to fetch an employee by reference idEmployeeService.java
– Service class that interact with the DAO layer methodsDefaultEmployees.java
– Bootstrap class to populate dummy data to the h2 database once the application is started successfullyEntityNotFoundException.java
– Exception class for throwing the not found exception when the entity is not present in the database. The exception class is annotated with the HTTP 404 error response codeFakerConfig.java
– Configuration class that provides a bean annotated method forFaker
class so that the object can be autowired
3.3.1 Implementation/Main class
Add the following code to the main class to bootstrap the application from the main method. Always remember, the entry point of the spring boot application is the class containing @SpringBootApplication
annotation and the static main method.
VersioningRestfulServicesApplication.java
package com.versioning; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; //lombok annotation @Slf4j //spring annotation @SpringBootApplication public class VersioningRestfulServicesApplication { public static void main(String[] args) { SpringApplication.run(VersioningRestfulServicesApplication.class, args); log.info("Versioning restful services in a spring boot application"); } }
3.3.2 Implementation of the Response class
Add the following code to the response class which will be responsible to map the database layer response. The class methods will map the response data based on the media-type header param received by the controller methods. Such mappers are helpful where the non required attributes for a particular type are to be set null. This class –
- Consists of two methods i.e.
fromV1(…)
andfromV2(…)
- Is annotated with the
@JsonInclude
annotation which ensures that null attributes are not sent in the response - Contains two properties annotated with the
@Nullable
annotation which tells the users that these properties can be null. Helpful during the documentation
EmployeeResponse.java
package com.versioning.dto; import com.fasterxml.jackson.annotation.JsonInclude; import com.versioning.entity.Employee; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.lang.Nullable; import java.util.UUID; //lombok annotations @Data @NoArgsConstructor @AllArgsConstructor //Jackson annotations //Ensures that the null properties are not included in the response //(i.e. only properties with non-null values are to be included) @JsonInclude(JsonInclude.Include.NON_NULL) public class EmployeeResponse { //while sending out the response to the users we are mapping the reference id stored in the db as a primary key //as an ideal practice we never share the primary key of the records UUID id; String fullName; String emailAddress; String gender; //makes it clear that this value can be null @Nullable //Swagger annotation is used to define a schema. @Schema(nullable = true, description = "Phone number can be null") String phoneNumber; //makes it clear that this value can be null @Nullable //Swagger annotation is used to define a schema. @Schema(nullable = true, description = "Work department can be null") String workDepartment; //this method will send the employee details where phone number and work department //is marked as null (meaning these two attributes are not required by the integrating parties) @Deprecated public static EmployeeResponse fromV1(final Employee employee) { return new EmployeeResponse( employee.getReferenceId(), employee.getFullName(), employee.getEmailAddress(), employee.getGender(), null, null); } //this method is newer version of 'fromV1(...)' method where in all the required details are //passed public static EmployeeResponse fromV2(final Employee employee) { return new EmployeeResponse( employee.getReferenceId(), employee.getFullName(), employee.getEmailAddress(), employee.getGender(), employee.getPhoneNumber(), employee.getWorkDepartment()); } }
3.3.3 Controller class
Add the following code to the controller class to specify the different endpoints. The controller methods are annotated with the HTTP GET mapping annotation wherein the annotation also excepts an Accept header in the incoming request whose value can either be application/vnd.jcg.app-1.0+json
or application/vnd.jcg.app-2.0+json
. Based on this media-type the response will be generated and you can verify it by hitting the application endpoints. For brevity we have –
- Skipped endpoints like HTTP POST and PUT as these will be used in a similar fashion with a difference that these two endpoints will accept a request body from you and the Content-type media header
- Skipped the HTTP DELETE endpoint where the Content-type media header would consist of an array –
consumes = { EMPLOYEE_V_1_0, EMPLOYEE_V_2_0 }
EmployeeController.java
package com.versioning.controller; import com.versioning.dto.EmployeeResponse; import com.versioning.entity.Employee; import com.versioning.exception.EntityNotFoundException; import com.versioning.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; //lombok annotation @Slf4j //spring annotations @RestController @RequestMapping("/api/employee") public class EmployeeController { //media types versioning example private static final String EMPLOYEE_V_1_0 = "application/vnd.jcg.app-1.0+json"; private static final String EMPLOYEE_V_2_0 = "application/vnd.jcg.app-2.0+json"; @Autowired EmployeeService service; //URL :: http://localhost:9800/api/employee/get-all //Request header :: Accept=application/vnd.jcg.app-1.0+json @GetMapping(value = "/get-all", produces = EMPLOYEE_V_1_0) @ResponseStatus(HttpStatus.OK) public List<EmployeeResponse> getEmployees() { log.info("Getting all employee for v1 media type"); final List<Employee> employees = service.getEmployees(); return employees.stream().map(EmployeeResponse::fromV1).collect(Collectors.toList()); } //URL :: http://localhost:9800/api/employee/get?id=e45f2c96-be5b-4779-803c-a611ff5f150f //Request header :: Accept=application/vnd.jcg.app-1.0+json @GetMapping(value = "/get", produces = EMPLOYEE_V_1_0) @ResponseStatus(HttpStatus.OK) public EmployeeResponse getEmployee(@RequestParam("id") final UUID id) throws EntityNotFoundException { log.info("Getting employee id = {} for v1 media type", id); final Employee e = service.getEmployeeById(id); return EmployeeResponse.fromV1(e); } //URL :: http://localhost:9800/api/employee/get-all //Request header :: Accept=application/vnd.jcg.app-2.0+json @GetMapping(value = "/get-all", produces = EMPLOYEE_V_2_0) @ResponseStatus(HttpStatus.OK) public List<EmployeeResponse> getEmployeesV2() { log.info("Getting all employee for v2 media type"); final List<Employee> employees = service.getEmployees(); return employees.stream().map(EmployeeResponse::fromV2).collect(Collectors.toList()); } //URL :: http://localhost:9800/api/employee/get?id=e45f2c96-be5b-4779-803c-a611ff5f150f //Request header :: Accept=application/vnd.jcg.app-2.0+json @GetMapping(value = "/get", produces = EMPLOYEE_V_2_0) @ResponseStatus(HttpStatus.OK) public EmployeeResponse getEmployeeV2(@RequestParam("id") final UUID id) throws EntityNotFoundException { log.info("Getting employee id = {} for v2 media type", id); final Employee e = service.getEmployeeById(id); return EmployeeResponse.fromV2(e); } }
4. Run the Application
To execute the application, right-click on the VersioningRestfulServicesApplication.java
class, Run As -> Java Application
.
5. Project Demo
When the application is started, open the Postman tool to hit the application endpoints. You are free to choose any tool of your choice and for this tutorial, we will use the spring swagger interface (accessible at the following URL – http://localhost:9800/swagger-ui-custom.html
).
Application endpoints
-- HTTP GET endpoints – //Endpoint name – Get all employees //media type – application/vnd.jcg.app-1.0+json //URL :: http://localhost:9800/api/employee/get-all //Request header :: Accept=application/vnd.jcg.app-1.0+json //Endpoint name – Get employee by id //URL :: http://localhost:9800/api/employee/get?id=e45f2c96-be5b-4779-803c-a611ff5f150f //Request header :: Accept=application/vnd.jcg.app-1.0+json //Endpoint name – Get all employees //media type – application/vnd.jcg.app-2.0+json //URL :: http://localhost:9800/api/employee/get-all //Request header :: Accept=application/vnd.jcg.app-2.0+json //Endpoint name – Get employee by id //URL :: http://localhost:9800/api/employee/get?id=e45f2c96-be5b-4779-803c-a611ff5f150f //Request header :: Accept=application/vnd.jcg.app-2.0+json
That is all for this tutorial and I hope the article served you whatever you were looking for. Happy Learning and do not forget to share!
6. Summary
In this section, you learned,
- Spring boot and Lombok introduction
- Steps to implement Versioning the REST API response in a spring boot application
You can download the sample application as an Eclipse project in the Downloads section.
7. Download the Project
In this tutorial, we showed a RESTful API by using the Accept Header versioning technique in a spring boot app.
You can download the full source code of this example here: Spring Boot RESTful Web Services Versioning Example