1. 程式人生 > >甲小蛙戰記:PHP2Java 排雷指南

甲小蛙戰記:PHP2Java 排雷指南

(馬蜂窩技術原創內容,申請轉載請在公眾後後臺留言,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,沒有反饋問題,結束了!

當馬蜂窩的頂樑柱真是不容易啊!

本文作者:馬蜂窩旅遊網旅遊平臺研發團隊。

&n