Recently, I migrated a project from JodaTime to the classes from the java.time package, which are added since Java 8 (JSR 310). While the JSR 310 API is inspired by JodaTime, it is certainly not the same. So a migration is necessary. This article describes my experience in migrating one project. It certainly doesn’t cover every possible scenario, but you might find my experiences useful.
Using the java.time classes
First of all, I removed the JodaTime dependency from my project’s
pom.xml. That’s easy. But it results in a massive amount of compilation errors. With a global regex-based find and replace in IntelliJ, I changed all imports. Find:
import org.joda.time(.*)
replace by:
import java.time$1
Now a lot of compile errors disappeared, but not all of them. That’s because the API’s are not exactly the same. I used this handy migration guide form the JodaTime-blog to change my code appropriately. My code made heavy use of the
LocalDate()and
LocalDateTime()constructors from the JodaTime API. In the java.time API, there are no constructors, but instead there are static factory methods. So instead of
new LocalDate(2015, 4, 23)
I had to write
LocalDate.of(2015, 4, 23)
I did this with a regex-based find and replace again. Find:
new LocalDateTime\(([0-9]+)[\,\s]+([0-9]+)[,\s]+([0-9]+)[,\s]+([0-9]+)[\,\s]+([0-9]+)[,\s]+([0-9]+)[,\s]+([0-9]+)\)
replace by:
LocalDateTime.of($1, $2, $3, $4, $5, $6, $7)
I had a number of variants on this theme, as there are multiple factory methods, matching multiple constructors of JodaTime. Then there were some other changes, that appeared less frequently in my code. Here’s a very incomplete list of things I had to change:
JodaTime | java.time |
---|---|
Years.yearsBetween, Minutes.minutesBetween, Seconds.secondsBetween, etc… |
Period.between |
new LocalDate() |
LocalDate.now() |
getMonthOfYear() |
getMonthValue() |
toLocalDateTime(...) |
atTime(...) |
ISODateTimeFormat.date() |
DateTimeFormatter.ISO_LOCAL_DATE |
DateTimeFormat.forPattern(...) |
DateTimeFormatter.ofPattern(...) |
dateTimeFormatter.print(...) |
dateTimeFormatter.format(...) |
dateTimeFormatter.parseLocalDate(localdate) |
LocalDate.parse(localdate, dateTimeFormatter) |
now.getYearOfCentury() |
now.getYear() % 100 |
Step 2: persistence
Persistence is somewhat more difficult than I expected. For JodaTime, we used the
@Typeannotation, with custom user types from the
org.jadira.usertype.jodatimepackage. There is an equivalent package with user types for java.time from Jadira, but I had problems with it. (I got a time difference between my code and the database, probably due to time zone conversions.) I didn’t want to spent much time on this, and I found an alternative, which didn’t had that problem and was easier to implement: it’s called ThreeTen. I just removed all
@Typeannotations, and instead added the ThreeTen package to the packages to scan for Hibernate. The ThreeTen website has instructions for adding
<class>-entries to the
persistence.xml. However, we use Spring-powered Java-based configuration, and I only had to add the ThreeTen package here:
entityManagerFactoryBean.setPackagesToScan( "my.own.package", "com.github.marschall.threeten.jpa");
So persistence took me a lot of time to get it working, but the solution was simple and even more elegant than all those
@Type-annotations we had before.
By the way, you might wonder why we need custom conversion anyway. The LocalDate en LocalDateTime we use are part of Java, after all? You are right and there is work going on to fix this. It looks like JPA 2.2 and Hibernate 5 will no longer need custom converters for the java.time types.
Final thoughts
All in all, migrating from JodaTime to java.time required a little more work than I had expected. However, I now have one less dependency in my project, which is a good thing, I think.