我一直在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
应从使用Joda Time正确发生的日期时间开始扣除5小时30分钟。它按预期显示以下输出。
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>
内部使用<p:calendar>
和SimpleDateFormat
,但{8}中的java.util.Date
和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>
时区完全透明地取决于用户的当前时区。
豆只有一个属性。
@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
,则会重新显示05-Jan-2016 05:30:00 AM IST
,因为转换器中05-Jan-2016 12:00:00 AM +0530
到UTC
的原始转换失败。
从偏移量为+05:30
的本地时区转换为UTC
,然后从UTC
转换回该时区,显然必须重新显示与通过日历输入的日期时间相同的日期时间组件,这是转换器的基本功能。
更新
JPA转换器转换为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);
}
}
答案 0 :(得分:32)
您的具体问题是您从Joda的无区域日期时间实例DateTime
迁移到Java8的分区日期时间实例ZonedDateTime
而不是Java8的无区域日期时间实例LocalDateTime
。
使用ZonedDateTime
(或OffsetDateTime
)代替LocalDateTime
至少需要进行2次更改:
请勿在日期时间转换期间强制使用时区(偏移)。相反,在解析期间将使用输入字符串的时区(如果有),并且在格式化期间必须使用存储在ZonedDateTime
实例中的时区。
DateTimeFormatter#withZone()
只会给ZonedDateTime
带来令人困惑的结果,因为它在解析过程中会起到后退的作用(只有在输入字符串或格式模式中没有时区时才会使用它),以及它将在格式化期间充当覆盖(存储在ZonedDateTime
中的时区完全被忽略)。这是您可观察到的问题的根本原因。只需在创建格式化程序时省略withZone()
即可修复它。
请注意,当您指定了转换器但没有timeOnly="true"
时,您就不需要指定<p:calendar timeZone>
。即使你这样做,也不想使用TimeZone.getTimeZone(zonedDateTime.getZone())
代替硬编码。
您需要在所有图层上携带时区(偏移),包括数据库。但是,如果您的数据库具有没有时区的&#34;日期时间&#34;列类型,然后在持久化期间时区信息丢失,从数据库返回时会遇到麻烦。
目前还不清楚您正在使用哪个数据库,但请注意,某些数据库不支持Oracle和{{{0}}已知的TIMESTAMP WITH TIME ZONE
列类型3}} DBs。例如,PostgreSQL。您需要第二列。
如果这些更改不可接受,则需要返回LocalDateTime
并依赖所有层(包括数据库)中的固定/预定义时区。通常使用UTC。
ZonedDateTime
将ZonedDateTime
与适当的TIMESTAMP WITH TIME ZONE
数据库列类型一起使用时,请使用以下JSF转换器在UI中的String
和模型中的ZonedDateTime
之间进行转换。此转换器将从父组件中查找pattern
和locale
属性。如果父组件本身不支持pattern
或locale
属性,只需将其添加为<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
和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());
}
}
LocalDateTime
当使用基于UTC的LocalDateTime
和基于UTC的TIMESTAMP
(没有时区!)DB列类型时,使用以下JSF转换器在UI中String
之间转换{模型中的{1}}。此转换器将从父组件中查找LocalDateTime
,pattern
和timeZone
属性。如果父组件本身不支持locale
,pattern
和/或timeZone
属性,只需将其添加为locale
即可。 <f:attribute name="..." value="...">
属性必须表示输入字符串的回退时区(当timeZone
不包含时区时),以及输出字符串的时区。
pattern
并使用下面的JPA转换器在模型中的@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;
}
}
和JDBC中的LocalDateTime
之间进行转换(体面的JDBC驱动程序将需要/使用它用于java.sql.Timestamp
类型的列:)
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>
,则需要将它们作为类似bean的属性添加到转换器类中,并在<my:convertLocalDateTime>
中注册它如答案所示:MySQL does not support it
*.taglib.xml