优雅的解决SpringBoot集成RabbitMQ序列化和反序列化的思路1

首先说一句我打脸了,Spring-AMQP 1.6之后就支持参数类型推断了,不需要这样麻烦,只要升级就行了。这套逻辑是我为了Spring-AMQP 1.3.9写的,但是最近写文章的时候,是看的Spring-AMQP-1.7.4的源码(新旧项目兼容,我是为了旧项目写的功能,但是写文章到时候看的是新的项目,尴尬)。我写的时候还在想为什么代码不跟印象里不一样了, 由于实在太晚了,就大概看看,没有仔细研究。今天2020-06-14,在研究写方案三的时候,经过仔细研读源码,发现了疏漏,就赶紧改一下,省的误导大家。
如果大家无法升级Spring-AMQP,还得用老版本,这个文章对大家还是有帮助的。

最近又充满的冲劲,打算再把博客捡起来,(っ•̀ω•́)っ✎⁾⁾ 我爱学习

原由

Spring-AMQP 支持 生产者 使用自定义的 Bean,然后通过 MessageConverter 转换成Message 进行发送。消费者 也可以通过 MessageConverterMessage再转化成自定义类。毕竟 Message 构建解析还是挺麻烦的。

MessageConverter有多个子类:

  • 通过 Java SerializableString,纯byte[] 进行传递 :SerializerMessageConverterSimpleMessageConverter
  • 通过 Xml 进行传递:MarshallingMessageConverter
  • 通过 Json 进行传递:JsonMessageConverterJackson2JsonMessageConverter
  • 包装其他的MessageConverter,根据 contentType 进行转发:ContentTypeDelegatingMessageConverter

其中Jackson2JsonMessageConverter 比较常用,使用 Jackson 2 进行转换(以下简称 JsonMessageConverter)。但是 MessageConverter 这个接口有个小问题,它为了封装最通用的转换逻辑,其中 fromMessage 只有一个参数,这导致无法获取 自定义 BeanClass消费者 就无法通过反射构建 Bean

// 将自定义对象转成Message, 生产者
Message toMessage(Object object, MessageProperties messageProperties)
// 将Message转成自定义对象, 消费者
Object fromMessage(Message message)

所以JsonMessageConverter通过实现 AMQP 协议,填充 headers__TypeId__ 的值,来传递 Class的Name。 它 的默认是 填充包括的包名的全称,说的直白点,那就是在不对它进行任何改造的前提下,发送消息的 Bean 和接受消息的 Bean 必须是一样的,不仅是要里面的字段一样,类名一样,连包名都要一样。

所以当 生产者 使用 JsonMessageConverter发送消息时,消费者 可以有如下几种方式来接收:

  • 消费者生产者 依赖相同的 jar
  • 不依赖jar,那就在自己系统建一个相同的 Class ,包名,类名都得相同。
  • 获取 Message,然后自己解析。

其中方案1,方案2 系统之间耦合性太强了,方案3 是个还可以方案,但是我被 SpringMVC 给惯坏了,感觉还是有点麻烦,就想自动转换成 Bean , 然后直接使用就行了。那我就开始研究源码,找到解决方案。

思路

上面已经说了,JsonMessageConverter 是通过填充 __TypeId__ 来实现 json 序列化和反序列化的,那我就找到对应的代码, 修改 __TypeId__ 就行了。虽然说得简单,但是我也是一步步分析源码找到的,接下来讲解我是如何一步步找到对应源码的。

消费者源码分析

首先我根据经验推断,从Message转成 Bean 的时候,必须知道对应 BeanClass ,不然不可能实现转换。然后找到了这段代码,特别好理解, targetJavaTypetargetClass 明确的“暗示”它的作用,毕竟我也是手动创建 并使用过 ObjectMapper 的人(✪ω✪)。
那咱们打开 函数 convertBytesToObject 来验证猜测,看到了熟悉的代码,证明我们的猜测,jsonObjectMapper 就是 Jackson 2 ObjectMapper 的实例。
看到这块我特别的开心,这说明JsonMessageConverter没有什么神秘的,跟 咱们平时 通过 Class来解析 Json 没有什么区别,咱们接下去的目标,就是找到 Class 是如何获取的。

	@Override
	public Object fromMessage(Message message)
			throws MessageConversionException {
		// ……省略无关代码……
					if (getClassMapper() == null) {
						JavaType targetJavaType = getJavaTypeMapper()
								.toJavaType(message.getMessageProperties());
						content = convertBytesToObject(message.getBody(),
								encoding, targetJavaType);
					}
					else {
						Class<?> targetClass = getClassMapper().toClass(
								message.getMessageProperties());
						content = convertBytesToObject(message.getBody(),
								encoding, targetClass);
					}
		// ……省略无关代码……
	}

	private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException {
		String contentAsString = new String(body, encoding);
		return this.jsonObjectMapper.readValue(contentAsString, targetJavaType);
	}

	private Object convertBytesToObject(byte[] body, String encoding, Class<?> targetClass) throws IOException {
		String contentAsString = new String(body, encoding);
		return this.jsonObjectMapper.readValue(contentAsString, this.jsonObjectMapper.constructType(targetClass));
	}

那咱们再次回到 函数 fromMessage 来看 targetJavaTypetargetClass 是如何获取的,然后发现了两个对象,ClassMapper 的实例 classMapperJackson2JavaTypeMapper 的实例 javaTypeMapperClassMapper 在抽象父类中,默认值为 null,也没有调用 函数 setClassMapper 的地方, Jackson2JavaTypeMapper 在当前类中,有默认值,是DefaultJackson2JavaTypeMapper的实例(以下简称 JavaTypeMapper),并且跟 JsonMessageConverter 类名包含相同的单词,Jackson2 。根据正常逻辑推测,子类是为了实现父类没有的功能,所以 大概率 是使用 javaTypeMapper 来获取 targetJavaType ,而没有 使用 classMapper 来获取 targetClass ,走的应该是这段代码,并且通过 Debug 验证了我的猜测:

JavaType targetJavaType = getJavaTypeMapper()
		.toJavaType(message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
		encoding, targetJavaType);

那我们继续深入,打开 JavaTypeMapper 的 函数 toJavaType, 终于找到了我在开篇所说的逻辑, 那就是根据 __TypeIKd__ 来获取 JavaType, 详细逻辑见注释。

	@Override
	public JavaType toJavaType(MessageProperties properties) {
	/*
	……省略无关代码……
	这块代码的作用是Spring-AMQP 1.6后的新特色,支持参数类型推测,1.6之前没有这块代码
		*/
		
		/* 终于看到可以理解的代码了,
		getClassIdFieldName 返回了 一个常量 DEFAULT_CLASSID_FIELD_NAME = "__TypeId__",
		这块的意思就是 获取 "__TypeId__" 的值
		*/
		String typeIdHeader = retrieveHeaderAsString(properties, getClassIdFieldName());

		if (typeIdHeader != null) {
			// 然后根据 "__TypeId__" 的值来 获取 JavaType 
			JavaType classType = getClassIdType(typeIdHeader);
			if (!classType.isContainerType() || classType.isArrayType()) {
			    // 如果不是容器类,就直接返回
				return classType;
			}
			/**
			……省略无关代码……
			这块的作用是,假如 __TypeId__ 对应的是个容器类,例如 Map,List,Set 等,
			根据 __ContentTypeId__,和 __KeyTypeId__ 来获取元素的类型,和 key的类型。
			我们现在没有使用到这块,正常情况都会外面包裹一层 自定义 Bean ,不直接发送容器对象。
			**/
		}
		// 	……省略无关代码……
	}

	public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
	public String getClassIdFieldName() {
		return DEFAULT_CLASSID_FIELD_NAME;
	}

那我们继续打开 函数 getClassIdType ,终于要扒开它最后 一层面纱了。

	private JavaType getClassIdType(String classId) {
		if (getIdClassMapping().containsKey(classId)) {
			// 如果Map中存储了 __TypeId__ 跟 class之间的映射关系那就直接返回
			return TypeFactory.defaultInstance().constructType(getIdClassMapping().get(classId));
		}

					// 	……省略无关代码……
					//如果没有配置映射关系, 那就通过className 来获取对应的类
					return TypeFactory.defaultInstance()
							.constructType(ClassUtils.forName(classId, getClassLoader()));

		// 	……省略无关代码……
	}

	private final Map<String, Class<?>> idClassMapping = new HashMap<String, Class<?>>();
	public Map<String, Class<?>> getIdClassMapping() {
		return Collections.unmodifiableMap(this.idClassMapping);
	}

生产者源码分析

走到这块,咱们已经基本研究明白了, 消费者 是如何获取 自定义 BeanClass,并将Message转成对应的 Bean
那咱们乘胜追击,研究一下, 生产者 是如何对 __TypeId__ 进行填充的。

按照相同的方式,咱们可以在 JsonMessageConverter 函数 createMessage 找到这行代码

getJavaTypeMapper().fromJavaType(this.jsonObjectMapper.constructType(objectToConvert.getClass()),
					messageProperties);

轻车熟路的咱们打开 打开 JavaTypeMapper 的 函数 fromJavaType
跟 函数 toJavaType 是相似的,只是互为逆操作,根据 Class 来填充 __TypeId__

	@Override
	public void fromJavaType(JavaType javaType, MessageProperties properties) {
		addHeader(properties, getClassIdFieldName(), javaType.getRawClass());
				/*
				……省略无关代码……
				填充的是容器对象,元素的类型,和 key的类型。
				*/
		}
	}
		protected void addHeader(MessageProperties properties, String headerName, Class<?> clazz) {
		if (this.classIdMapping.containsKey(clazz)) {
			// 如果 如果Map中存储了class 跟 __TypeId__ 之间的映射关系那就直接返回
			properties.getHeaders().put(headerName, this.classIdMapping.get(clazz));
		}
		else {
			// 否则填充 class的全称
			properties.getHeaders().put(headerName, clazz.getName());
		}
	}

结论与方向

到这,生产者消费者 的逻辑,咱们已经清楚了,并且了解了,为什么默认情况下, 发送消息的 Bean 和接受消息的 Bean 必须Class是一样的, 除非 获取 Message,然后自己解析。 因为如果没有配置映射,在默认情况下 生产者 填充 类全称, 消费者 根据类全称 来查找 Class,而Message可以直接传递给监听函数,不经过 Json 反序列化。
那咱们怎么改这个问题呢, 代码中已经明确提示了, 那就是 增加 映射, 可以在 生产者 那块增加, 自定义的Class,对应 消费者 实际有的类名,也可以反过来。但是这不是一个好的方案, 因为 生产者消费者 还是耦合性太强了,必须 有一方知道对方的类名, 如果某个新人不了解这个情况,建了一个新 Bean,使用相同的队列,那这个功能就废掉了。
那怎么避免这个问题了, 那我们就同时对 生产者消费者 都增加 映射,使用相同的 __TypeId__ ,而不跟具体的类发生依赖,这样就算新建类了,只要 __TypeId__ 不变 ,另一方就不需要修改。

实现

那咱们就研究一下,这个映射关系如何添加,现在有两个映射,idClassMappingclassIdMapping ,找到了设置的入口函数 setIdClassMapping。在设置 idClassMapping 后, classIdMapping 是对 idClassMapping 的倒转,将 KeyValue 互换,这样咱们就简单多了,生产者消费者 的代码是类似的。

	public void setIdClassMapping(Map<String, Class<?>> idClassMapping) {
		this.idClassMapping.putAll(idClassMapping);
		createReverseMap();
	}
	private void createReverseMap() {
		this.classIdMapping.clear();
		for (Map.Entry<String, Class<?>> entry : this.idClassMapping.entrySet()) {
			String id = entry.getKey();
			Class<?> clazz = entry.getValue();
			this.classIdMapping.put(clazz, id);
		}
	}

方案1

生产者 的配置

	@Bean
	public MessageConverter messageConverter(ObjectMapper mapper) {
		Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter(mapper);
		DefaultJackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper();
		Map<String, Class<?>> idClassMapping = new HashMap<>();
		idClassMapping.put("Bean1", producer.Bean1.class);// 生产者实际有的Bean
		idClassMapping.put("Bean2", producer.Bean2.class);
		// ……更多……
		javaTypeMapper.setIdClassMapping(idClassMapping);
		messageConverter.setJavaTypeMapper(javaTypeMapper);
		return messageConverter;
	}

消费者 的配置类似

	@Bean
	public MessageConverter messageConverter(ObjectMapper mapper) {
		Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter(mapper);
		DefaultJackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper();
		Map<String, Class<?>> idClassMapping = new HashMap<>();
		idClassMapping.put("Bean1", consumer.Bean1.class);// 消费者实际有的Bean
		idClassMapping.put("Bean2", consumer.Bean2.class);
		// ……更多……
		javaTypeMapper.setIdClassMapping(idClassMapping);
		messageConverter.setJavaTypeMapper(javaTypeMapper);
		return messageConverter;
	}

这样_生产者_ 和 消费者 使用相同的 __TypeId__, 例如 “Bean1”,但是分别对应不同的类,互相修改类名包名没有影响。

方案2

其实跟 方案1 逻辑类似,咱们可以通过 自定义 ClassMapperJavaTypeMapper 来实现相同的功能。在实际工作中我感觉没有必要,但是因为逻辑比较直白,为了大家更好的理解这个流程,我把代码放在这。

_生产者_的配置

	@Bean
	public Jackson2JsonMessageConverter customConverter(ObjectMapper mapper) {
		Map<Class<?>, String> classIdMapping = new HashMap<Class<?>, String>();
		classIdMapping.put(producer.Bean1.class, "Bean1");
		classIdMapping.put(producer.Bean2.class, "Bean2");
		// ……更多……
		Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(mapper);
		converter.setClassMapper(new ClassMapper() {
			// ……省略无关代码……
			@Override
			public void fromClass(Class<?> clazz, MessageProperties properties) {
				// 根据 Class 填充 __TypeId__
				properties.setHeader("__TypeId__", classIdMapping.get(clazz));
			}
		});
		return converter;
	}

消费者 的配置

	@Bean
	public Jackson2JsonMessageConverter customConverter(ObjectMapper mapper) {
		Map<String, Class<?>> idClassMapping = new HashMap<>();
		idClassMapping.put("Bean1", producer.Bean1.class);
		idClassMapping.put("Bean2", producer.Bean2.class);
		// ……更多……
		Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(mapper);
		converter.setClassMapper(new ClassMapper() {
			// ……省略无关代码……
			@Override
			public Class<?> toClass(MessageProperties properties) {
				// 获取 __TypeId__
				String typeId = (String) properties.getHeaders().get("__TypeId__");
				// 根据 __TypeId__ 获取消费者实际有的Class
				return idClassMapping.get(typeId);
			}

		});
		return converter;
	}

后记

写的已经很多了,大家好好理解消化一下, 那我在最后留一个扣,给自己挖个坑。
方案1 虽然已经很好了,但是没有最好,只有更好, 那你能不能发现它还有什么不足吗?
举例:

  1. 如果以后增加新的队列,那我们必须修改 配置类增加映射,而配置类 跟配置文件一样,最好不要经常修改,省的出错。那我们有什么办法解决呢? 可以不修改配置类,还可以动态加载 Class__TypeId__ 的映射关系。 提示一下,Spring是用什么方式来代替了 Xml 的配置。
  2. 现在 是用 Map 来保存 Class__TypeId__ 之间的映射关系,那这就有一个问题了,那就是 二者之间必须一一对应。 那假如有特殊的场景,虽然是 同一个 生产者,但是 分发到不同的 队列 中, 不同 队列 有不同的业务处理,消费者 只想看到跟自己业务相关的数据,其他数据不想看到,或者是 同一个 队列 ,但是根据不同的场景,有不同的 生产者 ,不同 生产者 只有自己的 Bean,也不想使用相同的 全字段 Bean。现在的方式就无法实现, 因为 Class__TypeId__ 是一一对应的。虽然 使用相同的 Bean 也不是不行,这种场景也不多,但是能 多对多更好,这样不同的业务使用自己的 Bean ,能更好的隔离数据和业务。

这就是我接下来两篇关于MessageConverter的博文内容,今天就先睡了,实在太晚了,明天再检查错别字。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在使用RabbitMQ时,序列化是一个重要的概念。序列化是将对象转化为可以在网络传输或存储中使用的字节序列的过程。使用序列化可以方便地将对象在不同系统之间进行传递和交互。 在RabbitMQ中,可以使用不同的序列化方式来处理消息的序列化。一种常用的方式是使用Jackson2JsonMessageConverter。这是一个基于Jackson库的消息转换器,可以将对象序列化为JSON格式的字符串进行传输。通过配置RabbitTemplate的bean,将消息转译器设置为Jackson2JsonMessageConverter,可以实现将对象序列化为JSON串。例如,在任意配置下提供以下代码: @Bean public RabbitTemplate jacksonRabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); return rabbitTemplate; } 除了Jackson2JsonMessageConverter,RabbitMQ还提供了其他的消息转换器实现,例如SimpleMessageConverter。这些不同的消息转换器实现了RabbitMQ的MessageConverter接口,可以根据需求选择合适的转换器。 通过选择适当的序列化方式,可以提高RabbitMQ处理大数据量时的性能。使用序列化可以将对象转化为可传输的字节序列,并且在接收端可以将字节序列反序列化为对象,实现消息的传递和消费。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [RabbitMq消息序列化简述](https://blog.csdn.net/zhaozhenzuo/article/details/46623213)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [工作随笔——rabbitmq消息的序列化反序列化springboot)](https://blog.csdn.net/qq_43585377/article/details/109203762)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值