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

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

问题描述

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

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

本地时区提前5小时30分钟 UTC / GMT 。因此,转换为 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 ,因为它依赖于以此格式提交区域偏移的< p:calendar> 。似乎不可能更改< p:calendar> 的此行为(否则不需要此问题本身)。

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

其中依次显示以下内容。

Which in turn displays the following.

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> 内部使用 SimpleDateFormat java.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?

失败测试场景。

转换器:

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

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 +0530 UTC

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.

更新:

JPA转换器转换为 java.sql.Timestamp java.time.ZonedDateTime

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的无日期时间实例 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 需要


  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,那么您不需要指定< p:calendar timeZone> 。即使你这样做,你宁愿使用 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不支持从 TIMESTAMP WITH TIME ZONE 列类型=https://docs.oracle.com/cd/B19306_01/server.102/b14225/ch4datetime.htm> Oracle PostgreSQL DB。例如, 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 DB列类型,使用以下JSF转换器在UI中的 String ZonedDateTime 模型。该转换器将从父组件中查找模式 locale 属性。如果父组件本身不支持模式 locale 属性,只需将它们添加为< f:attribute name =...value =...> 。如果 locale 属性不存在,则将使用(默认)< f:view 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转换器在JDBC中的 ZonedDateTime 之间进行转换(JDBC中的 java.util.Calendar 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数据库列类型,使用以下JSF转换器在之间转换 String LocalDateTime 。此转换器将查找模式 timeZone locale 属性从父组件。如果父组件本身不支持模式 timeZone 和/或 locale 属性,只需将它们添加为< f:attribute name =...value =...> timeZone 属性必须表示输入字符串的回退时区(当模式不包含时区时)和输出字符串的时区。


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

}





$ b b

LocalDateTimeConverter 应用于您的特定案例< p:calendar>



您需要更改以下内容:


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

You need to change the below:


  1. 由于 p:calendar> 不会通过 forClass 查找转换器,您需要重新注册 ;或者如下改变注释:

  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< p:calendar> code> timeZone ,并在弹出窗口中提供一个编辑它的选项,您需要删除 timeZone 属性, (模式中缺少时区时,才需要此属性)。

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

    这是完整的工作片段:

    Here's the full working snippet:

    <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& / code>的属性,你需要将它们作为类bean的属性添加到转换器类中,并将它们注册到 *。taglib.xml 如此答案中所示:为具有属性的Converter创建自定义标记

    <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天全站免登陆