The best way to handle time zones in a Java web application
source link: https://vladmihalcea.com/time-zones-java-web-application/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
If you are trading Stocks and Crypto using Revolut, then you are going to love RevoGain!
Introduction
In this article, I’m going to show you what is the best way to handle time zones when developing a Java web application.
I applied all these best practices while developing RevoGain, a web application that allows you to calculate the gains you realized while trading stocks, commodities, or crypto using Revolut.
What is difficult about time and time zones?
Handling time is very difficult. If you don’t believe me, check out this awesome list of time-related fallacies.
Now, to make matters worse, a web application request spans on at least three distinct layers:
- the browser
- the web server
- the database
Each of these layers can observe a different time zone, therefore making things even more difficult for us, software developers.
To simplify the time zone handling process, it’s best practice to store timestamps in UTC (Coordinated Universal Time) and convert the timestamps to local time in the web layer so that users can observe their local time zone.
Database time zone
Databases can use the local time zone of the underlying operating system or a custom define time zone.
For consistency, it’s best if all database timestamps are stored in UTC because, this way, it’s going to be much easier to calculate timestamp intervals since all user timestamps are relative to the same time zone.
More, if a user moves to a different time zone, there’s no need to change the already-stored user=specific date/time information since the conversion can be done in the web layer anyway.
So, if you are using MySQL, you can set the database time zone to UTC in the /etc/mysql/my.cnf
configuration file, like this:
default_time_zone=
'+00:00'
Or, if you are using PostgreSQL, you can set the database time zone to UTC in the /var/lib/postgresql/data/postgresql.conf
configuration file, as follows:
timezone =
'UTC'
If you happen to be using Amazon Aurora, then you don’t need to set the UTC time zone because Aurora uses UTC by default.
As I explained in this article, RevoGain uses Amazon Aurora MySQL, so no change was needed in order to use UTC on the database side.
Server time zone
By default, a Java application uses the system time zone. Again, if you are using AWS, then the default time zone is UTC. You can see that when requesting the application logs as the log messages timestamp are relative to UTC.
If your JVM is not using the UTC timezone, but the database is, then you have two options.
Setting the default server time zone
You can set the operating system timezone to UTC, or if you cannot change that, you can set the default JVM timezone.
The JVM time zone can be set using the user.timezone
property:
java -Duser.timezone=
"UTC"
com.revogain.RevoGainApplication
Convert to a given timezone using JDBC
If you cannot change the OS or the JVM timezone, you can still convert a Java Date/Time or Timestamp to a specific time zone using the following two JDBC methods:
If you are using Spring Boot, you can achieve this goal by setting the following configuration property in your application.properties
file:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Behind the scenes, this setting will instruct Hibernate to use the provided time zone when reading and writing timestamp column values.
For more details about the
hibernate.jdbc.time_zone
configuration property, check out this article.
The browser time zone
In the browser, JavaScript stores Date/Time in the local (host system) time zone.
We can, therefore, use the user time zone to convert all UTC timestamps stored in the database or created in the JVM. To do that, we need to store the user’s time zone during authentication.
In the login
page, the authentication form contains a timeZoneOffset
hidden field:
<
form
th:action
=
"@{/login}"
method
=
"post"
class
=
"form-signin"
>
...
<
input
type
=
"hidden"
id
=
"timeZoneOffset"
name
=
"timeZoneOffset"
value
=
""
/>
<
button
class
=
"btn btn-lg"
type
=
"submit"
>Login</
button
>
</
form
>
The timeZoneOffset
hidden input field value is set to the user time zone offset:
<script>
jQuery(document).ready(
function
() {
jQuery(
"#timeZoneOffset"
).val(
new
Date().getTimezoneOffset());
});
</script>
Saving the user-specific time zone on the web server
We can read the timeZoneOffset
value using the following web filter:
public
class
TimeZoneOffsetFilter
implements
Filter {
@Override
public
void
doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain)
throws
IOException, ServletException {
TimeZoneOffsetContext.set(request.getParameter(
"timeZoneOffset"
));
chain.doFilter(request, response);
TimeZoneOffsetContext.reset();
}
}
The TimeZoneOffsetContext
is just a placeholder utility that stores the time zone information so that we can read it after Spring Security authenticates the user login request:
public
class
TimeZoneOffsetContext {
private
static
final
ThreadLocal<String> timeZoneOffsetHolder =
new
ThreadLocal<>();
public
static
String get() {
return
timeZoneOffsetHolder.get();
}
public
static
void
set(String timeZoneOffset) {
timeZoneOffsetHolder.set(timeZoneOffset);
}
public
static
void
reset() {
timeZoneOffsetHolder.remove();
}
}
We can set the user time zone in the Spring Security UserDetails
object that’s associated to the currently logged user, like this:
@Service
@Transactional
(readOnly =
true
)
public
class
UserService
implements
UserDetailsService {
...
@Override
public
UserDetails loadUserByUsername(String username)
throws
UsernameNotFoundException {
User user = userRepository.findByEmail(username);
if
(user ==
null
) {
throw
new
UsernameNotFoundException(
""
"
This email or password are invalid.
Please review them and
try
again.
""
"
);
}
return
new
ApplicationUserDetails(user)
.setTimeZoneOffset(
TimeZoneOffsetContext.get()
);
}
...
}
The ApplicationUserDetails
stores the time zone information and provides timestamp formatting capabilities:
public
class
ApplicationUserDetails
implements
UserDetails {
public
static
final
DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern(
"dd/MM/uuuu HH:mm:ss"
);
private
User user;
private
ZoneOffset zoneOffset;
public
ApplicationUserDetails(User user) {
this
.user = user;
}
...
public
ZoneOffset getZoneOffset() {
return
zoneOffset;
}
public
ApplicationUserDetails setTimeZoneOffset(String timeZoneOffset) {
if
(timeZoneOffset !=
null
) {
int
offsetMinutes = Integer.valueOf(timeZoneOffset) * -
1
;
this
.zoneOffset = ZoneOffset.ofTotalSeconds(offsetMinutes *
60
);
}
return
this
;
}
public
String getFormattedDateTime(LocalDateTime dateTime) {
if
(zoneOffset !=
null
) {
OffsetDateTime serverOffsetDateTime = dateTime.atZone(
ZoneId.systemDefault()
).toOffsetDateTime();
OffsetDateTime clientOffsetDateTime = serverOffsetDateTime
.withOffsetSameInstant(zoneOffset);
return
DATE_TIME_FORMATTER.format(clientOffsetDateTime);
}
return
dateTime.format(DATE_TIME_FORMATTER);
}
}
Converting timestamps to the user time zone
Now, we can convert timestamps to the user-specific time zone. For instance, when displaying the activity log, we can shift the operation timestamp to the user’s time zone, as follows:
<
tr
th:each
=
"op, status : ${operationsPage}"
th:style
=
"${status.odd}? 'font-weight: normal;'"
>
<
td
th:text
=
"${op.id}"
></
td
>
<
td
th:text
=
"${userDetails.getFormattedDateTime(op.createdOn)}"
></
td
>
<
td
th:text
=
"${op.credits}"
></
td
>
<
td
th:text
=
"${op.type.label}"
></
td
>
</
tr
>
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
When dealing with time zones, it’s best to use UTC as much as possible and only convert the timestamp to the current user time zone when rendering the UI.
In a Java web application, we can use the Spring Security UserDetails
to store the user time zone and transpose the UTC timestamps fetched from the database or created on the web server to the local time zones of every logged user.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK