(马蜂窝技术原创内容,申请转载请在公众后后台留言,ID:mfwtech )
大家好,我是来自马蜂窝电商旅游平台的甲小蛙,从前是一名 PHP 工程师,现在可能是一名 PHJ 工程师,以后......
前阵子,我从大道消息听说公司商品订单技术栈要推 Java。我是一个喜欢走在时代前列线上的人,凡是要做到领先。我对 Java 也是仰慕已久,于是花了两天时间学习 Java,并调研各种框架和解决方案,决心要把商品和订单的主要功能用 Java 重构掉。
在经历了 798 难后现在这些东西都踉跄上线了,我也成了马蜂窝的顶梁柱。虽然表面看来风光无限,但是这一路走来相当不容易,累到有上觉没下觉,踩坑把腿踩断,才有了今天这篇战记。希望大家看完后不要吸取任何教训,抱着不撞南墙不回头的心态,继续从头踩坑。
风险提示:文章会先带大家入坑,然后出坑,请保持秩序不要拥挤;如果文章看了一半就去实践,有被队友打死的风险!
Part.1 准备篇
终于要开始学习了!
9 月 1 号,趁着开学季,买了《两天精通 Java》、《三天精通 SpringBoot》两本书,看到书名仿佛感觉胜利在向我招手。
9 月 2 号书就到了,两天没睡觉把书看完了。原来 Java 这么简单,也是各种 class,interface,abstract class。Java 还有一个响亮的口号——「万事万物皆对象」。
OK,可以开始编程了。隔壁甲小白凑过来说:「IntelliJ 写 Java 很爽,买一个吧,才 1000+ RMB~」。
……
咬咬牙!
环境搞好了,来,写个 Demo:
SpringBoot 果然名不虚传,一定有一个很懂用户的 PM,为我们省去了原来需要在 Spring 中配置的一堆 XML 文件,上手真的灰常简单。比较奇怪的是 Java 居然没有 echo,想对 Java 世界说声「你好」,居然还得写 System.out.println("哈喽,窝的")。切!【划重点:Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域 (rapid application development) 成为领导者】
感觉 Java 对于无所不能的我来讲真是太简单了,就是有点繁琐。万事万物皆对象,还建议把对象属性设置为 private。偶买嘎!可是对象属性也太多了吧!商品对象有近 100 个属性,你让我给他们挨个写 getter,setter 方法?别闹了!
为了显得专业点,我开始给这 100 个属性写 getter、setter 方法。花了一个小时写完了,可累死我了。一旁的甲小白看着我的代码说,「你在刷代码量么,为什么不用 @Getter @Setter 注解?」这什么东西?别跑!为什么不早说!【划重点:@Getter @Setter 是 Lombok 中的两个编译期注解】
话说,自从用了这个叫做注解的东西,手指也不疼了,腰也不酸了,好评!
好了,看来 Java 和 SpringBoot 已经被我研究通透了。突然想起来个事儿,商品要调用好多部门的 PHP 接口,可是他们没有 Java 版的接口,这可肿么办!然而机智的队友早已看穿一切,做了一个叫做 PHP 网关的东西,可以把 PHP 接口全部包装成 HTTP 请求。
万事俱备只欠南风,其他也都调研的差不多了,该动真格的了。试试连接数据库吧。听说 Java 有个连库的好东西,叫 Druid,整!【划重点:Druid 是阿里巴巴开源平台上一个数据库连接池实现,它结合了 C3P0、DBCP、PROXOOL 等 DB 池的优点,同时加入了日志监控,可以很好的监控 DB 池连接和 SQL 的执行情况,可以说是针对监控而生的 DB 连接池,据说是目前最好的连接池】
调试没问题,继续。我要连线上数据库了,但是没有账号密码,跟 DBA 要吧,结果 DBA 给我扣了个盗取数据库密码「叛窝」的罪名。最后扔下一张卡片,「健身游泳了解下」的上面赫然写着一行小字:SkipperClient。这是什么东东,咱也不知道咱也不敢问,Gitlab 一下吧,原来用这个就可以根据服务名换取对应的 DB 库的账号密码,懂了(还有更方便的用法,可以直接用微服务的配置中心+Spring 原生配置即可完成)。但是怎么链接到我们的内部 maven-repository 呢?找身边的同学 Copy 一份 Maven 的配置文件就好啦!【划重点:Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理】
到此,我已经顺利完成学业,准备出师开始搬砖了!
Part.2 实战挖坑篇
环境和项目已经搭建好,要开始写逻辑部分了。
首先我有这么个需求:保存商品的时候流程走的比较多,很多流程都要用到该商品的基本信息,但我又不想通过一层层 set 值进行传递。So 来个单例吧,Spring 有一个强大的东西叫做容器,配合 IOC 可以完美满足我的需求。【划重点:@Service 把需要用来承载商品关键信息的类注册为 Bean,由 Spring 容器管理其生命周期;@Resource 注解下要进行注入的变量即可完成依赖注入,再也不需要自己手动 set 了】
是的,就这么几行简单的代码就可以满足我的需求了,跑起来。
咦,怎么会有 NullPointerException 呢?一顿搜索之后才知道,原来想要使用这个 Bean,那使用这个 Bean 的类也要注册在 Spring 容器中,由 Spring 创建才行……就没别的办法吗?又一顿搜,找到了解决方案,可以手动获取 Spring 容器的上下文,终于让我毫不费神,非常费力地完成了逻辑部分的开发。
因为项目用到了很多反射(跟 PHP 反射类似),所以我很机智地给每一个可能跑异常的方法增加了 try catch 代码块,以体现我对异常的敏锐嗅觉,报出异常后可以第一时间锁定异常代码,同时加上了给用户的提示「服务出错」。
好了,测试下,没问题,可以看到异常日志。等等,好像哪里不对劲,为什么最内层的报异常后,记录了 4 条异常 log 呢?定睛一看,发现内层给用户返回的异常提示又被外层捕获了,导致多次日志记录。嗯…我承认这个问题有点傻了~但是我确实是想记录日志并提示用户,怎么办?请教了身边的甲小白。小白一把把我推开,在我的青轴键盘上一顿动次打次后,说:改好了,只需要把异常抛出,让最外层捕获就好了。【划重点:如果选择了让方法 throws Exception(指 Java 让强制抛的,非自定义的 throw new XXXException),那调用该方法的方法只要不处理异常,就需要继续 throws Exception,所以尽量不要嵌套 100 层方法】
还有一个头疼的问题,PHP 里字段大多使用的是蛇形字段(goods_info),而 Java 里好像更常用驼峰(goodsInfo)。我总不能让蛇形字段类来接收参数,然后再转成我的内部驼峰类吧。我本能地搜了一下,居然真的有解决方案,只需要用 @JSONField @JsonProperty 注解就可以搞定这个问题了。不得不说 Java 的生态还是比较完善的,提供了各种问题常用的解决方案。【划重点:@JSONFIeld @JsonProperty 是两个运行时注解,前者是 阿里巴巴 的 fastjson 包的注解,后者是 jackson 包的注解】
好了,遇到的问题基本都解决了,自测通过。提测。测试环境也部好了!
Part.3 边挖边填篇
甲小美同学开始测试了。我跟她讲解了目前的服务调用情况是这样的:PHP->Java->PHP(即 PHP 接收用户端请求,然后封装参数调用 Java 微服务,Java 微服务再调用一些 PHP 的接口进行数据校验)。
刚开始测试,就遇到个小问题。由于部署的 Java 微服务没有部署为上线状态,而是「内测中」,该怎么访问呢?这个时候想到了我们的浏览器插件。装插件,选分支,搞定!但是测试同学还是说没访问到,怎么办?这时候想到了浏览器插件的工作原理,带 Cookie。【划重点:PHP 如果要访问内测中 Java 微服务,PHP 中访问 Java 微服务时一定要把 Cookie 携带上】
甲小美同学埋头测了好久,我心想,看来是基本没什么问题,明天上线!
「甲小蛙,为什么我新创建了一个商品,列表页里没有新增呢,而且好像是把我刚才创建好的商品给改掉啦!」
听完赶紧把本地程序运行了起来,完美复现!瞬间背后一凉。我意识到可能是 @Service 的问题,发现注解后的类全局单例,也就是无数个用户会共享这一个对象。习惯了 PHP 进程内单例的我有些无法接受,这可咋整,顿时有点懵,其他同学也是刚刚入坑,好像没有好的办法,怎么办…实力不够,体力来凑,还是乖乖改造成了一层层传递对象的方式。(这个方式一直被沿用到线上,后来才发现还有个叫做 @Scope 的注解,可以控制 Bean 的作用域,一股悲凉袭上心头)【划重点:@Scope 可以指定 4 中 SpringBean 的作用域,有:单例(singleton)、原型(prototype)、会话(session)、请求(request)】
刚修好没多久,同样的问题又来了,真是一 Bug 未平一 Bug 又起。切换到开发环境,获取和保存了商品,咦,没问题啊……
我:你是不是打开方式不对啊?
甲小美:打开方式肯定没问题!
只好把小美同学拉到屏幕前,一次又一次得刷着链接:「你看,没问题吧!你看,你看,你看,崩了吧......」
报错信息:想不起来了。大致意思就是说连接无效,链接丢了。问了下 DBA,说我们 MySQL 的链接空闲断开时间是 30s,也就是说链接到数据库后,30s 内并没有再执行 SQL,这个链接就会被断开。搜了解决方案,如下:【划重点:setTestWhileIdle = true,是否在获得链接后检测其可用性;setTestOnBorrow = true,是否在连接放回连接池后检测其可用性】
果然好了,链接通畅,又能愉快地测试了。
但帅不过 3 分钟...唉!
按照场景复现了下,发现基本都是一个问题,也是 PHP 转 Java 比较头疼的问题。由于 PHP 的弱类型以及和没有前端同学约定好数据格式,导致前端可以传来各种各样的数据类型。原本以为的 Integer(如:10),前端可以传 Float(如:10.5);原本以为的 Float(如:999.9),前端可以传 String(如:999.9 元/人)。总之,这是一个比较大的坑,还是且行且珍惜啊! Java 应该是世界上最严谨的语言。【划重点:使用 Java 的好处之一,是在设计数据格式时,可以让我们的数据更加规范和严谨】
「为什么开发环境商品下线抛了事件,但是对应的行为没有执行呢?」我强忍住心中的痛楚,先是检查了事件的监听,又确认了 PHP 的订阅确实被执行了,但是发现 PHP 调用 Java 返回了错误。
怎么会 404?本地跑得好好的啊,明明有这个 Action,怎么回事?首先,这个 Action 是 Java 微服务中新增的,404 意味着没找到,有可能是访问到「已上线」的服务中去了,是不是没有带上 Cookie?带着疑问去验证,果然。(划重点:消息总线订阅者在访问 Java 微服务时是不会携带 Cookie 的,默认会走「服务中」的服务;如果 Java 服务在此过程中还需要访问 PHP,还需要在 Java 微服务中指定要访问哪个 Docker,要不然会迷路的)
「为什么商品编辑后,详情页的内容没有更新啊?」
「是不是更新详情页的事件没执行啊,你多保存几次,更新下数据」
「不行......」
好吧,来到最熟悉的 PHP 环境,各种 Debug,发现 PHP 里好多接口都加了一层缓存,突然间恍然大悟,在保存完商品后更新了下这个接口的缓存。(划重点:默认情况下 Ko 会在 aGet,aMultiGetList 等接口中增加一层 memcache 的主键缓存,如果用 Java 服务更新了数据,记得来清下 Ko 的主键缓存)
甲小美:「为什么1......」
甲小美:「为什么2......」
甲小美:「为什么3......」
Part.3 上线篇
终于熬到这一天,我自信地站在镜子前,笨拙系上红色领带的结,将头发梳成大人模样,穿上一身帅气西装,等会儿上线一定比想像顺~
「喂,小美,上线成功了,速度回归~」。天降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身...... 激动!痛快!为了表达此刻的心情,我要用表情包创建一个商品,放满了各种 Emoji,就是任性 !
我擦,怎么返回服务异常了?
「小美,你是不是大流程没覆盖到啊?」
「你给我冷静点~先看日志」,「哦」。我以迅雷不及掩耳盗铃之势把流量开关给关掉了,线上又走回了 PHP 的流程。日志显示如下:【划重点:对于一些大流程的改造,建议加上一个 switch 开关,方便线上出问题后马上切流量,比修改代码提 MR 再发布要高效和准确】
果然在线上也找到了解决方案,原因是对应的数据表的编码方式是 utf8,而 Emoji 存入数据库的编码是 utf8mb4,所以异常了【划重点:MySQL 在 5.5.3 版本以后增加了 utf8mb4 编码,其中 mb4 是 most bytes 4 的含义,用来兼容四个字节的 Unicode(万国码)。utf8mb4 是 utf8 的一个扩展,可以给数据表换个编码方式来解决这个问题】。但是为什么 PHP 可以把 Emoji 存入 utf-8 的表中,而 Java 不能?这个问题还在困扰着我...... 最好的语言,名不虚传。
1 个小时后......
2 个小时后......
4 个小时后......
8 个小时后......
Yeah,没有反馈问题,结束了!
当马蜂窝的顶梁柱真是不容易啊!
本文作者:马蜂窝旅游网旅游平台研发团队。