雪球内部很早就开始尝试使用 Spring Boot,我们团队是第一批吃螃蟹的人(使用的版本是 v1.2.4,发布于 2015 年中)。到目前 Spring Boot 已经成为公司内普及的基础底层框架,但各个团队之间使用上并不统一,1.2、1.3、1.5 版本都有在使用,也引发了一些中间件的版本依赖问题。

我们的版本是其中最古老的,已经偏离了四个大版本。其实很早就有升级的规划,在 1.5 版本期间有尝试过一次,因为涉及的改动很多加上当时业务繁忙所以停滞了。Spring Boot 2.0 release 版本在三月份发布,四月正好空余出较大块的时间,趁这个天时地利机会把整个后端模块全部升级到了 2.0 基准。

这次会分享一些升级过程中的一些经验及遇到问题、踩过的坑,旨在帮助大家能更顺畅的升级,顺便推动大家在合适的机会把版本升级到统一基准下。

简单介绍下新特性

Spinrg Boot 2.0 带来最大的新特性是 Reactive Spring & WebFlux,准确的说是因为引入了 Spring 5.0。

Reactive Programing(响应式编程,以下简称 RP)核心思想是利用异步和消息驱动构建一个流畅的高弹性的系统。 RP 不是什么新鲜的技术,这里就不展开介绍了。WebFlux 是基于 Reactive Streams 构建的一整套新的响应式编程模型,并提供像 WebClient 响应式的客户端,对标的是 Spring MVC 模块。

在 RP 模型下,务必要保证整个流程的异步性,如果一个 Callback 堵塞会直接影响到整系统,但目前大多数的 JAVA 库都是阻塞模型的,虽然 Spring 这次带来了一整套的响应式技术栈,但 JDBC 这种阻塞模型还没有得到支持(阿里内部通过 JVM 的 Wisp 协程技术解决此类问题,并基于 RX Java 也实现了一整套的 RP 模型框架),希望 Spring Data Reactive 也可以早日解决。

还有其他一些特性例如 HTTP/2 的支持、Actuator 模块的增强、Quartz 模块的支持等更多特性大家可以查阅官方文档。

除了新特性还有其他一些有意思的改动,或许算是技术方向的指引?

  • Lettuce 替代 Jedis
  • Caffeine 替代 Guava
  • HikariCP 替代 Tomcat Pool

市面上常用的 Redis Client 除了 Jedis 还有 Lettuce 和 Redisson,两者都是基于 Netty 实现,并也都支持了响应式的 API,Spring 选择 Lettuce 的原因我猜测或许是因为它相对 Redisson 来说足够简单~

Guava Cache 是非常常用的组件,Caffeine 采用了更高效的 LRU 算法,并优化了内存使用。官方性能评测优于 Guava 10 倍左右,这应该是 Spring 放弃 Guava 的原因。Caffeine 也提供了 Guava Adapter,可以让用户无缝迁移过来,目前我们团队也逐步替换中。

HikariCP 一直号称是最快的连接池组件,这次变为 Spring Boot 默认组件算是官方对它的认可了。雪球团队一直使用 Druid 连接池,主要是因为它在监控方面做的很全面,插件很多,还经过了国内诸多大厂的全面“测试”,使用起来较顺手。

升级过程

对于过于老旧的版本,官方建议的是先升级到 1.5 作为跳板再升级到 2.0。但我们的做法比较激进, 也导致涉及依赖包的升级,配置的变动,及代码层面的改动非常之多,在提交上线的 PR 时改动文件有 250+,看到还挺惊讶。

这里推荐几个组件,利用好 starters 可以有效减少依赖包的管理难度

  • spring-boot-starter-json
  • spring-boot-starter-test
  • spring-boot-starter-cache

这几个官方组件支持了一些常用的模块,例如 jackson,测试的 JUnit、mockito 及常用的 cache manager 支持等。

配置层面也有许多变化,重构了很多 key 结构,也提供了 spring-boot-properties-migrator 模块来协助迁移。 并提供了更松散的绑定规则(破折号,驼峰,下划线),但建议在 .properties 和 .yml 文件中使用小写字母+破折号规范(个人建议所有配置都使用这个标准)

1
2
3
4
#老配置
multipart.maxFileSize=1M
#新规范
spring.servlet.multipart.max-file-size=1M

以上这些改动虽然非常多但较容易发现,或是编译失败或无法启动。整个升级过程中最可怕的是一些隐性的风险,非常容易忽略。

Path Matching 默认关闭后缀匹配

在以前你可以任意修改路径后缀名都能得到正确的解析,例如 /fund/000961.xyz 也能正确解析到 /fund/000961。但是 2.0 默认禁止了后缀匹配模式,这也是 Spring 的官方建议。

其实在做升级的过程中已经知道有这个改动,但我们团队一直使用的 REST 风格不带后缀的形式,所以没太在意。但万万没想到,其他一个团队的使用了我们的开放接口,但却采用了他们自己的路径规则,加上了 .json 后缀,在以前一直稳定运行,但现在这部分接口全部挂掉。这也是整个升级过程造成的唯一线上事故,幸好及时发现修复,没有产生大的影响。

解决方案就是把后缀匹配打开:

1
spring.mvc.pathmatch.use-suffix-pattern=true
新增 HTTP PUT Method Filter

在升级后的测试过程中发现部分接口明明参数正确却一直提示失败,经排查发现 PUT 类型的参数出现了数据重叠

1
/invest/501021?pwd=abc -> pwd=abc,abc(后端拿到的值)

随后跟踪源码 debug 发现新增了几个 Filter,其中 HttpPutFormContentFilter 用来处理 PUT 及 PATCH 的 body 传参问题(这个补丁在 1.5 版本后就已经引入并默认开启)。

因为浏览器 form 表单只支持 GET 与 POST 请求,所以早期的 Spring 不支持 PUT 请求使用 body 传参,正确的解决方案应该是在后端做 Method 的隐式转换。但之前并没有使用此方案,而是客户端同学把参数改成以 param 传递,这本身没有太大问题,但却忘记了把 body 里参数去掉,在新版本就变成了重名参数,以字符数组接收(数值类的不受影响)

解决方案是暂时关闭这个 Filter,待客户端修复后,在未来不再支持这些有问题的老版本客户端时再打开:

1
spring.mvc.formcontent.putfilter.enabled=false
Swagger 官方不支持 Spring Boot 2.0

在做完了所有的升级后发现 Swagger 无法使用,去官方 repo 寻求方案,发现有人已经问过了,官方回应暂不支持 2.0 版本😱。幸好在其他帖子内发现有人说使用成功,但没有给出清晰的解决方案,只好自己一点点摸索了,好在最终还是解决了。

方案是去除老的 security 配置以及添加部分资源的映射

1
2
3
4
5
6
7
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
总结

本次升级涉及十多个子系统模块,从开发->测试->灰度->上线大概 3 周多时间,除了上面引发的小事故外整个升级过程比预期要顺利很多,总结下经验有以下几点

  • 对系统代码及方案足够的熟悉
  • 不要只看升级指南,要详读官方文档
  • 有较大块的时间及足够细心

有些人因为担心风险而不愿意升级,觉得够用就行。但在我看来合理的升级是必要的,收益也是显而易见的,只要使用了正确的升级方法,大部分风险还是可控的。对于升级这件事来说,时间越久,版本差异越大意味着风险也越高,制定合适的升级周期也能有效的降低风险。

小李是一枚资深产品汪,工作五年多,过着平凡充实的生活,偶尔也反思一下,什么时候能财务自由,迎娶白富美,走向人生巅峰呢?

有一天领导找来了小李…

领导:你看某某公司这个功能不错,吸了很多新用户,我们可以考虑做一下

小李:恩,是的,不过我们这个版本需求满了,可以加在下月的版本迭代中

经过半个多月的深度学(chao)习(xi),脑海中反复的迭代构思,小李终于想出一个绝佳的方案,一定能让老板满意。

3-30 周五 10:00

小李:来来来,我们开下个月的需求评审会,确定一下这个版本的需求

(会上大家非常踊跃,天马行空的提出了各自的想法和意见。)

B:这个需求看起来简单,为了保证通用性和稳定性,改动并不小,需要仔细的考虑下模块的设计。

D:这个功能确实不错,但是不太容易加入到我们目前视觉设计中,可能要想想其他办法

F:我建议引入 H5,这样更加的容易扩展和维护,这样APP的代码也不会太臃肿。

C:引入需谨慎啊,这样会影响测试的便利性

O:其实我觉得这个功能应该换一个方式可能更好…(此处省略500字),这样用户更容易理解,运营也更容易出效果

F:我觉得 O 的提议不错,这样前端目前的模块已经可以支持了

B:都可以,反正我们后端设计上都兼容,至于效果好坏可能还得调研下,可以做个 A/B Test

D:对了,这个输入的地方情况很多,如何给用户提示要仔细设计,不然体验非常不好

小李:是的,应该是弹框提醒吧

F:这不行,到处弹框太恶心了,给个 Toest 得了

D:重要信息还得弹啊,不然不够明显

A:不要在输入的时候主动发请求,让用户自主点击后再触发验证弹框,减少服务端压力

大家还在紧张的讨论中,小李一看手表,不知不觉快12点了,公司食堂要开饭了。

小李:好了,这次只是先让大家了解下需求,具体细节做的时候再讨论,吃饭吧。

13:10

饭后准备睡一会儿整理下混乱的大脑…

14:00

哇啦哇啦哦哇啦… 叫醒闹钟准时响起。下午大家各自进入到紧张的工作中,小李准备把下个迭代内容细化一下…

C: 李哥,这个需求需要你确定一下,功能上和需求文档不太一致

小李:好的,这不对吧,这俩处的文案需要不同,效果不一样

C:工程师说这样不行,结构上不统一,不好处理

B:你不能这么处理,一样的数据各处展现不一致,这没法处理,不能做成一致吗?

小李:em…我找设计同学来问问吧

D:这不行,外面这个宽度不适合这么长,太影响美观了

F:不能因为美观去改数据结构啊,这模块都不能抽象了!

小李:对,这里没必要放那么长

O:一个数据不同地方名称不一样,显得也太不专业了吧

B:那不然把里面的数据改成短的吧,这样就一致了

D:这样也行,短的大家应该也能理解吧?

F:可以的可以的,这样好

小李:……那就都改短吧。

说完手机传来嗡嗡声「15:06 luckin 送您一张2折优惠券,仅限今日哟」,去喝杯 coffee 冷静一下…

F/C/D/B:帮我带一杯,谢谢!

——

O:李哥,这产品的文案赶紧出一个,四点就上线推广了,还有十分钟!

小李:噢,好的,就写「内有红包,点击领取」吧。

O:这样不行,有合规问题,不能出现红包字眼

小李:「点我观看有奖励!」这个如何

O:不太专业,我觉得「学习小知识,领取奖学金」不错,还有内涵。

小李:恩,那这个吧

O:推广效果还不错,才半个小时有一百多个用户点击了呢!

小李:购买用户呢?

O:不太理想,只有三个

小李:持续观察吧,我先去听个内部分享

17:50

领导:小李,来我们开个会,对下当前进度和下个版本需求

小李:好…

19:00

会后室友来短信「今天我加班,记得早点回去遛狗,今天它有点拉肚子,多在外面待会儿…」

小李:同学们,明天上午我们进行这版本效果演示,都准备一下,家里有事先撤了…

随着移动互联网的迅猛发展,对手机流量的需求越来越强。联通近两年推出了各种互联网套餐,吞噬了大批的用户。我作为一个资深移动老用户也办了张「大王卡」。 在联通刚推出3G业务,而移动由于自身原因只能使用2G的时期我都忍住没有迁移至联通,但现在我几乎要离开移动了。日常使用全部大王卡,移动只有接收银行等重要信息、验证码等功能。姑且还称移动为主卡,联通为副卡吧。

如果手机不支持双卡双待,多卡带来的问题就是得多备一个手机,多花钱不说还得天天带着。对于我这种 iPhone党简直是噩梦,势必要解决它

需要解决的有俩部分

1.电话:主卡电话呼叫转移到副卡

2.短信: 希望把主卡短信全部转发到副卡上

呼叫转移

目前大部分智能手机已系统支持呼叫转移,注意的是,这部分是需要另付资费哦例如A/B俩卡都是免费接听,但如果 A 转接到 B 需要收取 A 的呼叫费用

使用 IFTTT 解决短信转发

要解决短信的转移前提是能获取到本机短信的权限,iOS 因为隐私保护暂时做不到,所以需要一台Android 手机供主卡使用,如果是小米还需要在安全中心里给 IFTTT 开启读取服务类短信,不然无法收到银行验证码等。

建议使用 Google Play 版本的 IFTTT(需翻墙)

下面表格列出邮件、Telegram、微信三种方案各自优缺点。个人推荐使用微信,毕竟每天都使用,更加习惯。

转发方案 优点 缺点
邮件 配置简单,稳定 响应稍慢
Telegram 安全,及时 俩个手机都需要翻墙
微信 习惯,及时 需要第三方推送服务

具体配置方法都很简单,就不详述了。推送到微信要配置成 Webhooks 的方式,推荐 Server酱 服务,绑定下微信就可以收到推送啦。

其他

其实还有个更简单的方案,就是使用 IFTTT 自身APP。俩部手机都下载 IFTTT 并用同一个账号登录,打开 Applets 的推送即可~