京東資深架構師代碼評審歪詩
賈言
架構師說, 用20個字描述代碼評審的內容, 自省也省人。由于是一字一含義, 不連貫, 為了增強趣味性, 每句都增加對應的歪解。只是對常見評審的描述, 不盡之處,歡迎補充!
驗幻空越重 -- 言歡空月蟲
驗: 公共方法都要做參數的校驗,參數校驗不通過明確拋出異常或對應響應碼。
- java bean驗證已經是一個很古老的技術了,會避免我們很多問題,可參考:http://beanvalidation.org/ http://www.infoq.com/cn/news/2010/03/javaee6-validation https://www.sitepoint.com/using-java-bean-validation-method-parameters-return-values/
- 在接口中也明確使用驗證注解修飾參數和返回值, 作為一種協議要求調用方按驗證注解約束傳參, 返回值驗證注解約束提供方按注解要求返回參數
幻: 在代碼中要杜絕幻數,幻數可定義為枚舉或常量以增強其可讀性
空: 要時刻警惕空指針異常
- 常見的 a.equals(b) 要把常量放到左側
- aInteger == 10 如果 aInteger 為空時會拋出空指針異常
- 不確認返回集合是否可為空時要做非空判斷, 再做for循環
- 使用空對象模式, 約定返回空集合, 而非null
- 使用StringUtils判斷字符串非空
越: 如果方法傳入數組下標作為參數,要在一開始就做下標越界的校驗,避免下標越界異常
重: 不要寫重復代碼,重復代碼要使用重構工具提取重構
命循頻異長 - 明勛品宜昌
命: 包 / 類 / 方法 / 字段 / 變量 / 常量的命名要遵循規范,要名副其實, 這不但可以增加可讀性,還可以在起名的過程中引導我們思考方法 / 變量 / 類的職責是否合適
有意義很重要, 典型無意義命名:
- public static final Integer CODE_39120 = 39120;
- public static final String MESSAGE_39120 = "[包裹]與[庫房號]不一致,確定裝箱?";
- public static final Integer CODE_39121 = 39121;
- public static final String MESSAGE_39121 = "[包裹]與[箱號]的承運類型不一致,確定裝箱?";
- Rule rule1 = request.getRuleMap().get("1050");
CODE_39120這個名字和幻數沒多大區別。
循: 不要在循環中調用服務,不要在循環中做數據庫等跨網絡操作
頻: 寫每一個方法時都要知道這個方法的調用頻率,一天多少,一分多少,一秒多少,峰值可能達到多少,調用頻率高的一定要考慮性能指標, 考慮是否會打垮數據庫,是否會擊穿緩存
異: 異常處理是程序員最基本的素質,不要處處捕獲異常,對于捕獲了只寫日志,沒有任何處理的 catch 要問一問自己,這樣吃掉異常,是否合理
下面是一個反例, 在導出文件的controller方法中做了兩層的try...catch, 在catch塊中記錄日志后什么都沒做, 這樣用戶看不到真正想要的內容, 研發也只有看日志才能發現錯誤, 而“看日志”, 通常只有業務方反饋問題時才會看, 就會導致研發人員發現錯誤會比現場人員還會晚。
- @RequestMapping(value = "/export")
- public void export(CityRelationDomain condition, HttpServletResponse response) {
- ZipOutputStream zos = null;
- BufferedWriter bufferedWriter = null;
- try {
- condition.setStart(0);
- condition.setSize(MAX_EXPORT_LINES);
- List<CityRelationDomain> list = cityRelationService.getOrdersByCondition(condition);
- response.setCharacterEncoding("GBK");
- response.setContentType("multipart/form-data");
- response.setHeader("Content-Disposition", "attachment;fileName=export.zip");
- zos = new ZipOutputStream(response.getOutputStream());
- bufferedWriter = new BufferedWriter(new OutputStreamWriter(zos, "GBK"));
- bufferedWriter.write("訂單類型編碼,始發城市-省,始發城市-市,目的城市-省,目的城市-市");
- ZipEntry zipEntry = new ZipEntry("export.csv");
- zos.putNextEntry(zipEntry);
- for (CityRelationDomain domain : list) {
- try {
- bufferedWriter.newLine();
- bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getOrderCode()));
- bufferedWriter.write(',');
- bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getProvinceNameFrom()));
- bufferedWriter.write(',');
- bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getCityNameFrom()));
- bufferedWriter.write(',');
- bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getProvinceNameTo()));
- bufferedWriter.write(',');
- bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getCityNameTo()));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- bufferedWriter.newLine();
- bufferedWriter.flush();
- zos.closeEntry();
- bufferedWriter.close();
- } catch (Exception e) {
- e.printStackTrace();
- logger.error("導出CSV文件異常");
- } finally {
- try {
- if (zos != null) {
- zos.close();
- }
- if (bufferedWriter != null) {
- bufferedWriter.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
長: 如果一行代碼過長,要分解開來;如果一個方法過長,要重構方法;如果一個類過長要考慮拆分類
依輪線日簡 - 依倫先日賤
依: 如果調用了外部依賴, 一定要搞清楚這個外部依賴可以提供的性能指標,***約定 SLA
輪: 不要重復造輪子,如果已經有成熟類庫實現了類似功能,要優先使用成熟類庫的方法,這是因為成熟類庫中的方法都經過很多人的測試驗證,通常情況下我們自己實現的質量***等同于成熟類庫的質量。
線: 要注意我們的 jsf 服務,web 應用,消費消息的 worker 都是多線程環境,要注意線程安全問題,最典型的 HashMap,SimpleDateFormat ,ArrayList 是非線程安全的,另外如果使用 Spring 自動掃描服務,那么這個服務默認是單例,其內部成員是多個線程共享的,如果直接用成員變量是有線程不安全的。
兩個典型的錯誤代碼片段:
- 無視 SimpleDateFormat 非線程安全
- @Service
- public class AService {
- private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
- public void doSomething() {
- //use FORMAT
- }
- }
- @Service
- public class BService {
- private Pojo b;
- public void doB() {
- b = getB();
- process(b);
- }
- }
日: 打印日志和設定合理的日志級別,如有必要要添加 if 條件限定是否打印日志,在日志中使用 JSON 序列化,生成長字符串的 toString() 都要做 if 限定打印,否則配置的日志級別沒達到,也會做大量字符串拼接,占用很多 gc 年輕代內存. 另外一定要通過log4j打印日志而不是直接把日志打印到控制臺。
典型錯誤示例:
- @Service
- public class FooService {
- private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);
- public void doFooThing(Foo foo) {
- LOGGER.debug("get parameter foo {}", JSONObject.toString(foo));
- try {/*do something*/} catch (Exception ex) {ex.printStackTrace();}
- }
- }
簡: 盡可能保持整體設計的簡潔,方法實現的簡潔,要根據情況使用內存緩存,redis 緩存,jmq 異步處理。這里的簡需要把握好分寸。
接偶正分壯 - 潔偶正粉妝
接: 接口是用來隔離變化的,如果一個業務有幾種不同的形態,但都有相同的處理,那么可以定義接口來隔離業務形態的不同,在服務調用處,通過業務類型字段來獲得不同的服務類。而不要實現一個類,然后在類的各個方法中都根據業務類型做 if else 或更復雜的各種判斷。
典型示例:
- 做法 1 :
- public interface BarService { void doBarThing(Bar b);
- void doBarFatherThing(Bar b);
- }
- public class BarServiceImpl implement BarService{
- public void doBarThing(Bar b) {
- if (b.getType() == BarType.A) {
- //do some logic
- } else (b.getType() == BarType.B) {
- //do some B type logic
- }
- //do other doBarThing logic
- }
- public void doBarFatherThing(Bar b) {
- if (b.getType() == BarType.A) {
- //do some logic
- } else (b.getType() == BarType.B) {
- //do some B type logic
- }
- //do other doBarFatherThing logic
- }
- }
- public interface BarService {
- void doBarThing(Bar b);
- void doBarFatherThing(Bar b);
- }
- public class BarServiceFactory {
- public BarService getBarService(BarType type) {
- // get bar service logic
- }
- }
- //如果有公共邏輯就定義, 沒有就不定義
- public class BaseBarService implement BarService {
- public void doBarThing(Bar b) {
- //do other doBarThing logic
- }
- public void doBarFatherThing(Bar b) {
- //do other doBarFatherThing logic
- }
- }
- public class TypeABarService extends BaseBarService implement BarService {
- public void doBarThing(Bar b) {
- // doATypeThing
- super.doBarThing(b);
- }
- public void doBarFatherThing(Bar b) {
- // do bar type A service
- super.doBarFatherThing(b); //如果需要就調用, 不需要就不調用父類
- }
- }
做法 2 的好處是將不同類型的邏輯解耦,各自發展,不會相互影響,如果添加類型也不必影響現有類型邏輯。
偶: 認識系統之間的耦合關系,通過同步數據來做兩個系統之間的交互是一種很強的耦合關系,會使數據接收方依賴于數據發送方的數據庫定義,如果發送方想改數據結構,必須要求下游接收方一起修改;通過接口調用是一種常見的系統耦合關系,接口的提供方要保證接口的可用性,接口的調用方要考慮接口不可用時的應對方案; mq 消息是一種解耦的方法,兩個系統不存在實時的耦合關系。但是 mq 解耦的方式不能濫用,在同一系統內不宜過多使用 mq 消息來做異步,要盡可能保證接口的性
能, 而不是通過 mq 防止出問題后重新消費。
正: 模塊之間依賴關系要正向依賴,不能讓底層模塊依賴于上層模塊;不能讓數據層依賴于服務層也不能讓服務層依賴于 UI 層; 也不能在模塊之間形成循環依賴關系。
分: 分而治之,復雜的問題要分解成幾個相對簡單的問題來解決,首先要分析出核心問題, 然后分析出核心的入參是什么,結果是什么,入參通過幾步變化可以得出結果。
壯: 時刻注意程序的健壯性,從兩個方面實踐提升健壯性:
- 契約,在設計接口時定義好協議參數,并在實現時***時間校驗參數,如果參數有問題,直接返回給調用方; 如果出現異常情況, 也按異常情況約定應對策略
- 考慮各種邊界條件的輸出,比如運單號查詢服務, 要考慮用戶輸入錯誤運單時怎么返回,有邊界的查詢條件,如果用戶查詢條件超過邊界了, 應該返回什么
- 為失敗做設計,如果出問題了有降級應對方案。
作者:趙玉開,十年以上互聯網研發經驗,2013年加入京東,在運營研發部任架構師,期間先后主持了物流系統自動化運維平臺、青龍數據監控系統和物流開放平臺的研發工作,具有豐富的物流系統業務和架構經驗。在此之前在和訊網負責股票基金行情系統的研發工作,具備高并發、高可用互聯網應用研發經驗。
【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的博客),公眾號id: kaitao-1234567】





















