【问题标题】:How to use java.time.ZonedDateTime / LocalDateTime in p:calendar如何在 p:calendar 中使用 java.time.ZonedDateTime / LocalDateTime
【发布时间】:2016-04-25 07:24:45
【问题描述】:

我一直在 Java EE 应用程序中使用 Joda Time 进行日期时间操作,其中关联客户端提交的日期时间字符串表示在提交到数据库之前已使用以下转换例程进行转换,即在JSF 转换器中的getAsObject() 方法。

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));

给出的本地时区比UTC / GMT 早5 小时30 分钟。因此,转换为UTC 应从给定的日期时间中减去 5 小时 30 分钟,这使用 Joda 时间正确发生。它按预期显示以下输出。

05-Jan-2016 09:34:44 AM +0000

► 时区偏移+0530 代替+05:30 已被采用,因为它依赖于<p:calendar>,它以这种格式提交时区偏移。似乎不可能改变<p:calendar> 的这种行为(否则这个问题本身就不需要)。


但是,如果尝试使用 Java 8 中的 Java Time API,同样的事情会被破坏。

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

为什么这两个 API 在这方面有不同的行为在这个问题中被完全忽略了。

尽管<p:calendar> 内部使用SimpleDateFormatjava.util.Date,但Java 8 中的<p:calendar> 和Java Time 可以考虑采用哪种中间方法以一致且连贯地工作?


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>

时区完全透明地依赖于用户当前的时区。

bean 只有一个属性。

@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.
    }
}

这将以意想不到的方式工作,如倒数第二个示例/前三个代码 sn-ps 中的中间所示。

具体来说,如果你输入05-Jan-2016 12:00:00 AM +0530,它会重新显示05-Jan-2016 05:30:00 AM IST,因为原来转换器中05-Jan-2016 12:00:00 AM +0530UTC的转换失败了。

从偏移量为+05:30 的本地时区转换为UTC,然后从UTC 转换回该时区显然必须重新显示通过日历组件输入的相同日期时间,这是基本功能给定的转换器。


更新:

java.sql.Timestampjava.time.ZonedDateTime 相互转换的JPA 转换器。

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);
    }
}

【问题讨论】:

    标签: jsf primefaces calendar timezone java-time


    【解决方案1】:

    您的具体问题是您从 Joda 的无区域日期时间实例 DateTime 迁移到 Java8 的区域日期时间实例 ZonedDateTime 而不是 Java8 的无区域日期时间实例 LocalDateTime

    使用ZonedDateTime(或OffsetDateTime)代替LocalDateTime需要至少2个额外的改变:

    1. 在日期时间转换期间不要强制使用时区(偏移)。相反,解析时会使用输入字符串的时区(如果有),格式化时必须使用存储在ZonedDateTime实例中的时区。

      DateTimeFormatter#withZone() 只会给出与ZonedDateTime 混淆的结果,因为它会在解析期间充当后备(仅在输入字符串或格式模式中不存在时区时使用),并且在格式化期间充当覆盖(存储在ZonedDateTime 中的时区被完全忽略)。这是您可观察到的问题的根本原因。只需在创建格式化程序时省略 withZone() 即可修复它。

      请注意,如果您指定了转换器,但没有timeOnly="true",则无需指定&lt;p:calendar timeZone&gt;。即使这样做,您也宁愿使用TimeZone.getTimeZone(zonedDateTime.getZone()) 而不是硬编码。

    2. 您需要在所有层(包括数据库)上携带时区(偏移量)。但是,如果您的数据库具有“没有时区的日期时间”列类型,那么时区信息会在持久化期间丢失,并且在从数据库返回时会遇到麻烦。

      不清楚您使用的是哪个 DB,但请记住,某些 DB 不支持 TIMESTAMP WITH TIME ZONE 列类型,如 OraclePostgreSQL DB 中已知的那样。例如,MySQL does not support it。您需要第二列。

    如果这些更改不可接受,那么您需要退回到 LocalDateTime 并在所有层(包括数据库)中依赖固定/预定义的时区。通常使用 UTC。


    在 JSF 和 JPA 中处理ZonedDateTime

    ZonedDateTime 与适当的TIMESTAMP WITH TIME ZONE DB 列类型一起使用时,使用下面的JSF 转换器在UI 中的String 和模型中的ZonedDateTime 之间进行转换。此转换器将从父组件中查找 patternlocale 属性。如果父组件本身不支持patternlocale 属性,只需将它们添加为&lt;f:attribute name="..." value="..."&gt;。如果locale 属性不存在,则将使用(默认)&lt;f:view locale&gt;没有 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 和 JDBC 中的 java.util.Calendar 之间进行转换(体面的 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());
        }
    
    }
    

    在 JSF 和 JPA 中处理LocalDateTime

    当使用基于 UTC 的 LocalDateTime 和适当的基于 UTC 的 TIMESTAMP(没有时区!)DB 列类型时,使用下面的 JSF 转换器在 UI 中的 String 和模型中的 LocalDateTime 之间进行转换。该转换器将从父组件中查找patterntimeZonelocale 属性。如果父组件本身不支持patterntimeZone 和/或locale 属性,只需将它们添加为&lt;f:attribute name="..." value="..."&gt;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 和 JDBC 中的 java.sql.Timestamp 之间进行转换(体面的 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();
        }
    
    }
    

    使用&lt;p:calendar&gt;LocalDateTimeConverter 应用于您的具体案例

    您需要更改以下内容:

    1. 由于&lt;p:calendar&gt; 不通过forClass 查找转换器,您需要在faces-config.xml 中使用&lt;converter&gt;&lt;converter-id&gt;localDateTimeConverter 重新注册它,或者更改如下注释

       @FacesConverter("localDateTimeConverter")
      
    2. 由于没有timeOnly="true"&lt;p:calendar&gt; 会忽略timeZone,并在弹出窗口中提供编辑它的选项,因此您需要删除timeZone 属性以避免转换器混淆(此属性仅当pattern 中没有时区时需要)。

    3. 您需要在输出期间指定所需的显示timeZone属性(使用ZonedDateTimeConverter时不需要此属性,因为它已存储在ZonedDateTime中。

    这是完整的工作 sn-p:

    <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>
    

    如果您打算使用属性创建自己的&lt;my:convertLocalDateTime&gt;,则需要将它们作为带有getter/setter 的类bean 属性添加到转换器类中,并将其注册到*.taglib.xml,如以下答案所示: Creating custom tag for Converter with attributes

    <h:outputText id="display" value="#{bean.dateTime}">
        <my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z" 
                                 timeZone="Asia/Kolkata" />
    </h:outputText>
    

    【讨论】:

    • 它正在工作。谢谢。答案至少值得一笔赏金,因为它花费了相当多的时间。我明天将开始赏金。
    • 如果用户将区域偏移量更改为 &lt;p:calendar&gt; 中的默认值以外的值,例如 -0500 (America/New_York),则会产生令人困惑的结果,因为转换器根据用户选择的区域继续工作。在&lt;p:calendar&gt; 中更改的区域将无效。如果输入07-Jan-2016 12:00:00 AM -0500,则要插入数据库的预期日期时间将为07-Jan-2016 05:00:00 AM ET,但将根据所选区域+0530 插入06-Jan-2016 06:30:00 PM ET。日历和转换器可以以某种方式同步吗?
    • 对不起,我无法复制它。更改时区后是否也更改了输入时间?
    • 更改&lt;p:calendar&gt; 中的时区后,日期和时间不会更改。关于 Java SE 环境,转换器本质上是 - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneId.of("Asia/Kolkata")); LocalDateTime localDateTime = ZonedDateTime.parse("07-Jan-2016 12:00:00 AM -0500", formatter).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime(); 在这种情况下,formatter.format(localDateTime) 返回 06-Jan-2016 06:30:00 PM ET 而预期返回 07-Jan-2016 05:00:00 AM ET-0500 转换为 -0000
    • @Gilberto:JSF 2.3 将通过f:convertDateTime 提供内置支持。
    猜你喜欢
    • 2013-05-28
    • 1970-01-01
    • 1970-01-01
    • 2020-05-27
    • 2021-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-06
    相关资源
    最近更新 更多