前言
前一阵开发过程遇到的问题, 用的 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 后再发送.
时区问题, 很好重现, 源码在:
- @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;
}
来源: http://blog.51cto.com/14528283/2462476