In this post we will build a microservice using Spring Boot. This
service will be responsible for interacting with our items that we are
storing in our MySQL database.
Layered Architecture
Spring Boot has been built with layered architectures in mind. In a layered architecture, the components within a specify layer only deal with logic that pertains to that layer. This gives us a clear separation of concerns. Also a layer may only speak to its adjacent layers, this makes it much easier for us to manage dependencies. Dependencies between layers will be handled by leveraging dependency injection. The real value of this will become apparent in an upcoming post on testing the individual layers.
Domain Model
We will leverage the Java Persistence API (JPA) to make interacting with
the database easier. Spring Boot uses Hibernate as the default
implementation, but the code below would be the same if you were using
the TopLink/EclipseLink implementations.
package cloudshop.itemservice.entity;
import javax.persistence.Entity;
import javax.persistence.GenerationType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;
@Entity
@Table(name="items")
public class Item {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private double price;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public boolean equals(Object obj) {
try {
if(null == obj) {
return false;
}
Item item = (Item) obj;
if(!Objects.equals(this.id, item.getId())) {
return false;
}
if(!Objects.equals(this.name, item.getName())) {
return false;
}
if(!Objects.equals(this.description, item.getDescription())) {
return false;
}
if(!Objects.equals(this.price, item.getPrice())) {
return false;
}
return true;
} catch (ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
return Long.hashCode(id);
}
}
Repository Layer
A Spring Boot repository is an abstraction to reduce the amount of
boiler plate code required to implement a data access layer. Since we
are using JPA, we can implement this quite easily by extending
CrudRepository and specifying our entity class (
Item) and the primary
key type (
Long).
package cloudshop.itemservice.repository;
import cloudshop.itemservice.entity.Item;
import org.springframework.data.repository.CrudRepository;
// This will be AUTO IMPLEMENTED by Spring into a Bean called itemRepository
// CRUD refers Create, Read, Update, Delete
public interface ItemRepository extends CrudRepository<Item, Long> {
}
Service Layer
The service layer is where we put the business logic that will interact with the repository layer. We idenitfy this class as a service using the
@Service annotation. We will leverage Spring Boot's dependency injection to gain access to our repository instead of configuring it directly.
package cloudshop.itemservice.service;
import cloudshop.itemservice.entity.Item;
import cloudshop.itemservice.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@Service
public class ItemService {
@Autowired
private ItemRepository itemRepository;
public Iterable<Item> getAllItems() {
return itemRepository.findAll();
}
public Optional<Item> getItem(@PathVariable long id) {
return itemRepository.findById(id);
}
public Item createItem(@RequestBody Item item) {
return itemRepository.save(item);
}
public Optional<Item> updateItem(Item item) {
Optional<Item> itemOptional = itemRepository.findById(item.getId());
if (!itemOptional.isPresent()) {
return itemOptional;
}
Item updatedItem = itemRepository.save(item);
return Optional.of(updatedItem);
}
public Optional<Item> deleteItem(long id) {
Optional<Item> itemOptional = itemRepository.findById(id);
if(itemOptional.isPresent()) {
itemRepository.delete(itemOptional.get());
}
return itemOptional;
}
}
REST Layer
The controller will interact with REST calls and then make the appropriate calls to the service layer. We idenitfy this class as a service using the
@Controller annotation. We
will leverage Spring Boot's dependency injection to gain access to our service instead of configuring it directly.
package cloudshop.itemservice.controller;
import cloudshop.itemservice.entity.Item;
import cloudshop.itemservice.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.Optional;
@Controller
@RequestMapping(path="/items")
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping
public @ResponseBody Iterable<Item> getAllItems() {
return itemService.getAllItems();
}
@GetMapping(value = "/{id}")
public @ResponseBody ResponseEntity<?> getItem(@PathVariable long id) {
Optional<Item> itemOptional = itemService.getItem(id);
if(!itemOptional.isPresent()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(itemOptional.get());
}
@PostMapping
public ResponseEntity<?> createItem(@RequestBody Item item) {
Item savedItem = itemService.createItem(item);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedItem.getId()).toUri();
return ResponseEntity.created(location).build();
}
@PutMapping("/{id}")
public ResponseEntity<?> updateItem(@RequestBody Item item, @PathVariable long id) {
item.setId(id);
Optional<Item> studentOptional = itemService.updateItem(item);
if (!studentOptional.isPresent()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteItem(@PathVariable long id) {
Optional<Item> itemOptional = itemService.deleteItem(id);
if(!itemOptional.isPresent()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
}
Application
Now we will implement an Application class to make our Spring Boot application runnable.
package cloudshop.itemservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Configuration
Below are the config files we need to make our application work.
Maven (pom.xml)
We will be leveraging Maven to make the management of our project a little easier.
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bdoughan</groupId>
<artifactId>item-service</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA Data (We are going to use Repositories, Entities, Hibernate, etc...) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Use MySQL Connector-J -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Spring Boot
The Item Service is still in the development stage, but we intend to
take in to production. The production environment will have different
configuration than our development environment. To handle this we can
utilize Spring Boot profiles.
Common Properties (application.properties)
Any common configuration can be put in a file called
"application.properties". Below we are stating that Hibernate should not
create anything in the database for us based on the JPA metadata.
spring.jpa.hibernate.ddl-auto=none
Dev Profile Properties (application-dev.properties)
The database connection metadata will definitely be different between
development and production so we will specify a properties file for a
dev profile. The naming convention for this file is
"application-{profile}.properties".
spring.datasource.url=jdbc:mysql://localhost:3306/cloudShopDb
spring.datasource.username=item_service_user
spring.datasource.password=item_service_password
Running the Application
We will use Maven to run our Spring Boot Application. Since we are using profiles, we must remember to specify the profile name.
mvn spring-boot:run -Dspring-boot.run.profiles=dev
Making the REST Calls
Now that our service is running we can use curl commands (or your favourite REST client) to try it out.
Get All Items
Here we see the items corresponding to the data we used to prepopulate our database.
curl -X GET http://localhost:8080/items
[{"id":1,"name":"apple","description":"A red apple","price":1.19},{"id":2,"name":"banana","description":"A yellow banana","price":0.87}]
Create an Item
We will use a POST command to create a new item. We do not specify an id since one will be generated for us. The URI of the newly created item is returned to us in the "Location" header. To see the response headers we made the call with the "-i" flag.
curl -X POST -i -H "Content-Type: application/json" -d "{\"name\":\"carrot\", \"description\":\"An orange carrot\", \"price\":0.99}" http://localhost:8080/items
HTTP/1.1 201
Location: http://localhost:8080/items/3
Content-Length: 0
Date: Tue, 16 Jul 2019 14:23:24 GMT
Read One Item
Now we can do a GET with the URI returned from the POST call to see our newly created item. We can also see that an id was generated for it.
curl -X GET http://localhost:8080/items/3
{"id":3,"name":"carrot","description":"An orange carrot","price":0.99}
Update an Item
A PUT call can be used to update the item.
curl -X PUT -i -H "Content-Type: application/json" -d "{\"id\":3,\"name\":\"carrot\", \"description\":\"A healthy orange carrot\", \"price\":0.99}" http://localhost:8080/items/3
HTTP/1.1 204
Date: Tue, 16 Jul 2019 14:26:07 GMT
Delete an Item
Finally a DELETE call can be made to remove an item.
curl -X DELETE -i http://localhost:8080/items/3
HTTP/1.1 204
Date: Tue, 16 Jul 2019 14:26:44 GMT
Useful Links
Below are some links that I found helpful when creating this post:
- Accessing Data with MySQL
https://spring.io/guides/gs/accessing-data-mysql/
- Creating a CRUD REST API/Service with Spring Boot, JPA and Hibernate
https://www.springboottutorial.com/spring-boot-crud-rest-service-with-jpa-hibernate
- Layered Architecture & Spring Boot
https://medium.com/@RogelioOrts/layered-architecture-spring-boot-af7dc071d2b5
No comments:
Post a Comment