Creating Microservices with Spring Boot

Creating Microservices with Spring Boot

Creating Microservices with Spring Boot

Microservices are an architectural style that structures an application as a collection of loosely coupled services. These services are designed to be small, independently deployable, and capable of performing a specific set of tasks. In this guide, we’ll explore how to build robust microservices using Spring Boot, a powerful framework that simplifies Java development. We will learn how to create a microservice project from scratch, set up the necessary components, and implement CRUD (Create, Read, Update, Delete) operations.

1. Introduction

In this tutorial, we’ll explore four primary components that constitute a microservice:

1.1 Entity

We will define an Entity, which represents the core data model. In this tutorial, our Entity will be the Product, and it will include attributes like ID, name, and store ID.

1.2 Repository

To interact with the database and manage data operations efficiently, we will implement a Repository. In our case, we’ll leverage Spring Data JPA to create the ProductRepository interface. Spring Data JPA provides a convenient way to perform database operations, including the essential CRUD (Create, Read, Update, Delete) operations.

1.3 Service

The service layer is where the business logic of our microservice resides. We will create a Service to manage the operations on the Product data. This service will define how our microservice handles tasks like fetching all products, creating new products, updating existing ones, and deleting products.

1.4 Controller

We will implement the ProductController to handle HTTP requests. This controller will expose RESTful endpoints that clients can use to interact with our microservice.

2. Setting Up the Development Environment

Creating a new Spring Boot project becomes straightforward using the Spring Initializr. Visit the Spring Initializr website, and choose the necessary dependencies like Spring Web, Spring Data JPA, and MySQL Driver. Next, click ‘Generate’ to obtain the project template.

2.1 Adding Necessary Dependencies

In the pom.xml file, make sure we have the following dependencies:

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
   <groupId>org.modelmapper</groupId>
   <artifactId>modelmapper</artifactId>
   <version>2.0.0</version>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <scope>runtime</scope>
   <optional>true</optional>
  </dependency>
  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>

2.2 Configuring Maven with MySQL

To link the Maven with MySQL, we’ll need to configure the database connection in the application.properties file:

spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update

The spring.jpa.hibernate.ddl-auto property with the value update tells Hibernate to update the database schema based on the entity model changes detected in the application. This means that Hibernate will compare the existing database schema to the entity model and update the schema as needed to match the entity model. This can include adding or removing tables, columns, and constraints.

3. Building a Simple RESTful API with Spring Boot

3.1 Creating Entities

We’ll start by defining our Product entity class.

@Entity
@Table(name = "products")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = { "createdAt", "updatedAt" }, allowGetters = true)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title")
    private String title;

    @Column(name = "description")
    private String description;

    @Column(name = "price")
    private Float price;

    @Column(name = "storeId")
    private String storeId;
    
    @Column(name = "stock")
    private Integer stock;

    @Column(nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    private Date createdAt;

    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @LastModifiedDate
    private Date updatedAt;

    // Getters and Setters
}

The @EntityListeners(AuditingEntityListener.class) annotation tells Spring Data JPA to use the AuditingEntityListener class to audit changes to our entity. The AuditingEntityListener class is a callback listener that is invoked before an entity is persisted or updated.

When the AuditingEntityListener class is invoked, it will set the following fields on the entity:

  • createdDate: The date and time the entity was created.
  • lastModifiedDate: The date and time the entity was last modified.
  • createdBy: The user who created the entity.
  • lastModifiedBy: The user who last modified the entity.

The createdBy and lastModifiedBy will be automatically populated if Spring Security is available in the project. 

3.2 Creating Data Transfer Object (DTO)

Instead of directly utilizing the entity class for requests, consider creating a DTO that includes only the fields meant for updates. This approach enables us to skip validation for fields that the user shouldn’t modify.

public class ProductDto {

    @NotNull(message = "Title must not be null")
    @Size(min = 2, max = 255)
    private String title;

    private String description;

    @NotNull(message = "Price must not be null")
    @Min(0)
    private Double price;

    @NotNull(message = "Store Id must not be null")
    private String storeId;

    @NotNull(message = "Stock number must not be null")
    private Integer stock;

    // Constructors, Getters and Setters
}

3.3 Creating Repositories

We’ll create a Spring Data JPA repository interface for our Product entity.

public interface ProductRepository extends CrudRepository<Product, Long> {
    List<Product> findByStoreId(String storeId);
}

3.4 Creating Services

We’ll create a service class to implement the business logic for our product CRUD operations.

@Service
public class ProductServiceImpl implements ProductService{

    @Autowired
    ProductRepository productRepository;

    @Override
    public ProductDto createProduct(ProductDto productDto) {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        Product product = modelMapper.map(productDto, Product.class);
        productRepository.save(product);
        return modelMapper.map(product, ProductDto.class);
    }

    @Override
    public List<ProductDto> getAllProducts() {
        List<Product> productList = (List<Product>) productRepository.findAll();
        Type listType = new TypeToken<List<ProductDto>>() {}.getType();
        return new ModelMapper().map(productList, listType);
    }

    @Override
    public List<ProductDto> getProductsByStoreId(String storeId) {
        List<Product> productList = productRepository.findByStoreId(storeId);
        Type listType = new TypeToken<List<ProductDto>>() {}.getType();
        return new ModelMapper().map(productList, listType);
    }

    @Override
    public ProductDto getProductById(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
        return new ModelMapper().map(product, ProductDto.class);
    }
    
    @Override
    public ProductDto updateProduct(Long id, ProductDto productDetails) {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
            
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found"));

        product.setTitle(productDetails.getTitle());
        product.setDescription(productDetails.getDescription());
        product.setPrice(productDetails.getPrice());
        product.setStoreId(productDetails.getStoreId());
        productRepository.save(product);
        return modelMapper.map(product, ProductDto.class);
    }

    @Override
    public void deleteProduct(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
        productRepository.delete(product);
    }
}

The ModelMapper is a library that simplifies the conversion between different Java objects, such as entities and DTOs. We use MatchingStrategies.STRICT matching strategy for the mapping. The strict matching strategy ensures the automatic mapping of fields with the same name and type.

3.4 Creating Controllers

We’ll develop a controller class to specify the RESTful endpoints responsible for our CRUD (Create, Read, Update, Delete) operations.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @Autowired
    ProductService productService;

    @GetMapping
    public ResponseEntity<Map<String, Object>> getAllProducts() {
        List<ProductDto> products = productService.getAllProducts();
        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("statusCode", HttpStatus.OK.value());
        response.put("message", PRODUCTS_RETRIEVED_SUCCESS);
        response.put("data", products);
        return ResponseEntity.ok(response);
    }

    @GetMapping("/store/{storeId}")
    public ResponseEntity<Map<String, Object>> getProductsByStoreId(@PathVariable String storeId) {
        List<ProductDto> products = productService.getProductsByStoreId(storeId);
        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("statusCode", HttpStatus.OK.value());
        response.put("message", PRODUCTS_STORE_RETRIEVED_SUCCESS);
        response.put("data", products);
        return ResponseEntity.ok(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Map<String, Object>> getProductById(@PathVariable Long id) {
        try {
            ProductDto product = productService.getProductById(id);
            Map<String, Object> response = new HashMap<>();
            response.put("status", "success");
            response.put("statusCode", HttpStatus.OK.value());
            response.put("message", PRODUCTS_RETRIEVED_SUCCESS);
            response.put("data", product);
            return ResponseEntity.ok(response);
        } catch (ResourceNotFoundException e) {
            Map<String, Object> response = new HashMap<>();
            response.put("status", "error");
            response.put("statusCode", HttpStatus.NOT_FOUND.value());
            response.put("message", PRODUCT_NOT_FOUND);
            return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
        }
    }

    @PostMapping
    public ResponseEntity<Map<String, Object>> createProduct(@Valid @RequestBody ProductDto product) {
        ProductDto createdProduct = productService.createProduct(product);
        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("statusCode", HttpStatus.CREATED.value());
        response.put("message", PRODUCT_CREATED_SUCCESS);
        response.put("data", createdProduct);
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Map<String, Object>> updateProduct(@PathVariable Long id, @Valid @RequestBody ProductDto productDetails) {
        try {
            ProductDto updatedProduct = productService.updateProduct(id, productDetails);
            Map<String, Object> response = new HashMap<>();
            response.put("status", "success");
            response.put("statusCode", HttpStatus.OK.value());
            response.put("message", PRODUCT_UPDATED_SUCCESS);
            response.put("data", updatedProduct);
            return ResponseEntity.ok(response);
        } catch (ResourceNotFoundException e) {
            Map<String, Object> response = new HashMap<>();
            response.put("status", "error");
            response.put("statusCode", HttpStatus.NOT_FOUND.value());
            response.put("message", PRODUCT_NOT_FOUND);
            return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Map<String, Object>> deleteProduct(@PathVariable Long id) {
        try {
            productService.deleteProduct(id);
            Map<String, Object> response = new HashMap<>();
            response.put("status", "success");
            response.put("statusCode", HttpStatus.NO_CONTENT.value());
            response.put("message", PRODUCT_DELETED_SUCCESS);
            return new ResponseEntity<>(response, HttpStatus.NO_CONTENT);
        } catch (ResourceNotFoundException e) {
            Map<String, Object> response = new HashMap<>();
            response.put("status", "error");
            response.put("statusCode", HttpStatus.NOT_FOUND.value());
            response.put("message", PRODUCT_NOT_FOUND);
            return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
        }
    }
}

4. Logging and Exception Handling

4.1 Configuring Logging

Spring Boot provides excellent support for logging. We can configure logging levels in the application.properties file:

logging.level.root=WARN
logging.level.com.yourpackage=DEBUG

Replace com.yourpackage with the actual package name for which we want to set the logging level.

Spring Boot uses Logback as its default logging library. We can configure Logback either using the logback-spring.xml file or by adding properties directly to the application.properties file, as shown in our example.

Here’s an example of how we can use a logger in our service class:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {

    private static final Logger logger = LoggerFactory.getLogger(ProductService.class);

    // ... other methods ...

    public ProductDto getProductById(Long id) {
        logger.info("Fetching product with ID: {}", id);
        // ... rest of the method ...
    }
}

4.2 Implementing Custom Exception Handling

For a more user-friendly experience, we can create custom exception handling. Here’s how we can do it:

Create a custom exception class, such as ResourceNotFoundException. This exception can be thrown when a requested resource is not found:

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

2. Next, let’s create a custom exception response class:

public class CustomErrorResponse {
    private String message;
    private int status;

    // Getters and Setters
}

3. We’ll create a global exception handler using @ControllerAdvice:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<CustomErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        CustomErrorResponse response = new CustomErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND.value());
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<CustomErrorResponse> handleGenericException(Exception ex) {
        CustomErrorResponse response = new CustomErrorResponse(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value());
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // Add more exception handlers as needed...
}

The GlobalExceptionHandler comes into play when an exception is thrown from a controller method and not explicitly handled within that method. If we want the ResourceNotFoundException to be handled by the global exception handler, we need to remove the try-catch block from the controller method. Let the exception propagate up to the controller advice, and it will be appropriately handled there.

5. Testing with JUnit and Mokito

Testing is essential to ensure the quality of our microservices. We’ll write unit tests using JUnit and Mockito to test our product microservices thoroughly.

First, create a new test class for ProductServiceImpl. Name it something like ProductServiceImplTest. In the test class, we use Mockito to mock the dependencies that ProductServiceImpl relies on. For example, if ProductServiceImpl depends on a repository or another service, mock those components using @MockBean or @Mock. The @InjectMocks annotation is then used to inject these mock objects into the fields of our class under test.

@SpringBootTest
public class ProductServiceImplTest {

    @InjectMocks
    private ProductServiceImpl productService;

    @Mock
    private ProductRepository productRepository;

    @Test
    public void testCreateProduct() {
        ProductDto productDto = new ProductDto();
        Product product = new Product();
        when(productRepository.save(any(Product.class))).thenReturn(product);
        ProductDto result = productService.createProduct(productDto);
        assertNotNull(result);
    }

    @Test
    public void testGetAllProducts() {
        List<Product> products = Arrays.asList(new Product());
        when(productRepository.findAll()).thenReturn(products);
        List<ProductDto> result = productService.getAllProducts();
        assertEquals(1, result.size());
    }

    @Test
    public void testGetProductsByStoreId() {
        List<Product> products = Arrays.asList(new Product());
        when(productRepository.findByStoreId(anyString())).thenReturn(products);
        List<ProductDto> result = productService.getProductsByStoreId("storeId");
        assertEquals(1, result.size());
    }

    @Test
    public void testGetProductById() {
        Product product = new Product();
        when(productRepository.findById(anyLong())).thenReturn(Optional.of(product));
        ProductDto result = productService.getProductById(1L);
        assertNotNull(result);
    }

    ...
}

Next, we create a new test class for ProductController. Name it something like ProductControllerTest. The @AutoConfigureMockMvc annotation in Spring Boot is used for unit testing web applications using the MockMvc framework. MockMvc is a Spring framework class that allows us to perform HTTP requests against our application’s controllers without starting a full server. It provides methods to simulate HTTP requests (such as GET, POST, etc.) and verify the responses.

@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @InjectMocks
    private ProductController productController;

    @Mock
    private ProductService productService;

    private ProductDto productDto;
    private Long productId = 1L;

    @BeforeEach
    public void setUp() {
        reset(productService); // Reset the mock behavior
        productDto = new ProductDto("Product title", "Product description", 349.90, "S-4", 100);
        when(productService.createProduct(any(ProductDto.class))).thenReturn(productDto);
        when(productService.getProductById(productId)).thenReturn(productDto); // Return the original productDto
        when(productService.updateProduct(eq(productId), any(ProductDto.class))).thenReturn(new ProductDto("Updated title", "Updated description", 299.90, "S-4", 50)); // Return a new instance with updated values
        doNothing().when(productService).deleteProduct(productId);
    }

    @Test
    public void testGetAllProducts() throws Exception {
        mockMvc.perform(get("/api/products"))
            .andExpect(status().isOk());
    }

    @Test
    public void testGetProductsByStoreId() throws Exception {
        mockMvc.perform(get("/api/products/store/storeId"))
            .andExpect(status().isOk());
    }

    @Test
    public void testGetProductById() throws Exception {
        Long productId = 1L;
        mockMvc.perform(get("/api/products/" + productId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data.title", is(productDto.getTitle())))
            .andExpect(jsonPath("$.data.description", is(productDto.getDescription())))
            .andExpect(jsonPath("$.data.price", is(productDto.getPrice())))
            .andExpect(jsonPath("$.data.storeId", is(productDto.getStoreId())))
            .andExpect(jsonPath("$.data.stock", is(productDto.getStock())));
    }

    @Test
    public void testGetProductByIdNotFound() throws Exception {
        when(productService.getProductById(anyLong())).thenThrow(new ResourceNotFoundException("Product not found"));
        mockMvc.perform(get("/api/products/1"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.status", is("error")))
            .andExpect(jsonPath("$.message", is("Product not found!")));
    }

    ... 
}
image-1 Creating Microservices with Spring Boot

6. Managing Different Environments

We often need different configurations for various environments such as development, testing, and production. Spring Boot simplifies the management of these configurations.

6.1 Creating Environment-Specific Configuration Files

To manage environment-specific configurations, we can create separate property files for each environment:

  • application-dev.properties for development.
  • application-test.properties for testing.
  • application-prod.properties for production.

Inside these files, we can define environment-specific properties, like database connections, logging levels, and other configurations relevant to that environment.

6.2 Setting Up Profiles

In Spring Boot, we use a profile to activate specific configurations based on the environment. We can set the active profile in several ways:

6.2.1 In the application.properties File:

In the application.properties file of the project, specify the active profile using:

spring.profiles.active=dev

6.2.2 As a Command-Line Argument:

When running our Spring Boot application, we can specify the active profile as a command-line argument. For example, to activate the prod profile:

java -jar your-app.jar --spring.profiles.active=prod

6.2.3 In the Code (e.g., in a Test Class):

We can also activate profiles programmatically in the code. For instance, in a test class, use the @ActiveProfiles annotation to activate the test profile:

@ActiveProfiles("test")
public class MyTest {
    // Test methods go here
}

6.2.4 Using Maven Command:

We’re using Maven to run our Spring Boot application, we can specify the active profile as an argument:

mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=dev

Activating the appropriate profile ensures that we use the correct configurations for each environment. For example, using spring.profiles.active=dev will load configurations from application-dev.properties.

7. Conclusion

In this comprehensive tutorial, we’ve explored essential concepts and practices for building robust RESTful APIs. From setting up the development environment to implementing CRUD operations. Full source code is available on GitHub. In the next tutorial, we will walk through inter-service communication in a Spring Boot application using Spring Cloud OpenFeign.

Share this content:

Leave a Comment

Discover more from nnyw@tech

Subscribe now to keep reading and get access to the full archive.

Continue reading