Tuesday, December 19, 2023

Multiple Data Sources in Spring Boot 3 with Java 21

 

In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sources. This setup is useful when dealing with databases of different types, such as MySQL, Oracle, and PostgreSQL. 






Technology


  • Spring Boot 3.2.0
  • Java 21 (Zulu)
  • Docker Compose
  • Maven 
  • IntelliJ IDEA  2023.3.1 Community 
  • Postman


Project Structure


multiple-data-sources-jpa
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry
|                   |-- configuration
|                   |-- model
|                   |-- repository
|                   |-- service
|                   |-- controller
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml


Application.yaml Configuration



The application file contains configuration deatils for each data source, including URLs, usernames, passowrds, and driver class names.


server:
  port: 9000
  servlet:
    context-path: /

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true
      username: test
      password: test_pass
      driverClassName: com.mysql.cj.jdbc.Driver
      ddlAuto: create-drop
    postgres:
      url: jdbc:postgresql:postgre_test
      username: postgre_test
      password: postgre_test
      driverClassName: org.postgresql.Driver
      ddlAuto: create-drop
    oracle:
      url: jdbc:oracle:thin:@//localhost:1521/xe
      username: system
      password: oracle
      driverClassName: oracle.jdbc.OracleDriver
      ddlAuto: update


Configuration Classes


We've created configuration classes for each data source - MysqlConfig, PostgreConfig, and OracleConfig:


package com.henry.configuration;

import com.henry.record.OracleRecord;
import com.henry.record.PostgreRecord;
import com.henry.record.MysqlRecord;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@ConfigurationProperties("spring.datasource")
@Data
public class DataSourceProperties {
   private MysqlRecord mysql;
   private PostgreRecord postgres;
   private OracleRecord oracle;

}


package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.user",
        entityManagerFactoryRef = "userEntityManager",
        transactionManagerRef = "userTransactionManager"
)
public class MysqlConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean userEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(userDataSource());
        em.setPackagesToScan("com.henry.model.user");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(userHibernateProperties());

        return em;
    }

    @Bean
    @Primary
    public DataSource userDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getMysql().driverClassName());
        dataSource.setUrl(dsProperties.getMysql().url());
        dataSource.setUsername(dsProperties.getMysql().username());
        dataSource.setPassword(dsProperties.getMysql().password());

        return dataSource;
    }

    private Properties userHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto",dsProperties.getMysql().ddlAuto());

        return properties;
    }

    @Primary
    @Bean
    public PlatformTransactionManager userTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(userEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.brand",
        entityManagerFactoryRef = "brandEntityManager",
        transactionManagerRef = "brandTransactionManager"
)
public class OracleConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean brandEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(brandDataSource());
        em.setPackagesToScan("com.henry.model.brand");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(brandHibernateProperties());

        return em;
    }

    @Bean
    public DataSource brandDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getOracle().driverClassName());
        dataSource.setUrl(dsProperties.getOracle().url());
        dataSource.setUsername(dsProperties.getOracle().username());
        dataSource.setPassword(dsProperties.getOracle().password());

        return dataSource;
    }

    private Properties brandHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getOracle().ddlAuto());

        return properties;
    }


    @Bean
    public PlatformTransactionManager brandTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(brandEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.company",
        entityManagerFactoryRef = "companyEntityManager",
        transactionManagerRef = "companyTransactionManager"
)
public class PostgreConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean companyEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(companyDataSource());
        em.setPackagesToScan("com.henry.model.company");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(companyHibernateProperties());

        return em;
    }

    @Bean
    public DataSource companyDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getPostgres().driverClassName());
        dataSource.setUrl(dsProperties.getPostgres().url());
        dataSource.setUsername(dsProperties.getPostgres().username());
        dataSource.setPassword(dsProperties.getPostgres().password());

        return dataSource;
    }

    private Properties companyHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getPostgres().ddlAuto());

        return properties;
    }

    @Bean
    public PlatformTransactionManager companyTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(companyEntityManager().getObject());
        return transactionManager;
    }
}


Record Classes


To encapsulate the configuration properties for each data source, we've defined record classes - MysqlRecord, PostgreRecord, and OracleRecord:


package com.henry.record;

public record MysqlRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

package com.henry.record;

public record OracleRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}


package com.henry.record;

public record PostgreRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

Repositories


Repository interfaces extend the CrudRepository for each entity (Brand, Company, User):


package com.henry.repository.brand;

import com.henry.model.brand.Brand;
import org.springframework.data.repository.CrudRepository;

public interface BrandRepository extends CrudRepository<Brand,Long> {
}

package com.henry.repository.company;

import com.henry.model.company.Company;
import org.springframework.data.repository.CrudRepository;

public interface CompanyRepository extends CrudRepository<Company,Long> {
}

package com.henry.repository.user;

import com.henry.model.user.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User,Long> {
}

Services


Service classes (BrandServiceImpl, CompanyServiceImpl, UserServiceImpl) implement a common interface (DefaultService) for generic CRUD operations:

package com.henry.service;

public sealed interface DefaultService<T, G> permits UserServiceImpl, CompanyServiceImpl, BrandServiceImpl {

    T save(T obj);
    Iterable<T>  findAll();
    T findById(G id);
}

package com.henry.service;

import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import org.springframework.stereotype.Service;

@Service
public final class BrandServiceImpl implements DefaultService<Brand, Long> {

    private final BrandRepository brandRepository;

    public BrandServiceImpl(BrandRepository brandRepository) {
        this.brandRepository = brandRepository;
    }


    @Override
    public Brand save(Brand obj) {
        return brandRepository.save(obj);
    }

    @Override
    public Iterable<Brand> findAll() {
        return brandRepository.findAll();
    }

    @Override
    public Brand findById(Long id) {
        return brandRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import org.springframework.stereotype.Service;

@Service
public final class CompanyServiceImpl implements DefaultService<Company, Long> {

    private final CompanyRepository companyRepository;

    public CompanyServiceImpl(CompanyRepository companyRepository) {
        this.companyRepository = companyRepository;
    }

    @Override
    public Company save(Company obj) {
        return companyRepository.save(obj);
    }

    @Override
    public Iterable<Company> findAll() {
        return companyRepository.findAll();
    }

    @Override
    public Company findById(Long id) {
        return companyRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import org.springframework.stereotype.Service;

@Service
public final class UserServiceImpl implements DefaultService<User,Long> {

    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User save(User obj) {
        return userRepository.save(obj);
    }

    @Override
    public Iterable<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    public User findById(Long id) {
        return userRepository.findById(id).get();
    }
}

REST Controllers


REST controllers (BrandController, CompanyController, UserController) handle HTTP requests for creating entities:


package com.henry.controller;

import com.henry.model.brand.Brand;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v3")
public class BrandController {

    private final DefaultService<Brand, Long> defaultService;

    public BrandController(DefaultService<Brand, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/brands")
    public Brand createEmployee(@RequestBody Brand brand) {
        return defaultService.save(brand);
    }

}

package com.henry.controller;

import com.henry.model.company.Company;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v2")
public class CompanyController {

    private final DefaultService<Company,Long> defaultService;

    public CompanyController(DefaultService<Company, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/companies")
    public Company createEmployee(@RequestBody Company company) {
        return defaultService.save(company);
    }
}

package com.henry.controller;

import com.henry.model.user.User;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class UserController {

    private final DefaultService<User,Long> defaultService;

    public UserController(DefaultService<User, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/users")
    public User createEmployee(@RequestBody User user) {
        return defaultService.save(user);
    }
}


Test Classes


JUnit test classes (BrandServiceTest, CompanyServiceTest, UserServiceTest) use Mockito for mocking dependencies:


package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import com.henry.service.BrandServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class BrandServiceTest {

    @Mock
    private BrandRepository brandRepository;

    @InjectMocks
    private BrandServiceImpl brandService;

    @Test
    void testSaveBrand() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();

        when(brandRepository.save(any())).thenReturn(brand);

        Brand saved = brandService.save(brand);

        assertEquals(brand, saved);
        verify(brandRepository).save(any());
    }

    @Test
    void testFindAllBrands() {
        Brand brand1 = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();
        Brand brand2 = Brand.builder()
                .id(2L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findAll()).thenReturn(Arrays.asList(brand1, brand2));

        Iterable<Brand> result = brandService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(brandRepository).findAll();
    }

    @Test
    void testFindBrandById() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findById(1L)).thenReturn(Optional.of(brand));

        Brand result = brandService.findById(1L);

        assertEquals(brand, result);
        verify(brandRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import com.henry.service.CompanyServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CompanyServiceTest {

    @Mock
    private CompanyRepository companyRepository;

    @InjectMocks
    private CompanyServiceImpl companyService;

    @Test
    void testSaveCompany() {
        Company company =
                Company.builder()
                        .id(1L)
                        .name("Test Corp")
                        .build();

        when(companyRepository.save(any())).thenReturn(company);

        Company saved = companyService.save(company);

        assertEquals(company, saved);
        verify(companyRepository).save(any());
    }

    @Test
    void testFindAllCompanies() {
        Company comp1 = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();
        Company comp2 = Company.builder()
                .id(2L)
                .name("Demo Corp")
                .build();

        when(companyRepository.findAll()).thenReturn(Arrays.asList(comp1, comp2));

        Iterable<Company> result = companyService.findAll();


        assertEquals(2, Iterables.size(result));
        verify(companyRepository).findAll();
    }

    @Test
    void testFindCompanyById() {
        Company company = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();

        when(companyRepository.findById(1L)).thenReturn(Optional.of(company));

        Company result = companyService.findById(1L);

        assertEquals(company, result);
        verify(companyRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import com.henry.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    public void saveUser_shouldPersistUser() {
/**

 int x = 10;
 int y = 20;
 String result = STR."\{x} + \{y} = \{x + y}";
 string templates are a preview feature and are disabled by default.
 Are string templates supported yet? - IntelliJ support - JetBrains
 https://intellij-support.jetbrains.com/hc/en-us/community/posts/13451540436114-Are-string-templates-supported-yet-
 **/

        // Create a User
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.save(any())).thenReturn(user);

        User saved = userService.save(user);

        assertEquals(user, saved);
        verify(userRepository).save(any());
    }

    @Test
    void testFindAllUsers() {
        User user1 = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();
        User user2 = User.builder()
                .id(2L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));

        Iterable<User> result = userService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(userRepository).findAll();
    }

    @Test
    void testFindUserById() {
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        User result = userService.findById(1L);

        assertEquals(user, result);
        verify(userRepository).findById(1L);
    }
}


Maven Configuration


The pom.xml file manages project dependencies and build configuration:


<?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>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>multiple-data-sources-jpa</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>multiple-data-sources-jpa</name>
	<description>Demo project multiple data sources jpa</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<!-- Exclude the Tomcat dependency -->
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<!-- Use Jetty instead -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jetty</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- MARIADB Connector -->

		<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
		</dependency>

		<!-- Mysql Connector -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
		</dependency>

		<!-- ORACLE database driver -->
		<!-- https://mvnrepository.com/artifact/oracle/ojdbc6 -->
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>11.2.0.4</version>
		</dependency>

		<!-- POSTGRESQL database driver -->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>31.0.1-jre</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.antlr/stringtemplate
		<dependency>
			<groupId>org.antlr</groupId>
			<artifactId>stringtemplate</artifactId>
			<version>4.0.2</version>
		</dependency>
		-->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>21</source>
					<target>21</target>
					<compilerArgs>--enable-preview</compilerArgs>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>


Environment Setup with Docker Compose


To simplify the setup of your application's environment, you can use Docker Compose. Below is a docker-compose-multiple-db.yml file that defines services for Oracle, PostgreSQL, and MySQL databases:

version: '3'
services:
  oracle:
    image: pengbai/docker-oracle-xe-11g-r2
    ports:
      - "1521:1521"
      - "8080:8080"
    shm_size: 1g

  postgres:
    image: postgres:14.1
    container_name: postgre_test
    environment:
      POSTGRES_USER: postgre_test
      POSTGRES_PASSWORD: postgre_test
      POSTGRES_DB: postgre_test
    ports:
      - "5432:5432"

  mysql:
    image: mysql/mysql-server:latest
    container_name: mysql-docker-container
    environment:
      MYSQL_ROOT_PASSWORD: mypass
      MYSQL_DATABASE: test_db
      MYSQL_USER: test
      MYSQL_PASSWORD: test_pass
    ports:
      - "3306:3306"

Run command:

 
docker-compose -f docker-compose-multiple-db.yml up -d



Only for oracle:

mvn install:install-file "-Dfile=path/to/your/ojdbc6.jar" "-DgroupId=com.oracle" "-DartifactId=ojdbc6" "-Dversion=11.2.0.4" "-Dpackaging=jar" "-DgeneratePom=true"


Run & Test


Run Spring Boot application with command: by console, IntelliJ etc.


mvn test -Dtest=UserServiceTest

mvn test -Dtest=BrandServiceTest

mvn test -Dtest=CompanyServiceTest







Run locally: mvn spring-boot:run





POST
http://localhost:9000/api/v1/users

{
    "name""henry",
    "lastName""x"
}







POST
http://localhost:9000/api/v2/companies
{
    "name""company1"
}











POST
http://localhost:9000/api/v3/brands

{
    "name""Demo Brand"
}












Source Code


Here on GitHub.


Notes: 


Possible issues:
  • Lombok Plugin check if it was installed in IntelliJ 
  • Others issues please -> File | Invalidate Caches
  • Change the port if you have in local databases. Otherwise you have conflicts.






Sunday, November 12, 2023

Deploying Spring Boot Apps Locally with Skaffold, Helm and Docker for Kubernetes

Introduction:

In the ever-evolving landscape of software development and deployment, the integration of Spring Boot,Skaffold, Helm, Docker, and local Kubernetes enviroment has become a powerful combination for building scalable and containerized applications.




Skaffold: Automating Kubernetes Development Workflows

Skaffold is a powerful command-line tool designed to streamline the development and deployment workflows for Kubernetes applications. It acts as the orchestrator, automating tasks such as building container images, deploying to Kubernetes clusters, and monitoring code changes in real-time.

Helm: Simplifying Kubernetes Application Management


Helm is a package manager for Kubernetes applications, making it easier to define, install, and upgrade applications. Helm uses charts, which encapsulate pre-configured Kubernetes resources, allowing developers to share and deploy applications with ease.

Kubernetes: The Backbone of Container Orchestration


Kubernetes is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications. It provides a robust framework for running and coordinating applications in a distributed environment.

Docker Desktop: Localized Development Environment for Containers


Docker Desktop is an application that simplifies the development and deployment of containerized applications. It includes a local Kubernetes cluster, providing developers with a consistent environment for building, testing, and deploying containers.

Prerequisites

Before we dive into the world of streamlined Spring Boot development with Skaffold, Helm, Docker, and Kubernetes, ensure you have the following prerequisites installed on your machine:

Skaffold:

Follow the official Skaffold installation guide based on your operating system: Skaffold Installation Guide.

Helm:

Install Helm by following the Helm installation instructions: Helm Installation Guide.

Kubernetes:

Choose one of the following options based on your preference:

Docker Desktop:

Install Docker Desktop, which includes Kubernetes support. Download it from the official Docker website: Docker Desktop.

Minikube:

If you prefer using Minikube, follow the installation instructions: Minikube Installation Guide.

Ingress-Nginx Controller:


Install the Ingress-Nginx Controller for routing external HTTP/S traffic to services in the Kubernetes cluster. Use one of the following methods:

Using kubectl:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.41.2/deploy/static/provider/cloud/deploy.yaml
Using helm:

helm install --namespace default nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginxSpring Boot 3:

For more details, refer to the official Ingress-Nginx deployment guide: Ingress-Nginx Deployment Guide.

If still not work after install then stop and start Docker Desktop Kubernetes.

Directory etc/folder:


Adding the IP-to-hostname mapping in /etc/hosts: in this post
127.0.0.1       myapp.local

Spring Boot 3:

Create a Spring Boot 3 application. 

Environment Setup

Helm Version:

helm version
version.BuildInfo{Version:"v3.13.0", GitCommit:"825e86f6a7a38cef1112bfa606e4127a706749b1", GitTreeState:"clean", GoVersion:"go1.20.8"}
Skaffold Version:

skaffold version
v2.9.0
Kubernetes Nodes:

kubectl get nodes
NAME             STATUS   ROLES           AGE    VERSION
docker-desktop   Ready    control-plane   7d4h   v1.28.2


Project Structure: 


skaffold-dockerfile-demo
|-- myapp-chart
|   |-- templates
|   |   |-- deployment.yaml
|   |   |-- ingress.yaml
|   |   |-- service.yaml
|   |-- Chart.yaml
|   |-- values.yaml
|-- Dockerfile
|-- skaffold.yaml
|-- src
|-- ...

myapp-chart:

This directory encapsulates the Helm chart for your application. Helm charts provide a way to package, distribute, and manage Kubernetes applications.

templates: Contains YAML templates for Kubernetes resources like Deployment, Ingress, and Service.
  • deployment.yaml: Defines the deployment configuration for your Spring Boot application.
  • apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ .Chart.Name }}
    spec:
      replicas: {{ .Values.replicaCount }}
      selector:
        matchLabels:
          app: {{ .Chart.Name }}
      template:
        metadata:
          labels:
            app: {{ .Chart.Name }}
        spec:
          containers:
            - name: {{ .Chart.Name }}
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              ports:
                - name: http
                  containerPort: {{ .Values.service.port }}
  • ingress.yaml: Configures the Ingress resource for routing external traffic.
  • apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: {{ .Chart.Name }}-ingress
    spec:
      defaultBackend:
        service:
          name: {{ .Chart.Name }}-service
          port:
            number: {{ .Values.service.port }}
      ingressClassName: nginx
      rules:
        - host: myapp.local
          http:
            paths:
              - pathType: Prefix
                backend:
                  service:
                    name: {{ .Chart.Name }}-service
                    port:
                      number: {{ .Values.service.port }}
                path: /
  • service.yaml: Specifies the Kubernetes Service for your application.
  • apiVersion: v1
    kind: Service
    metadata:
      name: {{ .Chart.Name }}-service
    spec:
      ports:
        - name: http-web
          port: {{ .Values.service.port }}
          protocol: TCP
          #targetPort: 8080
          nodePort: 30090
      selector:
        app: {{ .Chart.Name }}
      type: NodePort
      #type: LoadBalancer
Chart.yaml: Describes the metadata and configuration of the Helm chart.

apiVersion: v2
name: myapp
description: A Helm chart for my Spring Boot application
version: 1.0.0
appVersion: 1.0.0

values.yaml: Contains default configuration values for your Helm chart.

replicaCount: 1

image:
  repository: skaffold-demo-docker
  tag: latest

service:
  port: 8080

ingress:
  enabled: true


Dockerfile: Check the reference help me for created Dockerfile 

This file instructs Docker on how to build the Docker image for your Spring Boot application.
FROM eclipse-temurin:17-jdk-focal

WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]


skaffold.yaml:

Skaffold configuration file that automates the development workflow. It defines how to build and deploy your application. In this post the skaffokd build by Dockerfile, however in the GitHub also I added by Jib Maven.

apiVersion: skaffold/v2beta10
kind: Config
build:
  artifacts:
    - image: skaffold-demo-docker
      context: .
      docker:
        dockerfile: Dockerfile

deploy:
  helm:
    releases:
      - name: myapp
        chartPath: ./myapp-chart
        valuesFiles:
          - ./myapp-chart/values.yaml

Spring Boot App


UserController:
 
import com.henry.dockerfiledemo.record.UserRecord;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world";
    }

    @GetMapping("/user")
    public UserRecord helloUser() {
        return new UserRecord("Henry", "Xiloj");
    }
}


Test:

Run next command: we can check in dev mode our changes. Otherwise try with skaffold run. For more command check official documentation

skaffold dev














Curl:
 curl http://myapp.local/hello








curl http://myapp.local/user








Helm Commands:

helm list




Kubectl commands: for check deployement, sevices, ingress etc


kubectl get all
kubectl get deployment
kubectl get svc
kubectl get ingress


Source Code


Here on GitHub.





References


https://skaffold.dev/
https://helm.sh/
https://www.docker.com/blog/kickstart-your-spring-boot-application-development/
https://stackoverflow.com/questions/65193758/enable-ingress-controller-on-docker-desktop-with-wls2
https://kubernetes.github.io/ingress-nginx/deploy/



Monday, September 25, 2023

GraphQL Error Handling with Spring Boot 3

 Why HTTP 200 is Different from REST

GraphQL servers typically return an HTTP 200 status code for all responses, regardless of whether the request was successful or encountered an error. This behavior is by design and is different from RESTful APIs, where different HTTP status codes are used to indicate the success or failure of a request.

In GraphQL, errors are typically handled differently than in RESTful APIs. Instead of using HTTP status codes, GraphQL responses include an "errors" field in the JSON response object when there are issues with the request. This "errors" field contains an array of error objects, each of which includes information about the specific error, such as a message and location within the query.



GraphQL response might look like when there are errors:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": ["user"]
    }
  ]
}

The request to fetch a user encountered an error, and the server responds with an HTTP 200 status code but includes an "errors" field in the response to provide details about the error.

If you're working with a GraphQL client or library, it should be designed to check for errors in the response and handle them accordingly. Typically, successful responses will have a "data" field with the requested data, and errors will be present in the "errors" field.

However we can send customized error messages in the "errors" field in the response and we can send something like BAD_REQUEST or NOT_FOUND etc.

GraphQL Error Handling

DataFetcherExceptionResolverAdapter is a Spring for GraphQL class that allows you to implement custom error handling for your GraphQL API. It is an extension of the DataFetcherExceptionResolver interface, which is used to resolve exceptions that occur during the execution of data fetchers.

pom.xml

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

Config

public class CustomGraphQLException extends RuntimeException {

    private final int statusCode;
    public CustomGraphQLException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }
    public int getStatusCode() {
        return statusCode;
    }

}

DataFetcherExceptionResolverAdapter

Sample example HTTP 404 Not Found and HTTP 400 Bad Request

@Component
public class CustomGraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof CustomGraphQLException) {
            ErrorType errorType;
            if(((CustomGraphQLException) ex).getStatusCode()==400){
                errorType = ErrorType.BAD_REQUEST;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            if(((CustomGraphQLException) ex).getStatusCode()==404){
                errorType = ErrorType.NOT_FOUND;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            else {
                return GraphqlErrorBuilder.newError().build();
            }

        } else {
            return GraphqlErrorBuilder.newError().build();
        }
    }

    private GraphQLError graphQLError(ErrorType errorType, CustomGraphQLException ex,DataFetchingEnvironment env){
        return GraphqlErrorBuilder.newError()
                .errorType(errorType)
                .message(ex.getMessage())
                .path(env.getExecutionStepInfo().getPath())
                .location(env.getField().getSourceLocation())
                .build();
    }
}

Resolver

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(400, "An error occurred while processing your request.");
    }

GraphQLError with the appropriate classification:

{
    "errors": [
        {
            "message": "An error occurred while processing your request.",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "BAD_REQUEST"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

GraphQLError without classification:

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(401, "Unauthorized");
    }
{
    "errors": [
        {
            "message": "INTERNAL_ERROR for 54ab7ea3-3bc1-a5b1-0c1c-e45fbda75659",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "INTERNAL_ERROR"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

Complete exist CRUD example:

Here on GitHub.















Multiple Data Sources in Spring Boot 3 with Java 21

  In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sour...