diff --git a/pom.xml b/pom.xml index cd4202d..014c1af 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.11 + 2.7.2 com.yupi diff --git a/src/main/java/com/yupi/springbootinit/common/NumberSerializer.java b/src/main/java/com/yupi/springbootinit/common/NumberSerializer.java new file mode 100644 index 0000000..6d2d298 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/common/NumberSerializer.java @@ -0,0 +1,37 @@ +package com.yupi.springbootinit.common; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; + +/** + * Long 序列化规则 + * + * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 + * + * @author 星语 + */ +@JacksonStdImpl +public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { + + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); + + public NumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, serializers); + } else { + gen.writeString(value.toString()); + } + } +} diff --git a/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeDeserializer.java b/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeDeserializer.java new file mode 100644 index 0000000..a293487 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeDeserializer.java @@ -0,0 +1,27 @@ +package com.yupi.springbootinit.common; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 反序列化器 + * + * @author 老五 + */ +public class TimestampLocalDateTimeDeserializer extends JsonDeserializer { + + public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 将 Long 时间戳,转换为 LocalDateTime 对象 + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } + +} diff --git a/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeSerializer.java b/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeSerializer.java new file mode 100644 index 0000000..4aa18db --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/common/TimestampLocalDateTimeSerializer.java @@ -0,0 +1,26 @@ +package com.yupi.springbootinit.common; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 序列化器 + * + * @author 老五 + */ +public class TimestampLocalDateTimeSerializer extends JsonSerializer { + + public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 将 LocalDateTime 对象,转换为 Long 时间戳 + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } + +} diff --git a/src/main/java/com/yupi/springbootinit/config/RabbitMQConfig.java b/src/main/java/com/yupi/springbootinit/config/RabbitMQConfig.java index 980b298..23b149d 100644 --- a/src/main/java/com/yupi/springbootinit/config/RabbitMQConfig.java +++ b/src/main/java/com/yupi/springbootinit/config/RabbitMQConfig.java @@ -1,6 +1,13 @@ package com.yupi.springbootinit.config; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.rabbitmq.client.ConnectionFactory; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; @@ -18,9 +25,20 @@ public class RabbitMQConfig { public Queue hostInfoQueue(){ return new Queue(QUEUE,true); } + +// +// @Bean +// public MessageConverter messageConverter(){ +// return new Jackson2JsonMessageConverter(); +// } + @Bean - public MessageConverter messageConverter(){ - return new Jackson2JsonMessageConverter(); + public MessageConverter messageConverter() { + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + om.registerModule(new JavaTimeModule()); + return new Jackson2JsonMessageConverter(om); } } \ No newline at end of file diff --git a/src/main/java/com/yupi/springbootinit/config/YudaoJacksonAutoConfiguration.java b/src/main/java/com/yupi/springbootinit/config/YudaoJacksonAutoConfiguration.java new file mode 100644 index 0000000..e864f7d --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/config/YudaoJacksonAutoConfiguration.java @@ -0,0 +1,53 @@ +package com.yupi.springbootinit.config; + +import cn.hutool.core.collection.CollUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import com.yupi.springbootinit.common.NumberSerializer; +import com.yupi.springbootinit.common.TimestampLocalDateTimeDeserializer; +import com.yupi.springbootinit.common.TimestampLocalDateTimeSerializer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import com.yupi.springbootinit.utils.JsonUtils; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.context.annotation.Bean; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@AutoConfiguration +@Slf4j +public class YudaoJacksonAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public JsonUtils jsonUtils(List objectMappers) { + // 1.1 创建 SimpleModule 对象 + SimpleModule simpleModule = new SimpleModule(); + simpleModule + // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型 + .addSerializer(Long.class, NumberSerializer.INSTANCE) + .addSerializer(Long.TYPE, NumberSerializer.INSTANCE) + .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) + .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE) + .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) + .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) + // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳 + .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + // 1.2 注册到 objectMapper + objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule)); + + // 2. 设置 objectMapper 到 JsonUtils + JsonUtils.init(CollUtil.getFirst(objectMappers)); + log.info("[init][初始化 JsonUtils 成功]"); + return new JsonUtils(); + } + +} diff --git a/src/main/java/com/yupi/springbootinit/model/entity/ServerBigBrother.java b/src/main/java/com/yupi/springbootinit/model/entity/ServerBigBrother.java index 3a870af..b7fefb8 100644 --- a/src/main/java/com/yupi/springbootinit/model/entity/ServerBigBrother.java +++ b/src/main/java/com/yupi/springbootinit/model/entity/ServerBigBrother.java @@ -4,8 +4,17 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; + +import java.time.LocalDateTime; import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; /* * @author: ziin @@ -98,4 +107,8 @@ public class ServerBigBrother { */ @TableField(value = "tenant_id") private Long tenantId; + + @TableField(value = "create_time") + + private LocalDateTime createTime; } \ No newline at end of file diff --git a/src/main/java/com/yupi/springbootinit/rabbitMQ/MQSender.java b/src/main/java/com/yupi/springbootinit/rabbitMQ/MQSender.java index df5eb54..d2bddbd 100644 --- a/src/main/java/com/yupi/springbootinit/rabbitMQ/MQSender.java +++ b/src/main/java/com/yupi/springbootinit/rabbitMQ/MQSender.java @@ -12,6 +12,7 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.time.LocalDateTime; import java.util.List; @Slf4j @@ -37,10 +38,11 @@ public class MQSender { public void bigBrotherSend(ServerBigBrother bigBrothers){ try { // log.info("{} 接收到的消息数量----------->{}", DateTime.now(),list.size()); - this.rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); +// this.rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); //指定你队列的名字 rabbitTemplate.convertAndSend("BIG_BROTHER_QUEUE",bigBrothers); }catch (Exception e){ + log.error(e.getMessage() ); throw new BusinessException(ErrorCode.QUEUE_ERROR); } } diff --git a/src/main/java/com/yupi/springbootinit/service/impl/ServerBigBrotherServiceImpl.java b/src/main/java/com/yupi/springbootinit/service/impl/ServerBigBrotherServiceImpl.java index a8bb9e1..a0c86c9 100644 --- a/src/main/java/com/yupi/springbootinit/service/impl/ServerBigBrotherServiceImpl.java +++ b/src/main/java/com/yupi/springbootinit/service/impl/ServerBigBrotherServiceImpl.java @@ -33,13 +33,16 @@ public class ServerBigBrotherServiceImpl extends ServiceImpl + * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null + * + * @param text 字符串 + * @param typeReference 类型引用 + * @return 指定类型的对象 + */ + public static T parseObjectQuietly(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + return null; + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + + /** + * 判断字符串是否为 JSON 类型的字符串 + * @param str 字符串 + */ + public static boolean isJsonObject(String str) { + return JSONUtil.isTypeJSONObject(str); + } + +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e4c6044 --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yupi.springbootinit.config.YudaoJacksonAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9e5d91c..9427ee0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,7 +38,12 @@ spring: username: root password: 123asd jackson: - date-format: yyyy-MM-dd HH:mm:ss + # date-format: yyyy-MM-dd HH:mm:ss + serialization: + write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean servlet: multipart: # 大小限制 diff --git a/src/main/resources/mapper/ServerBigBrotherMapper.xml b/src/main/resources/mapper/ServerBigBrotherMapper.xml index fa22aa3..eb6d1c2 100644 --- a/src/main/resources/mapper/ServerBigBrotherMapper.xml +++ b/src/main/resources/mapper/ServerBigBrotherMapper.xml @@ -18,10 +18,6 @@ - - - - diff --git a/src/test/java/com/yupi/springbootinit/MainApplicationTests.java b/src/test/java/com/yupi/springbootinit/MainApplicationTests.java deleted file mode 100644 index c1133bf..0000000 --- a/src/test/java/com/yupi/springbootinit/MainApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.yupi.springbootinit; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * 主类测试 - * - * @author 程序员鱼皮 - * @from 编程导航知识星球 - */ -@SpringBootTest -class MainApplicationTests { - - @Test - void contextLoads() { - } - -}