代碼規范+Java日志LogNDC和MDC


一次代碼評審,差點過不了試用期!

 


作者:小傅哥

博客:https://bugstack.cn

沉淀、分享、成長,讓自己和他人都能有所收獲!😄

一、前言

好的代碼往往也很好看

代碼是給機器運行的,但同樣也是給人看的,並且隨着上線還需要由人來運維。那么寫出可擴展易維護好讀懂的代碼就顯得非常重要。

對於新人來說,互聯網大廠項目開發與平常自己學習的代碼還是有很大的差別的。日常學習時候通常只要能運行出結果即可,並不會有其他的要求。也不會說有;PRD評審、研發設計評審、代碼開發、代碼評審以及中間一些列的提交物,直到測試完成,上線驗證,開量對外等等。

所以很多新人剛從學校畢業或者從小公司進入大廠,在規范制約下會有一些不習慣,甚至犯錯誤。那么為了讓大家更好的知曉這些問題,小傅哥特意整理了一些例子,歡迎參考。

二、會議室

謝飛機,剛剛入職沒多久,興奮的寫着leader給的需求,🐎碼的飛快。恰巧組長走過來:“飛機,帶着你的電腦,跟我來碼雲會議室,做下代碼評審。”

leader:飛機,你這代碼咋這么粗魯!

飛機:啊?😱

leader:我要不攔着你,我感覺你這代碼都能飛。

leader:你看哈,就說這行,這日志打的,上線后出了問題,你能查到原因嗎?

飛機:好像...

leader:還有這,這idea都提示你了,都報黃色了,你怎么不看看。還有,這代碼也不格式化,一個月后它認識你,你還認識它嗎。

leader:給你發的入職編碼規范看了?

飛機:哦,看一些,寫的時候忘了。

leader:先別着急寫,看會了再寫代碼,這還有一個不錯的工程:《Netty+JavaFx實戰:仿桌面版微信聊天》,可以參考。

寫代碼不是以完成功能就算完事,還需要寫的漂亮。評審后,飛機,坐回工位,收起了躁動的心,安心熟讀手冊並練習。

三、代碼評審

1. 日志規范

日志是整個代碼開發過程中非常重要的環節,如果日志打的不好,那么遇到的線上bug就沒法快速定位,定位不了問題也就沒法快速解決問題。直接帶來的結果可能包括;客訴更多、資損更大、修復更慢。

就像下面這段代碼中的日志

public Result execRule(RuleReq req) { try { logger.info("執行服務規則 req:{}", JSON.toJSONString(req)); // 業務流程 return Result.buildSuccess(); } catch (Exception e) { logger.error("執行服務規則失敗", e); return Result.buildError(e); } } 
  • 看似沒什么問題,但在這段異常代碼中,沒有打方法的入參信息。如果方法異常時只是拋出一些異常棧信息,那么是很難定位具體的由次調用觸發的。
  • 另外如果你的系統監控服務,沒有類似方法跟蹤ID的功能,最好還需要在日志中把本次調用具有標識性的id,作為查詢條件打到日志中。

修改后的日志

public Result execRule(RuleReq req) { try { logger.info("執行服務規則{}開始 req:{}", req.getrId(), JSON.toJSONString(req)); // 業務流程 logger.info("執行服務規則{}完成 res:{}", req.getrId(), "業務流程,必要的結果信息"); return Result.buildSuccess(); } catch (Exception e) { logger.error("執行服務規則{}失敗 req:{}", req.getrId(), JSON.toJSONString(req), e); return Result.buildError(e); } } 
  • 那么現在這樣改成這樣打日志,就可以非常方便的查詢問題,例如搜索;執行服務規則100098921,那么它的一整串關於這次調用的信息就可以都搜索出來了,方便排查問題。
  • 在異常中打印入參是為了更加方便的定位問題,不需要比對上下文。
  • 打日志還有很多技巧,但所有打的日志目的都為了在出問題時可以快速定位問題,但也注意不要打太多日志,精簡好用即可。

2. IDEA提示

很多時候因為你,走神、疏忽、手滑,寫出來的錯誤代碼,IntelliJ IDEA,都會給你警告⚠提示,只是你,沒有去看、沒有去看、沒有去看!

來自idea的警告

小傅哥 & idea警告

  • Idea在警告提示這方面非常優秀,只要你能看得見,按照它的提示修改,就可以減少很多的錯誤。
  • 如果你還希望有更強的提示,那么你可以按照 p3c 插件,幫你檢查代碼錯誤。

3. 代碼格式

可能這並不是一個致命的問題,但代碼格式化最大的好處是,提升可讀性、規整性、以及可以讓整組人都在一個標准下執行。因為很多時候一個組的程序員,會在一個類下開發,有人格式化、有人不格式化除了不好看以外,合並代碼有時候也會遇到麻煩。

不格式化的代碼缺少靈魂

小傅哥 & 代碼格式化

  • 對於嚴格要自己的程序員來說,代碼沒有格式化還是很難受的。
  • 看一段代碼,只要發現差一個空格位置,都知道這是格式化還是沒格式化。

4. 單元測試

單測?覆蓋率?寫代碼不是寫完就可以了嗎?

當然不是,你寫的代碼你需要保證它能你跑通你所有的流程節點,確保這份功能是沒有問題的,才能提交給測試,否則來回反復,耗時耗力。這也就是寫單測的目的!甚至好一點的研發可以通過單測驅動開發,在這個階段能把一些共用的方法合並、抽離,避免過多的冗余方法。

單測長什么樣

  • 單測完整基本也就是代碼的健壯性更好,能把單測寫好,基本提交的代碼就不會有那么多測試妹子找你聊天。
  • 在很多公司中一般都會要求單測覆蓋率超過多少,否則是不允許編譯提交的,這有插件可以和Jenkins配合使用。

5. 分支規范

可能有些人看到分支規范根本沒有感覺,因為他們開發的項目較小,沒有多人開發,上線周期也短,也不會開發中添加需求。

但在互聯網中並不是這樣,往往一個系統需要幾個人維護,並同時進行開發。一般這里會包括;master分支、test分支、本次需求的分支,有這么多分支怎么用呢,如下;

  1. master分支,是主分支,也是上線分支,不允許在上面直接修改代碼。
  2. test分支,是測試環境分支,每個人都需要把自己開發完的分支,提測后合並到test分支,交由測試驗證。
  3. 需求分支,也是個人開發的分支,同一個需求下,大家在這個分支寫代碼,當然也可能這個系統模塊的分支就一個人在開發。

重點,如果有人不遵守分支規范或者壓根沒概念,把自己的需求代碼寫在test分支上,並且是多次修改提交都在test分支寫。那么就危險了,嚴重會耽誤上線;為什么?

  1. test分支,是由大家把自己的代碼合並過來共用的,那么這個分支就會包含2個或者更多的並行需求,當你需要上線的時候,需要把自己的代碼合並到master,但test分支代碼是不能合並到master的,那么多未知的內容,根本沒有在上線范圍。
  2. 那么你又想上線,又不能避開test分支,就需要把你寫的代碼,重新粘貼過去,這個時間成本非常大。
  3. test分支,還隨時有刪除重新拉的可能,如果有人通知大家刪除重新拉,那你的代碼就會丟失。

6. 夾帶需求

提交測試,但還藏一個需求

研發開發需求代碼時候,有時候會額外加一些其他代碼,而且這些代碼可能跟本次需求並沒有關系。那為什么會這樣呢?

  1. 以前留下來的bug,想修復下,但忘記告知測試
  2. 在開發這個需求時,其他產品又找過來讓加功能,並說功能很小,沒有發郵件通知相關測試人員
  3. 看到某塊以前寫的代碼太亂了,就想着優化下,自信心很高,不必告訴測試

那這時候你提交的代碼,如果不在測試范圍又出了問題,只能研發自己抗。並且在所有的研發團隊,幾乎是不會讓夾帶需求上線的,這樣的做完了不算功勞,做出了問題還會被罵。

所以,千萬不要私自夾帶!哪怕你是好心!

7. 異常流程

擦屁屁的紙,80%的面積都是保護手的!

這句話是我經常用的,因為我們編程很多時候都是在處理異常流程,正常流程往往並不難,難的是分析出這段開發的代碼有多少異常流程有沒有處理。

那么,會有哪些異常呢?

  1. 支付成功MQ消息發送失敗,需要worker補償
  2. PRC接口調用失敗,網絡超時,實際成功
  3. 接口冪等性,多次調用結果一致性

等等,這些都是異常流程,尤其在一些交易提現環節,會出現各種異常,那么不可能把這些異常都反饋用戶展示到界面。而是要有一些非常友好的提示,並且在服務端的流程里,有一定的補償機制,來保證最終的調用成功,或者逆反。

8. 代碼成坨

小傅哥 & 代碼成坨

CRUD往往可能是因為你的設計,換個人寫也許不同

很多時候研發寫代碼,根本不考慮是否要擴展,總之一個類 + 幾十行ifelse,能搞定所有需求。等下次在開發類似的,就粘貼過去再修修補補,能用就行。

缺少寫出良好代碼的研發,一方面是經歷有限,另外一方面是學了很多理論但是不好落地。比如設計模式,但自己實際寫代碼的時候還是很暈。

這里推薦一本我寫的《重學Java設計模式》,全書共計22個真實業務場景對應59組案例工程、編寫了18萬字271頁的PDF、包含交易、營銷、秒殺、中間件、源碼等22個真實場景。可以添加小傅哥微信獲取:fustack

9. SQL性能

select * from table where status = 1 limit 200; 

這是一段定時任務掃描庫表的SQL,這段sql會定時掃庫,將庫表中狀態是1的掃描出來進行處理,每次掃描200行。你發現有什么問題了嗎?

  1. 掃描必要字段即可,不需要全部字段
  2. 這段sql會越來越慢,即使狀態字段加了索引。因為status並不能大量排掉其他狀態字段,隨着數據越來越多依然是全表掃描。

那么怎么優化呢,其實優化也比較簡單,需要先根據狀態查詢到符合條件的最小的id,之后再sql的查詢條件中添加id > xx,即可。另外如果你的任務需要多個worker掃描,增加效率,可以增加門牌號設計,提升掃描效率,如下;

小傅哥 & 門牌號掃描

10. 結伴編程

評審代碼最后這點想說說,陪伴式開發,可能這不是結伴編程,不是共同合作,而是一個研發需要另外一個研發不斷的提供幫助。有時候可能就是很簡單的問題,也不想查,或者說沒有意識去查,只是問。

業務開發的過程,只要把流程定下來,研發設計評審完,其他的開發過程中遇到的小點並不難,只要查一查就可以搞定。當日也不是說完全不能問,只不過特別普遍,簡單的代碼問題,自己搞定就可以了,但這個時候還像保姆似的陪伴,就會拖累整個團隊的進展,最終大家都需要扛起那個慢的。

所以,如果你是那個需要陪伴的,要及早斷奶,學會自己攻克,快速成長。而如果你是那個卷紙,可哪擦屁股的,要把卷紙傳遞給他。一個人擦一次是能力體現,反反復復擦一個人,就惹屎上身了。

四、總結

  • 以上介紹了代碼評審中涉及到的比較常見的點,基本也是很多研發容易忽略和犯錯誤的地方。這些問題點但拿出哪一個看,都不大。但運行在代碼中,確都有可能發生致命或者麻煩的事情。
  • 想讓自己能把代碼寫好,就不只面試時候造飛機的回答,什么時間復雜度、什么可重入鎖、什么紅黑樹,什么DDD,只要你不能正確的落地和運用這些技術,說的再多都是空談。
  • 多學一些、多看一些、多問一些,沒有壞處,但要自己能成長,把吸取到的經驗心得,運用到業務開發中,寫出可擴展、可維護的代碼,才能讓自己真的升職加薪。也能讓既有留下的本事,也有出去的能力。

五、系列推薦

  • 握草,你竟然在代碼里下毒!
  • DDD領域驅動設計落地方案
  • 重學Java設計模式(22個真實開發場景) 面經手冊(上最快的車
  • 拿最貴的offer) 字節碼編程(非入侵式全鏈路監控實踐)

公眾號:bugstack蟲洞棧 | 作者小傅哥多年從事一線互聯網 Java 開發的學習歷程技術匯總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果能為您提供幫助,請給予支持(關注、點贊、分享)!

 

Java日志Log4j或者Logback的NDC和MDC功能

01

NDC和MDC的區別

Java中使用的日志的實現框架有很多種,常用的log4j和logback以及java.util.logging,而log4j是apache實現的一個開源日志組件(Wrapped implementations),logback是slf4j的原生實現(Native implementations)。需要說明的slf4j是Java簡單日志的門面(The Simple Logging Facade for Java),如果使用slf4j日志門面,必須要用到slf4j-api,而logback是直接實現的,所以不需要其他額外的轉換以及轉換帶來的消耗,而slf4j要調用log4j的實現,就需要一個適配層,將log4j的實現適配到slf4j-api可調用的模式。

說完基本的日志框架的區別之后,我們再看看NDC和MDC。

不管是log4j還是logback,打印的日志要能體現出問題的所在,能夠快速的定位到問題的症結,就必須攜帶上下文信息(context information),那么其存儲該信息的兩個重要的類就是NDC(Nested Diagnostic Context)和MDC(Mapped Diagnositc Context)。

NDC采用棧的機制存儲上下文,線程獨立的,子線程會從父線程拷貝上下文。其調用方法如下:

1.開始調用 NDC.push(message); 2.刪除棧頂消息 NDC.pop(); 3.清除全部的消息,必須在線程退出前顯示的調用,否則會導致內存溢出。NDC.remove(); 4.輸出模板,注意是小寫的[%x] log4j.appender.stdout.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ssS}] [%x] : %m%n

MDC采用Map的方式存儲上下文,線程獨立的,子線程會從父線程拷貝上下文。其調用方法如下:

1.保存信息到上下文 MDC.put(key, value); 2.從上下文獲取設置的信息 MDC.get(key); 3.清楚上下文中指定的key的信息 MDC.remove(key); 4.清除所有 clear() 5.輸出模板,注意是大寫[%X{key}] log4j.appender.consoleAppender.layout.ConversionPattern = %-4r [%t] %5p %c %x - %m - %X{key}%n

最后需要注意的是:

  • Use %X Map中全部數據
  • Use %X{key} 指定輸出Map中的key的值
  • Use %x 輸出Stack中的全部內容

02

MDC的使用例子

//MdcUtils.java

// import ...MdcConstants // 這個就是定義一個常量的類,定義了SERVER、SESSION_ID等 import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; public class MdcUtils { private final static Logger logger = LoggerFactory.getLogger(MdcUtils.class); private static void put(String key, Object value) { if (value != null) { String val = value.toString(); if (StringUtils.isNoneBlank(key, val)) { MDC.put(key, val); } } } public static String getServer() { return MDC.get(MdcConstants.SERVER); } public static void putServer(String server) { put(MdcConstants.SERVER, server); } public static String getSessionId() { return MDC.get(MdcConstants.SESSION_ID); } public static void putSessionId(String sId) { put(MdcConstants.SESSION_ID, sId); } public static void clear() { MDC.clear(); logger.debug("mdc clear done."); } }

上述工具類中MdcConstants是定義一個常量的類,定義了SERVER、SESSION_ID等,put方法就是調用了slf4j的MDC的put方法。其他方法類比。

看看使用該工具類的具體方式:

// MdcClearInterceptor.java

import ...MdcUtils; // 導入上面的工具類 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;i mport javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MdcClearInterceptor extends HandlerInterceptorAdapter { @Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception { MdcUtils.clear(); } }

在該攔截器中,重寫了afterConcurrentHandlingStarted方法,該方法執行了工具類的clear方法,也就是通過調用slf4j的clear方法清除了本次會話上下文的日志信息。為什么要放在afterConcurrentHandlingStarted方法中呢?這恐怕得從springmvc的攔截器的實現說起。

springmvc的攔截HandlerInterceptor接口定義了三個方法(代碼如下),具體說明在方法注釋上:

public interface HandlerInterceptor { //在控制器方法調用前執行 //返回值為是否中斷,true,表示繼續執行(下一個攔截器或處理器) //false則會中斷后續的所有操作,所以我們需要使用response來響應請求 boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; //在控制器方法調用后,解析視圖前調用,我們可以對視圖和模型做進一步渲染或修改 void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception; //整個請求完成,即視圖渲染結束后調用,這個時候可以做些資源清理工作,或日志記錄等 void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; }

很多時候,我們只需要上面這3個方法就夠了,因為我們只需要繼承HandlerInterceptorAdapter就可以了,HandlerInterceptorAdapter間接實現了HandlerInterceptor接口,並為HandlerInterceptor的三個方法做了空實現,因而更方便我們定制化自己的實現。

相對於HandlerInterceptor,HandlerInterceptorAdapter多了一個實現方法afterConcurrentHandlingStarted(),它來自HandlerInterceptorAdapter的直接實現類AsyncHandlerInterceptor,AsyncHandlerInterceptor接口直接繼承了HandlerInterceptor,並新添了afterConcurrentHandlingStarted()方法用於處理異步請求,當Controller中有異步請求方法的時候會觸發該方法時,異步請求先支持preHandle、然后執行afterConcurrentHandlingStarted。異步線程完成之后執行preHandle、postHandle、afterCompletion。

那至於這些可能用到的日志字段從什么地方賦值呢,也就是什么地方調用MDCUtils.put()方法呢?一般我們都會實現一個RequestHandlerInterceptor,在preHandler方法中處理日志字段即可。如下:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { return true; } // 開始保存信息到日志上下文 MdcUtils.putServer(request.getServerName()); String sId = request.getHeader(HeaderConstants.SESSION_ID); MdcUtils.putSessionId(sId); if (sessionWhiteList.contains(request.getPathInfo())) { return true; } // TODO 處理其他業務 }

還沒完,就目前看,我們已經有兩個自定義的攔截器實現了。怎么使用,才能將日志根據我們的意願正確的打印呢?必然,攔截器是有順序的,如果配置了多個攔截器,會形成一條攔截器鏈,執行順序類似於AOP,前置攔截先定義的先執行,后置攔截和完結攔截(afterCompletion)后注冊的后執行。

Soga,我們需要清除上次請求的一些無用的信息,再次將我們的信息寫入到MDC中(攔截器的配置在DispatcherServlet中),由於afterConcurrentHandlingStarted()方法需要異步請求觸發,因此我們需要在web.xml的DispatchServlet配置增加<async-supported>true</async-supported>配置。

<mvc:interceptors> <bean class="com.xxx.handler.MdcClearInterceptor"/> <bean class="com.xxx.handler.RequestContextInterceptor"/> </mvc:interceptors>

或者這樣:

<mvc:interceptors> <mvc:interceptor> <bean class="com.xxx.handler.MdcClearInterceptor"/> </mvc:interceptor> <mvc:interceptor> <bean class="com.xxx.handler.RequestContextInterceptor"/> </mvc:interceptor> </mvc:interceptors>
轉發:https://www.cnblogs.com/xiaofuge/p/13671488.html+https://cloud.tencent.com/developer/article/1526878


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM