feat(gson): 添加 Gson 适配器以支持 JSR-310 中常用的类

Reviewed-on: #57
This commit is contained in:
zhouxy108 2025-06-07 01:02:15 +08:00
commit 336d99d4ba
5 changed files with 379 additions and 6 deletions

View File

@ -55,6 +55,13 @@
<optional>true</optional>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<optional>true</optional>
</dependency>
<!-- ========== Test Dependencies ========== -->
<dependency>
@ -111,12 +118,6 @@
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>test</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -0,0 +1,187 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.gson.adapter;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import xyz.zhouxy.plusone.commons.util.AssertTools;
/**
* 包含 JSR-310 相关数据类型的 {@code TypeAdapter}
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @since 1.1.0
* @see TypeAdapter
* @see com.google.gson.GsonBuilder
*/
public class JSR310TypeAdapters {
/**
* {@code LocalDate} {@code TypeAdapter}
* 用于 Gson {@code LocalDate} 进行相互转换
*/
public static final class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
private final DateTimeFormatter dateTimeFormatter;
/**
* 默认构造函数
* 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化
*/
public LocalDateTypeAdapter() {
this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE;
}
/**
* 构造函数
* 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化
*
* @param formatter 用于序列化 {@link LocalDate} 的格式化器不可为 {@code null}
*/
public LocalDateTypeAdapter(DateTimeFormatter formatter) {
AssertTools.checkArgumentNotNull(formatter, "formatter can not be null.");
this.dateTimeFormatter = formatter;
}
/** {@inheritDoc} */
@Override
public void write(JsonWriter out, LocalDate value) throws IOException {
out.value(dateTimeFormatter.format(value));
}
/** {@inheritDoc} */
@Override
public LocalDate read(JsonReader in) throws IOException {
return LocalDate.parse(in.nextString(), dateTimeFormatter);
}
}
/**
* {@code LocalDateTime} {@code TypeAdapter}
* 用于 Gson {@code LocalDateTime} 进行相互转换
*/
public static final class LocalDateTimeTypeAdapter extends TypeAdapter<LocalDateTime> {
private final DateTimeFormatter dateTimeFormatter;
/**
* 默认构造函数
* 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化
*/
public LocalDateTimeTypeAdapter() {
this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
}
/**
* 构造函数
* 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化
*
* @param formatter 用于序列化 {@link LocalDateTime} 的格式化器不可为 {@code null}
*/
public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) {
AssertTools.checkArgumentNotNull(formatter, "formatter can not be null.");
this.dateTimeFormatter = formatter;
}
/** {@inheritDoc} */
@Override
public void write(JsonWriter out, LocalDateTime value) throws IOException {
out.value(dateTimeFormatter.format(value));
}
/** {@inheritDoc} */
@Override
public LocalDateTime read(JsonReader in) throws IOException {
return LocalDateTime.parse(in.nextString(), dateTimeFormatter);
}
}
/**
* {@code ZonedDateTime} {@code TypeAdapter}
* 用于 Gson {@code ZonedDateTime} 进行相互转换
*/
public static final class ZonedDateTimeTypeAdapter extends TypeAdapter<ZonedDateTime> {
private final DateTimeFormatter dateTimeFormatter;
/**
* 默认构造函数
* 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化
*/
public ZonedDateTimeTypeAdapter() {
this.dateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME;
}
/**
* 构造函数
* 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化
*
* @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器不可为 {@code null}
*/
public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) {
AssertTools.checkArgumentNotNull(formatter, "formatter can not be null.");
this.dateTimeFormatter = formatter;
}
/** {@inheritDoc} */
@Override
public void write(JsonWriter out, ZonedDateTime value) throws IOException {
out.value(dateTimeFormatter.format(value));
}
/** {@inheritDoc} */
@Override
public ZonedDateTime read(JsonReader in) throws IOException {
return ZonedDateTime.parse(in.nextString(), dateTimeFormatter);
}
}
/**
* {@code Instant} {@code TypeAdapter}
* 用于 Gson {@code Instant} 进行相互转换
*
* <p>
* 使用 {@link DateTimeFormatter#ISO_INSTANT} 进行 {@link Instant} 的序列化与反序列化
* </p>
*/
public static final class InstantTypeAdapter extends TypeAdapter<Instant> {
/** {@inheritDoc} */
@Override
public void write(JsonWriter out, Instant value) throws IOException {
out.value(DateTimeFormatter.ISO_INSTANT.format(value));
}
/** {@inheritDoc} */
@Override
public Instant read(JsonReader in) throws IOException {
return Instant.parse(in.nextString());
}
}
private JSR310TypeAdapters() {
throw new IllegalStateException("Utility class");
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Gson 相关类型适配器
*/
package xyz.zhouxy.plusone.commons.gson.adapter;

View File

@ -0,0 +1,20 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Gson 相关辅助工具
*/
package xyz.zhouxy.plusone.commons.gson;

View File

@ -0,0 +1,145 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.gson.adapter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.commons.gson.adapter.JSR310TypeAdapters.*;
@Slf4j
public final class JSR310TypeAdaptersTests {
final Gson gsonWithDefaultFormatter = new GsonBuilder()
.registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter().nullSafe())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter().nullSafe())
.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter().nullSafe())
.registerTypeAdapter(Instant.class, new InstantTypeAdapter().nullSafe())
.create();
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
final DateTimeFormatter localDateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy/MM/dd HH:mm:ss")
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.toFormatter();
final DateTimeFormatter zonedDateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy/MM/dd HH:mm:ss")
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.appendZoneId()
.toFormatter();
final Gson gsonWithSpecifiedFormatter = new GsonBuilder()
.registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter(dateFormatter).nullSafe())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter(localDateTimeFormatter).nullSafe())
.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter(zonedDateTimeFormatter).nullSafe())
.create();
final LocalDate date = LocalDate.of(2025, 6, 6);
final LocalDateTime localDateTime = date.atTime(6, 6, 6, 666000000);
final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("+08:00"));
final Instant instant = zonedDateTime.toInstant();
@DisplayName("测试使用 TypeAdapter 中默认的 formatter 进行序列化")
@Test
void test_serialize_defaultFormatter() {
Foo foo = new Foo();
foo.localDate = date;
foo.localDateTime = localDateTime;
foo.zonedDateTime = zonedDateTime;
foo.instant = instant;
String json = String.format(
"{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\",\"instant\":\"%s\"}",
DateTimeFormatter.ISO_LOCAL_DATE.format(date),
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime),
DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime),
DateTimeFormatter.ISO_INSTANT.format(instant)
);
assertEquals(json, gsonWithDefaultFormatter.toJson(foo));
}
@DisplayName("测试指定 formatter 进行序列化")
@Test
void test_serialize_specifiedFormatter() {
Foo foo = new Foo();
foo.localDate = date;
foo.localDateTime = localDateTime;
foo.zonedDateTime = zonedDateTime;
String json = String.format(
"{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}",
dateFormatter.format(date),
localDateTimeFormatter.format(localDateTime),
zonedDateTimeFormatter.format(zonedDateTime)
);
assertEquals(json, gsonWithSpecifiedFormatter.toJson(foo));
}
@DisplayName("测试使用 TypeAdapter 中默认的 formatter 进行反序列化")
@Test
void test_deserialize_defaultFormatter() {
String json = String.format(
"{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\",\"instant\":\"%s\"}",
DateTimeFormatter.ISO_LOCAL_DATE.format(date),
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime),
DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime),
DateTimeFormatter.ISO_INSTANT.format(instant)
);
Foo foo = gsonWithDefaultFormatter.fromJson(json, Foo.class);
assertEquals(date, foo.localDate);
assertEquals(localDateTime, foo.localDateTime);
assertEquals(zonedDateTime, foo.zonedDateTime);
assertEquals(instant, foo.instant);
}
@DisplayName("测试指定 formatter 进行反序列化")
@Test
void test_deserialize_specifiedFormatter() {
String json = String.format(
"{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}",
dateFormatter.format(date),
localDateTimeFormatter.format(localDateTime),
zonedDateTimeFormatter.format(zonedDateTime)
);
Foo foo = gsonWithSpecifiedFormatter.fromJson(json, Foo.class);
assertEquals(date, foo.localDate);
assertEquals(localDateTime, foo.localDateTime);
assertEquals(zonedDateTime, foo.zonedDateTime);
}
static class Foo {
LocalDate localDate;
LocalDateTime localDateTime;
ZonedDateTime zonedDateTime;
Instant instant;
}
}