Fair warning, before I begin, this is a rant.

I spent the last two weekends trying to put together a project using Spring Boot, JDBI, Postgres, and Liquibase. I spent the entire time fighting with the tooling. Spring, for all its magic, provides no feedback, no guidance to the developer attempting to use it.

Ergonomics matter. And Spring simply is not ergonomic.

Let me offer a taste of what I tried doing. I fetched a springboot-starter archetype and wound up with a pom.xml file as follows:

<?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/xsd/maven-4.0.0.xsd">  
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.experiment.springboot</groupId>
    <artifactId>testspringbootproj</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot Blank Project (from https://github.com/making/spring-boot-blank)</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.experiment.springboot.App</start-class>
        <java.version>1.8</java.version>
        <lombok.version>1.14.8</lombok.version>
        <log4jdbc.log4j2.version>1.16</log4jdbc.log4j2.version>
        <rest.assured.version>2.3.3</rest.assured.version>
        <jdbi.version>2.78</jdbi.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.jayway.restassured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>${rest.assured.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.bgee.log4jdbc-log4j2</groupId>
            <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
            <version>${log4jdbc.log4j2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.4-1206-jdbc42</version>
        </dependency>
        <dependency>
            <groupId>org.jdbi</groupId>
            <artifactId>jdbi</artifactId>
            <version>${jdbi.version}</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-maven-plugin</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.unitils</groupId>
            <artifactId>unitils-dbunit</artifactId>
            <version>3.4.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>springloaded</artifactId>
                        <version>${spring-loaded.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.liquibase</groupId>
                <artifactId>liquibase-maven-plugin</artifactId>
                <version>3.4.1</version>
                <configuration>
                    <propertyFile>src/main/resources/liquibase.properties</propertyFile>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>  

Seems straightforward but it was not at all. This POM file is the result of hours of googling, experimentation, and ripped out hair. It installs Spring Boot, JDBI, Postgres, and Liquibase, just like I wanted. But how do I make them work together?

I thought all I would have to do is create a local database:

$ psql
$ createdb -h localhost -p 5432 -U postgres springbootdb password *********

and wire it up via Spring's configuration file:

// application.properties
spring.datasource.url= jdbc:postgresql://localhost:5432/springbootdb  
spring.datasource.username=<username>  
spring.datasource.password=<password>  
spring.jpa.hibernate.ddl-auto=create-drop  

Now that the data layer is connected to my application, all I should need to do now is run a migration to create my tables and test models. Right?

Right???

// liquibase migration file
// src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:  
  - changeSet:
      id: 1
      author: dopatraman
      changes:
        - createTable:
            tableName: person
            columns:
              - column:
                  name: id
                  type: int
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: first_name
                  type: varchar(255)
                  constraints:
                    nullable: false
              - column:
                  name: last_name
                  type: varchar(255)
                  constraints:
                    nullable: false
  - changeSet:
      id: 2
      author: dopatraman
      changes:
        - insert:
            tableName: person
            columns:
              - column:
                  name: first_name
                  value: Prakash
              - column:
                  name: last_name
                  value: Venkatraman

Now if I could just run liquibase....

In theory I should be able to do this through Maven. I have a plugin that allows me to. But when I run mvn clean install and then mvn compile I get this goodness:

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'liquibase' defined in class path resource [org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]: Invocation of init method failed; nested exception is liquibase.exception.DatabaseException: org.postgresql.util.PSQLException: FATAL: password authentication failed for user "postgres"  
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1572)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:755)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:759)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:480)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:117)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:689)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:969)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:958)
    at com.experiment.springboot.App.main(App.java:9)

Check out the readability of this stacktrace. Crystal, isn't it? This is what I'm complaining about when I talk about Java's complete lack of ergonomic(ity?). Nothing is straightforward or known. And when something goes wrong...

After hours of searching I find a lonely github project that has a file called liquibase.properties. Maybe that's what I'm missing?

// liquibase.properties
changeLogFile=src/main/resources/db/changelog/db.changelog-master.yaml  
driver = org.postgresql.Driver  
url = jdbc:postgresql://localhost:5432/springbootdb  
username=<username>  
password=<password>  
verbose=true  
dropFirst=false  

Phew! One weekend down the drain, but atleast I'm able to compile. For now.

For this project to be complete, I need to be able to read and write to the database. I wanted to write a test to do this, but first I need a DAO (Data Access Object) to fetch data from and write data to the data base:

package com.experiment.springboot;

import org.skife.jdbi.v2.sqlobject.SqlQuery;  
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;

/**
 * Created by root on 5/13/17.
 */
public interface HelloDao {  
    @SqlQuery(
            "SELECT * FROM person"
    )
    @Mapper(PersonMapper.class)
    Person getPerson();
}

And ofcourse my models:

package com.experiment.springboot;

/**
 * Created by root on 5/13/17.
 */
public class Person {  
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

And... a... uh... mapper?

package com.experiment.springboot;

import org.skife.jdbi.v2.StatementContext;  
import org.skife.jdbi.v2.tweak.ResultSetMapper;

import java.sql.ResultSet;  
import java.sql.SQLException;

/**
 * Created by root on 5/13/17.
 */
public class PersonMapper implements ResultSetMapper<Person> {  
    public Person map(int index, ResultSet r, StatementContext ctx) throws SQLException {
        return new Person(r.getString("first_name"), r.getString("last_name"));
    }
}

And finally, my test:

@Transactional(TransactionMode.ROLLBACK)
@RunWith(UnitilsBlockJUnit4ClassRunner.class)
public class HelloDaoTest {  
    @TestDataSource
    DataSource dataSource;

    private DBI dbi;
    private HelloDao helloDao;

    @Before
    public void setUp() throws Exception {
        dbi = new DBI(dataSource);
        helloDao = dbi.onDemand(HelloDao.class);
    }

    @Test
    @DataSet("HelloDaoTest.testSet.xml")
    public void helloDao() throws Exception {
        Person p = helloDao.getPerson();
        assertEquals("First", p.getFirstName());
    }

}

Look at all those stray annotations. Why do I need to annotate a DataSource object with @TestDataSource? I assume so that Spring can mock it, but why isn't this apparent either in the code or in the definition of the annotation?

Oh well. Now, atleast, we should be good to go...
...

...

NOT!
Spring DI cannot inject an interface into a class, so there needs to be a class that configures a class that implements the interface. Isn't that complete, total, utter common sense??

I hope the sarcasm is palpable.

package com.experiment.springboot;

import org.skife.jdbi.v2.DBI;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;

/**
 * Created by root on 5/13/17.
 */
@Configuration
public class HelloDaoConfig {  
    @Bean
    HelloDao helloDao(DBI dbi) { return dbi.onDemand(HelloDao.class); }
}

and a Bean for the DBI itself. (Why do we need this? ¯\_(ツ)_/¯)

@Autowired
    @Bean
    public DBI dbi(DataSource dataSource) {
        synchronized (DBI.class) {
            return new DBI(dataSource);
        }
    }

^^ I figured out that I needed this after so much pain. Spring DI discovers components during runtime, so they must be annotated with @Autowired. It boggles the mind why a language built for compile-time type checking would rely on runtime class definitions, but this is exactly the kind of backwards overengineering that holds the Java ecosystem, and by extension Java Programmers, back. It was enough pain to make me want to scrap this project and use Django instead. That's right, I was this close to using Django. Thanks, Spring.

Finally, I should be able to run my test. Two weekends, close to 40 hours, lots of black coffee, and my dignity. This is what I have to show for it:

java.lang.NullPointerException  
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:820)
    at org.unitils.database.transaction.impl.DefaultUnitilsTransactionManager.rollback(DefaultUnitilsTransactionManager.java:157)
    at org.unitils.database.DatabaseModule.rollbackTransaction(DatabaseModule.java:425)
    at org.unitils.database.DatabaseModule.endTransactionForTestMethod(DatabaseModule.java:390)
    at org.unitils.database.DatabaseModule$DatabaseTestListener.afterTestTearDown(DatabaseModule.java:486)
    at org.unitils.core.Unitils$UnitilsTestListener.afterTestTearDown(Unitils.java:315)
    at org.unitils.core.junit.AfterTestTearDownStatement.evaluate(AfterTestTearDownStatement.java:48)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.unitils.core.junit.BeforeTestClassStatement.evaluate(BeforeTestClassStatement.java:41)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

After all this effort I arrived at The Billion Dollar Mistake. No explanation, no leads, nothing to do except pull one's hair out. I was tapped out at this point and am still looking for an answer as to what went wrong. The sad part is that this kind of thing is all too common among Java developers. Programmers spend more time fighting the tooling than building software. Its frustrating, time-consuming, and backward.

Isn't it time we make Spring better?