如何在 p:calendar 中使用 java.time.ZonedDateTime/LocalDateTime [英] How to use java.time.ZonedDateTime / LocalDateTime in p:calendar

查看:33
本文介绍了如何在 p:calendar 中使用 java.time.ZonedDateTime/LocalDateTime的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I had been using Joda Time for date-time manipulation in a Java EE application in which a string representation of date-time submitted by the associated client had been converted using the following conversion routine before submitting it to a database i.e. in the getAsObject() method in a JSF converter.

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 应该从使用 Joda 时间正确发生的给定日期时间减去 5 小时 30 分钟.它按预期显示以下输出.

The local time zone given is 5 hours and 30 minutes ahead of UTC / GMT. Therefore, the conversion to UTC should deduct 5 hours and 30 minutes from the date-time given which happens correctly using Joda Time. It displays the following output as expected.

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

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

► The time zone offset +0530 in place of +05:30 has been taken because it is dependent upon <p:calendar> which submits a zone offset in this format. It does not seem possible to change this behaviour of <p:calendar> (This question itself would not have been needed otherwise).

然而,如果尝试在 Java 8 中使用 Java Time API,同样的事情就会被破坏.

The same thing is however broken, if attempted using the Java Time API in 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));

意外显示以下错误输出.

It unexpectedly displays the following incorrect output.

05-Jan-2016 03:04:44 PM +0000

显然,转换的日期时间不是根据它应该转换的UTC.

Obviously, the date-time converted is not according to UTC in which it is supposed to convert.

它需要通过以下更改才能正常工作.

It requires the following changes to be adopted for it to work correctly.

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

Z has been replaced with z and +0530 has been replaced with +05:30.

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

Why these two APIs have a different behaviour in this regard has been ignored wholeheartedly in this question.

<p:calendar> 和 Java Time 在 Java 8 中可以考虑采用什么中间方式,以便通过 <p:calendar> 在内部保持一致和连贯的工作使用 SimpleDateFormatjava.util.Date?

What middle way approach can be considered for <p:calendar> and Java Time in Java 8 to work consistently and coherently though <p:calendar> internally uses SimpleDateFormat along with java.util.Date?

JSF 中不成功的测试场景.

The unsuccessful test scenario in 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.

XHTML having <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只有一个属性.

The bean having nothing other than a single property.

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

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

This will work in an unexpected way as demonstrated in the second last example / middle in the first three code snippets.

具体来说,如果您输入05-Jan-2016 12:00:00 AM +0530,它将重新显示05-Jan-2016 05:30:00 AM IST> 因为在转换器中 05-Jan-2016 12:00:00 AM +0530UTC 的原始转换失败.

Specifically, if you enter 05-Jan-2016 12:00:00 AM +0530, it will redisplay 05-Jan-2016 05:30:00 AM IST because the original conversion of 05-Jan-2016 12:00:00 AM +0530 to UTC in the converter fails.

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

Conversion from a local time zone whose offset is +05:30 to UTC and then conversion from UTC back to that time zone must obviously redisplay the same date-time as inputted through the calendar component which is the rudimentary functionality of the converter given.

更新:

java.sql.Timestampjava.time.ZonedDateTimejava.time.ZonedDateTime 之间转换的 JPA 转换器.

The JPA converter converting to and from java.sql.Timestamp and 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);
    }
}

推荐答案

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

Your concrete problem is that you migrated from Joda's zoneless date time instance DateTime to Java8's zoned date time instance ZonedDateTime instead of Java8's zoneless date time instance LocalDateTime.

使用 ZonedDateTime(或 OffsetDateTime) 而不是 LocalDateTime 需要至少 2 个额外的更改:

Using ZonedDateTime (or OffsetDateTime) instead of LocalDateTime requires at least 2 additional changes:

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

  1. Do not force a time zone (offset) during date time conversion. Instead, the time zone of the input string, if any, will be used during parsing, and the time zone stored in ZonedDateTime instance must be used during formatting.

DateTimeFormatter#withZone() 只会用 ZonedDateTime 给出令人困惑的结果,因为它会在解析过程中充当后备(它仅在没有时区时使用)输入字符串或格式模式),它将在格式化期间充当覆盖(存储在 ZonedDateTime 中的时区被完全忽略).这是您的可观察问题的根本原因.在创建格式化程序时省略 withZone() 应该可以修复它.

The DateTimeFormatter#withZone() will only give confusing results with ZonedDateTime as it will act as fallback during parsing (it's only used when time zone is absent in input string or format pattern), and it will act as override during formatting (the time zone stored in ZonedDateTime is entirely ignored). This is the root cause of your observable problem. Just omitting withZone() while creating the formatter should fix it.

请注意,当您指定了转换器,并且没有 timeOnly="true" 时,则不需要指定 ;.即使您这样做,您也更愿意使用 TimeZone.getTimeZone(zonedDateTime.getZone()) 而不是对其进行硬编码.

Do note that when you have specified a converter, and don't have timeOnly="true", then you don't need to specify <p:calendar timeZone>. Even when you do, you'd rather like to use TimeZone.getTimeZone(zonedDateTime.getZone()) instead of hardcoding it.

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

You need to carry the time zone (offset) along over all layers, including the database. If your database, however, has a "date time without time zone" column type, then the time zone information gets lost during persist and you will run into trouble when serving back from database.

不清楚您使用的是哪个 DB,但请记住,某些 DB 不支持 OraclePostgreSQL 数据库.例如,MySQL 不支持.您需要第二列.

It's unclear which DB you're using, but keep in mind that some DBs doesn't support a TIMESTAMP WITH TIME ZONE column type as known from Oracle and PostgreSQL DBs. For example, MySQL does not support it. You'd need a second column.

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

If those changes are not acceptable, then you need to step back to LocalDateTime and rely on fixed/predefined time zone throughout all layers, including the database. Usually UTC is used for this.

当将 ZonedDateTime 与适当的 TIMESTAMP WITH TIME ZONE 数据库列类型一起使用时,请使用以下 JSF 转换器在 UI 中的 String 之间进行转换和模型中的 ZonedDateTime.此转换器将从父组件中查找 patternlocale 属性.如果父组件本身不支持 patternlocale 属性,只需将它们添加为 <f:attribute name="...";值=...">.如果 locale 属性不存在,则将使用(默认).没有没有 timeZone 属性,原因如上面#1 中所述.

When using ZonedDateTime with an appropriate TIMESTAMP WITH TIME ZONE DB column type, use the below JSF converter to convert between String in the UI and ZonedDateTime in the model. This converter will lookup the pattern and locale attributes from the parent component. If the parent component doesn't natively support a pattern or locale attribute, simply add them as <f:attribute name="..." value="...">. If the locale attribute is absent, the (default) <f:view locale> will be used instead. There is no timeZone attribute for the reason as explained in #1 here above.

@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 类型列):

And use the below JPA converter to convert between ZonedDateTime in the model and java.util.Calendar in JDBC (the decent JDBC driver will require/use it for TIMESTAMP WITH TIME ZONE typed column):

@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 转换器在 String 之间进行转换 在 UI 和 LocalDateTime 在模型中.此转换器将从父组件中查找 patterntimeZonelocale 属性.如果父组件本身不支持 patterntimeZone 和/或 locale 属性,只需将它们添加为 <f:attribute name="...";值=...">.timeZone 属性必须表示输入字符串的回退时区(当 pattern 不包含时区时)和输出字符串的时区.


Dealing with LocalDateTime in JSF and JPA

When using UTC based LocalDateTime with an appropriate UTC based TIMESTAMP (without time zone!) DB column type, use the below JSF converter to convert between String in the UI and LocalDateTime in the model. This converter will lookup the pattern, timeZone and locale attributes from the parent component. If the parent component doesn't natively support a pattern, timeZone and/or locale attribute, simply add them as <f:attribute name="..." value="...">. The timeZone attribute must represent the fallback time zone of the input string (when the pattern doesn't contain a time zone), and the time zone of the output string.

@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 类型列):

And use the below JPA converter to convert between LocalDateTime in the model and java.sql.Timestamp in JDBC (the decent JDBC driver will require/use it for TIMESTAMP typed column):

@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 应用于您的特定情况

您需要更改以下内容:


Applying LocalDateTimeConverter to your specific case with <p:calendar>

You need to change the below:

  1. 由于 不通过 forClass 查找转换器,您要么需要使用 重新注册它faces-config.xml中的localDateTimeConverter,或者修改如下注解

  1. As the <p:calendar> doesn't lookup converters by forClass, you'd either need to re-register it with <converter><converter-id>localDateTimeConverter in faces-config.xml, or to alter the annotation as below

 @FacesConverter("localDateTimeConverter")

  • 由于 没有 timeOnly="true" 忽略了 timeZone,并在弹出编辑选项,需要去掉timeZone属性以免转换器混淆(只有pattern中没有时区时才需要这个属性).

  • As the <p:calendar> without timeOnly="true" ignores the timeZone, and offers in the popup an option to edit it, you need to remove the timeZone attribute to avoid that the converter gets confused (this attribute is only required when the time zone is absent in the pattern).

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

    You need to specify the desired display timeZone attribute during output (this attribute is not required when using ZonedDateTimeConverter as it's already stored in 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>,您需要将它们作为带有 getter/setter 的 bean 类属性添加到转换器类中并注册它在 *.taglib.xml 中,如本答案所示:为具有属性的转换器创建自定义标记

    In case you intend to create your own <my:convertLocalDateTime> with attributes, you'd need to add them as bean-like properties with getters/setters to the converter class and register it in *.taglib.xml as demonstrated in this answer: 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>
    

    这篇关于如何在 p:calendar 中使用 java.time.ZonedDateTime/LocalDateTime的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆