D瓜哥在 Spring 扩展点概览及实践 中概要性地介绍了一下 Spring 的核心扩展点。里面也提到了 BeanFactoryPostProcessor 和 BeanDefinitionRegistryPostProcessor,但仅仅提了一句,没有深入研究。在 Spring 扩展点实践:整合 MyBATIS 中,由于 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,也只是简单介绍了一些作用,又一次没有深入研究。
最近,在开发一个插件时,遇到了一个问题:利用 BeanFactoryPostProcessor 对一些特定 BeanDefinition 设置属性,但生成的 Bean 却没有相关的属性值。由此,对 BeanFactoryPostProcessor 做了一些研究。记录一下,以备不时之需。
Spring 启动流程简介 在 Spring 启动流程概述 中,D瓜哥对 Spring 的启动流程做了比较详细的介绍。同时画了一张启动流程图,如下:
图 1. AbstractApplicationContext.refresh — 重塑容器 从该图中可以明显看到,如果需要对 Spring 的 BeanDefinition 做些修改,那么,就需要通过实现 BeanFactoryPostProcessor 接口,来对 Spring 做些扩展。坦白讲,为了上述流程图只展示了一个非常概要性的流程。如果深入一下 invokeBeanFactoryPostProcessors 方法的细节,会发现这又是一番天地。
BeanFactoryPostProcessor 调用详解 D瓜哥把 invokeBeanFactoryPostProcessors 方法的流程图也画了出来,细节如下:
图 2. BeanDefinitionRegistryPostProcessor & BeanFactoryPostProcessor 调用过程 从这张流程图上可以看出 BeanFactoryPostProcessor 的调用过程,比在 Spring 启动流程概述 中介绍的要复杂很多:
首先,执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法,顺序如下:
关于 BeanDefinitionRegistryPostProcessor 的处理流程,D瓜哥在 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中有更详细的描述,不了解的朋友请参考那篇文章的介绍。
公司最近一年在推进降本增效,在用尽各种手段之后,发现应用太多,每个应用都做跨机房容灾部署,则最少需要 4 台机器(称为容器更合适)。那么,将相近应用做一个合并,减少维护项目,提高机器利用率就是一个可选方案。
经过前后三次不同的折腾,最后探索出来一个可行方案。记录一下,分享出来,希望对有相关需求的研发童鞋有所帮助。下面按照四种可能的方案,分别做介绍。另外,为了方便做演示,专门整了两个演示项目:
diguage/merge-demo-boot — 合并项目,下面简称为 boot。
diguage/merge-demo-web — 被合并项目,下面简称为 web。
Jar 包引用 这个方式,可能是给人印象最容易的方式。仔细思考一下,从维护性的角度来看,这个方式反而是最麻烦的方式,理由如下:
web 项目每次更新,都需要重新打包发布新版; boot 项目也需要跟着更新发布。拉一次屎,脱两次裤子。属实麻烦。
还需要考虑 web 项目的加载问题,类似下面要描述的,是否共用容器:
共用容器 — 这是最容器想到的方式。但是这种方式,需要解决 Bean 冲突的问题。
不共用容器 — 这种方式需要处理 web 容器如何加载的问题。默认应该是无法识别。
基于这些考虑,这种方式直接被抛弃了。
仓库合并,公用一套容器 这是第一次尝试使用的方案。也是遇到问题最多的方案。
将两个仓库做合并。
将 web 仓库的地址配置到 boot 项目里: git remote add web git@github.com:diguage/merge-demo-web.git;
在 boot 项目里,切出来一个分支: git switch -c web;
将 web 分支的提交清空: git update-ref -d HEAD,然后做一次提交;
将 web 项目的代码克隆到 web 分支上: git pull --rebase --allow-unrelated-histories web master;注意,这里需要加 --allow-unrelated-histories 参数,以允许不相干的仓库进行合并。
从 boot 项目的 master 分支上,切出来一个合并分支: git switch -c merge;
将 web 项目向 boot 项目合并: git merge --allow-unrelated-histories web;注意,这里需要加 --allow-unrelated-histories 参数,以允许不相干的仓库进行合并。
处理代码冲突,完成合并即可。
最近有小伙伴在开发时,遇到了一个 Spring 占位符,例如 ${token}, 在不同环境下处理不一致的问题,正好对 Spring 对占位符的处理也有一些不清楚的地方,趁此机会,把 Spring 对占位符的处理机制深入了解一下,方便后续排查问题。
经常阅读D瓜哥博客的朋友可能知道,D瓜哥在 Spring 扩展点实践:整合 Apache Dubbo(一): Spring 插件机制简介 中已经介绍了 Spring 的插件机制。在阅读以下内容之前,建议大家先去阅读一下这篇文章中“Spring 插件机制简介”章节的内容,以便于无缝衔接。
在分析的过程中发现, Spring 对占位符有两种截然不同的出来阶段:① XML 配置文件中的占位符;② Java 源代码中 @Value 注解中的占位符。由于内容较多,一篇讲解完有些过长,所以分三篇文章来分别介绍这两种处理过程。
本篇首先来介绍一下对 XML 配置文件中的占位符的处理。
示例代码 在正式开始之前,先来看一下示例代码:
UserRpc.java /** * @author D瓜哥 · https://www.diguage.com * @since 2023-05-02 10:23:49 */ public class UserRpc { @Value("${user.appId}") private String appId; // 这里不使用注解,而是使用 XML 配置 // @Value("${user.token}") private String token; } token.properties user.appId=dummyAppId user.token=dummyToken spring.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- @author D瓜哥 · https://www.diguage.com --> <context:annotation-config/> <bean id="userRpc" class="com.diguage.truman.context.UserRpc"> <!-- XML 配置的占位符实例在此 --> <property name="token" value="${user.token}"/> </bean> <context:property-placeholder location="classpath:token.properties"/> </beans> <bean> 标签处理 在 Spring 启动流程概述 中,已经介绍过,Spring 的启动过程几乎都被封装在 AbstractApplicationContext#refresh 方法中。在 refresh 方法中调用了 refreshBeanFactory 方法;在 refreshBeanFactory 方法执行过程中,调用了 loadBeanDefinitions 方法。而 BeanDefinition 的加载是由 org.springframework.context.support.AbstractRefreshableApplicationContext#loadBeanDefinitions 来完成的。通过 XML 文件配置的 Bean 是由 org.springframework.context.support.AbstractXmlApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory) (AbstractRefreshableApplicationContext 的子类)处理完成的。处理过程的时序图如下:
在 关于升级 Spring 等依赖的一些经验 中介绍了 D瓜哥在升级项目依赖时,遇到的一些问题以及一些需要注意的地方。但是,这里还存在一个问题:各个依赖的版本依然散落在各个项目中;升级依赖,需要在所有项目中,把所有相关项目的依赖都巴拉一下,费时费力。解决这个问题的一个比较好的办法是制定一个组织内部的 Maven BOM,集中管理相关依赖的版本。这样升级的时候,还需要修改 BOM 的版本号即可。
Maven BOM 介绍 BOM(Bill of Materials)是由 Maven 提供的功能,它通过定义一整套相互兼容的 jar 包版本集合,使用时只需要依赖该BOM文件,即可放心的使用需要的依赖 jar 包,且无需再指定版本号。
一些基本原则 Spring & Spring Boot 是 Java 生态中,全世界广泛使用的开发框架,在各种场景中都经受过考验。所以,Spring & Spring Boot 选择的 Jar 在稳定性和兼容性方面都有保证。另外,Spring Boot 本身就集成了非常非常多的依赖,并为此创建了一个网页 Spring Boot Dependency versions 来说明它集成的依赖及版本。故而,可以选择以 Spring Boot 为底本,来制作自己的 BOM。
如果不需要 Spring 相关依赖,可以将 Spring 相关依赖删除掉,然后在其之上增加组织内部依赖而创建自己的 BOM。
如果需要 Spring 相关依赖,那么直接继承
在稳定性方面,经过更多人检验的版本,则稳定性更有保障。所以,选择最近两年下载次数比较多的版本。
更新的版本,更容易获得技术升级带来的红利。所以,在可能的情况下,优先选择高版本。
优先考虑目标 JDK 的支持情况。例如,一些依赖的高版本或低版本不支持 Java 8,但是 Java 8 是生产环境部署的主要版本,那么太高的版本和低版本都不适合。
外部 Jar 包选择标准 尽量将外部中间件统一到同一种依赖的同一个版本上。例如:数据库连接池全部使用 HikariCP;JSON 处理统一使用 Jackson。
选择最近两年发布的版本中,下载次数最多的版本为准。如果有发布的小版本升级,则在该版本基础上,该版本的最新修订版。例如,1.2.3 是最近两年下载最多的版本,但是 1.2.4 已经发布,则优先选择使用 1.2.4。
如果有两个大版本,高版本符合条件的情况下,优先选择高版本。低版本大概率是先淘汰的,高版本相对来说维护时间更长,另外高版本的代码优化得更佳。例如,Ehcache 的选择。
如果传递依赖造成依赖 Jar 包版本冲突,则尽可能选择高版本的 Jar。
持续演进的项目的依赖优先级更高;相反,临近淘汰的项目优先级降低,甚至不予考虑。
两年以上未更新的依赖,在 API 兼容的情况下,直接升级到最新版。
没有显示使用而是间接引入的依赖,不再单独声明,由直接依赖来引入。如果需要解决冲突,则按照上面的原则来处理。
前面系统研究了 Hessian 序列化协议。并以此为契机,顺带实例对比了 Hessian、MessagePack 和 JSON 的序列化。早在 2012 年,Martin Kleppmann 就写了一篇文章 《Schema evolution in Avro, Protocol Buffers and Thrift》,也是基于实例,对比了 Avro、ProtoBuf、Thrift 的差别。现在翻译出来,方便做系列研究。
整个“序列化系列”目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
前段时间,翻译了 Hessian 2.0 的序列化协议,发布在了 Hessian 2.0 序列化协议(中文版)。但是,其中有很多言语不详之处。所以,接下来会用几篇文章来详细解释并实践一下 Hessian 序列化协议,以求做到知其然知其所以然。
目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
本文用实际来对比一下 JSON、Hessian 和 MessagePack 的区别。
前段时间,翻译了 Hessian 2.0 的序列化协议,发布在了 Hessian 2.0 序列化协议(中文版)。但是,其中有很多言语不详之处。所以,接下来会用几篇文章来详细解释并实践一下 Hessian 序列化协议,以求做到知其然知其所以然。目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
在上一篇文章 Hessian 协议解释与实战(四):数组与集合 中研究了数组和集合的处理方式。接下来介绍对象和映射的处理。
基础工具方法 基础工具方法就不再赘述,请直接参考 Hessian 协议解释与实战(一):基础工具方法 中提到的几个方法。
前段时间,翻译了 Hessian 2.0 的序列化协议,发布在了 Hessian 2.0 序列化协议(中文版)。但是,其中有很多言语不详之处。所以,接下来会用几篇文章来详细解释并实践一下 Hessian 序列化协议,以求做到知其然知其所以然。目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
在上一篇文章 Hessian 源码分析(Java) 对 Hessian 的 Java 实现做了一个概要的分析,对处理流程以及整体架构做了一个简单的分析。接下来,回到主题,继续来解释 Hessian 序列化协议。这篇文章,我们来重点分析一下数组与集合相关的操作。
前面通过几篇文章,解释并实践了一下 Hessian 的序列化协议。文章目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
该系列第四篇文章准备详细介绍一下 Hessian 对对象、链表以及 Map 等处理。但是,越调试代码,越发觉得应该先对 Hessian 的实现做一个源码分析。于是,就有了本文。
这里有几点需要声明一下:
在上面“解释与实战”系列文章中提到的代码就不再重复说明。
通过“解释与实战”系列文章,大家应该可以领略到,处理序列化有大量的细节。但是,本文并不打算涉及。本文重点是介绍 Hessian 的 Java 实现的架构蓝图。相当于给指明一条路,沿着这条路,大家就可以探索 Hessian 的各种细节。
本文的介绍,全部基于 Hessian 4.0.60 的源码。由于没有找到 Hessian 的仓库,D瓜哥从 Hessian 的网站下,下载了源码包,解压后发布在了 GitHub 上: Hessian — The source code of Hessian Library.。
前段时间,翻译了 Hessian 2.0 的序列化协议,发布在了 Hessian 2.0 序列化协议(中文版)。但是,其中有很多言语不详之处。所以,接下来会用几篇文章来详细解释并实践一下 Hessian 序列化协议,以求做到知其然知其所以然。目录如下:
Hessian 2.0 序列化协议(中文版) — Hessian 序列化协议的中文翻译版。根据后面的“协议解释与实战”系列文章,增加了协议内容错误提示。
Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 — 介绍布尔型数据、日期类型、浮点类型数据和整数类型数据等四种类型的数据的处理。
Hessian 协议解释与实战(二):长整型、二进制数据与 Null — 介绍长整数类型数据、二进制数据和 null 等三种类型的数据的处理。
Hessian 协议解释与实战(三):字符串 — 专门介绍了关于字符串的处理。由于字符串需要铺垫的基础知识比较多,处理细节也有繁琐,所以单独成篇来介绍。
Hessian 源码分析(Java) — 开始第四篇分析之前,先来介绍一下 Hessian 的源码实现。方便后续展开说明。
Hessian 协议解释与实战(四):数组与集合 — 铺垫了一些关于实例对象的处理,重点介绍关于数组和集合的相关处理。
Hessian 协议解释与实战(五):对象与映射 — 重点介绍关于对象与映射的相关处理。
Hessian、Msgpack 和 JSON 实例对比 — 用实例对比 JSON、Hessian 和 MessagePack 的区别。
Avro、ProtoBuf、Thrift 的模式演进之路 — 翻译的 Martin Kleppmann 的文章,重点对比了 Avro、ProtoBuf、Thrift 的序列化处理思路。
在上一篇文章 Hessian 协议解释与实战(二):长整型、二进制数据与 Null 中研究了长整型、二进制数据与 null 等三种数据类型的处理方式。接下来,我们再来介绍字符串的处理情况。