简体   繁体   中英

Java Time & Rest API

I've red multiple articles and discussions and still I have some uncertainty: I'm not sure if I should use Instant or any other type to store Booking – in the sense of “Online booking” (so participants that are from different countries/time zones needs meet in the same moment on the timeline). I tend to use LocalDateTime , since DB and Backend is set to UTC and since incoming “create booking” json message contains ISO 8601 (with offset) startDateTime & endDateTime fields.

Let's take this setup: 1. Database (UTC, Oracle, MSSQL), Java 11 Backend (UTC, SpringBoot, Spring JDBC template), Angular 13 Frontend.

Sources:

  1. https://apiux.com/2013/03/20/5-laws-api-dates-and-times
  2. https://medium.com/decisionbrain/dates-time-in-modern-java-4ed9d5848a3e
  3. What's the difference between Instant and LocalDateTime?
  4. java.time.LocalDate vs Instant for a 'business date'
  5. https://mattgreencroft.blogspot.com/2014/12/java-8-time-choosing-right-object.html

What is clear for now:

  • Use ISO 8601 in REST API
  • Accept any time zone
  • Store it in UTC
  • Return it in UTC
  • Don't use time if you don't need it
  • Use "date-time values as count-from-epoch" as less as possible, since debugging and logging becomes very difficult. Use it just for technical stuff (eg ping).

Next:

And eg article 2) is telling: Many applications can be written only using LocalDate, LocalTime, and Instant , with the time zone added at the UI layer.

And the 3) is telling: So business app developers use Instant and ZonedDateTime classes most commonly. Nearly all of your backend, database, business logic, data persistence, data exchange should all be in UTC. But for presentation to users you need to adjust into a time zone expected by the user. This is the purpose of the ZonedDateTime class.

Let's summarize & verify examples:

LocalTime

  • Company has a policy that lunchtime starts at 12:30 PM at each of its factories worldwide.

LocalDate

LocalDate + time zone (in separate db column)

  • Birthday - if it were crucial to know someone's age to the very day, then a date-only value is not enough. With only a date, a person appearing to turn 18 years old in Tokyo Japan would still be 17 in Toledo Ohio US.
  • Due date in a contract. If stated as only a date, a stakeholder in Japan will see a task as overdue while another stakeholder in Toledo sees the task as on-time.

LocalDateTime

  • Christmas starts at midnight on the 25th of December 2015.
  • Booking – in the sense of “local” dentist appointment

Instant

  • Some critical function must start every day at 1:30am (LocalDateTime & ZonedDateTime are wrong in this case, because eg during switching from/to standard time to summer time, function runs twice/not at all).

Neither. For appointments at a given location (be it for a video call meeting or simply a barber's appointment), you want ZonedDateTime .

Story time!

Let's say you're planning to be in Amsterdam somewhere in summer 2024, and for some strange reason you wanna make sure you look great, so you go a little nuts and decide to make an appointment at a barber's, for 14:00, May 2nd, 2024 (that'll be a wednesday), at some specific barbershop in Amsterdam.

We could calculate precisely how many seconds need to pass before your barbershop appointment is up: There will be some moment in time, and you can tell anybody on the planet to wait those exact number of seconds and then clap their hands. When they clap their hands? That exact time you have your appointment. It will be very early in the morning in California if someone were to clap in California, of course, but you could do that. That's the point - you're talking about a specific moment in time, which means it'll be a different time of day depending on where on the planet.

But, and this is not even hypothetical: The EU has decided a while ago that the EU should stop with the daylight savings time. The EU is busy with a few other things at the moment so enforcement of this directive is still being hashed out, but it's been decided - so now the debate is about when the EU stops with summer time and which time zone(s) the what used to be known as 'central european time', used by the vast majority of EU countries (it's a very large timezone), needs to become.

Notably, the EU has not decided on that, yet. In fact, each country is as of yet free to pick whatever they please. It is therefore quite plausible that eg The Netherlands, which is on the western edge of this very large zone, and where your barbershop appointment it, will decide to stick with what is currently 'winter time', because germany is likely to do that and citizens of both countries have grown rather used to it being the same time in both countries. But, sticking with summer time is also quite plausible. We simply don't know yet.

One day, in parliament, a hammer will come down. When that hammer comes down, that is the very moment in time when the dutch timezone definition has officially changed, and let's say they choose to stick with wintertime, at that exact moment that hammer comes down, your barber appointment just moved by 1 hour . It's still at 14:00, May 2nd, 2024, in Amsterdam - but the number of seconds until your appointment shifts by 3600 seconds the very moment that hammer lands in the dutch parliament in The Hague. Had you stored this moment in time in UTC, it would now be wrong . Had you stored this moment in time in terms of seconds left to go, or seconds from UTC epoch midnight newyears 1970, it would now be wrong . So don't store appointments in those ways.

That explains the pragmatic difference between Instant+timezone, and ZonedDateTime objects.

A ZonedDateTime object represents the notion of 'I have an appointment, in Europe/Amsterdam , at 14:00, on May 2nd, in 2024.

Given that that is exactly what it represents, if your tzdata files are updated to reflect that hammer coming down, that's okay. It's still 14:00, May 2nd, in 2024. In terms of 'how many seconds until the appointment', the moment in time represented by that object would shift by 1 hour the moment your JVM is updated with the new tz info.

Instant is different. Instant represents an exact moment in time in terms of 'how many seconds until that moment'. The act of storing that appointment as an Instant would involve calculating the amount of seconds until it happens and then storing that . And, as we just determined, that is incorrect and will mean you're going to miss your appointment. It's not how an appointment works (which works in terms of a year, month, day, hour, minute, and second, and a possibly implied timezone - no agenda any human uses is based on the idea of 'what does your day look like 134823958150154 seconds from now?') - and you should always use the data type (and store data) in terms of how it works, and not some conversion of it, because if you store conversions, you 'encode' your reliance on it into your storage and stuff goes wrong if your conversion no longer works. Which is why storing it as an Instant is the mistake here.

The story also explains why the only non-stupid way to store timezone info is as something like Europe/Amsterdam - because something like "CET" (Central European Time) may not even exist 2 years hence. There is absolutely no guarantee every single last one of the many many countries that are in CET today will pick the same zone when the EU directive grows teeth, whenever that may be. So if you store the appointment as 'in the CET zone', then all you know, come 2024, is that you now have no idea when your barbershop's appointment actually is. Not all that useful.

Coordinating a time to meet, without the context of place

Every so often you want to coordinate a time to meet, but not a place to meet - a virtual meeting, for example. Usually these are still 'localized' (somebody is deemed the host, they plan the meeting for, say, 17:30 their time, all participants store that ZonedDateTime construct, and their agenda systems will tell them when to log in for that meeting at the appropriate time, regardless of where on the planet they are). So I'd just do that.

But given that the meeting is virtual, 'a timezone is implied' no longer has to neccessarily be true. In which case it is fine to store things as an Instant (and a ZonedDateTime where the zone is UTC is so close to the concept of Instant , there is basically no difference at all), in which case eg someone in The Netherlands would see their appointment move by an hour the moment that hammer comes down. Assuming they did it right and indicated to their calendaring tool that the appointment is in the UTC zone.

It's possible you want this instead. But it's more common to go with the 'host zone' route. (there isn't that much of a difference; the calendaring tool needs to support the concept of saving an appointment with the timezone (and not convert it to the right time in the local zone, as that will break if either the host zone or the target zone decides to make a change to their tz data, which happens all the time - once a calendaring tool supports this, you could choose to use UTC as host zone for such a virtual meeting).

DBs

You've more or less correctly concluded that there are 3 fundamentally different, and therefore utterly incompatible notions of date and time; you cannot convert one to the other without losing something on the way. These are:

  • LocalDate (and LocalTime, and LocalDateTime): If you want to wake up 08:00 and you want that alarm to continue to wake you up at 08:00 locally even if you fly to different zones, this is the thing you want.
  • ZonedDateTime: For appointments.
  • Instant: To log that something happens or will happen X seconds from now or X seconds in the past; a slam dunk for eg timestamps on log messages, and obviously the one to use for space stuff (that solar flare will occur on X - when that hammer comes down in dutch parliament, and a newspaper calculated the solar flare out to an exact date, hour, and minute and printed that earlier, that is now wrong, of course, and they'd have to print a correction. Had they printed an instant, unwieldy as that might be to us humans, they would not have had to do that).

Unfortunately, most languages (including java's obsolete java.util.Date and java.util.Calendar approaches) mess this up and didn't realize time is just that complicated, and that the above 3 concepts cannot be converted without losing an important nuance.

As a consequence, DBs sometimes play fast and loose too. More to the point, JDBC which is usually the 'glue' that java apps use to talk to DBs, has an API that involves the types java.sql.Date and java.sql.Timestamp , and both of those are based on juDate (they extend it), and juDate, name notwithstanding, is equivalent to Instant - it stores a moment-in-time, does not store a timezone in the first place, and will break if timezones up and change definition on you.

So, the actual JDBC calls you have to use to get the right data type in and out is tricky. The theoretical correct answer is LDT and ZDT directly:

ResultSet rs = somePreparedStatement.query();
/* WRONG */ {
  Date x = rs.getDate(1);
  ZonedDateTime y = convertToZDT(x);
}
/* RIGHT */ {
  ZonedDateTime y = rs.getObject(1, ZonedDateTime.class);
}

However, many JDBC drivers don't support this. Very painful. Similarly, to replace question marks in prepared statements with dates, the correct move is ps.setObject(1, someZdtInstance) , not with a juDate instance or even an java.sql.Date instance. Whether they support that, oof. You'd have to check - many do not.

I suggest you check the DB docs about the type(s) for dates they support and how it actually stores them. If it sounds good (that is, it stores an actual year, month, day, hour, minute, second, and full timezone name, all in a single column), use that. If none of the data types support that, do it yourself and make 7 columns.

Assuming you found a data type that does the job, figure out if .getObject(x, ZDT.class) works, and ps.setObject(x, zdtInstance) . Let's hope so. If it does, that's your answer. If it does not, kludge around to figure out what the workaround is: Write code that converts and check what arrives - after all, if the DB is storing actual y/m/d/h/m/s/zone info, and you give it a juTimestamp object which does not have that info, then the DB is going to have to be converting that back to ymdhmsz info again. Assuming that works out fine, then this workaround is not going to break on you. Even if the appointment is in Amsterdam and that hammer comes down between now and then.

store Booking – in the sense of “Online booking” (so participants that are from different countries/time zones needs meet in the same moment on the timeline). I tend to use LocalDateTime, since DB and Backend is set to UTC and since incoming “create booking” json message contains ISO 8601 (with offset) startDateTime & endDateTime fields.

You did not really define what you mean by "booking".

If what you meant is to pinpoint a moment, a specific point on the timeline, then LocalDateTime is exactly the wrong class to use.

For example, suppose a missile launching company has offices in Japan, Germany, and Houston Texas US. Staff at all offices want to be in a group conference call during a launch. We would specify the moment of launch with an offset of zero from UTC. For that we use Instant class. The Z in the string below means an offset of zero, +00:00 , and is pronounced “Zulu”.

Instant launch = Instant.parse( "2022-05-23T05:31:23.000Z" ) ;

Let's tell each office their deadline to join the call.

ZonedDateTime tokyo = launch.atZone( ZoneId.of( "Asia/Tokyo" ) ) ;
ZonedDateTime berlin = launch.atZone( ZoneId.of( "Europe/Berlin" ) ) ;
ZonedDateTime chicago = launch.atZone( ZoneId.of( "America/Chicago" ) ) ;  // Houston time zone is "America/Chicago".

We have four date-time objects, all representing the very same simultaneous moment.

As for the current default time zone of your database session and of your backend server, those should be irrelevant to your programming. As a programmer, those are out of your control, and are subject to change. So always specify your desired/expected time zone.

As for the rest of your Question, I cannot discern a specific question. So all I can do is comment on your examples.

LocalTime

Company has a policy that lunchtime starts at 12:30 PM at each of its factories worldwide.

Yes, LocalTime is correct for representing a time-of-day for any place in the world.

Let's ask if lunch has started at the factory in Delhi India.

LocalTime lunchStart = LocalTime.of( 12 , 30 ) ;
ZoneId zKolkata = ZoneId.of( "Asia/Kolkata" ) ;
ZonedDateTime nowKolkata = ZonedDateTime.now( zKolkata ) ;
boolean isBeforeLunchKolkata = nowKolkata.toLocalTime().isBefore( lunchStart ) ;

Actually, we have a subtle bug there. On some dates in some zones, the time 12:30 may not exist. For example, suppose that time zone on that date has a Daylight Saving Time (DST) cutover, "Springing ahead" one hour at noon to 1 PM. In that case, there would be no 12:30. The time 13:30 would be the "new 12:30".

To fix this bug, let's adjust from nowKolkata to use a different time of day. During this adjustment, java.time considers any anomalies such as DST, and handles them.

Note that this moving to a new time-of-day results in a new fresh separate ZonedDateTime . The java.time classes use immutable objects .

ZonedDateTime lunchTodayKolkata = nowKolkata.with( lunchStart ) ;

Now we should rewrite that test for whether lunch has started.

boolean isBeforeLunchKolkata = nowKolkata.isBefore( lunchTodayKolkata ) ;

Let's ask how long until lunch starts. Duration class represents a span of time not attached to the timeline, on the scale of hours-minutes-seconds.

Duration duration = Duration.between( nowKolkata , lunchTodayKolkata ) ;

We can use that duration as another way of determining if lunch has started there at that factory. If the duration is negative, we know we passed the start of lunch — meaning we would have to go back in time to see lunch start.

boolean isAfterLunchKolkata = duration.isNegative() ;
boolean isBeforeLunchKolkata = ! duration.isNegative() ;

LocalDate

The LocalDate class represents a date only, without a time-of-day, and without the context of a time zone or offset-from-UTC.

Be aware that for any given moment the date varies around the globe by zone. So right now it can be “tomorrow” in Tokyo Japan while simultaneously “yesterday” in Toledo Ohio US. So a LocalDate is inherently ambiguous with regard to the timeline.

Birthday - if it were crucial to know someone's age to the very day, then a date-only value is not enough. With only a date, a person appearing to turn 18 years old in Tokyo Japan would still be 17 in Toledo Ohio US.

If you want to be precise about someone's age, you need their time of birth in addition to the date of birth and time zone of birth. Just the date and time zone of their birth place is not enough to narrow down their age to the first moment of their 18th year.

The time zone for Toledo Ohio US is America/New_York .

LocalDate birthDate = LocalDate.of( 2000 , Month.JANUARY , 23 ) ;
LocalTime birthTime = LocalTime.of( 3 , 30 ) ;
ZoneId birthZone = ZoneId.of( "America/New_York" ) ;  
ZonedDateTime birthMoment = ZonedDateTime.of( birthDate , birthTime , birthZone ) ;

To determine if the person is 18 years of age precisely, add 18 years to that moment of birth. Then compare to current moment.

ZonedDateTime turning18 = birthMoment.plusYears( 18 ) ;
ZonedDateTime nowInBirthZone = ZonedDateTime.now( birthZone ) ;
boolean is18 = turning18.isBefore( nowInBirthZone ) ;

Due date in a contract. If stated as only a date, a stakeholder in Japan will see a task as overdue while another stakeholder in Toledo sees the task as on-time.

True.

A contract needing moment precision must state a date, a time-of-day, and a time zone.

Let's say a contract expires after the 23rd of May next year as seen in Chicago IL US.

I suggest avoiding the imprecise notion of "midnight". Focus on the first moment of a day on a certain date in a certain zone. So "after the 23rd" means the first moment of the 24th.

Some dates in some zones may start a time other than 00:00. Let java.time determine the first moment of a day.

LocalDate ld = LocalDate.of( 2023 , Month.MAY , 23 ) ;
ZoneId zChicago = ZoneId.of( "America/Chicago" ) ;
ZonedDateTime expiration = ld.plusDays( 1 ).atStartOfDay( zChicago ) ;
boolean isContractInEffect = ZonedDateTime.now( zChicago ).isBefore( expiration ) ; 

LocalDateTime

Christmas starts at midnight on the 25th of December 2015.

Use LocalDateTime when you mean a date and time as seen in any locality.

Christmas starts at the first moment of the 25th of December in 2015 in every locality. So yes, use LocalDateTime .

LocalDateTime xmas2015 = LocalDateTime.of( 2015 , Month.DECEMBER , 25 , 0 , 0 , 0 , 0 ) ;

That class purposely lacks the context of a time zone or offset-from-UTC. So an object of this class is inherently ambiguous with regard to the timeline. This class cannot represent a moment.

In other words, Santa arrives in Kiribati (the most advanced time zone, at 14 hours ahead of UTC) before arriving in Japan, and arrives still later in India, still later in Europe, and so on. The red sleigh chases each new day dawning successively in zone after zone around the world all night long.

Let's determine the moment Christmas started then in Jordan.

ZoneId zAmman = ZoneId.of( "Asia/Amman" ) ;
ZonedDateTime xmasAmman = xmas2015.atZone( zAmman) ;

Adjust that moment to the time zone for Denver Colorado US.

ZoneId zDenver = ZoneId.of( "America/Denver" ) ;
ZonedDateTime xmasStartingInAmmanAsSeenInDenver = xmasAmman.withZoneSameInstant( zDenver ) ;

If you interrogate that xmasStartingInAmmanAsSeenInDenver object, you will see that when Christmas was starting in Jordan, the date in Denver was still the 24th, the day before Christmas. Christmas would not reach Colorado for several more hours.

Booking – in the sense of “local” dentist appointment

Appointments in the future that are intended to stick with their assigned time-of-day without regard to any changes to the time zone's rules must be tracked as three separate pieces:

  • Date with time-of-day
  • Time zone by which to interpret that date & time.
  • Duration, for how long the appointment will last.

Let's book an appointment for next January 23rd at 3 PM in New Zealand.

LocalDateTime ldt = LocalDateTime.of( 2023 , 1 , 23 , 15 , 0 , 0 , 0 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
Duration d = Duration.ofHours( 1 ) ;

Each of those three items must be stored in your database.

  • The first can be stored in a column of a type akin to the SQL standard type TIMESTAMP WITHOUT TIME ZONE . Notice the "WITHOUT", not "WITH".
  • The time zone can be stored as text, by its standardized name in format of Continent/Region . Never use 2-4 letter pseudo-zones such as IST , CST , etc.
  • I would store the duration in standard ISO 8601 format, PnYnMnDTnHnMnS . The P marks the beginning, which the T separates the two portions. So 1 hour is PT1H .

When you need to create a schedule, you must determine a moment. To do that, combine your parts.

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId z = ZoneId.of( myResultSet.getString( … ) ) ;
Duration d = Duration.parse( myResultSet.getString( … ) ) ;

ZonedDateTime start = ldt.atZone( z ) ;
ZonedDateTime end = start.plus( d ) ;

I and others have explained this multiple times, so search to learn more. The key point is that politicians around the world have shown a penchant for frequently changing the rules of the time zones in their jurisdictions. And they do so with surprisingly little forewarning. So the moment of that 3PM appointment might come an hour earlier that you expect now, or a half-hour later, or who knows what. Expecting your time zones of interest to remain stable is to doom your software to an eventual fail. You may think me paranoid. Let's check back in some years… when all the customers in your booking app are arriving at the wrong time.

Instant

Some critical function must start every day at 1:30am (LocalDateTime & ZonedDateTime are wrong in this case, because eg during switching from/to standard time to summer time, function runs twice/not at all).

No, incorrect, not Instant . If you want to run a task at 1:30 AM daily, you need to use LocalTime combined with a ZoneId . This is similar in concept to the appointments discussion directly above.

If we want the server in a Chicago office to run that task in the wee hours, we would define the date-time and zone.

ZoneId z = ZoneId.of( "America/Chicago" ) ;
LocalTime targetTime = LocalTime.of( 1 , 30 ) ;

To determine the next run, capture the current moment.

ZonedDateTime now = ZonedDateTime.now( z ) ;

See if we are before the target time, then calculate time to wait. We use "not after" as a shorter way of saying "if before or equal to". Think about if the current moment happens to be exactly 1:30:00.000000000 AM.

if( ! now.toLocalTime().isAfter( targetTime ) ) {  // If before or equal to the target time.
    Duration d = Duration.between( now , now.with( targetTime ) ) ;
    …
}

If the target time has passed for today, add a day. Then calculate time to elapse until the next run.

else {  // Else, after target time. Move to next day.
    LocalDate tomorrow = now.toLocalDate().plusDays( 1 ) ;
    ZonedDateTime nextRun = ZonedDateTime.of( tomorrow , targetTime , z ) ;
    Duration d = Duration.between( now , nextRun ) ;
    …
}

The ZonedDateTime.of method automatically adjusts if your specified time-of-day does not exist on that date in that zone.

If you are using an executor service to run your task at that same local time, then you cannot use ScheduledExecutorService with the methods scheduleAtFixedRate or scheduleWithFixedDelay . Use the schedule method with a single delay.

Pass a reference to that executor service to the constructor of your task, your Runnable / Callable . Then write your task in such as way that the last thing it does in submit itself to the executor service with a fresh newly-calculated delay. On most days that delay will be 24 hours. But on anomalous days such as Daylight Saving Time (DST) cutover, the delay might be something like 23 hours or 25 hours. So each task run schedules the next task run.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM