Как использовать java.время.ZonedDateTime / LocalDateTime в p: календарь
я использовал время Joda для обработки даты-времени в приложении Java EE, в котором строковое представление даты-времени, представленное связанным клиентом, было преобразовано с помощью следующей процедуры преобразования перед отправкой его в базу данных, т. е. в getAsObject()
метод в преобразователе JSF.
org.joda.time.format.DateTimeFormatter formatter = org.joda.time.format.DateTimeFormat.forPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(DateTimeZone.UTC);
DateTime dateTime = formatter.parseDateTime("05-Jan-2016 03:04:44 PM +0530");
System.out.println(formatter.print(dateTime));
местный часовой пояс задан на 5 часов и 30 минут раньше UTC
/ GMT
. Поэтому преобразование в UTC
нужно вычесть 5 часов и 30 минуты с даты-времени, который происходит правильно, используя Joda Time. Он отображает следующие выходные данные, как ожидалось.
05-Jan-2016 09:34:44 AM +0000
► смещение часового пояса +0530
на месте +05:30
был взят, потому что он зависит от <p:calendar>
который отправляет смещение зоны в этом формате. Кажется невозможным изменить это поведение <p:calendar>
(этот вопрос сам по себе не был бы необходим в противном случае).
то же самое, однако, нарушается, если попытка использования API времени Java в Java 8.
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneOffset.UTC);
ZonedDateTime dateTime = ZonedDateTime.parse("05-Jan-2016 03:04:44 PM +0530", formatter);
System.out.println(formatter.format(dateTime));
он неожиданно отображает следующий неправильный вывод.
05-Jan-2016 03:04:44 PM +0000
очевидно, дата-время преобразовано не в соответствии с UTC
в котором он должен преобразовываться.
для правильной работы требуются следующие изменения.
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a z").withZone(ZoneOffset.UTC);
ZonedDateTime dateTime = ZonedDateTime.parse("05-Jan-2016 03:04:44 PM +05:30", formatter);
System.out.println(formatter.format(dateTime));
, который, в свою очередь, показывает следующее.
05-Jan-2016 09:34:44 AM Z
Z
был заменен на z
и +0530
заменено на +05:30
.
какой подход среднего пути можно рассматривать для <p:calendar>
и время Java в Java 8 для работы последовательно и когерентно, хотя <p:calendar>
внутренне использует SimpleDateFormat
вместе с java.util.Date
?
неудачный тестовый сценарий в JSF.
конвертер :
@FacesConverter("dateTimeConverter")
public class DateTimeConverter implements Converter {
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
return ZonedDateTime.parse(value, DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneOffset.UTC));
} catch (IllegalArgumentException | DateTimeException e) {
throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, null, "Message"), e);
}
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (value == null) {
return "";
}
if (!(value instanceof ZonedDateTime)) {
throw new ConverterException("Message");
}
return DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a z").withZone(ZoneId.of("Asia/Kolkata")).format(((ZonedDateTime) value));
// According to a time zone of a specific user.
}
}
в XHTML, имеющего <p:calendar>
.
<p:calendar id="dateTime"
timeZone="Asia/Kolkata"
pattern="dd-MMM-yyyy hh:mm:ss a Z"
value="#{bean.dateTime}"
showOn="button"
required="true"
showButtonPanel="true"
navigator="true">
<f:converter converterId="dateTimeConverter"/>
</p:calendar>
<p:message for="dateTime"/>
<p:commandButton value="Submit" update="display" actionListener="#{bean.action}"/><br/><br/>
<h:outputText id="display" value="#{bean.dateTime}">
<f:converter converterId="dateTimeConverter"/>
</h:outputText>
часовой пояс полностью прозрачно зависимости от текущего часового пояса пользователя.
Боб, не имеющий ничего, кроме одного свойства.
@ManagedBean
@ViewScoped
public class Bean implements Serializable {
private ZonedDateTime dateTime; // Getter and setter.
private static final long serialVersionUID = 1L;
public Bean() {}
public void action() {
// Do something.
}
}
это будет работать неожиданным образом, как показано во втором последнем примере / середине в первых трех фрагментах кода.
в частности, если вы введете 05-Jan-2016 12:00:00 AM +0530
, это будет redisplay 05-Jan-2016 05:30:00 AM IST
потому что оригинальное преобразование 05-Jan-2016 12:00:00 AM +0530
to UTC
в преобразователе происходит сбой.
преобразование из локального часового пояса, смещение которого +05:30
to UTC
а затем преобразование из UTC
назад к этому часовому поясу, очевидно, должен повторно отображать ту же дату-время, что и введенный через компонент календаря, который является рудиментарной функциональностью данного конвертера.
обновление:
конвертер СПД преобразования java.sql.Timestamp
и java.time.ZonedDateTime
.
import java.sql.Timestamp;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter(autoApply = true)
public final class JodaDateTimeConverter implements AttributeConverter<ZonedDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(ZonedDateTime dateTime) {
return dateTime == null ? null : Timestamp.from(dateTime.toInstant());
}
@Override
public ZonedDateTime convertToEntityAttribute(Timestamp timestamp) {
return timestamp == null ? null : ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.UTC);
}
}
1 ответов
ваша конкретная проблема заключается в том, что Вы перенесли из Джода дата экземпляр zoneless времени DateTime
к экземпляру zoned date time Java8 ZonedDateTime
вместо Java8 по zoneless дата время экземпляр LocalDateTime
.
используя ZonedDateTime
(или OffsetDateTime
) вместо LocalDateTime
требуется не менее 2 дополнительных изменений:
-
не заставляйте часовой пояс (смещение) во время даты преобразование. Вместо этого во время синтаксического анализа будет использоваться часовой пояс входной строки, если таковой имеется, и часовой пояс, хранящийся в
ZonedDateTime
экземпляр должен использоваться во время форматирования.на
DateTimeFormatter#withZone()
даст только запутанные результаты сZonedDateTime
поскольку он будет действовать как резервный во время синтаксического анализа (он используется только тогда, когда часовой пояс отсутствует во входной строке или шаблоне формата), и он будет действовать как переопределение во время форматирования (часовой пояс, хранящийся вZonedDateTime
полностью игнорировали). Это коренная причина вашей наблюдаемой проблемы. Просто опускаяwithZone()
при создании форматера следует его исправить.обратите внимание, что когда вы указали конвертер, и не имеют
timeOnly="true"
, вам не нужно указывать<p:calendar timeZone>
. Даже когда вы это делаете, вы хотели бы использоватьTimeZone.getTimeZone(zonedDateTime.getZone())
вместо жесткого кодирования. -
необходимо пронести часовой пояс (смещение) по всем слоям, включая базу данных. Если ваша база данных, однако имеет тип столбца "дата без часового пояса", тогда информация о часовом поясе теряется во время persist, и вы столкнетесь с проблемами при обслуживании из базы данных.
неясно, какую БД вы используете, но имейте в виду, что некоторые DBs не поддерживают
TIMESTAMP WITH TIME ZONE
тип столбца как известно из Oracle и PostgreSQL DBs. Например, MySQL не поддерживает его. Тебе нужна секунда. колонна.
если эти изменения неприемлемы, то вам нужно вернуться к LocalDateTime
и полагаться на фиксированный / предопределенный часовой пояс во всех слоях, включая базу данных. Обычно для этого используется UTC.
работа с ZonedDateTime
в JSF и JPA
при использовании ZonedDateTime
соответствующей TIMESTAMP WITH TIME ZONE
тип столбца DB, используйте приведенный ниже конвертер JSF для преобразования между String
в пользовательском интерфейсе и ZonedDateTime
в модели. Этот конвертер будет искать pattern
и locale
атрибуты родительского компонента. Если родительский компонент изначально не поддерживает a pattern
или , просто добавьте их как <f:attribute name="..." value="...">
. Если locale
атрибут отсутствует, (по умолчанию) <f:view locale>
будет использоваться вместо этого. Есть нет timeZone
атрибут по причине, как описано в #1 здесь выше.
@FacesConverter(forClass=ZonedDateTime.class)
public class ZonedDateTimeConverter implements Converter {
@Override
public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
if (modelValue == null) {
return "";
}
if (modelValue instanceof ZonedDateTime) {
return getFormatter(context, component).format((ZonedDateTime) modelValue);
} else {
throw new ConverterException(new FacesMessage(modelValue + " is not a valid ZonedDateTime"));
}
}
@Override
public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
if (submittedValue == null || submittedValue.isEmpty()) {
return null;
}
try {
return ZonedDateTime.parse(submittedValue, getFormatter(context, component));
} catch (DateTimeParseException e) {
throw new ConverterException(new FacesMessage(submittedValue + " is not a valid zoned date time"), e);
}
}
private DateTimeFormatter getFormatter(FacesContext context, UIComponent component) {
return DateTimeFormatter.ofPattern(getPattern(component), getLocale(context, component));
}
private String getPattern(UIComponent component) {
String pattern = (String) component.getAttributes().get("pattern");
if (pattern == null) {
throw new IllegalArgumentException("pattern attribute is required");
}
return pattern;
}
private Locale getLocale(FacesContext context, UIComponent component) {
Object locale = component.getAttributes().get("locale");
return (locale instanceof Locale) ? (Locale) locale
: (locale instanceof String) ? new Locale((String) locale)
: context.getViewRoot().getLocale();
}
}
и используйте приведенный ниже конвертер JPA для преобразования между ZonedDateTime
и java.util.Calendar
в JDBC (приличный драйвер JDBC потребует / использует его для TIMESTAMP WITH TIME ZONE
типизированные колонки):
@Converter(autoApply=true)
public class ZonedDateTimeAttributeConverter 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());
}
}
работа с LocalDateTime
в JSF и JPA
при использовании UTC на основе LocalDateTime
С соответствующим UTC на основе TIMESTAMP
(без часового пояса!) Тип столбца DB, используйте ниже конвертер JSF для преобразования между String
в пользовательском интерфейсе и LocalDateTime
в модели. Этот конвертер будет искать pattern
, timeZone
и locale
атрибуты родительского компонента. Если родитель компонент не поддерживает тег pattern
, timeZone
и/или , просто добавьте их как <f:attribute name="..." value="...">
. The timeZone
атрибут должен представлять резервный часовой пояс входной строки (когда pattern
не содержит часовой пояс) и часовой пояс выходной строки.
@FacesConverter(forClass=LocalDateTime.class)
public class LocalDateTimeConverter implements Converter {
@Override
public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
if (modelValue == null) {
return "";
}
if (modelValue instanceof LocalDateTime) {
return getFormatter(context, component).format(ZonedDateTime.of((LocalDateTime) modelValue, ZoneOffset.UTC));
} else {
throw new ConverterException(new FacesMessage(modelValue + " is not a valid LocalDateTime"));
}
}
@Override
public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
if (submittedValue == null || submittedValue.isEmpty()) {
return null;
}
try {
return ZonedDateTime.parse(submittedValue, getFormatter(context, component)).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
} catch (DateTimeParseException e) {
throw new ConverterException(new FacesMessage(submittedValue + " is not a valid local date time"), e);
}
}
private DateTimeFormatter getFormatter(FacesContext context, UIComponent component) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(getPattern(component), getLocale(context, component));
ZoneId zone = getZoneId(component);
return (zone != null) ? formatter.withZone(zone) : formatter;
}
private String getPattern(UIComponent component) {
String pattern = (String) component.getAttributes().get("pattern");
if (pattern == null) {
throw new IllegalArgumentException("pattern attribute is required");
}
return pattern;
}
private Locale getLocale(FacesContext context, UIComponent component) {
Object locale = component.getAttributes().get("locale");
return (locale instanceof Locale) ? (Locale) locale
: (locale instanceof String) ? new Locale((String) locale)
: context.getViewRoot().getLocale();
}
private ZoneId getZoneId(UIComponent component) {
Object timeZone = component.getAttributes().get("timeZone");
return (timeZone instanceof TimeZone) ? ((TimeZone) timeZone).toZoneId()
: (timeZone instanceof String) ? ZoneId.of((String) timeZone)
: null;
}
}
и используйте приведенный ниже конвертер JPA для преобразования между LocalDateTime
и java.sql.Timestamp
в JDBC (приличный драйвер JDBC потребует/использует его для TIMESTAMP
набирается колонка):
@Converter(autoApply=true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(LocalDateTime entityAttribute) {
if (entityAttribute == null) {
return null;
}
return Timestamp.valueOf(entityAttribute);
}
@Override
public LocalDateTime convertToEntityAttribute(Timestamp databaseColumn) {
if (databaseColumn == null) {
return null;
}
return databaseColumn.toLocalDateTime();
}
}
применение LocalDateTimeConverter
к вашему конкретному случаю с <p:calendar>
вам нужно изменить ниже:
-
как
<p:calendar>
не ищет конвертеры поforClass
, вам либо нужно будет перерегистрировать его с<converter><converter-id>localDateTimeConverter
наfaces-config.xml
, или изменить аннотацию, как показано ниже@FacesConverter("localDateTimeConverter")
как
<p:calendar>
безtimeOnly="true"
игнорироватьtimeZone
, и предлагает во всплывающем окне возможность отредактируйте его, вам нужно удалитьtimeZone
атрибут, чтобы избежать путаницы преобразователя (этот атрибут требуется только тогда, когда часовой пояс отсутствует вpattern
).необходимо указать нужный дисплей
timeZone
атрибут во время вывода (этот атрибут не требуется при использованииZonedDateTimeConverter
так как он уже хранится вZonedDateTime
).
вот полный рабочий фрагмент:
<p:calendar id="dateTime"
pattern="dd-MMM-yyyy hh:mm:ss a Z"
value="#{bean.dateTime}"
showOn="button"
required="true"
showButtonPanel="true"
navigator="true">
<f:converter converterId="localDateTimeConverter" />
</p:calendar>
<p:message for="dateTime" autoUpdate="true" />
<p:commandButton value="Submit" update="display" action="#{bean.action}" /><br/><br/>
<h:outputText id="display" value="#{bean.dateTime}">
<f:converter converterId="localDateTimeConverter" />
<f:attribute name="pattern" value="dd-MMM-yyyy hh:mm:ss a Z" />
<f:attribute name="timeZone" value="Asia/Kolkata" />
</h:outputText>
в случае, если вы намерены создайте свой собственный <my:convertLocalDateTime>
с атрибутами вам нужно будет добавить их как бобовые свойства с геттерами/сеттерами в класс конвертера и зарегистрировать его в *.taglib.xml
как показано в этот ответ: создание пользовательского тега для конвертера с атрибутами
<h:outputText id="display" value="#{bean.dateTime}">
<my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z"
timeZone="Asia/Kolkata" />
</h:outputText>