SKM IT World

Just another blog about IT


Leave a comment

Using Testcontainers in Spring Boot Tests For Database Integration Tests

In this blog post I’d like to demonstrate how I integrate Testcontainers in Spring Boot tests for running integration tests with a database. I’m not using Testcontainers’ Spring Boot modules. How it works with them, I will show in a separate blog post. All samples can be found on GitHub.

Why Testcontainers?

Testcontainers is a library that helps to integrate infrastructure components like database in integration tests based on Docker Container. It helps to avoid writing integrated tests. These are kind of tests that will pass or fail based on the correctness of another system. With Testcontainers I have the control over these dependent systems.

Introducing the domain

The further samples shows different approach how to save some hero objects through different repository implementations in a database and how the corresponding tests could look like.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;

public class Hero {
    private Long id;
    private String name;
    private String city;
    private ComicUniversum universum;

    public Hero(String name, String city, ComicUniversum universum) {
        this.name = name;
        this.city = city;
        this.universum = universum;
    }

    public String getName() {
        return name;
    }

    public String getCity() {
        return city;
    }

    public ComicUniversum getUniversum() {
        return universum;
    }
}

All further repositories are parts of a Spring Boot web application. So at the end of this blog post I will demonstrate how to write a test for the whole web application including a database. Let’s start with an easy sample, a repository based on JDBC.

Testing Repository Based on JDBC

Assume we have following repository implementation based on JDBC. We have two methods, one for adding a hero into the database and one for getting all heroes from the database.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.Collection;

@Repository
public class HeroClassicJDBCRepository {

    private final JdbcTemplate jdbcTemplate;

    public HeroClassicJDBCRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void addHero(Hero hero) {
        jdbcTemplate.update("insert into hero (city, name, universum) values (?,?,?)",
                hero.getCity(), hero.getName(), hero.getUniversum().name());

    }

    public Collection<Hero> allHeros() {
        return jdbcTemplate.query("select * From hero",
                (resultSet, i) -> new Hero(resultSet.getString("name"),
                                            resultSet.getString("city"),
                                            ComicUniversum.valueOf(resultSet.getString("universum"))));
    }

}

For this repository, we can write a normal JUnit5 tests without Spring application context loading. So first at all, we have to set up the dependencies to the test libraries, in this case, JUnit5 and Testcontainers. As build tool, I use Maven. Both test libraries provide so called BOM “bill of material”, that helps to avoid a version mismatch in my used dependencies. As database, I want to use MySQL. Therefore, I use the Testcontainers’ module mysql additional to the core module testcontainers. It provides a predefined MySQL container. For simplifying the container setup specifically in JUnit5 test code, Testcontainers provides a JUnit5 module junit-jupiter.

    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>${junit.jupiter.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.testcontainers</groupId>
                <artifactId>testcontainers-bom</artifactId>
                <version>${testcontainers.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Now, we have everything to write the first test.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import ...

@Testcontainers
class HeroClassicJDBCRepositoryIT {
    @Container
    private MySQLContainer database = new MySQLContainer();

    private HeroClassicJDBCRepository repositoryUnderTest;

    @Test
    void testInteractionWithDatabase() {
        ScriptUtils.runInitScript(new JdbcDatabaseDelegate(database, ""),"ddl.sql");
        repositoryUnderTest = new HeroClassicJDBCRepository(dataSource());

        repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));

        Collection<Hero> heroes = repositoryUnderTest.allHeros();

        assertThat(heroes).hasSize(1);
    }

    @NotNull
    private DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl(database.getJdbcUrl());
        dataSource.setUser(database.getUsername());
        dataSource.setPassword(database.getPassword());
        return dataSource;
    }
}

Let’s have a look how the database is prepared for the test. Firstly, we annotate the test class with @Testcontainers. Behind this annotation hides a JUnit5 extension provided by Testcontainers. It checks if Docker is installed on the machine, starts and stops the container during the test. But how Testcontainers knows which container it should start? Here, the annotation @Container helps. It marks container that should manage by the Testcontainers extension. In this case, a MySQLContainer provided by Testcontainers module mysql. This class provides a MySQL Docker container and handles such things like setting up database user, recognizing when the database is ready to use etc. As soon as the database is ready to use, the database schema has to be set up. Testcontainers can also provide support here. ScriptUtils.runInitScript(new JdbcDatabaseDelegate(database, ""),"ddl.sql"); ensure that the schema is set up like it defines in SQL script ddl.sql.

-- ddl.sql
create table hero (id bigint AUTO_INCREMENT PRIMARY KEY, city varchar(255), name varchar(255), universum varchar(255)) engine=InnoDB

Now we are ready to set up our repository under test. Therefore, we need the database connection information for the DataSource object. Under the hood, Testcontainers searches after an available port and bind the container on this free port. This port number is different on every container start via Testcontainers. Furthermore, it configures the database in container with a user and password. Therefore, we have to ask the MySQLContainer object how the database credentials and the JDBC URL are. With this information, we can set up the repository under test (repositoryUnderTest = new HeroClassicJDBCRepository(dataSource());) and finish the test.

If you run the test and you get the following error message:

17:18:50.990 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@1adc57a8
17:18:51.492 [ducttape-1] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.493 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@3e5b3a3b
17:18:51.838 [main] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause LastErrorException ([111] Verbindungsaufbau abgelehnt)
17:18:51.851 [main] DEBUG org.rnorth.tcpunixsocketproxy.ProxyPump - Listening on localhost/127.0.0.1:41039 and proxying to /var/run/docker.sock
17:18:51.996 [ducttape-0] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.997 [ducttape-1] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.997 [ducttape-0] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@5d43d23e
17:18:51.997 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@7abf08d2
17:18:52.002 [tcp-unix-proxy-accept-thread] DEBUG org.rnorth.tcpunixsocketproxy.ProxyPump - Accepting incoming connection from /127.0.0.1:41998
17:19:01.866 [main] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - ProxiedUnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null)
17:19:01.870 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
17:19:01.872 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed)
17:19:01.873 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed)
17:19:01.874 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause LastErrorException ([111] Verbindungsaufbau abgelehnt)
17:19:01.875 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     ProxiedUnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null)
17:19:01.875 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - As no valid configuration was found, execution cannot continue
17:19:01.900 [main] DEBUG ūüź≥ [mysql:5.7.22] - mysql:5.7.22 is not in image name cache, updating...
Mai 01, 2020 5:19:01 NACHM. org.junit.jupiter.engine.execution.JupiterEngineExecutionContext close
SEVERE: Caught exception while closing extension context: org.junit.jupiter.engine.descriptor.MethodExtensionContext@2e6a5539
org.testcontainers.containers.ContainerLaunchException: Container startup failed
	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:322)
	at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:302)
	at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.start(TestcontainersExtension.java:173)
	at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.access$100(TestcontainersExtension.java:160)
	at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$null$3(TestcontainersExtension.java:50)
	at org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$0(ExtensionValuesStore.java:81)
	at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:182)
	at org.junit.jupiter.engine.execution.ExtensionValuesStore.closeAllStoredCloseableValues(ExtensionValuesStore.java:58)
	at org.junit.jupiter.engine.descriptor.AbstractExtensionContext.close(AbstractExtensionContext.java:73)
	at org.junit.jupiter.engine.execution.JupiterEngineExecutionContext.close(JupiterEngineExecutionContext.java:53)
	at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.cleanUp(JupiterTestDescriptor.java:222)
	at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.cleanUp(JupiterTestDescriptor.java:57)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$cleanUp$9(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.cleanUp(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:83)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
	at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
	at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.testcontainers.containers.ContainerFetchException: Can't get Docker image: RemoteDockerImage(imageNameFuture=java.util.concurrent.CompletableFuture@539d019[Completed normally], imagePullPolicy=DefaultPullPolicy(), dockerClient=LazyDockerClient.INSTANCE)
	at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1265)
	at org.testcontainers.containers.GenericContainer.logger(GenericContainer.java:600)
	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:311)
	... 47 more
Caused by: java.lang.IllegalStateException: Previous attempts to find a Docker environment failed. Will not retry. Please see logs and check configuration
	at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:78)
	at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:115)
	at org.testcontainers.LazyDockerClient.getDockerClient(LazyDockerClient.java:14)
	at org.testcontainers.LazyDockerClient.inspectImageCmd(LazyDockerClient.java:12)
	at org.testcontainers.images.LocalImagesCache.refreshCache(LocalImagesCache.java:42)
	at org.testcontainers.images.AbstractImagePullPolicy.shouldPull(AbstractImagePullPolicy.java:24)
	at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:62)
	at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:25)
	at org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20)
	at org.testcontainers.utility.LazyFuture.get(LazyFuture.java:27)
	at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1263)
	... 49 more



org.testcontainers.containers.ContainerLaunchException: Container startup failed

This error message means that the Docker daemon is not running. After ensuring that Docker daemon is running, the test run is successful.

There are very many debug messages in the console output. The logging output in tests can be configured by a logback.xml file in src/test/resources:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <root level="info">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

Spring Boot documentation about logging recommends to use logback-spring.xml as configuration file. But normal JUnit5 tests don’t recognize it, only @SpringBootTest annotated tests. logback.xml is used by both kind of tests.

Testing Repository based on JPA Entity Manager

Now, we want to implement a repository based on JPA with a classic entity manager. Assume, we have following implementation with three methods, adding heroes to the database, finding heroes by search criteria and getting all heroes from the database. The entity manager is configured by Spring’s application context (@PersistenceContext is responsible for that).

package com.github.sparsick.testcontainerspringboot.hero.universum;

import ...

@Repository
public class HeroClassicJpaRepository {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void addHero(Hero hero) {
        em.persist(hero);
    }

    public Collection<Hero> allHeros() {
        return em.createQuery("Select hero FROM Hero hero", Hero.class).getResultList();
    }

    public Collection<Hero> findHerosBySearchCriteria(String searchCriteria) {
        return em.createQuery("SELECT hero FROM Hero hero " +
                        "where hero.city LIKE :searchCriteria OR " +
                        "hero.name LIKE :searchCriteria OR " +
                        "hero.universum = :searchCriteria",
                Hero.class)
                .setParameter("searchCriteria", searchCriteria).getResultList();
    }

}

As JPA implementation, we choose Hibernate and MySQL as database provider. We have to configure which dialect should Hibernate use.

# src/main/resources/application.properties
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

In application.properties you also configure the database connection etc.

For setting up the entity manager in a test correctly, we have to run the test with an application context, so that entity manager is configured correctly by Spring.

Spring Boot brings some test support classes. Therefore, we have to add a further test dependency to the project.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

This starter also includes JUnit Jupiter dependency and dependencies from other test library, so you can remove these dependencies from your dependency declaration if you want to.

Now, we have everything for writing the test.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import ...

@SpringBootTest
@Testcontainers
class HeroClassicJpaRepositoryIT {
    @Container
    private static MySQLContainer database = new MySQLContainer();

    @Autowired
    private HeroClassicJpaRepository repositoryUnderTest;

    @Test
    void findHeroByCriteria(){
        repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));

        Collection<Hero> heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");

        assertThat(heros).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
    }
   
    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}

The test class is annotated with some annotations. The first one is @SpringBootTest thereby the Spring application context is started during the test. The last one is @Testcontainers . This annotation we already know from the last test. It is a JUnit5 extension that manage starting and stopping the docker container during the test. Like we see at above JDBC test, we annotate database container private static MySQLContainer database = new MySQLContainer(); with @Container. It marks that this container should be managed by Testcontainers. Here is a little difference to above JDBC set up. Here, MySQLContainer database is static and in the JDBC set up it is a normal class field. Here, it has to be static because the container has to start before the application context starts, so that we have a change to pass the database connection configuration to the application context. For this, static method databaseProperties is responsible. Here, it is important that this method is annotated by @DynamicPropertySource. It overrides the application context configuration during the start phase. In our case, we want to override the database connection configuration with the database information that we get from database container object managed by Testcontainers. The last step is to set up the database schema in the database. Here JPA can help. It can create a database schema automatically. You have to configure it with

# src/test/resources/application.properties
spring.jpa.hibernate.ddl-auto=update

Now, we can inject the repository into the test (@Autowired private HeroClassicJpaRepository repositoryUnderTest). This repository is configured by Spring and ready to test.

Testing Repository based on Spring Data JPA

Today, it is common in a Spring Boot application to use JPA in combination with Spring Data, so we rewrite our repository to use Spring Data JPA instead of plain JPA. The result is an interface that extends Spring Data’s CrudRepository, so we have all basic operation like save, delete, update find by id etc. . For searching by criteria functionality, we have to define a method with @Query annotation that have a JPA query.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface HeroSpringDataJpaRepository extends CrudRepository<Hero, Long> {

    @Query("SELECT hero FROM Hero hero where hero.city LIKE :searchCriteria OR hero.name LIKE :searchCriteria OR hero.universum = :searchCriteria")
    List<Hero> findHerosBySearchCriteria(@Param("searchCriteria") String searchCriteria);
}

As mentioned above in classic JPA sample so also here, we have to configure which SQL dialect our chosen JPA implementation Hibernate should use and how the database schema should set up.

The same with the test configuration, again we need a test with a Spring application context to configure the repository correctly for the test. But here we don’t need to start the whole application context with @SpringBootTest. Instead, we use @DataJpaTest. This annotation starts an application context only with beans that are needed for the persistence layer.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import ...

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class HeroSpringDataJpaRepositoryIT {
    @Container
    private static MySQLContainer database = new MySQLContainer();

    @Autowired
    private HeroSpringDataJpaRepository repositoryUnderTest;

    @Test
    void findHerosBySearchCriteria() {
        repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));

        Collection<Hero> heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");

        assertThat(heros).hasSize(1).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
    }

    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url",database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}

@DataJpaTest starts an in-memory database as default. But we want that a containerized database is used, provided by Testcontainers. Therefore, we have to add the annotation @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE). This disables starting an in-memory database. The remaining test configuration is the same as the configuration in above test for the plain JPA example.

Testing Repositories but Reusing a database

With the increasing number of tests, it becomes more and more important that each test takes quite a long time, because each time a new database is started and initialized. One idea is to reuse the database in each test. Here the Single Container Pattern can help. A database is started and initialized once before all tests start running. For that, each test that need a database has to extend an abstract class, that is responsible for starting and initializing a database once before all tests run.

package com.github.sparsick.testcontainerspringboot.hero.universum;

import ...


public abstract class DatabaseBaseTest {
    static final MySQLContainer DATABASE = new MySQLContainer();

    static {
        DATABASE.start();
    }

    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", DATABASE::getJdbcUrl);
        registry.add("spring.datasource.username", DATABASE::getUsername);
        registry.add("spring.datasource.password", DATABASE::getPassword);
    }
}

In this abstract class we configure the database that is started once for all tests that extend this abstract class and the application context with that database. Please note, that we don’t use Testcontainers’ annotations here, because this annotation takes care that the container is started and stopped after each test. But this we would avoid. Therefore, we start the database by ourselves. For stopping database we don’t need to take care. For this Testcontainers’ side-car container ryuk takes care.

Now, each test class, that need a database, extends this abstract class. The only thing, that we have to configure, is how the application context should be initialized. That means, when you need the whole application context then use @SpringBootTest. When you need only persistence layer then use @DataJpaTest with @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE).

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class HeroSpringDataJpaRepositoryReuseDatabaseIT extends DatabaseBaseTest {

    @Autowired
    private HeroSpringDataJpaRepository repositoryUnderTest;

    @Test
    void findHerosBySearchCriteria() {
        repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));

        Collection<Hero> heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");

        assertThat(heros).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
    }
}

Testing the whole Web Application including Database

Now we want to test our whole application, from controller to database. The controller implementation looks like this:

@RestController
public class HeroRestController {

    private final HeroSpringDataJpaRepository heroRepository;

    public HeroRestController(HeroSpringDataJpaRepository heroRepository) {
        this.heroRepository = heroRepository;
    }

    @GetMapping("heros")
    public Iterable<Hero> allHeros(String searchCriteria) {
        if (searchCriteria == null || searchCriteria.equals("")) {
            return heroRepository.findAll();

        }
        return heroRepository.findHerosBySearchCriteria(searchCriteria);
    }

    @PostMapping("hero")
    public void hero(@RequestBody Hero hero) {
        heroRepository.save(hero);
    }
}

The test class that test the whole way from database to controller looks like that

@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class HeroRestControllerIT {

    @Container
    private static MySQLContainer database = new MySQLContainer();

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private HeroSpringDataJpaRepository heroRepository;

    @Test
    void allHeros() throws Exception {
        heroRepository.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
        heroRepository.save(new Hero("Superman", "Metropolis", ComicUniversum.DC_COMICS));

        mockMvc.perform(get("/heros"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[*].name", containsInAnyOrder("Batman", "Superman")));
    }

    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}

The test set up for the database and the application is known by the test from the above sections. One thing is different. We add MockMVC support with @AutoConfigureMockMvc. This helps to write tests through the HTTP layer.

Of course, you can also use the single container pattern in which the abstract class DatabaseBaseTest is extended.

Conclusion and Overview

This blog post shows how we can write tests for some persistence layer implementations in Spring Boot with Testcontainers. We also see how to reuse database instance for several tests and how to write test for the whole web application from controller tor database. All code snippet can be found on GitHub. In a further blog post I will show how to write test with Testcontainers Spring Boot modules.

Do you have other ideas for writing tests for persistence layer? Please let me know and write a comment.

Further Information

  1. Concept of BOM “bill of material”
  2. Testcontainers
  3. Spring Boot Documentation – Logging
  4. Spring Boot Documentation – Auto-configured Data JPA Tests
  5. Testcontainers – Single Container Pattern
  6. Spring Boot Documentation – MockMVC
  7. Full example in GitHub repository


Leave a comment

Strategy Pattern Revisited With Spring

This blog post wants to show another approach how to implement the Strategy Pattern with dependency injection. As DI framework, I choose Spring framework

From Wikipedia

Firstly, let’s have a look how the Strategy Pattern is implemented in the classic way.
As starting point, we have a HeroController that should add a hero in HeroRepository depends on which repository was chosen by the user.

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class HeroControllerClassicWay {

    @PostMapping("/hero/new")
    public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
        HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
        heroRepository.addHero(newHeroModel.getHero());
        return "redirect:/hero";
    }

    private HeroRepository findHeroRepository(String repositoryName) {
        if (repositoryName.equals("Unique")) {
            return new UniqueHeroRepository();
        }

        if(repositoryName.equals(("Duplicate")){
            return new DuplicateHeroRepository();
        }

        throw new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName));
    }
}
package com.github.sparsick.springbootexample.hero.universum;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Repository;

@Repository
public class UniqueHeroRepository implements HeroRepository {

    private Set<Hero> heroes = new HashSet<>();

    @Override
    public String getName() {
        return "Unique";
    }

    @Override
    public void addHero(Hero hero) {
        heroes.add(hero);
    }

    @Override
    public Collection<Hero> allHeros() {
        return new HashSet<>(heroes);
    }

}
package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Repository
public class DuplicateHeroRepository implements HeroRepository {

    private List<Hero> heroes = new ArrayList<>();

    @Override
    public void addHero(Hero hero) {
        heroes.add(hero);
    }

    @Override
    public Collection<Hero> allHeros() {
        return List.copyOf(heroes);
    }

    @Override
    public String getName() {
        return "Duplicate";
    }
}

This implementation has some pitfalls. The creation of the repository implementations aren’t managed by the Spring Context (it breaks the dependency injection / inverse of control). This will be painful as soon as you want to expand the repository implementation with further feature that need to inject other classes (for example, counting the usage of this class with MeterRegistry).

package com.github.sparsick.springbootexample.hero.universum;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Repository;

@Repository
public class UniqueHeroRepository implements HeroRepository {

    private Set<Hero> heroes = new HashSet<>();
    private Counter addCounter;

    public UniqueHeroRepository(MeterRegistry meterRegistry) {
        addCounter = meterRegistry.counter("hero.repository.unique");
    }

    @Override
    public String getName() {
        return "Unique";
    }

    @Override
    public void addHero(Hero hero) {
        addCounter.increment();
        heroes.add(hero);
    }

    @Override
    public Collection<Hero> allHeros() {
        return new HashSet<>(heroes);
    }

}

It breaks also the separation of concern. When I want to test the controller class, I have no possibility to mock the repository interface easily. So the first idea is to put the creation of repository implementation to the Spring context. The repository implementation are annotated with @Repository annotation. So Spring’s component scan find them.
The next question how to inject them into the controller class. Here, a Spring feature can help. I define a list of HeroRepository in the controller. This list has to be filled during the creation of the controller instance.

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class HeroControllerRefactoringStep1 {

    private List<HeroRepository> heroRepositories;

    public HeroControllerRefactoringStep1(List<HeroRepository> heroRepositories) {
        this.heroRepositories = heroRepositories;
    }

    @PostMapping("/hero/new")
    public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
        HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
        heroRepository.addHero(newHeroModel.getHero());
        return "redirect:/hero";
    }

    private HeroRepository findHeroRepository(String repositoryName) {
        return heroRepositories.stream()
                .filter(heroRepository -> heroRepository.getName().equals(repositoryName))
                .findFirst()
                .orElseThrow(()-> new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName)));

    }
}

Spring searches in its context for all implementation of the interface HeroRepostiory and put them all to the list. One disadvantage has this solution, every adding a hero browses the list of HeroRepository to find the right implementation. This can be optimized by creating a map in the controller constructor that has the repository name as key and the corresponded implementation as value.

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class HeroControllerRefactoringStep2 {

    private Map<String, HeroRepository> heroRepositories;

    public HeroControllerRefactoringStep2(List<HeroRepository> heroRepositories) {
        this.heroRepositories = heroRepositoryStrategies(heroRepositories);
    }

    private Map<String, HeroRepository> heroRepositoryStrategies(List<HeroRepository> heroRepositories){
        Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>();
        heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository));
        return heroRepositoryStrategies;
    }

    @PostMapping("/hero/new")
    public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
        HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
        heroRepository.addHero(newHeroModel.getHero());
        return "redirect:/hero";
    }

    private HeroRepository findHeroRepository(String repositoryName) {
        HeroRepository heroRepository = heroRepositories.get(repositoryName);
        if(heroRepository != null) {
            return  heroRepository;
        }
        throw new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName));
    }
}

The final question is what if other classes in the application need the possibility to choose a repository implementation during the runtime. I could copy and paste the private method in each class that have this need or I move the creation of the map to the Spring Context and inject the Map to each class.

package com.github.sparsick.springbootexample.hero;

import com.github.sparsick.springbootexample.hero.universum.HeroRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SpringBootApplication
public class HeroApplicationRefactoringStep3 {

	public static void main(String[] args) {
		SpringApplication.run(HeroApplication.class, args);
	}

	@Bean
    Map<String, HeroRepository> heroRepositoryStrategy(List<HeroRepository> heroRepositories){
        Map<String, HeroRepository> heroRepositoryStrategy = new HashMap<>();
        heroRepositories.forEach(heroRepository -> heroRepositoryStrategy.put(heroRepository.getName(), heroRepository));
        return heroRepositoryStrategy;
    }
}

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.Map;

@Controller
public class HeroControllerRefactoringStep3 {

    private Map<String, HeroRepository> heroRepositoryStrategy;

    public HeroControllerRefactoringStep3(Map<String, HeroRepository> heroRepositoryStrategy) {
        this.heroRepositoryStrategy = heroRepositoryStrategy;
    }

    @PostMapping("/hero/new")
    public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
        HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
        heroRepository.addHero(newHeroModel.getHero());
        return "redirect:/hero";
    }

    private HeroRepository findHeroRepository(String repositoryName) {
        return heroRepositoryStrategy.get(repositoryName);
    }

}

This solution is a little bit ugly, because it isn’t obvious that the Strategy Pattern is used. So the next refactoring step is moving the map of hero repositories to an own component class. Therefore, the bean definition heroRepositoryStrategy in the application configuration can be removed.

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component
public class HeroRepositoryStrategy {

    private Map<String, HeroRepository> heroRepositoryStrategies;

    public HeroRepositoryStrategy(Set<HeroRepository> heroRepositories) {
        heroRepositoryStrategies = createStrategies(heroRepositories);
    }

    HeroRepository findHeroRepository(String repositoryName) {
        return heroRepositoryStrategies.get(repositoryName);
    }

    Set<String> findAllHeroRepositoryStrategyNames () {
        return heroRepositoryStrategies.keySet();
    }

    Collection<HeroRepository> findAllHeroRepositories(){
        return heroRepositoryStrategies.values();
    }


    private Map<String, HeroRepository> createStrategies(Set<HeroRepository> heroRepositories){
        Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>();
        heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository));
        return heroRepositoryStrategies;
    }

}

package com.github.sparsick.springbootexample.hero.universum;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Controller
public class HeroController {

    private HeroRepositoryStrategy heroRepositoryStrategy;

    public HeroController(HeroRepositoryStrategy heroRepositoryStrategy) {
        this.heroRepositoryStrategy = heroRepositoryStrategy;
    }

    @PostMapping("/hero/new")
    public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
        HeroRepository heroRepository = heroRepositoryStrategy.findHeroRepository(newHeroModel.getRepository());
        heroRepository.addHero(newHeroModel.getHero());
        return "redirect:/hero";
    }
}

The whole sample is hosted on GitHub.


Leave a comment

Configuration over JNDI in Spring Framework

From a certain point on, an application has to be configurable.¬† Spring Framework has a nice auxiliary tool for this issue since the first version 0.9 ,¬†the class PropertyPlaceholderConfigurer and since Spring Framework 3.1 the class PropertySourcesPlaceholderConfigurer. When you start a Google search for PropertyPlaceholderConfigurer, you will find many examples where the configuration items are saved in properties files. But in many Java enterprise applications, it is common that the configuration items are loaded over JNDI look ups. I’d like to demonstrate how the PropertyPlaceholderConfigurer (before Spring Framework 3.1) and accordingly PropertySourcesPlaceholderConfigurer (since Spring Framework 3.1) can help to ease the configuration over JNDI look ups in our application.

Initial Situation

We have an web application that has a connection to a database. This database connection has to be configurable. The configuration items are defined in a web application context file.


<Context docBase="/opt/tomcat/warfiles/jndi-sample-war.war" antiResourceLocking="true">
  <Environment name="username" value="demo" type="java.lang.String" override="false"/>
  <Environment name="password" value="demo" type="java.lang.String" override="false"/>
  <Environment name="url" value="jdbc:mysql://192.168.56.101:3306/wicket_demo" type="java.lang.String" override="false"/>
</Context> 

For loading these configuration items, the JNDI look up mechanism is used.

In our application we define a data source bean in a  Spring context XML file. This bean represents the database connection.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="url" value="${url}" />
        <property name="username" value="${username}" />
        <property name="password" value="${password}" />
  <bean> 
</beans> 

Every value that starts and ends with ${} should be replaced by PropertyPlaceholderConfigurer and accordingly PropertySourcesPlaceholderConfigurer at the time when launching the application. The next step is to set up PropertyPlaceholderConfigurer and accordingly PropertySourcesPlaceholderConfigurer.

Before Spring Framework 3.1 – PropertyPlaceholderConfigurer Set Up for JNDI Look Up

We define a PropertyPlaceholderConfigurer  bean  in a Spring context XML file. This bean contains to an inner bean that maps the property names of the data source bean to the corresponding JNDI name. The JNDI name consists of two parts. The first part is the name of the context in which the resource is (in our case java:comp/env/) and the second part is the name of the resource (in our case either username, password or url).

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="properties">
        <bean class="java.util.Properties">
            <constructor-arg>
                <map>
                    <entry key="username">
                        <jee:jndi-lookup jndi-name="java:comp/env/username" />
                    </entry>
                    <entry key="password">
                        <jee:jndi-lookup jndi-name="java:comp/env/password" />
                    </entry>
                    <entry key="url">
                        <jee:jndi-lookup jndi-name="java:comp/env/url" />
                    </entry>
                </map>
            </constructor-arg>
        </bean>
    </property>
</bean>

Since Spring Framework 3.1 – PropertySourcesPlaceholderConfigurer Set Up for JNDI Look Up

Since Spring 3.1 PropertySourcesPlaceholderConfigurer should be used instead of PropertyPlaceholderConfigurer. This effects that since Spring 3.1 the <context:property-placeholder/> namespace element registers an instance of PropertySourcesPlaceholderConfigurer (the namespace definition must be spring-context-3.1.xsd) instead of PropertyPlaceholderConfigurer (you can simulate the old behaviour when you use the namespace definition spring-context-3.0.xsd). So our Spring XML context configuration is very short, when you comply some convention (based on the principle Convention over Configuration).

<context:property-placeholder/>

The default  behavior is that the PropertySourcesPlaceholderConfigurer iterates through a set of PropertySource to collect all properties values. This set contains JndiPropertySource per default in a Spring based web application. By default, JndiPropertySource looks up after JNDI resource names prefixed with java:comp/env. This means if your property is ${url}, the corresponding JNDI resource name has to be java:comp/env/url.

The source code of the sample  web application is hosted on GitHub.


5 Comments

Spring Web Application With Hessian Services As a Maven Project

This post describes how to set up a Maven project for a Spring web application with Hessian Service. It also shows how to set up the deployment for exposing the Hessian service and how to set up a client to consume the Hessian service. The Spring Framework version is 3.1.1.RELEASE and the Hessian version is 4.0.7.

Maven Set Up For Server

Our Maven project has three modules

  • hello-world-api
  • hello-world-impl
  • hello-word-war

<modules>
  <module>hello-world-api</module>
  <module>hello-world-impl</module>
  <module>hello-world-war</module>
</modules>

The module hello-world-api contains the interfaces of the services that Hessian server and Hessian client need for the communication. The module hello-world-impl contains the implementation of the services that are deployed on the server side. The module hello-world-war contains the configuration for the servlet container and the configuration which services should be exported as a  Hessian service.

The Service Interface Definition

The module hello-world-api should become a JAR artifact so the packaging jar for this Maven module:


<?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 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.github.skosmalla.spring.hessian</groupId>
    <artifactId>hello-world-spring-hessian</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>hello-world-api</artifactId>

  <name>Hello World Api</name>

</project>

The jar contains the interfaces of the services. An example


package com.github.skosmalla.hello.world.spring.hessian;

public interface HelloWorld {

  public String welcome();

}

The Service Implementation

The module hello-world-impl also becomes a JAR artifact. This jar contains the service implementation for the server. The service implementation could look like following code:


package com.github.skosmalla.hello.world.spring.hessian;

public class HelloWorldImpl implements HelloWorld {

  @Override
  public String welcome() {
    return "Hello World";
  }

}

Thereby this implementation can be used as a Hessian service, it has to be defined as a Spring bean. Therefore we need a Spring configuration file hello-world-service-config.xml. The location for this file is src/main/resources/META-INF/spring. The bean configuration looks like the following code:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="helloWorldService" class="com.github.skosmalla.hello.world.spring.hessian.HelloWorldImpl"/>

</beans>

In this module we have two dependencies, one to the API module and one to the spring-beans module.


<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
  </dependency>
  <dependency>
    <groupId>com.github.skosmalla.spring.hessian</groupId>
    <artifactId>hello-world-api</artifactId>
  </dependency>
</dependencies>

The WAR Deployment

The module hello-world-war  describes the configuration for the server deployment. The artifact of this module becomes a WAR file. Therefore, the packaging of this Maven module is war.


<?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 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.github.skosmalla.spring.hessian</groupId>
    <artifactId>hello-world-spring-hessian</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>hello-world-war</artifactId>

  <name>Hello World WAR </name>

  <packaging>war</packaging>

  <build>
    <defaultGoal>install</defaultGoal>
  </build>
</project>

Now, we have to do two things for running our server application on a servlet container:

  1. Add configuration of Spring application context with our implementation to the servlet container.
  2. Add configuration to dispatch request to our Hessian service.

To create an ApplicationContext instance in a web application, we have to configure an ContextLoaderListener in our Web Application Deployment Descriptor (location: src/main/webapp/WEB-INF/web.xml):

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:META-INF/spring/*.xml</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

The ContextLoaderListener builds the root application context from all Spring configuration files located in the classpath under META-INF/spring (That pattern matches our hello-world-service-config.xml).  But no request can be processed. Therefore we have to configure a servlet that dispatch the request to the service. Here, the Spring Framework supports us with a DispatcherServlet. To use it we have to add that servlet to our Web Application Deployment Descriptor (location: src/main/webapp/WEB-INF/web.xml):

<servlet>
  <servlet-name>hessian</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>hessian</servlet-name>
  <url-pattern>/hessian/*</url-pattern>
</servlet-mapping>

That configuration means that servlet is named as hessian and it is responsible for all request to the URL http://<URL to the Tomcat instanz>/<webapp-context>/hessian/* . Now, we have to configure the Hessian service interface that are dispatched by the servlet hessian. For it, we have to add a Spring configuration file in the same location like Web Application Deployment Descriptor (src/main/webapp/WEB-INF). The name of that file have to be hessian-servlet.xml (the pattern is <servlet name>-servlet.xml). Here, we configure the Hessian service interface:


<bean name="/HelloWorldService" class="org.springframework.remoting.caucho.HessianServiceExporter">
  <property name="service" ref="helloWorldService" />
  <property name="serviceInterface" value="com.github.skosmalla.hello.world.spring.hessian.HelloWorld" />
</bean>

That Spring configuration file defines a new application context. It is child application context of the root application context, loaded by the ContextLoaderListener. A child application context can see every bean of the root application context, but the root application context cannot see beans of its child application context (for more information, have look at the Spring Framework reference). The HessianServiceExporter has a reference to the service implementation, defined in the root application context.
The URL of that Hessian service interface is http://<URL to the Tomcat instanz>/<webapp-context>/hessian/HelloWorldService (Pattern is http://<URL to the Tomcat instanz>/<webapp-context>/hessian/<bean name of the HessianServiceExporter>).

So that these configurations can run in a servlet container, we have to add the dependencies, that contains HessianServiceExporter, DispatcherServlet and ContextLoaderListener, to the pom.xml :


<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
  <groupId>com.github.skosmalla.spring.hessian</groupId>
  <artifactId>hello-world-impl</artifactId>
</dependency>
<dependency>
  <groupId>com.caucho</groupId>
  <artifactId>hessian</artifactId>
</dependency>

The Hessian dependency is needed at runtime and the hello-world-impl contains our business logic and the spring configuration file for the root application context.

With a mvn clean install Maven builds a WAR file in the project’s target folder. This WAR file can be deployed on a Tomcat.

The Hessian Test Client With Spring

Now we write a client to test the Hessian service. Therefore, we create a new Maven module.


<?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 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.github.skosmalla.spring.hessian</groupId>
    <artifactId>hello-world-spring-hessian</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>hello-world-client</artifactId>

  <name>Hello World Client</name>

</project>

Spring Framework offers a HessianProxyFactoryBean for calling the remote HelloWorld service. The configuration for this HessianProxyFactoryBean could look like the following code snippet:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="helloWorldService"
  class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
  <property name="serviceUrl"
    value="http:/localhost:8080/hello-world/hessian/HelloWorldService" />
  <property name="serviceInterface"
    value="com.github.skosmalla.hello.world.spring.hessian.HelloWorld" />
</bean>
</beans>

In the property serviceInterface we define the interface of the Hessian service, here com.github.skosmalla.hello.world.spring.hessian.HelloWorld. In the property serviceUrl we define the URL to the Hessian Service deployed on the Tomcat. In our sample the Tomcat is on localhost with port number 8080 and the web application is hello-world.

Now, this factory bean creates Hessian service proxy for us:


package com.github.skosmalla.hello.world.spring.hessian;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HessianClient {

  public static void main(String[] args) {
    ApplicationContext appContext = new ClassPathXmlApplicationContext("META-INF/spring/hessian-config.xml");

    HelloWorld service = (HelloWorld) appContext.getBean("helloWorldService");

    String welcomeMessage = service.welcome();

    System.out.println(welcomeMessage);

  }
}

The dependencies for the client are the following one:

<dependencies>
  <dependency>
    <groupId>com.github.skosmalla.spring.hessian</groupId>
    <artifactId>hello-world-api</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
  </dependency>
  <dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

If we start this client we will get “Hello World” on our command line.

Now, we have seen a full example how to set up a Hessian service in a Spring web application and how to call such a service remotely. The full code you can find on Github.

Links


Leave a comment

Read Classpath Resource from Jar Files with Spring

To read resources from classpath, you can use the Spring class ClassPathResource. The constructor with one argument reads only resources from file system.  But sometimes you want to read a resources from a jar file in the classpath.  For this case, you must build an own URLClassLoader from the URL of the jar file and use the ClassPathResource constructor with the arguments path and classLoader.  The path value is the path of the resource in the jar file.

An Example

We have a jar file, named example.jar. The content looks as follows:

example.jar
|_ META-INF
   |_ sample-resource.txt

We want to read the sample-resource.txt.  The source code for this example looks as follows:

URL jarUrl = new File("path_to_jar_file").toURI().toURL();
URLClassLoader jarLoader = new URLClassLoader(new URL[]{jarUrl}, Thread.currentThread().getContextClassLoader());
ClassPathResource sampleResource = new ClassPathResource("META-INF/sample-resource.txt",jarLoader);