新聞中心
1、可讀性
1.1 準(zhǔn)確命名
每種編程語言都有自己的命名規(guī)范,不同語言的風(fēng)格差異有大有小,下面以Java為例:

Java整體命名風(fēng)格為UpperCamelCase或lowerCamelCase形式。不管是類還是變量命名要見名知意,切勿使用縮寫或中文;風(fēng)格統(tǒng)一,盡量使用英文名詞,切勿中英文混合;盡量避免和Java自帶類庫重名,切勿使用Java關(guān)鍵字命名。
- 包命名規(guī)范
包命名使用小寫英文名詞,使用“.”分割,每個被分割的單元盡量只有一個名詞,命名規(guī)范為:
域名.公司/個人名稱.項目名稱.模塊名稱
- 類命名規(guī)范
類采用UpperCamelCase命名風(fēng)格,一些特殊的縮寫可以采用全大寫,如XML。類命名使用名詞描述類的作用。
- 接口命名規(guī)范
接口采用UpperCamelCase命名風(fēng)格,由于接口定義的是一類功能或動作,所以接口的命名一般使用形容詞或動詞描述接口的行為。
- 抽象類命名規(guī)范
抽象類除了滿足UpperCamelCase風(fēng)格外,一般需要加上Abstract前綴。
- 異常類命名規(guī)范
異常類除了滿足UpperCamelCase風(fēng)格外,一般需要加上Exception或Error后綴,使用名詞描述什么異常或錯誤。
- 枚舉類命名規(guī)范
枚舉類除了滿足UpperCamelCase風(fēng)格外,一般加上Enum后綴,枚舉類中的枚舉值采用全部大寫風(fēng)格,單詞與單詞之間使用“_”進行分割。
- 方法命名規(guī)范
方法命名采用lowerCamelCase風(fēng)格,一般使用動詞+名詞來命名,比較常見的有doXxx,handleXxx,findXxxx。
- 變量命名規(guī)范
變量命名采用lowerCamelCase風(fēng)格,一般使用名詞描述變量的作用,需要注意的是區(qū)別于常量,盡量不要使用特殊符號前綴或使用“_”分割符號。
- 常量命名規(guī)范
常量命名采用全部大寫,單詞與單詞之間使用“_”進行分割。
1.2 代碼風(fēng)格
在日常的項目開發(fā)中,一個項目有很多人協(xié)同開發(fā),每個人使用的開發(fā)工具不一樣,比如大家常用的vs code和idea,不同的開發(fā)工具或代碼習(xí)慣也會導(dǎo)致代碼風(fēng)格不一致,我們在開發(fā)時可能習(xí)慣性的進行代碼格式化,就會導(dǎo)致整個類改動非常多,在代碼合并時容易沖突。
我們可以在項目中增加.editorconfig文件來統(tǒng)一代碼風(fēng)格。
root = true
[*.{adoc,bat,groovy,html,java,js,jsp,kt,kts,md,properties,py,rb,sh,sql,svg,txt,xml,xsd}]
charset = utf-8
[*.{groovy,java,kt,kts,xml,xsd}]
indent_style = tab #tab鍵縮進,可選"space"、"tab"
indent_size = 4 #縮進空格為4個
end_of_line = lf #結(jié)尾換行符,可選"lf"、"cr"、"crlf"
charset = utf-8 #文件編碼
trim_trailing_whitespace = true #不保留行末的空格
insert_final_newline = true #文件末尾增加一個空行
curly_bracket_next_line = false #大括號不另起一行
spaces_around_operators = true #運算符兩邊都有空格
indent_brace_style = 1tbs #條件語句格式是1tbs
1.3 注釋規(guī)約
類注釋
類注釋采用/**......*/,在每個類的頭部要有必要的注釋信息,包括:作者、創(chuàng)建時間、類功能描述
/**
* 簡單分流算法實驗, 每次分流只用考慮當(dāng)前的桶, 不用回溯歷史版本
* {@link https://duapp.yuque.com/team_tech/confluence-data-iwskfg/dzmogk}
* @author hufei
* @date 2021/6/8 7:59 下午
*/
接口注釋
接口注釋采用/**......*/,在滿足類注釋的基礎(chǔ)上,接口注釋應(yīng)該包含接口的目的、如何使用。
/**
* AB分桶算法接口規(guī)范
* 對外暴露實驗桶計算接口,該接口有一個抽象實現(xiàn)類AbstractBucketAlgorithm,具體的分桶算法實現(xiàn)這個抽象類
* @author hufei
* @date 2021/6/8 6:06 下午
*/
方法注釋
方法注釋采用/**......*/,描述方法的功能、輸入、輸出及返回值說明
/**
* 計算實驗層的桶信息
* @param layerId 分層 id
* @param expId 實驗 Id
* @param expRatio 新的實驗占層流量比例
* @param existsLayerBucket 老的層流量實驗配比
* @return 新的層流量實驗配比
* @throws BucketAlgorithmException
*/
方法內(nèi)部注釋
代碼做了些什么以及為什么這樣做,特別是復(fù)雜的邏輯處理部分,要盡可能的給出詳細(xì)的注釋。
全局變量注釋
包括變量的功能、取值范圍、注意事項等的說明。
/**
* 代表kafka收到消息的答復(fù)數(shù),0就是不要答復(fù),愛收到?jīng)]收到.1就是有一個leader broker答復(fù)就行,all是所有broker都要收到才行
* 0: Producer不等待kafka服務(wù)器的答復(fù),消息立刻發(fā)往socket buffer,這種方式不能保證kafka收到消息,設(shè)置成這個值的時候retries參數(shù)就失效了,因為producer不知道kafka收沒收到消息,所以所謂的重試就沒有意義了,發(fā)送返回值的offset全默認(rèn)是-1.
* 1: 等待leader記錄數(shù)據(jù)到broker本地log即可.不等待leader同步到其他followers,那么假如此時剛好leader收到消息并答復(fù)后,leader突然掛了,其他fowller還沒來得及復(fù)制消息呢,那么這條消息就會丟失了.
* all:等待所有broker記錄消息.保證消息不會丟失(只要從節(jié)點沒全掛),這種方式是最高可用的 acks默認(rèn)值是1.
*/
private String acks = "0";
局部變量注釋
主要的局部變量必須有注釋,無特別意義的情況下可以不加注釋。
2可靠性
2.1 增強健壯性
慎用遞歸算法
遞歸算法寫起來很簡單,但用的不好容易導(dǎo)致堆棧溢出和死循環(huán)問題。因此盡量不要使用遞歸算法,如果要使用需要注意以下幾個問題:
把退出條件放在函數(shù)最上方,這樣比較清晰,防止程序一直不滿足退出條件而導(dǎo)致堆棧溢出
public int recursiveAlgorithm(){
if (退出條件)
return 0;
......
}
避免在遞歸函數(shù)中出現(xiàn)過大的局部變量,這會加速堆??臻g的消耗
public int recursiveAlgorithm(){
char buf[] = new char[1024];
}
增加一個最大遞歸深度,防止出現(xiàn)死循環(huán)導(dǎo)致堆棧溢出
使用參數(shù)校驗
項目中有很多接口提供給前端,有一些參數(shù)在后續(xù)的邏輯處理中不能為空并且沒有做非空校驗,靠口頭的約定參數(shù)不能為空,如果前端調(diào)用時沒有傳參就會出現(xiàn)空指針異常。最好的解決辦法是統(tǒng)一參數(shù)校驗框架,在接口的入?yún)⑦M行非空限制,如果為空,統(tǒng)一拋出異常。
@Valids({
@Valid(names = "request.expStatus",required = true,regex = "[1,2]",error = "實驗狀態(tài)必須為1或者2"),
@Valid(names = "request.weekDateList,request.type" , required = true)
})冪等校驗
我們的系統(tǒng)應(yīng)該做好冪等校驗,等于同一筆業(yè)務(wù)操作,不管調(diào)用多少次,得到的結(jié)果都是一樣的。比如用戶重復(fù)下單、MQ消息重復(fù)消費、前端重復(fù)提交表單等,這些都是需要保證冪等操作。
2.2 善始善終
異常處理
我們在進行異常處理時,一定要把必須執(zhí)行的語句放到finally塊中,比如讀寫文件時,在finally塊中關(guān)閉IO連接。
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("")));
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
加鎖釋放鎖
我們在日常的開發(fā)中經(jīng)常會碰到資源競爭的問題,此時我們需要對競爭的資源進行加鎖處理,如果我們忘記釋放鎖,就會導(dǎo)致其他的請求阻塞。
Lock lock = new ReentrantLock();
lock.lock();
......
lock.unlock();
資源釋放
我們在日常的開發(fā)中會用到數(shù)據(jù)庫或網(wǎng)絡(luò)資源,此時我們需要建立數(shù)據(jù)庫連接或網(wǎng)絡(luò)連接,如果我們忘記釋放連接就會導(dǎo)致數(shù)據(jù)庫資源或網(wǎng)絡(luò)資源一直被占用,直到連接失效。雖然我們在日常的開發(fā)中不會直接創(chuàng)建連接而是使用連接池,但是連接數(shù)最大連接數(shù)如果設(shè)置的過大,也會導(dǎo)致資源的耗盡。
ResultSet resultSet = null;
Statement statement = null;
Connection connection = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(url,user,pwd);
statement = connection.createStatement();
resultSet = statement.executeQuery("select * from student");
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
} catch (SQLException e) {
log.error(e.getMessage(),e);
}
try {
if (statement != null) {
statement.close();
}
} catch (SQLException e){
log.error(e.getMessage(),e);
}
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e){
log.error(e.getMessage(),e);
}
}
2.3 異常處理
盡量使用非受檢異常
受檢異常的缺陷:
- 受檢異常使接口聲明脆弱
例如我們定義一個接口
interface User {
public void changePassword() throws MySecurityException;
}隨著業(yè)務(wù)的開發(fā),接口拋出的異常增加,比如新增一個RejectChangeException,那就需要修改User接口,這就會導(dǎo)致所有的User接口調(diào)用者都要追加對RejectChangeException異常的處理。
- 受檢異常使代碼的可讀性降低
一個方法增加了受檢異常,則調(diào)用者必須對異常進行處理,比如調(diào)用無受檢異常:
public static void main(String[] args) {
userImpl.changePassword();
}如果調(diào)用受檢異常就不一樣了:
public static void main(String[] args) {
try {
userImpl.changePassword();
} catch(Exception e){
e.printStackTrace();
}
}
如果一個方法使用了受檢異常,那么調(diào)用者就必須處理,特別是在多個異常的情況下,要增加多個catch塊進行處理,就會增加代碼復(fù)雜度。
- 受檢異常增加了開發(fā)工作量
不要在finally塊中處理返回值
在finally塊中進行return會導(dǎo)致如下問題:
- 覆蓋了try代碼塊中的return返回值,比如下面的方法,我們傳入100返回結(jié)果也是-1。
public static void main(String[] args) {
calculate(100);
}
public static int calculate(int number) throws Exception {
try {
if (number < 100) {
throw new DataFormatException("數(shù)據(jù)格式錯誤");
} else {
return number;
}
} catch (Exception e) {
throw e;
} finally {
return -1;
}
}
- 屏蔽異常
當(dāng)我們在try塊中拋出異常時,異常線程在監(jiān)視到有異常發(fā)生時,就會在異常表中登記當(dāng)前的異常類型為DataFormatException,但是當(dāng)執(zhí)行器執(zhí)行finally代碼塊時,就會重新為方法賦值,也就是告訴調(diào)用者“該方法執(zhí)行正確,沒有異常產(chǎn)生”,比如我們調(diào)用上面的方法,傳入-1,不會拋出異常。
異常封裝
Java中提供了異常處理機制,可以保證程序的健壯性,但是Java中提供的都是通用異常,我們在項目的開發(fā)中需要封裝一些業(yè)務(wù)的異常。
統(tǒng)一異常處理
在項目開發(fā)中,可以用切面統(tǒng)一異常處理或依賴SpringMVC的ControllerAdvice,將錯誤信息按項目統(tǒng)一格式返回給前端,這樣在開發(fā)過程中只用拋出異常就可以了。
禁止直接吞掉異常
吞掉異常會導(dǎo)致難以排查程序運行過程中出現(xiàn)的問題,應(yīng)該將異常向上拋出。
2.4 留意編譯告警
程序中的編譯告警容易被忽略,因為即使出現(xiàn)了告警,源文件仍能被編輯通過并運行,尤其是我們在開發(fā)的過程中使用IDE,但這些告警中往往隱藏著一些潛在的問題。
2.5 盡早暴露問題
一個bug在項目的開發(fā)、自測、測試、發(fā)布等階段被發(fā)現(xiàn),其修復(fù)成本是不一樣的,越往后修復(fù)成本越高,尤其到了線上可能還會造成一定的資損。項目研發(fā)在開發(fā)和自測的過程中,應(yīng)重視代碼質(zhì)量,自測或使用bug掃描工具來盡早的發(fā)現(xiàn)問題。
SpotBugs
SpotBugs提供靜態(tài)字節(jié)代碼分析,它使用靜態(tài)分析來查找400多種錯誤模式,例如空指針取消引用、無限遞歸循環(huán),對Java庫的錯誤使用和死鎖。
3、可維護性
3.1 記錄日志
- 所有后臺都要有操作日志、數(shù)據(jù)變更日志
- 日志要配置異步寫盤
- 線上僅保留WARN和ERROR級別日志
- 所有日志都要有traceId
- 異常日志要有堆棧、入?yún)?、能說清楚是什么錯誤的信息
- 打印日志時,禁止直接用JSON工具將對象轉(zhuǎn)換成String
3.2 明確錯誤提示
在產(chǎn)品的使用中,我們會提示一些錯誤信息給用戶,但是如果提供籠統(tǒng)的錯誤提示可能令用戶感到困惑,如:“服務(wù)暫不可用”,尤其我們不能提示:“系統(tǒng)內(nèi)部錯誤,請聯(lián)系系統(tǒng)管理員!”給用戶,這會降低用戶對產(chǎn)品的信任度。我們可以提示具體的錯誤信息,如“xxx信息未填,請先填寫完成?!?/p>
3.3 保持代碼簡潔性
避免嵌套if/else
代碼中經(jīng)常會進行空值判斷和邏輯判斷,if/else嵌套會使得代碼邏輯看起來非常復(fù)雜。
public static String getDepartmentNameOfUser(String username) {
Resultresult = getUserByName(username);
if (result != null) {
User user = result.getData();
if (user != null) {
Department department = user.getDepartment();
if (department != null) {
return department.getName();
}
}
}
return "未知部門";
}
盡量避免嵌套if/else,可以這樣寫:
public static String getDepartmentNameOfUser(String username) {
Result result = getUserByName(username);
if (result == null) {
return "未知部門";
}
User user = result.getData();
if (user == null) {
return "未知部門";
}
Department department = user.getDepartment();
if (department == null) {
return "Department為空";
}
return department.getName();
} 抽取類、方法
讓類或方法的職責(zé)更加明確,不要把所有的邏輯寫到一個方法里面。
public boolean addExp(ExpVO expVO){
//校驗參數(shù)是否正確,如果失敗直接拋出異常
checkParamValidate(expVO);
//新增實驗信息
addExp(expVO);
//新增實驗組
expGroupService.addExpGroup(expVO);
//新增實驗層
expLayerService.addExpLayer(expVO);
//計算實驗流量
List flowEntityList = flowService.calculateFlow(expVO);
//保存流量信息
flowService.saveFlow(flowEntityList);
} 不要使用魔法值
不要在代碼中使用魔法值,這樣后續(xù)如果值變化改動起來會漏掉。
public boolean addExp(){
//服務(wù)端實驗
expEntity.setExpType(1);
}
可以使用枚舉:
public boolean addExp(){
expEntity.setExpType(ExpTypeEnum.SERVER.getExpType());
}
3.4 使用開源工具
使用一些開源工具可以減少我們重復(fù)造輪子,而且常用的開源工具都有完整的單元測試覆蓋,可以有效的減少bug的出現(xiàn)。
Google Guava
Guava是一組來自Google的核心Java庫,其中包括集合、緩存、原生類型、并發(fā)、常用注解、基本字符串操作和I/O等等。
比如集合的交、并、查集,使用Google Guava就很方便。
Setsets = Sets.newHashSet(1, 2, 3, 4, 5, 6);
Setsets2 = Sets.newHashSet(3, 4, 5, 6, 7, 8, 9);
//交集
SetViewintersection = Sets.intersection(sets, sets2);
//差集
SetViewdiff = Sets.difference(sets, sets2);
//并集
SetViewunion = Sets.union(sets, sets2);
Apache Commons
Apache Commons是對JDK的擴展,包含了很多開源的工具,下面是我們項目中常用的工具:
Commons Lang3:處理Java基本對象方法的工具類,提供對字符、數(shù)組等基本對象的操作。
Commons Codec:提供常用的編碼和解碼方法,如DES、SHA1、Base64。
Commons BeanUtils:提供Bean的動態(tài)生成。
Commons HttpClient:簡化HTTP客戶端與服務(wù)端的各種通訊。
Log4j
各大開源框架和項目中用的最多的日志框架。
4、可擴展性
我們寫的代碼都是為了特定的需求服務(wù)的,但是這些需求并不是一成不變的,當(dāng)需求變更了,如果我們代碼的擴展性很好,我們可能只需要簡單的添加或者刪除模塊就行了,如果擴展性不好,可能所有的代碼都需要重寫,所以提供代碼的擴展性是必須的,我們在寫代碼的時候使用設(shè)計模式可以使代碼具備很好的擴展性。
比如AB分流算法,分流算法根據(jù)不同的場景不同的需求有不同的實現(xiàn),我們定義好算法的接口,不同的分流算法實現(xiàn)這個接口,那么我們在使用的時候只需要考慮使用哪個算法就行了,不需要關(guān)心算法的實現(xiàn)。
/**
* AB分桶算法
* @author hufei
* @date 2021/6/8 6:06 下午
*/
public interface BucketAlgorithmTemplate {
/**
* ab分桶算法
*
* @param layerId
* @param expId 實驗id
* @param expRatio 實驗占層流量比
* @param expGroups 實驗組詳情
* @param existsBucket 已存在的實驗層和實驗組流量詳情,第一次創(chuàng)建傳null
* @return
* @throws BucketAlgorithmException
*/
public MapcalculateBucket(Integer layerId, Integer expId, Integer expRatio, Map expGroups, Map > existsBucket) throws BucketAlgorithmException;
}
/**
* AB分桶算法
* @author hufei
* @date 2021/6/8 6:39 下午
*/
public abstract class AbstractBucketAlgorithm implements BucketAlgorithmTemplate {
/**
* @param expId 實驗id
* @param expRatio 實驗占層流量比
* @param expGroups 實驗組詳情
* @param existsBucket 已存在的實驗層和實驗組流量詳情,第一次創(chuàng)建傳null
* @return
* @author hufei
* @description ab分桶算法
* @date 2021/6/8 6:31 下午
*/
public MapcalculateBucket(Integer layerId, Integer expId, Integer expRatio, Map expGroups, Map > existsBucket) throws BucketAlgorithmException {
calculateVerify();
MapbucketMap = new HashMap<>();
JSONObject layerObj = new JSONObject();
JSONObject expObj = new JSONObject();
if (existsBucket == null) {
//如果不存在歷史分流信息,說明層是新建,實驗也是新建
layerObj = newLayer(layerId, expId, expRatio);
expObj = newExp(expGroups);
} else if (existsBucket.get(layerId) != null && existsBucket.size() == 1) {
//只有層的歷史記錄,但是沒有實驗的歷史記錄,說明層已經(jīng)存在實驗是新建
layerObj = calculateLayer(layerId, expId, expRatio, existsBucket.get(layerId));
expObj = newExp(expGroups);
} else if (existsBucket.get(layerId) != null && existsBucket.get(expId) != null) {
//有層和實驗的歷史記錄,說明層不是新建并且實驗也不是新建
layerObj = calculateLayer(layerId, expId, expRatio, existsBucket.get(layerId));
expObj = calculateExp(expGroups, existsBucket.get(expId));
}
bucketMap.put(layerId, layerObj);
bucketMap.put(expId, expObj);
return bucketMap;
}
/**
* @param layerId
* @param expId
* @param expRatio
* @return
* @author hufei
* @description 新建層
* @date 2021/6/10 11:24 上午
*/
public JSONObject newLayer(Integer layerId, Integer expId, Integer expRatio) {
return new JSONObject();
}
public JSONObject calculateLayer(Integer layerId, Integer expId, Integer expRatio, ListlayerHistoryFlow) {
return new JSONObject();
}
/**
* @return
* @author hufei
* @description 新建實驗
* @date 2021/6/10 11:24 上午
*/
public JSONObject newExp(MapexpGroups) {
return new JSONObject();
}
public JSONObject calculateExp(MapexpGroups, List expHistoryFlow) {
return new JSONObject();
}
}
/**
* 簡單分流算法實驗, 每次分流只用考慮當(dāng)前的桶, 不用回溯歷史版本
* {@link https://duapp.yuque.com/team_tech/confluence-data-iwskfg/dzmogk}
* @author hufei
* @date 2021/6/8 7:59 下午
*/
public class SimpleBucketAlgorithm extends AbstractBucketAlgorithm {
/**
* @param layerId
* @param expId
* @param expRatio
* @return
* @author hufei
* @description 新建層
* @date 2021/6/10 11:24 上午
*/
@Override
public JSONObject newLayer(Integer layerId, Integer expId, Integer expRatio) {
......
}
@Override
public JSONObject calculateLayer(Integer layerId, Integer expId, Integer expRatio, ListlayerHistoryFlow) {
......
}
/**
* @return
* @author hufei
* @description 新建實驗
* @date 2021/6/10 11:24 上午
*/
@Override
public JSONObject newExp(MapexpGroups) {
......
}
/**
* 實驗組流量變更:
* 1.優(yōu)先從右邊空白開始分配
* 2.先增后減
*
* @param expGroups
* @param expHistoryFlow
* @return
*/
@Override
public JSONObject calculateExp(MapexpGroups, List expHistoryFlow) {
.......
}
/**
* 遞歸計算
*/
private void calculateExpRecursion(JSONObject currentExpFlow, MapgroupCountMap, List positiveRatioList, List negativeRatioList, Map groupNeedAddOrReduceRatioMap) {
}
}
5、效率?
5.1 代碼優(yōu)化
循環(huán)優(yōu)化
for (int i=0;i...
}
for (int i=0,size=list.size();i...
}
不要在循環(huán)中創(chuàng)建對象
集合優(yōu)化
在初始化集合時,盡量指定可預(yù)知的集合大小,減少集合的擴容次數(shù)。
5.2 引入并發(fā)
并發(fā)可以很好的提升程序的執(zhí)行時間,但是使用不好也會帶來很多問題。如果任務(wù)和任務(wù)之間沒有關(guān)聯(lián)性,我們并發(fā)的執(zhí)行任務(wù)來縮短整體時間。
CompletableFuture[] cfs = tailorEntry.getValue().values().stream().map(layerExtraInfo -> CompletableFuture.supplyAsync(() -> layerCalculate(layerExtraInfo, userHitWhiteListMap, request.getUserId(), needGroupId, request.getCurrentGroupParm()), asyncFlowServiceExecutor).whenComplete((r, e) -> {
if (!r.isEmpty()) {
hitGroupList.addAll(r);
r.forEach(g -> {
needGroupId.add(g.getId());
});
}
})).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(cfs).join();
提高代碼質(zhì)量是一個復(fù)雜且持續(xù)的工作,一篇文章的講解也很有限,我們在項目的開發(fā)中需要持續(xù)不斷的迭代優(yōu)化,來保證代碼的質(zhì)量。?
*文
分享文章:如何提高Java代碼的質(zhì)量
標(biāo)題來源:http://m.5511xx.com/article/cdjpspg.html


咨詢
建站咨詢
