| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.framework.config; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import com.fasterxml.jackson.core.JsonGenerator; |
| | | import com.fasterxml.jackson.core.JsonParser; |
| | | import com.fasterxml.jackson.core.JsonToken; |
| | | import com.fasterxml.jackson.databind.BeanDescription; |
| | | import com.fasterxml.jackson.databind.BeanProperty; |
| | | import com.fasterxml.jackson.databind.DeserializationConfig; |
| | | import com.fasterxml.jackson.databind.DeserializationContext; |
| | | import com.fasterxml.jackson.databind.JsonDeserializer; |
| | | import com.fasterxml.jackson.databind.JsonSerializer; |
| | | import com.fasterxml.jackson.databind.SerializationConfig; |
| | | import com.fasterxml.jackson.databind.SerializerProvider; |
| | | import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; |
| | | import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; |
| | | import com.fasterxml.jackson.databind.deser.SettableBeanProperty; |
| | | import com.fasterxml.jackson.databind.introspect.AnnotatedMember; |
| | | import com.fasterxml.jackson.databind.module.SimpleModule; |
| | | import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; |
| | | import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; |
| | | import com.fasterxml.jackson.databind.ser.ContextualSerializer; |
| | | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | import java.io.IOException; |
| | | import java.text.ParsePosition; |
| | | import java.text.SimpleDateFormat; |
| | | import java.time.Instant; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Date; |
| | | import java.util.HashSet; |
| | | import java.util.Iterator; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.TimeZone; |
| | | |
| | | /** |
| | | * æ¶é´æ ¼å¼å
¨å±é
ç½®ï¼æ¥å£åºåä¸å¾ç²¾ç¡®å°åï¼{@code yyyy-MM-dd HH:mm}ï¼ï¼å
¥å宽æ¾å
¼å®¹å¸¦/ä¸å¸¦ç§ã |
| | | * |
| | | * <p><b>为ä»ä¹ç¨å
¨å±é
ç½®è䏿¯éåæ®µæ¹æ³¨è§£ï¼</b>ç³»ç»é |
| | | * {@code @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")} æ 182 å¤ã{@code @DateTimeFormat} 73 å¤ï¼ |
| | | * éä¸ªæ¹æ¢å®¹ææ¼ï¼æ¼ä¸å¤å°±æ¯æäº¤ 400ï¼ï¼ä¹è¦çä¸å°æªå 注解ç {@code Date} åæ®µã |
| | | * è¿éç¨ Jackson ç {@link BeanSerializerModifier} / {@link BeanDeserializerModifier} |
| | | * å¨<b>䏿¹ä»»ä½æ³¨è§£</b>çåæä¸ç»ä¸æ ¼å¼åå£å¾ã |
| | | * |
| | | * <p><b>è§åï¼</b> |
| | | * <ol> |
| | | * <li>åºååï¼ææ {@code Date} è¾åº {@code yyyy-MM-dd HH:mm}ï¼æ²¿ç¨ |
| | | * {@link ApplicationConfig} é
ç½®ç JVM é»è®¤æ¶åºï¼åæ®µä¸æ¾å¼å£°æäº |
| | | * {@code @JsonFormat(timezone=...)} ç仿å
¶æ¶åºï¼ï¼</li> |
| | | * <li>ååºååï¼å
¼å®¹ {@code yyyy-MM-dd HH:mm:ss} / {@code yyyy-MM-dd HH:mm} / |
| | | * {@code yyyy-MM-dd} / ææ åé / ISO8601 / æ¶é´æ³ ââ è¿æ ·å端åå¡«"æ ç§"çå¼åæ ·æäº¤ä¹ä¸ä¼ 400ï¼</li> |
| | | * <li>è¿ç»´æ¥å¿ï¼æä½æ¥å¿/ç»å½æ¥å¿/宿¶ä»»å¡æ¥å¿ï¼<b>ä¿çç§</b>ï¼ä¾¿äºææ¥é®é¢ã</li> |
| | | * </ol> |
| | | * |
| | | * <p><b>ä¸å½±åçèå´ï¼</b>æ°æ®åºåå¨ï¼ä»ä¸º datetimeï¼ç§ä½ç
§å¸¸ä¿çï¼ã |
| | | * Excel 导å
¥å¯¼åºï¼èµ° {@code @Excel.dateFormat}ï¼æªæ¹å¨ï¼ã |
| | | * è´è¿å忍鿥æï¼èªå»º {@code SimpleDateFormat}ï¼æªæ¹å¨ï¼ã |
| | | * ééæ¶æ¯æ¶é´ï¼{@code FundNotifyMessageBuilder} èªå»ºæ ¼å¼ï¼ã |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Configuration |
| | | public class JacksonDateConfig { |
| | | |
| | | /** æ ç§æ ¼å¼ */ |
| | | private static final String PATTERN_NO_SECONDS = "yyyy-MM-dd HH:mm"; |
| | | /** æç§æ ¼å¼ï¼è¿ç»´æ¥å¿ç¨ï¼ */ |
| | | private static final String PATTERN_WITH_SECONDS = "yyyy-MM-dd HH:mm:ss"; |
| | | |
| | | /** ä¿çç§çç±»ï¼è¿ç»´æ¥å¿ï¼ç§å¯¹ææ¥æç¨ */ |
| | | private static final Set<String> KEEP_SECONDS_CLASSES = new HashSet<String>(Arrays.asList( |
| | | "com.ruoyi.system.domain.SysOperLog", |
| | | "com.ruoyi.system.domain.SysLogininfor", |
| | | "com.ruoyi.quartz.domain.SysJobLog" |
| | | )); |
| | | |
| | | /** ååºååå°è¯é¡ºåºï¼ä»æå
·ä½å°æå®½æ¾ */ |
| | | private static final String[] PARSE_PATTERNS = new String[]{ |
| | | "yyyy-MM-dd HH:mm:ss.SSS", |
| | | "yyyy-MM-dd HH:mm:ss", |
| | | "yyyy-MM-dd HH:mm", |
| | | "yyyy-MM-dd'T'HH:mm:ss.SSS", |
| | | "yyyy-MM-dd'T'HH:mm:ss", |
| | | "yyyy-MM-dd'T'HH:mm", |
| | | "yyyy/MM/dd HH:mm:ss", |
| | | "yyyy/MM/dd HH:mm", |
| | | "yyyy-MM-dd", |
| | | "yyyy/MM/dd", |
| | | "yyyyMMddHHmmss", |
| | | "yyyyMMdd" |
| | | }; |
| | | |
| | | /** |
| | | * 2026-10-15 æ¹é ï¼ä¸åç¨ {@code Jackson2ObjectMapperBuilderCustomizer} + {@code modulesToInstall}ã |
| | | * |
| | | * <p><b>为ä»ä¹æ¹ï¼</b>{@code Jackson2ObjectMapperBuilder.modulesToInstall(...)} çè¯ä¹æ¯ |
| | | * "设置"模åå表ï¼ä¸æ¯è¿½å ï¼ï¼è Spring Boot èªèº«ä¹ç¨å䏿¹æ³æ³¨åå®èªå¸¦ç模åï¼JavaTime çï¼ï¼ |
| | | * 两个å®å¶å¨äºç¸è¦çï¼è°åæ§è¡è°çå表çæã宿µåæï¼æ¬æ¨¡å被丢æ ââ |
| | | * 表ç°ä¸º"ç±»å¨å
éãbean ä¹å建äºï¼ä½ Date ç宽æ¾è§£æä¸å»ç§åºååé½ä¸èµ·ä½ç¨"ï¼ |
| | | * å端æ {@code yyyy-MM-dd} æäº¤æ¶ç´æ¥ 400ï¼Cannot deserialize ... expected format yyyy-MM-dd HH:mm:ssï¼ã |
| | | * |
| | | * <p>æ¹ä¸ºææ¨¡åå£°ææ {@code SimpleModule} beanï¼Spring Boot éè¿ {@code ObjectProvider<Module>} |
| | | * ç»ä¸æ¶éææ Module bean 并䏿¬¡æ§å®è£
ï¼ä¸å®å¶å¨çæ§è¡é¡ºåºæ å
³ã |
| | | */ |
| | | @Bean |
| | | public SimpleModule dateNoSecondsModule() { |
| | | SimpleModule module = new SimpleModule("dateNoSecondsModule"); |
| | | module.addSerializer(Date.class, new NoSecondsDateSerializer()); |
| | | module.setSerializerModifier(new NoSecondsDateSerializerModifier()); |
| | | module.setDeserializerModifier(new LenientDateDeserializerModifier()); |
| | | // å¯å¨èªè¯ï¼è¿æ¡æ¥å¿åºç°å³è¯´ææ¨¡å已被å建并ä¼è¢« Spring Boot å®è£
|
| | | LoggerFactory.getLogger(JacksonDateConfig.class).info( |
| | | "[JacksonDateConfig] å·²å¯ç¨ï¼Date åºåå»ç§({})ï¼å
¥å宽æ¾å
¼å®¹({})", |
| | | PATTERN_NO_SECONDS, String.join(" / ", PARSE_PATTERNS)); |
| | | return module; |
| | | } |
| | | |
| | | /** |
| | | * åºååå¨ï¼æåéè¾åºã |
| | | * åªä½ç¨äºåºååï¼<b>ä¸å½±åå
¥åè§£æ</b>ã |
| | | */ |
| | | static class NoSecondsDateSerializer extends JsonSerializer<Date> implements ContextualSerializer { |
| | | |
| | | private final DateTimeFormatter formatter; |
| | | |
| | | NoSecondsDateSerializer() { |
| | | this.formatter = null; |
| | | } |
| | | |
| | | private NoSecondsDateSerializer(TimeZone timeZone, boolean keepSeconds) { |
| | | this.formatter = DateTimeFormatter |
| | | .ofPattern(keepSeconds ? PATTERN_WITH_SECONDS : PATTERN_NO_SECONDS) |
| | | .withZone((timeZone == null ? TimeZone.getDefault() : timeZone).toZoneId()); |
| | | } |
| | | |
| | | @Override |
| | | public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) { |
| | | TimeZone timeZone = resolveTimeZone(prov.getConfig().getTimeZone(), |
| | | property == null ? null : property.getMember()); |
| | | return new NoSecondsDateSerializer(timeZone, isKeepSeconds(property == null ? null : property.getMember())); |
| | | } |
| | | |
| | | @Override |
| | | public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException { |
| | | if (value == null) { |
| | | gen.writeNull(); |
| | | return; |
| | | } |
| | | if (formatter != null) { |
| | | gen.writeString(formatter.format(value.toInstant())); |
| | | return; |
| | | } |
| | | // å
åºï¼æªè¢« contextualize æ¶æå½åé
ç½®æ¶åºãæ ç§è¾åº |
| | | gen.writeString(DateTimeFormatter.ofPattern(PATTERN_NO_SECONDS) |
| | | .withZone(serializers.getConfig().getTimeZone().toZoneId()) |
| | | .format(value.toInstant())); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 让 Bean 屿§ä¸çæ¥æä¹èµ°ä¸é¢çåºååå¨ã |
| | | * åæ®µçº§ {@code @JsonFormat} çæçåºååå¨ä¼å
级æ´é«ï¼å¿
é¡»å¨è¿éæ¿æ¢æï¼å¦åæ³¨è§£åæ®µä»å¸¦ç§ã |
| | | */ |
| | | static class NoSecondsDateSerializerModifier extends BeanSerializerModifier { |
| | | @Override |
| | | @SuppressWarnings({"unchecked", "rawtypes"}) |
| | | public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, |
| | | List<BeanPropertyWriter> beanProperties) { |
| | | boolean keepSeconds = KEEP_SECONDS_CLASSES.contains(beanDesc.getBeanClass().getName()); |
| | | for (BeanPropertyWriter writer : beanProperties) { |
| | | if (Date.class.isAssignableFrom(writer.getType().getRawClass())) { |
| | | TimeZone timeZone = resolveTimeZone(config.getTimeZone(), writer.getMember()); |
| | | writer.assignSerializer((JsonSerializer) new NoSecondsDateSerializer(timeZone, keepSeconds)); |
| | | } |
| | | } |
| | | return beanProperties; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * å
¥åè§£æï¼å
¼å®¹å¸¦ç§/ä¸å¸¦ç§/çº¯æ¥æ/ISO/æ¶é´æ³ï¼é¿å
"åºåæ ç§ãåå¡«ååæäº¤"å¯¼è´ 400ã |
| | | */ |
| | | static class LenientDateDeserializerModifier extends BeanDeserializerModifier { |
| | | @Override |
| | | @SuppressWarnings({"unchecked", "rawtypes"}) |
| | | public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, |
| | | BeanDeserializerBuilder builder) { |
| | | Iterator<SettableBeanProperty> properties = builder.getProperties(); |
| | | List<SettableBeanProperty> replaced = new ArrayList<SettableBeanProperty>(); |
| | | while (properties.hasNext()) { |
| | | SettableBeanProperty property = properties.next(); |
| | | if (property.getType() != null && Date.class.isAssignableFrom(property.getType().getRawClass())) { |
| | | TimeZone timeZone = resolveTimeZone(config.getTimeZone(), property.getMember()); |
| | | replaced.add(property.withValueDeserializer(new LenientDateDeserializer(timeZone))); |
| | | } |
| | | } |
| | | // ä¸è½è¾¹éåè¾¹æ¹ï¼å
æ¶éåæ¿æ¢ |
| | | for (SettableBeanProperty property : replaced) { |
| | | builder.addOrReplaceProperty(property, true); |
| | | } |
| | | return builder; |
| | | } |
| | | } |
| | | |
| | | /** å®½æ¾æ¥æè§£æå¨ */ |
| | | static class LenientDateDeserializer extends JsonDeserializer<Date> { |
| | | |
| | | private final TimeZone timeZone; |
| | | |
| | | LenientDateDeserializer(TimeZone timeZone) { |
| | | this.timeZone = timeZone == null ? TimeZone.getDefault() : timeZone; |
| | | } |
| | | |
| | | @Override |
| | | public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| | | if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { |
| | | return new Date(p.getLongValue()); |
| | | } |
| | | String text = p.getValueAsString(); |
| | | if (text == null || text.trim().isEmpty()) { |
| | | return null; |
| | | } |
| | | text = text.trim(); |
| | | for (String pattern : PARSE_PATTERNS) { |
| | | SimpleDateFormat format = new SimpleDateFormat(pattern); |
| | | format.setLenient(false); |
| | | format.setTimeZone(timeZone); |
| | | ParsePosition position = new ParsePosition(0); |
| | | Date parsed = format.parse(text, position); |
| | | // å¿
é¡»æ´ä¸²è¢«æ¶è´¹ï¼å¦å "yyyy-MM-dd" 伿 "2026-10-08 15:23" çæ¶åéé»ä¸¢æ |
| | | if (parsed != null && position.getIndex() == text.length()) { |
| | | return parsed; |
| | | } |
| | | } |
| | | try { |
| | | return Date.from(Instant.parse(text)); |
| | | } catch (Exception ignore) { |
| | | // è½å°ä¸é¢ç»ä¸æ¥é |
| | | } |
| | | throw ctxt.weirdStringException(text, Date.class, |
| | | "æ æ³è§£æçæ¶é´æ ¼å¼ï¼æ¯æ yyyy-MM-dd[ HH:mm[:ss]]"); |
| | | } |
| | | } |
| | | |
| | | /** 屿§ä¸æ¾å¼å£°æçæ¶åºä¼å
ï¼å¦åç¨å
¨å±é
ç½®ï¼å³ JVM é»è®¤æ¶åºï¼ */ |
| | | private static TimeZone resolveTimeZone(TimeZone fallback, AnnotatedMember member) { |
| | | if (member != null) { |
| | | try { |
| | | JsonFormat jsonFormat = member.getAnnotation(JsonFormat.class); |
| | | if (jsonFormat != null && jsonFormat.timezone() != null |
| | | && !jsonFormat.timezone().isEmpty() |
| | | && !"##default".equals(jsonFormat.timezone())) { |
| | | return TimeZone.getTimeZone(jsonFormat.timezone()); |
| | | } |
| | | } catch (Exception ignore) { |
| | | // åä¸å°æ³¨è®°æ¶ç¨å
¨å±æ¶åº |
| | | } |
| | | } |
| | | return fallback == null ? TimeZone.getDefault() : fallback; |
| | | } |
| | | |
| | | /** è¿ç»´æ¥å¿ç±»ä¿çç§ */ |
| | | private static boolean isKeepSeconds(AnnotatedMember member) { |
| | | return member != null && KEEP_SECONDS_CLASSES.contains(member.getDeclaringClass().getName()); |
| | | } |
| | | } |