Converting a Java Spring application to Scala

by
Tags: , , ,

A number of people are programming in Scala now. Supposedly Scala requires a completely different programming style. But what happens when you just want a Spring application? Is it possible to program in Scala without jumping in the deep end?
What happens if you take a basic Spring application and convert it to Scala? Let’s try that out, and take a look at the difference between the two.
Let’s start with the Java version.

@RestController
@EnableAutoConfiguration
@ComponentScan
public class Example {
  @RequestMapping("/")
  String home() {
    return
        "

Names

" +
        exampleRepository.getAllNames().toString() +
        "

First Names

" +
        exampleRepository.getAllFirstNames().toString();
  }
  public static void main(String... args) throws Exception {
    SpringApplication.run(Example.class, args);
  }
}

With the exception of setting up some data and autowiring the repository, that’s pretty much it.
Let’s create a Name class for the data and then take a look at the Repository.

public class Name {
  private int id;
  private String lastName;
  private String firstName;
  private Optional middleName;
  private LocalDateTime lastUpdate;
  ...
  // All the get/set methods, plus a constructor and toString
}

We’ll need a RowMapper.

private final RowMapper rowMapper =
    (rs, rowIndex) -> new Name(
      rs.getInt(1),
      rs.getString(2),
      rs.getString(3),
      Optional.ofNullable(rs.getString(4)),
      rs.getLocalDateTime(5)
);

And that does not work. It seems as thought there is no getLocalDateTime method on ResultSet. Hopefully that will be added soon, but for now let’s create a ResultSets helper interface to quickly get past this.

public interface ResultSets {
  default LocalDateTime convertTimestampToLocalDateTime(
      Timestamp timestamp) {
    return LocalDateTime.ofInstant(
        Instant.ofEpochMilli(timestamp.getTime()),
        ZoneId.systemDefault());
  }
}

And we’ll add 2 get methods so we can display some data on the page.

@Repository
public class ExampleRepository implements ResultSets {
private final RowMapper rowMapper =
    (rs, rowNum) -> new Name(
      rs.getInt(1),
      rs.getString(2),
      rs.getString(3),
      Optional.ofNullable(rs.getString(4)),
      convertTimestampToLocalDateTime(rs.getTimestamp(5))
  );
  public List getAllNames() {
    return jdbcTemplate.query("select * from name", rowMapper);
  }
  public List getAllFirstNames() {
    return jdbcTemplate.query(
        "select first_name from name",
        (rs, rowNum) -> rs.getString(1));
  }
}

OK. Name is a bit verbose, but overall fairly straightforward. The full source is available on github.
Let’s try the Scala version.
It seems that SpringApplication.run needs to be in an object instead of a class, but otherwise pretty similar.

@RestController
@EnableAutoConfiguration
@ComponentScan
class Example {
  @RequestMapping(Array("/"))
  def home() : String = {
    "

Names

" +
    exampleRepository.getAllNames.toString +
    "

First Names

" +
    exampleRepository.getFirstNames.toString
  }
}
object Example {
  def main(args: Array[String]) {
    val configuration : Array[Object] = Array(classOf[Example])
    SpringApplication.run(configuration, args)
  }
}

Name is much nicer

case class Name(
                 id : Int, lastName : Option[String],
                 firstName : Option[String],
                 lastUpdate : LocalDateTime)

Yup. That’s it for name.
Let’s take a look at the Scala repository.

@Repository("exampleRepository")
class ExampleRepository {
}

The repository needs to be named. OK. No problem. Now we’ll just add the rowMapper… and we drown in the deep end. It turns out Scala is not completely compatible with Java 8.
After spending too much time trying to get this working, I called a friend for help.
Special thanks to Sujan Kapadia for the Scala Repository code.
I did modify it a bit, so any mistakes are no doubt my own.
Here’s the support code. Additional comments about this are available in the code on github.

trait JdbcTemplateUtils {
  protected var jdbcTemplate: JdbcTemplate = _
  def query(sql: String) = new {
    def apply[T](f: (ResultSet, Int) => T)(implicit ev:
        ((ResultSet, Int) => T) => RowMapper[T]): List[T] =
        jdbcTemplate.query(sql, ev(f)).asScala.toList
  }
}
object JdbcTemplateUtils {
  /** Performs implicit conversion from a closure to
      RowMapper[T]: Note the closure matches the
      signature of RowMapper.mapRow */
  implicit def mapRow[T](rowMapper: (ResultSet, Int) => T):
      RowMapper[T] = {
    new RowMapper[T] {
      override def mapRow(rs: ResultSet, rowNum: Int): T =
          rowMapper(rs, rowNum)
    }
  }
  implicit def timestampToLocalDateTime(timestamp: Timestamp):
      LocalDateTime = LocalDateTime.ofInstant(
          Instant.ofEpochMilli(timestamp.getTime()),
          ZoneId.systemDefault())
  implicit def resultSetToRichResultSet(rs: ResultSet):
      RichResultSet = new RichResultSet(rs)
}
/** Wrapper class for ResultSet that adds helper methods */
final class RichResultSet(rs: ResultSet) {
  def getLocalDateTime(colIndex: Int): LocalDateTime =
      LocalDateTime.ofInstant(Instant.ofEpochMilli(
          rs.getTimestamp(colIndex).getTime()),
          ZoneId.systemDefault())
}

and the Repository end result looks nice.

val queryAllNames = query("select * from name")
  def getAllNames: List[Name] = {
    query("select * from name") { (rs, rowNum) =>
      Name(
        rs.getInt(1),
        rs.getString(2),
        rs.getString(3),
        Option(rs.getString(4)),
        rs.getLocalDateTime(5))
    }
  }
  def getAllFirstNames: List[String] = {
    queryAllNames { (rs, rowNum) => rs.getString(3) }
  }

It was mentioned to me that constructor injection is preferred, but that does not seem to work, so setter injection was used.
spring-scala does have some support for this. Maybe a future blog post?
Ignoring the Scala support code, the Scala code is clearer than the Java code. However it would be nice if the support code was not needed and Scala supported Java 8 directly. While it looks like that is the plan, it is not there yet. Looks like I’ll be sticking with Java 8 for now.