shenzhenzsw 2019-12-27
前一阵开发过程遇到的问题,用的 rabbitmq template 发送消息,消息body里的时间是比当前时间少了8小时的,这种一看就是时区问题了。
就说说为什么出现吧。
之前的配置是这样的:
@Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); template.setMessageConverter(new Jackson2JsonMessageConverter()); template.setMandatory(true); ... return template; }
要发送出去的消息vo是这样的:
@Data public class TestVO { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date testDate; }
然后,出现的问题就是,消息体里,时间比当前时间少了8个小时。
{"testDate":"2019-12-27 05:45:26"}
原因
我们是这么使用rabbitmq template的:
@Autowired private RabbitTemplate rabbitTemplate; @Autowired private RedisRepository redisRepository; /** * 发送消息 * @param exchange 交换机名称 * @param routingKey 路由键 * @param msgMbject 消息体,无需序列化,会自动序列化为json */ public void send(String exchange, String routingKey, final Object msgMbject) { CorrelationData correlationData = new CorrelationData(GUID.generate()); CachedMqMessageForConfirm cachedMqMessageForConfirm = new CachedMqMessageForConfirm(exchange, routingKey, msgMbject); redisRepository.saveCacheMessageForConfirms(correlationData,cachedMqMessageForConfirm); //核心代码:这里,发送出去的msgObject其实就是一个vo或者dto,rabbitmqTemplate会自动帮我们转为json rabbitTemplate.convertAndSend(exchange,routingKey,msgMbject,correlationData); }
注释里我解释了,rabbitmq会自动做转换,转换用的就是jackson。
跟进源码也能一探究竟:
org.springframework.amqp.rabbit.core.RabbitTemplate#convertAndSend @Override public void convertAndSend(String exchange, String routingKey, final Object object, @Nullable CorrelationData correlationData) throws AmqpException { // 这里调用了convertMessageIfNecessary(object) send(exchange, routingKey, convertMessageIfNecessary(object), correlationData); } 调用了convertMessageIfNessary: protected Message convertMessageIfNecessary(final Object object) { if (object instanceof Message) { return (Message) object; } // 获取消息转换器 return getRequiredMessageConverter().toMessage(object, new MessageProperties()); }
获取消息转换器的代码如下:
private MessageConverter getRequiredMessageConverter() throws IllegalStateException { MessageConverter converter = getMessageConverter(); if (converter == null) { throw new AmqpIllegalStateException( "No ‘messageConverter‘ specified. Check configuration of RabbitTemplate."); } return converter; }
getMessageConverter就是获取rabbitmqTemplate 类中的一个field。
public MessageConverter getMessageConverter() { return this.messageConverter; } 我们只要看哪里对它进行赋值即可。 然后我想起来,就是在我们业务代码里赋值的: @Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); // 下面这里赋值了。。。差点搞忘了 template.setMessageConverter(new Jackson2JsonMessageConverter()); template.setMandatory(true); return template; }
反正呢,总体来说,就是rabbitmqTemplate 会使用我们自定义的messageConverter转换message后再发送。
时区问题,很好重现,源码在:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/jackson-demo
@Data public class TestVO { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date testDate; }
测试代码:
@org.junit.Test public void normal() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); TestVO vo = new TestVO(); vo.setTestDate(new Date()); String value = mapper.writeValueAsString(vo); System.out.println(value); }
输出:
{"testDate":"2019-12-27 05:45:26"}
解决办法
指定默认时区配置
@org.junit.Test public void specifyDefaultTimezone() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); SerializationConfig oldSerializationConfig = mapper.getSerializationConfig(); /** * 新的序列化配置,要配置时区 */ String timeZone = "GMT+8"; SerializationConfig newSerializationConfig = oldSerializationConfig.with(TimeZone.getTimeZone(timeZone)); mapper.setConfig(newSerializationConfig); TestVO vo = new TestVO(); vo.setTestDate(new Date()); String value = mapper.writeValueAsString(vo); System.out.println(value); }
在field上加注解
@Data public class TestVoWithTimeZone { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date testDate; } 我们这里,新增了timezone,手动指定了时区配置。 测试代码: @org.junit.Test public void specifyTimezoneOnField() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); TestVoWithTimeZone vo = new TestVoWithTimeZone(); vo.setTestDate(new Date()); String value = mapper.writeValueAsString(vo); System.out.println(value); }
上面两种的输出都是正确的。
这里没有去分析源码,简单说一下,在序列化的时候,会有一个序列化配置;这个配置由两部分组成:默认配置+这个类自定义的配置。 自定义配置会覆盖默认配置。
我们的第二种方式,就是修改了默认配置;第三种方式,就是使用自定义配置覆盖默认配置。
jackson 还挺重要,尤其是 spring cloud 全家桶, feign 也用了这个, restTemplate 也用了,还有 Spring MVC 里的 httpmessageConverter 有兴趣的同学,去看下面这个地方就可以了。
如果对JsonFormat的处理感兴趣,可以看下面的地方:
com.fasterxml.jackson.annotation.JsonFormat.Value#Value(com.fasterxml.jackson.annotation.JsonFormat) (打个断点在这里,然后跑个test就到这里了)
总结
差点忘了,针对rabbitmq template的问题,最终我们的解决方案就是:
@Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); ObjectMapper mapper = new ObjectMapper(); SerializationConfig oldSerializationConfig = mapper.getSerializationConfig(); /** * 新的序列化配置,要配置时区 */ String timeZone = environment.getProperty(CadModuleConstants.SPRING_JACKSON_TIME_ZONE); SerializationConfig newSerializationConfig = oldSerializationConfig.with(TimeZone.getTimeZone(timeZone)); mapper.setConfig(newSerializationConfig); Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter(mapper); template.setMessageConverter(messageConverter); template.setMandatory(true); ...设置callback啥的 return template; }