Spring Boot REST Application. Part 1
Creating a web application can seem complex and time-consuming. A web application has a lot of boilerplate code which has to be repeated over and over again. Message transformations, database connectivity, service monitoring, etc are some of the repetitive tasks. As a result, frameworks to simplify development appear. Spring boot is one such framework. It allows creating web applications very quickly. The boilerplate code does not have to be repeated. In this post, the very simple Spring boot application will be created. All required information can be found in Spring Boot documentation.
A web console for managing product items will be implemented. E.g. it can be an online store that sells a set of predefined product items. For the sake of simplicity, the application will have minimum dependencies. This application will be improved in future posts.
Application Skeleton
Spring provides an excellent tool for creating a Spring boot application skeleton. It can be generated with Spring initializr. Using this page all required parameters and dependencies can be specified. For our simple example, this page has the next form.
In this configuration, Java 11 is used as a programming language. As was described in the How to Become a Java Developer post there are two main build tools Maven and Gradle. In this post, Gradle is used. However, a similar configuration with Maven can also be used. As a spring boot version, we choose the last available release version. In the Project Metadata section artifact’s group and name are specified. Package name contains the package hierarchy for our application.
In terms of dependencies, only three are used. It is worth mentioning that this application is a basic one and will be improved and extended in the future.
Spring Web. This dependency will be used to create REST endpoints for our application. One of the main features of Spring boot is its self-sufficiency. In a typical web application, a separate application server (e.g. Apache Tomcat) is used. The application is packaged as a war file that is deployed to the application server. In the case of Spring boot application server is embedded into the jar package (we will use Apache Tomcat which is a default one for Spring boot). As a result, out web application can be run as a typical java application.
java -jar application.jar
Hibernate validation. Hibernate will not be used in this application (wait for future posts). However, Hibernate Validation will be used to validate data transfer objects. It is a handy and commonly used tool to validate POJOs (plain old java objects) with annotations. In the case of complex validations, custom annotations can be used.
Lombok. Lombok is a java library that eliminates the need to work with boilerplate code such as getters, setters, builders, loggers, etc with the help of annotation. As a result, the source code is much cleaner and readable.
After pressing the GENERATE button zip archive with the application is created.
Source Code Layout
Let’s look at the core elements of the repository.
Gradle Wrapper
Gradle is used as a build tool for this application. To do it we can install Gradle on a local machine. However, it is not considered to be a good practice because multiple team members can work on the same repository. They can have different versions of a Gradle. As a result, some problems can occur. To overcome this problem Gradle Wrapper is used (the same is applicable for Maven Wrapper).
gradle-wrapper.properties contains the URL of the Gradle which will be downloaded and used as a build tool.
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
To use it we need to replace our command from gradle to gradlew. E.g.
./gradlew clean build
Gradle configuration file
build.gradle contains all configuration information for the build.
plugins {
id 'org.springframework.boot' version '2.4.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
Spring boot with version 2.4.5 is used. In order to eliminate the problem with inconsistent dependencies dependency-management plugin is specified. java plugin is responsible for source code compilation and assembling it into the jar file.
configurations { compileOnly { extendsFrom annotationProcessor } }
Annotation processors will be used during source code compilation (e.g. Lombok).
repositories { mavenCentral() }
Maven Central Repository is the location of all the dependencies. I mean, all the project dependencies (e.g. Spring boot) are fetched from this repository.
test { useJUnitPlatform() }
Enable JUnit test platform. Actually, we do not need this dependency for now. Testing will be provided as a separate post.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.data:spring-data-commons:2.5.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
implementation means that the dependency is added to the compile and runtime classpath, compileOnly — only to the compile classpath, testImplementation is similar to the implementation but for tests.
In our case spring boot dependencies are used both at compile and runtime stages. However, Lombok is used only during compilation. Basically, there is an annotation processor which processes annotations at compile time “adding” code to the classpath. After the code is “added” Lombok is not needed at runtime.
Spring Boot Configuration
Spring boot configuration is located in application.yml ( YAML). Also, application.properties can be used, but YAML is more popular.
Source code layout
We are building a typical web application that has some provided API, business logic to process incoming requests and data storage to save data. As a result, we have three top-level packages: api, service, and data (Separation of Concerns).
Spring Boot Auto Configuration
SpringBootSimpleApplication is an entry point for our web application. Earlier in the post it was mentioned that a lot of boilerplate code is hidden from the user. It is done by the annotation @SpringBootApplication. Basically, it configures auto configuration and component scanning for the application.
API Layer
Let’s start with the API layer. As it was already mentioned, we are developing a web console for managing product items. Basically, we have to implement CRUD operations. It is done in the ItemController.
@RestController
@RequiredArgsConstructor
@RequestMapping(value = ENDPOINT, produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
public class ItemController {
public static final String ENDPOINT = "/items";
public static final String ENDPOINT_BY_ID = "/{id}";
private final ItemService service;
private final ItemApiMapper mapper;
@GetMapping
public Page<Item> find(@PageableDefault(sort = "id") Pageable pageable) {
return service.findAll(pageable);
}
@GetMapping(value = ENDPOINT_BY_ID)
public Item get(@PathVariable Long id) {
return service.getOne(id);
}
@ResponseStatus(CREATED)
@PostMapping
public Item create(@RequestBody CreateItemDto createItemDto) {
return service.create(mapper.map(createItemDto));
}
@PutMapping(value = ENDPOINT_BY_ID)
public Item update(@PathVariable Long id, @RequestBody UpdateItemDto updateItemDto) {
return service.update(id, mapper.map(updateItemDto));
}
@ResponseStatus(NO_CONTENT)
@DeleteMapping(value = ENDPOINT_BY_ID)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
We are following REST in this controller. To make it simple, we have Item resource and next operations:
- GET /items — get all product items
- GET /items/{id} — get single product item
- POST /items — create product item
- PUT /items/{id} — update product item
- DELETE /items/{id} — delete product item
Product item is very simple in our example and has only id and title fields.
Spring Controller
@RestController is a combination of @Controller (annotation tells that this controller is expected to handle requests) and @ResponseBody (method return value is bound to the response body). It is used in combination with @RequestMapping (web requests are mapped to the Java methods). As you can see, our controller expects and produces JSON values.
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping are used to map our methods o the specific REST operations.
@ResponseStatus is used to indicate response status code in case the method was successful. Exception handling will be explained in future posts.
Used Practices in Controller
To begin with, REST is used. We have Item resource and a set of operation defined for this resource.
Secondly, constructor injection is used. There are other types of injections (e.g. field, setter, etc.) but constructor injection is considered to be a best practice in a general case. Lombok @RequiredArgsConstructor generates a constructor for final fields. Dependencies are injected as constructor arguments. We do not need any getter/setter methods for fields. This class can be used in tests and without Spring. You only need to provide requited objects to the constructor.
Thirdly, endpoint URLs are saved in static Strings and can be reused in other places e.g. tests.
Fourthly, the single responsibility principle from SOLID is used. CreateItemDto and UpdateItemDto are mapped into the domain objects using the specialized ItemApiMapper class.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateItemDto {
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateItemDto {
private String title;
}
@Component
public class ItemApiMapper {
public CreateItemRequest map(CreateItemDto createItemDto) {
return CreateItemRequest.builder()
.title(createItemDto.getTitle())
.build();
}
public UpdateItemRequest map(UpdateItemDto updateItemDto) {
return UpdateItemRequest.builder()
.title(updateItemDto.getTitle())
.build();
}
}
You can see a bunch of Lombok annotations: @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor. They are adding boilerplate code such as contructors, getters, setters, etc.
@Component annotation on ItemApiMapper tells Spring to create a bean which can be injected into the ItemController.
You can see that ItemApiMapper contains a code that just copies fields from one place to another. For big DTOs mapper can be pretty big. In future posts this mapper will be replaced with MapStruct or Orika.
Last but not least, Page<Item> find method have paging capabilities. As a result, in cases when there are a lot of product items in the database they will be returned in small pages.
Service Layer
Service Layer is pretty straightforward here.
ItemService interface specifies required operations.
@Validated
public interface ItemService {
Page<Item> findAll(@NotNull Pageable pageable);
Item getOne(@NotNull Long id);
Item create(@NotNull @Valid CreateItemRequest createItemRequest);
Item update(@NotNull Long id, @Valid UpdateItemRequest updateItemRequest);
void delete(@NotNull Long id);
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateItemRequest {
@NotNull
@Size(min = 1, max = 255)
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateItemRequest {
@NotNull
@Size(min = 1, max = 255)
private String title;
}
Hibernate Validation
Hibernate Validation is used to validate provided objects.
@Validated specifies that interface method arguments have to be validated, @NotNull — provided argument has to be not null, @Valid — the provided argument has to be validated based on the annotations specified on the argument class, @Size(min = 1, max = 255) — String has to be from 1 to 255 characters in length.
DefaultItemService is the implementation of the ItemService interface.
@Service
@RequiredArgsConstructor
public class DefaultItemService implements ItemService {
private final ProductItemRepository repository;
private final ProductItemMapper mapper;
@Override
public Page<Item> findAll(Pageable pageable) {
return mapper.map(repository.findAll(pageable));
}
@Override
public Item getOne(Long id) {
return mapper.map(repository.getOne(id));
}
@Override
public Item create(CreateItemRequest createItemRequest) {
return mapper.map(repository.save(mapper.map(createItemRequest)));
}
@Override
public Item update(Long id, UpdateItemRequest updateItemRequest) {
final var item = repository.getOne(id);
mapper.map(item, updateItemRequest);
return mapper.map(repository.save(item));
}
@Override
public void delete(Long id) {
final var item = repository.getOne(id);
repository.delete(item);
}
}
@Component
public class ProductItemMapper {
public Item map(ProductItem productItem) {
return Item.builder()
.id(productItem.getId())
.title(productItem.getTitle())
.build();
}
public Page<Item> map(Page<ProductItem> page) {
return page.map(this::map);
}
public ProductItem map(CreateItemRequest createItemRequest) {
return ProductItem.builder()
.title(createItemRequest.getTitle())
.build();
}
public void map(ProductItem item, UpdateItemRequest updateItemRequest) {
item.setTitle(updateItemRequest.getTitle());
}
}
What is important here.
Firstly, Spring @Service is used. It works the same way as @Component which was presented previously but has different semantic.
Secondly, you can see that the Item is returned from the method. However, the data layer responds with ProductItem. It is done because of two reasons. Data layer ProductItem is not propagated to the API layer. Moreover, there can be problems with transactions (which will be added in future posts).
Thirdly, you can see that in cases when item does not exist in the repository, exception will occur. It will be handled as a common code in the future posts.
Other elements are the same as for the API layer.
Data Layer
Data layer manages entities in the memory. Database will be added in the future posts.
@Repository
public class ProductItemRepository {
private final Map<Long, ProductItem> data = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(3L);
@PostConstruct
public void init() {
data.put(
1L,
ProductItem.builder()
.id(1L)
.title("Book")
.build()
);
data.put(
2L,
ProductItem.builder()
.id(2L)
.title("Pencil")
.build()
);
}
public Page<ProductItem> findAll(Pageable pageable) {
long offset = pageable.getOffset();
int pageSize = pageable.getPageSize();
// Sorting is ignored
final var items = data.values().stream()
.skip(offset)
.limit(pageSize)
.sorted(comparing(ProductItem::getId))
.collect(Collectors.toList());
return new PageImpl<>(items, pageable, data.size());
}
public ProductItem getOne(Long id) {
return Optional.ofNullable(data.get(id))
.orElseThrow(() -> new IllegalArgumentException("Entity not found: " + id));
}
public ProductItem save(ProductItem item) {
if (item.getId() == null) {
item.setId(nextId.getAndIncrement());
}
data.put(item.getId(), item);
return item;
}
public void delete(ProductItem item) {
if (!data.containsKey(item.getId())) {
throw new IllegalArgumentException("Entity not found: " + item.getId());
}
data.remove(item.getId());
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductItem {
private Long id;
private String title;
}
As for now, data are located in Map<Long, ProductItem>. What is important here:
@Repository is the same as @Component but with the other semantic. However, it has an additional feature: exception translation. There are a lot of databases that can throw various types of exceptions. These exceptions will be converted by Spring into the standard set of exceptions.
Run Application
Application can be cloned from Github.
To build the application next command has to be executed from the directory root:
./gradlew clean build
To run application use:
./gradlew bootRun
Use HTTP client e.g. Postman or curl.
Postman collection can be found at Github.
Let’s say we want to see the second page of the items with the item page to be equal to 1.
Request:
curl -i --location --request GET 'localhost:8080/items?page=1&size=1' \ --header 'Content-Type: application/json' \ --data-raw ''
Response:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 09 May 2021 20:42:29 GMT
{
"content":[
{
"id":2,
"title":"Pencil"
}
],
"pageable":{
"sort":{
"sorted":true,
"unsorted":false,
"empty":false
},
"pageNumber":1,
"pageSize":1,
"offset":1,
"paged":true,
"unpaged":false
},
"last":true,
"totalPages":2,
"totalElements":2,
"sort":{
"sorted":true,
"unsorted":false,
"empty":false
},
"first":false,
"number":1,
"numberOfElements":1,
"size":1,
"empty":false
}
Summary
To sum up, in this post simple REST Spring Boot application was created. However, a lot of open questions remained for use in production:
- Remove repetitive code from mappers
- Add database to store data
- Handle database transactions
- Improve performance of the get operation using cache
- Testing
- Monitoring
- And other
All these items will be reviewed in future posts. Please wait for updates.
Originally published at https://datamify.com on May 9, 2021.