ZonedDateTime - это класс восьмой джавы для представления даты/времени в определенной временнОй зоне. Это может быть запуск ракеты или выход нового релиза программы
ZonedDateTime является immutable, т.е. при операциях с объектами класса создается новый объект.
Postgres предоставляет все средства для работы с такими данными.
Для полей
ZonedDateTime в БД будем использовать тип
TIMESTAMP WITH TIME ZONE.
Вам точно понадобится конвертер:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.TimeZone;
@Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Calendar> {
@Override
public Calendar convertToDatabaseColumn(ZonedDateTime entityAttribute) {
if (entityAttribute == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(entityAttribute.toInstant().toEpochMilli());
calendar.setTimeZone(TimeZone.getTimeZone(entityAttribute.getZone()));
return calendar;
}
@Override
public ZonedDateTime convertToEntityAttribute(Calendar databaseColumn) {
if (databaseColumn == null) {
return null;
}
return ZonedDateTime.ofInstant(databaseColumn.toInstant(),
databaseColumn
.getTimeZone()
.toZoneId());
}
} |
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.TimeZone;
@Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Calendar> {
@Override
public Calendar convertToDatabaseColumn(ZonedDateTime entityAttribute) {
if (entityAttribute == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(entityAttribute.toInstant().toEpochMilli());
calendar.setTimeZone(TimeZone.getTimeZone(entityAttribute.getZone()));
return calendar;
}
@Override
public ZonedDateTime convertToEntityAttribute(Calendar databaseColumn) {
if (databaseColumn == null) {
return null;
}
return ZonedDateTime.ofInstant(databaseColumn.toInstant(),
databaseColumn
.getTimeZone()
.toZoneId());
}
}
(Обратите внимание, что
ZonedDateTime приводится к типу
Calendar.)
который используем так:
import javax.persistence.Convert;
...
@Column(name = "resolution_date")
@Convert(converter = ZonedDateTimeConverter.class)
private ZonedDateTime resolutionDate; |
import javax.persistence.Convert;
...
@Column(name = "resolution_date")
@Convert(converter = ZonedDateTimeConverter.class)
private ZonedDateTime resolutionDate;
Cериализатор:
[ваше имя пакета]
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
@Override
public void serialize(ZonedDateTime dateTime, JsonGenerator generator, SerializerProvider provider)
throws IOException {
String dateTimeString = dateTime.format(
DateTimeFormatter
.ISO_INSTANT
.withZone(ZoneId.systemDefault()));
generator.writeString(dateTimeString);
}
} |
[ваше имя пакета]
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
@Override
public void serialize(ZonedDateTime dateTime, JsonGenerator generator, SerializerProvider provider)
throws IOException {
String dateTimeString = dateTime.format(
DateTimeFormatter
.ISO_INSTANT
.withZone(ZoneId.systemDefault()));
generator.writeString(dateTimeString);
}
}
Десериализатор:
[ваше имя пакета]
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.time.ZonedDateTime;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
private static final String NULL_VALUE = "null";
@Override
public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
String dateString = node.textValue();
ZonedDateTime dateTime = null;
if (!NULL_VALUE.equals(dateString)) {
dateTime = ZonedDateTime.parse(dateString, ISO_DATE_TIME);
}
return dateTime;
}
} |
[ваше имя пакета]
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.time.ZonedDateTime;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
private static final String NULL_VALUE = "null";
@Override
public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
String dateString = node.textValue();
ZonedDateTime dateTime = null;
if (!NULL_VALUE.equals(dateString)) {
dateTime = ZonedDateTime.parse(dateString, ISO_DATE_TIME);
}
return dateTime;
}
}
Приведу пример использования
ZonedDateTime в параметрах методов репозитория spring data:
[ваше имя пакета]
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.format.annotation.DateTimeFormat;
@RestResource
List<Topic> findByResolutionDateBetweenAndAuthorId(
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateLower") ZonedDateTime resolutionDateLower,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateUpper") ZonedDateTime resolutionDateUpper,
@Param("authorId") Long authorId); |
[ваше имя пакета]
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.format.annotation.DateTimeFormat;
@RestResource
List<Topic> findByResolutionDateBetweenAndAuthorId(
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateLower") ZonedDateTime resolutionDateLower,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateUpper") ZonedDateTime resolutionDateUpper,
@Param("authorId") Long authorId);
Как можно понять из названия метода, он находит список объектов Topic, у которых дата находится в промежутке (resolutionDateLower, resolutionDateUpper) и у которых в качестве автора указан конктретный пользователь.
Протестировать можно так:
List<Payment> list = paymentRepo.
findByResolutionDateBetweenAndAuthorId(
ZonedDateTime.parse("2017-01-23T00:00:00.000Z", DateTimeFormatter.ISO_DATE_TIME),
ZonedDateTime.parse("2017-01-23T23:59:59.999Z", DateTimeFormatter.ISO_DATE_TIME),
137L);
assertTrue("list has an element", list.size() == 1);
assertEquals("found topic has particular id", Long.valueOf(10L), list.get(0).getId()); |
List<Payment> list = paymentRepo.
findByResolutionDateBetweenAndAuthorId(
ZonedDateTime.parse("2017-01-23T00:00:00.000Z", DateTimeFormatter.ISO_DATE_TIME),
ZonedDateTime.parse("2017-01-23T23:59:59.999Z", DateTimeFormatter.ISO_DATE_TIME),
137L);
assertTrue("list has an element", list.size() == 1);
assertEquals("found topic has particular id", Long.valueOf(10L), list.get(0).getId());
Запрос по HTTP будет иметь вид:
.../topics/search/findByResolutionDateBetweenAndAuthorId?resolutionDateLower=2017-01-23T00%3A00%3A00.000Z&resolutionDateUpper=2017-01-23T23%3A59%3A59.999Z&authorId=137
(не забываем про urlEncode для даты/времени) |
.../topics/search/findByResolutionDateBetweenAndAuthorId?resolutionDateLower=2017-01-23T00%3A00%3A00.000Z&resolutionDateUpper=2017-01-23T23%3A59%3A59.999Z&authorId=137
(не забываем про urlEncode для даты/времени)
Несколько слов по поводу сравнения объектов ZonedDateTime. Разберем такой код:
import java.time.temporal.ChronoUnit;
...
String dateInString1 = "2016-10-06T10:40:21+03:00";
String dateInString2 = "2016-10-06T12:40:21+05:00";
String dateInString3 = "2016-10-06T07:40:21Z";
ZonedDateTime zdt1 = ZonedDateTime
.parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zdt2 = ZonedDateTime
.parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zdt3 = ZonedDateTime
.parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME);
System.out.println(zdt1);
System.out.println(zdt2);
System.out.println(zdt3);
System.out.println("=========================");
System.out.println("result of comparison 1: " + zdt1.compareTo(zdt2));
System.out.println("result of comparison 2: " + zdt3.compareTo(zdt1));
Comparator<ZonedDateTime> comparator = Comparator.comparing(
zdt -> zdt.truncatedTo(ChronoUnit.SECONDS));
System.out.println("result of comparison 3: " + comparator.compare(zdt1, zdt2));
System.out.println("result of comparison 4: " + comparator.compare(zdt3, zdt1));
long seconds = ChronoUnit.SECONDS.between(zdt1, zdt2);
System.out.println("difference 1: " + seconds);
seconds = ChronoUnit.SECONDS.between(zdt3, zdt2);
System.out.println("difference 2: " + seconds);
System.out.println("=========================");
ZoneId zone = ZoneId.of("Asia/Muscat");
zdt1 = ZonedDateTime
.parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
zdt2 = ZonedDateTime
.parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
zdt3 = ZonedDateTime
.parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
System.out.println(zdt1);
System.out.println(zdt2);
System.out.println(zdt3);
System.out.println("result of comparison 5: " + comparator.compare(zdt1, zdt2));
System.out.println("result of comparison 6: " + comparator.compare(zdt3, zdt2));
System.out.println("result of comparison 7: " + zdt1.compareTo(zdt2));
System.out.println("result of comparison 8: " + zdt3.compareTo(zdt2)); |
import java.time.temporal.ChronoUnit;
...
String dateInString1 = "2016-10-06T10:40:21+03:00";
String dateInString2 = "2016-10-06T12:40:21+05:00";
String dateInString3 = "2016-10-06T07:40:21Z";
ZonedDateTime zdt1 = ZonedDateTime
.parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zdt2 = ZonedDateTime
.parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zdt3 = ZonedDateTime
.parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME);
System.out.println(zdt1);
System.out.println(zdt2);
System.out.println(zdt3);
System.out.println("=========================");
System.out.println("result of comparison 1: " + zdt1.compareTo(zdt2));
System.out.println("result of comparison 2: " + zdt3.compareTo(zdt1));
Comparator<ZonedDateTime> comparator = Comparator.comparing(
zdt -> zdt.truncatedTo(ChronoUnit.SECONDS));
System.out.println("result of comparison 3: " + comparator.compare(zdt1, zdt2));
System.out.println("result of comparison 4: " + comparator.compare(zdt3, zdt1));
long seconds = ChronoUnit.SECONDS.between(zdt1, zdt2);
System.out.println("difference 1: " + seconds);
seconds = ChronoUnit.SECONDS.between(zdt3, zdt2);
System.out.println("difference 2: " + seconds);
System.out.println("=========================");
ZoneId zone = ZoneId.of("Asia/Muscat");
zdt1 = ZonedDateTime
.parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
zdt2 = ZonedDateTime
.parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
zdt3 = ZonedDateTime
.parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
System.out.println(zdt1);
System.out.println(zdt2);
System.out.println(zdt3);
System.out.println("result of comparison 5: " + comparator.compare(zdt1, zdt2));
System.out.println("result of comparison 6: " + comparator.compare(zdt3, zdt2));
System.out.println("result of comparison 7: " + zdt1.compareTo(zdt2));
System.out.println("result of comparison 8: " + zdt3.compareTo(zdt2));
Как видно, мы работаем с тремя объектами, которые представляют собой один и тот же момент времени в разных часовых поясах. Код дает такой вывод:
2016-10-06T10:40:21+03:00
2016-10-06T12:40:21+05:00
2016-10-06T07:40:21Z
=========================
result of comparison 1: -1
result of comparison 2: -1
result of comparison 3: -1
result of comparison 4: -1
difference 1: 0
difference 2: 0
=========================
2016-10-06T11:40:21+04:00[Asia/Muscat]
2016-10-06T11:40:21+04:00[Asia/Muscat]
2016-10-06T11:40:21+04:00[Asia/Muscat]
result of comparison 5: 0
result of comparison 6: 0
result of comparison 7: 0
result of comparison 8: 0
Сравнения с помощью компаратора или ZonedDateTime одинаковых моментов времени в разных поясах не дают 0 (признака равенства). Надежно сравнить на равенство можно только с помощью ChronoUnit.
Напоследок приведу пример преобразования между ZonedDateTime и java.util.Date (получение даты в заданном часовом поясе):
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
ZonedDateTime now = zdt.withZoneSameInstant(zoneId);
String formattedZdt = now.format(dtf);
Date date = null;
try {
date = sdf.parse(formattedZdt);
} catch (ParseException e) {
e.printStackTrace();
} |
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
ZonedDateTime now = zdt.withZoneSameInstant(zoneId);
String formattedZdt = now.format(dtf);
Date date = null;
try {
date = sdf.parse(formattedZdt);
} catch (ParseException e) {
e.printStackTrace();
}
А так можно вычислить границы суток в определенном часовом поясе:
// Date day - календарный день, границы которого надо расчитать в зоне zoneId
ZonedDateTime zdt = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault());
ZonedDateTime zdtTodayStart = zdt.toLocalDate().atStartOfDay(zoneId);
ZonedDateTime zdtTodayEnd = zdtTodayStart.plusDays(1).minusNanos(1l);
String lowerBound = zdtTodayStart.format(DateTimeFormatter.ISO_DATE_TIME);
String upperBound = zdtTodayEnd.format(DateTimeFormatter.ISO_DATE_TIME); |
// Date day - календарный день, границы которого надо расчитать в зоне zoneId
ZonedDateTime zdt = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault());
ZonedDateTime zdtTodayStart = zdt.toLocalDate().atStartOfDay(zoneId);
ZonedDateTime zdtTodayEnd = zdtTodayStart.plusDays(1).minusNanos(1l);
String lowerBound = zdtTodayStart.format(DateTimeFormatter.ISO_DATE_TIME);
String upperBound = zdtTodayEnd.format(DateTimeFormatter.ISO_DATE_TIME);