The way SpringBoot configures DataSource programmatically

  • 2021-06-28 12:27:14
  • OfStack

Spring Boot uses a fixed algorithm to scan and configure DataSource.This allows us to easily get a fully configured DataSource implementation by default.

Spring Boot also configures connection pools (HikariCP, Apache Tomcat, or Commons DBCP) automatically and quickly in sequence, depending on which classes are in the path.

Although DataSource auto-configuration for Spring Boot works well in most cases, sometimes we need higher levels of control, so we must set up our own DataSource implementation and ignore the auto-configuration process.

Maven Dependency

Overall, it is very simple to programmatically create an DataSource implementation.

To learn how to achieve this goal, we will implement a simple repository layer that will perform CRUD operations on some JPA entities.

Let's look at the dependencies of our demonstration project:


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.4.1</version> 
  <scope>runtime</scope> 
</dependency>

We will use the in-memory H2 database instance to run the repository layer.By doing so, we will be able to test the programmatically configured DataSource without having to perform expensive database operations.

Let's make sure to see the latest version of spring-boot-starter-data-jpa on Maven Central.

Configure DataSource

If we insist on using DataSource from Spring Boot to automatically configure and run our project in its current state, the program will work as expected.

Spring Boot will complete all heavy infrastructure pipelines for us.This includes creating an H2 DataSource implementation that will be automatically processed by HikariCP, Apache Tomcat, or Commons DBCP and setting up an in-memory database instance.

In addition, we do not even need to create the application.properties file because Spring Boot also provides some default database settings.

As we mentioned earlier, sometimes we need a higher level of customization, so we must programmatically configure our own DataSource implementation.

The easiest way to do this is to define the DataSource factory method and place it in a class that uses the @Configuration annotation:


@Configuration
public class DataSourceConfig {
   
  @Bean
  public DataSource getDataSource() {
    DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
    dataSourceBuilder.driverClassName("org.h2.Driver");
    dataSourceBuilder.url("jdbc:h2:mem:test");
    dataSourceBuilder.username("SA");
    dataSourceBuilder.password("");
    return dataSourceBuilder.build();
  }
}

In this case, we programmatically create our custom DataSource object using the convenient DataSourceBuilder class, a concise Joshua Bloch builder pattern.

This is a good approach because the builder can easily configure DataSource with a few common properties.In addition, it can use the underlying connection pool.

Externalize the DataSource configuration using the application.properties file

Of course, we can also partially externalize our DataSource configuration.For example, we can define some basic DataSource properties in a factory method:


@Bean
public DataSource getDataSource() { 
  DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 
  dataSourceBuilder.username("SA"); 
  dataSourceBuilder.password(""); 
  return dataSourceBuilder.build(); 
}

And specify a few additional configurations in the application.properties file:


spring.datasource.url=jdbc:h2:mem:test
spring.datasource.driver-class-name=org.h2.Driver

Properties defined in external sources, such as the application.properties file above or by using classes annotated with @ConfigurationProperties, will override those defined in Java API.

Clearly, in this way, we no longer save the DataSource configuration settings in one place.

On the other hand, it allows us to keep compile-time and run-time configurations separate from each other and well.

This is great because it allows us to easily set binding points.This allows us to include different DataSources from other sources without having to refactor our bean factory method.

Testing DataSource configuration
Testing our custom DataSource configuration is easy.The process boils down to creating the JPA entity, defining the basic repository interface, and testing the repository layer.

Create JPA entity

Let's start defining our sample JPA entity class, which will model the user:


@Entity
@Table(name = "users")
public class User {
   
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  private String name;
  private String email;
 
  // standard constructors / setters / getters / toString
   
}
Repository Layer

We need to implement a basic repository layer that allows us to perform CRUD operations on instances of the User entity class defined above.

Since we are using Spring Data JPA, we do not have to create our own DAO implementation from scratch.We only need to extend the CrudRepository interface to get a working repository implementation:


@Repository
public interface UserRepository extends CrudRepository<User, Long> {}
Test Repository Layer

Finally, we need to check if our programming configuration DataSource actually works.We can easily accomplish this task through integrated testing:


@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryIntegrationTest {
   
  @Autowired
  private UserRepository userRepository;
  
  @Test
  public void whenCalledSave_thenCorrectNumberOfUsers() {
    userRepository.save(new User("Bob", "bob@domain.com"));
    List<User> users = (List<User>) userRepository.findAll();
     
    assertThat(users.size()).isEqualTo(1);
  }  
}

The UserRepositoryIntegrationTest class is the test case.It simply runs the CRUD method of the two repository interfaces to persist and find entities.

Note that whether we decide to configure the DataSource implementation programmatically or split it into the Java configuration method and the application.properties file, we should always get a valid database connection.

Run sample application

Finally, we can run our demo application using the standard main () method:


@SpringBootApplication
public class Application {
 
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
 
  @Bean
  public CommandLineRunner run(UserRepository userRepository) throws Exception {
    return (String[] args) -> {
      User user1 = new User("John", "john@domain.com");
      User user2 = new User("Julie", "julie@domain.com");
      userRepository.save(user1);
      userRepository.save(user2);
      userRepository.findAll().forEach(user -> System.out.println(user);
    };
  }
}

We have tested the repository layer, so we are sure our DataSource has been successfully configured.Therefore, if we run the sample application, we should see a list of User entities stored in the database in the console output.


Related articles: