目錄
前言
1.不注重代碼格式
1.1 空格
1.2 換行
2.隨意的命名
2.1 有意義的參數名
2.2 見名知意
2.3 參數名風格一致
3.出現大量重復代碼
4.從不寫注釋
5.方法過長
6.參數過多
7.代碼層級太深
8.判斷條件太多
9.硬編碼
10.事務過大
11.在循環中遠程調用
11.1 批量操作
11.2 并發操作
12.頻繁捕獲異常
12.1 濫用場景1
12.2 濫用場景2
13.不正確的日志打印
14.沒校驗入參
15.返回值格式不統一
16.提交到git的代碼不完整
17.不處理沒用的代碼
18.隨意修改接口名和參數名
19.使用map接收參數
20.從不寫單元測試
前言
今天跟大家聊一個有趣的話題:如何寫出讓人抓狂的代碼?
大家看到這個標題,第一印象覺得這篇文章可能是一篇水文。但我很負責的告訴你,它是一篇有很多干貨的技術文。
曾幾何時,你在閱讀別人代碼的時候,有沒有抓狂,想生氣,想發火的時候?
今天就跟大家一起聊聊,這20種我看了會抓狂的代碼,看看你中招了沒?
1.不注重代碼格式
代碼格式說起來很虛,下面我用幾個案例演示一下,不注重代碼格式的效果。作為這篇文章的開胃小菜吧。
1.1 空格
有時候必要的空格沒有加,比如:
@Service
@Slf4j
public class TestService1{
public void test1(){
addLog("test1");if (condition1){if (condition2){if (condition3){log.info("info:{}",info);}}}
}
}
你看了這段代碼有何感想,有沒有血壓飆升的感覺?
代碼好像揉到一起去了。
那么,如何把血壓降下來呢?
答:加上空格即可。
正解:
@Service
@Slf4j
public class TestService1 {public void test1() {addLog("test1");if (condition1) {if (condition2) {if (condition3) {log.info("info:{}", info);}}}}
}
只加了一些空格,稍微調整了一下,這段代碼的層次結構一下子變得非常清晰了。
好吧,我又冷靜下來了。
1.2 換行
寫代碼時,如果有些必要的換行沒有加,可能會出現這樣的代碼:
public void update(User user) {if (null != user.getId()) {User oldUser = userMapper.findUserById(user.getId());if(null == oldUser)throw new RuntimeException("用戶id不存在");oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress());userMapper.updateUser(oldUser);} else { userMapper.insertUser(user);}
}
看了這段代碼,是不是有點生無可戀的感覺?
簡單的加點空格優化一下:
public void update(User user) {if (null != user.getId()) {User oldUser = userMapper.findUserById(user.getId());if(null == oldUser) {throw new RuntimeException("用戶id不存在");}oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress());userMapper.updateUser(oldUser);} else {userMapper.insertUser(user);}
}
代碼邏輯一下子變得清晰了許多。
2.隨意的命名
java中沒有強制規定參數、方法、類或者包名該怎么起名。但如果我們沒有養成良好的起名習慣,隨意起名的話,可能會出現很多奇怪的代碼。
2.1 有意義的參數名
有時候,我們寫代碼時為了省事(可以少敲幾個字母),參數名起得越簡單越好。假如同事A寫的代碼如下:
int a = 1;
int b = 2;
String c = "abc";
boolean b = false;
一段時間之后,同事A離職了,同事B接手了這段代碼。
他此時一臉懵逼,a是什么意思,b又是什么意思,還有c...然后心里一萬個草泥馬。
給參數起一個有意義的名字,是非常重要的事情,避免給自己或者別人埋坑。
正解:
int supplierCount = 1;
int purchaserCount = 2;
String userName = "abc";
boolean hasSuccess = false;
2.2 見名知意
光起有意義的參數名還不夠,我們不能就這點追求。我們起的參數名稱最好能夠見名知意
,不然就會出現這樣的情況:
String yongHuMing = "寒舞";
String 用戶Name = "寒舞";
String ha5 = "寒舞";
String hanWu = "寒舞";
這幾種參數名看起來是不是有點怪怪的?
為啥不定義成國際上通用的(地球人都能看懂)英文單詞呢?
String userName = "寒舞";
String hanwu = "寒舞";
上面的這兩個參數名,基本上大家都能看懂,減少了好多溝通成本。
所以建議在定義不管是參數名、方法名、類名時,優先使用國際上通用的英文單詞,更簡單直觀,減少溝通成本。少用漢子、拼音,或者數字定義名稱。
2.3 參數名風格一致
參數名其實有多種風格,列如:
//字母全小寫
int suppliercount = 1;//字母全大寫
int SUPPLIERCOUNT = 1;//小寫字母 + 下劃線
int supplier_count = 1;//大寫字母 + 下劃線
int SUPPLIER_COUNT = 1;//駝峰標識
int supplierCount = 1;
如果某個類中定義了多種風格的參數名稱,看起來是不是有點雜亂無章?
所以建議類的成員變量、局部變量和方法參數使用supplierCount,這種駝峰風格
,即:第一個字母小寫,后面的每個單詞首字母大寫。例如:
int supplierCount = 1;
此外,為了好做區分,靜態常量建議使用SUPPLIER_COUNT,即:大寫字母
?+
?下劃線
分隔的參數名。例如:
private static final int SUPPLIER_COUNT = 1;
3.出現大量重復代碼
ctrl + c
?和?ctrl + v
可能是程序員使用最多的快捷鍵了。
沒錯,我們是大自然的搬運工。哈哈哈。
在項目初期,我們使用這種工作模式,確實可以提高一些工作效率,可以少寫(實際上是少敲)很多代碼。
但它帶來的問題是:會出現大量的代碼重復。例如:
@Service
@Slf4j
public class TestService1 {public void test1() {addLog("test1");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}
@Service
@Slf4j
public class TestService2 {public void test2() {addLog("test2");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}
@Service
@Slf4j
public class TestService3 {public void test3() {addLog("test3");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}
在TestService1、TestService2、TestService3類中,都有一個addLog方法用于添加日志。
本來該功能用得好好的,直到有一天,線上出現了一個事故:服務器磁盤滿了。
原因是打印的日志太多,記了很多沒必要的日志,比如:查詢接口的所有返回值,大對象的具體打印等。
沒辦法,只能將addLog方法改成只記錄debug日志。
于是乎,你需要全文搜索,addLog方法去修改,改成如下代碼:
private void addLog(String info) {if (log.isDebugEnabled()) {log.debug("debug:{}", info);}
}
這里是有三個類中需要修改這段代碼,但如果實際工作中有三十個、三百個類需要修改,會讓你非常痛苦。改錯了,或者改漏了,都會埋下隱患,把自己坑了。
為何不把這種功能的代碼提取出來,放到某個工具類中呢?
@Slf4j
public class LogUtil {private LogUtil() {throw new RuntimeException("初始化失敗");}public static void addLog(String info) {if (log.isDebugEnabled()) {log.debug("debug:{}", info);}}
}
然后,在其他的地方,只需要調用。
@Service
@Slf4j
public class TestService1 {public void test1() {LogUtil.addLog("test1");}
}
如果哪天addLog的邏輯又要改了,只需要修改LogUtil類的addLog方法即可。你可以自信滿滿的修改,不需要再小心翼翼了。
我們寫的代碼,絕大多數是可維護性的代碼,而非一次性的。所以,建議在寫代碼的過程中,如果出現重復的代碼,盡量提取成公共方法。千萬別因為項目初期一時的爽快,而給項目埋下隱患,后面的維護成本可能會非常高。
4.從不寫注釋
有時候,在項目時間比較緊張時,很多人為了快速開發完功能,在寫代碼時,經常不喜歡寫注釋。
此外,還有些技術書中說過:好的代碼,不用寫注釋,因為代碼即注釋
。這也給那些不喜歡寫代碼注釋的人,找了一個合理的理由。
但我個人覺得,在國內每個程序員的英文水平都不一樣,思維方式和編碼習慣也有很大區別。你要把前人某些復雜的代碼邏輯真正搞懂,可能需要花費大量的時間。
我們看到spring
的核心方法refresh
,也是加了很多注釋的:
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();}catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;}finally {// Reset common introspection caches in Spring's core, since we// might not ever need metadata for singleton beans anymore...resetCommonCaches();}}}
如果你寫的代碼完全不寫注釋,可能最近一個月、三個月、半年還記得其中的邏輯。但一年、兩年,甚至更久的時間之后,你確定還能想起當初的邏輯,而不需要花費大量的時間去重新看自己的代碼梳理邏輯?
說實話,不寫注釋,到了項目后期,不光是把自己坑了,還會坑隊友。
為什么把這一條單獨拿出來?
因為我遇到過,接過鍋,被坑慘了。
5.方法過長
我們平時在寫代碼時,有時候思路來了,一氣呵成,很快就把功能開發完了。但也可能會帶來一個小問題,就是方法過長。
偽代碼如下:
public void run() {List<User> userList = userMapper.getAll();//經過一系列的數據過濾//此處省略了50行代碼List<User> updateList = //最終獲取到user集合if(CollectionUtils.isEmpty(updateList)) {return;}for(User user: updateList) {//經過一些復雜的過期時間計算//此處省略30行代碼}//分頁更新用戶的過期時間//此處省略20行代碼//發mq消息通知用戶//此處省略30行代碼
}
上面的run方法中包含了多種業務邏輯,雖說確實能夠實現完整的業務功能,但卻不能稱之為好。
為什么呢?
答:該方法總長度超過150行,里面的代碼邏輯很雜亂,包含了很多關聯性不大的代碼塊。該方法的職責太不單一了,非常不利于代碼復用和后期的維護。
那么,如何優化呢?
答:做方法拆分
,即把一個大方法拆分成多個小方法。
例如:
public void run() {List<User> userList = userMapper.getAll();List<User> updateList = filterUser(userList);if(CollectionUtils.isEmpty(updateList)) {return;}for(User user: updateList) {clacExpireDay(user);}updateUser(updateList);sendMq(updateList);
}private List<User> filterUser(List<User> userList) {//經過一系列的數據過濾//此處省略了50行代碼List<User> updateList = //最終獲取到user集合return updateList;
}private void clacExpireDay(User user) {//經過一些復雜的過期時間計算//此處省略30行代碼
}private void updateUser(List<User> updateList) {//分頁更新用戶的過期時間//此處省略20行代碼
}private void sendMq(List<User> updateList) {//發mq消息通知用戶//此處省略30行代碼
}
這樣簡單的優化之后,run方法的代碼邏輯一下子變得清晰了許多,光看它調用的子方法的名字,都能猜到這些字方法是干什么的。
每個子方法只專注于自己的事情,別的事情交給其他方法處理,職責更單一了。
此外,如果此時業務上有一個新功能,也需要給用戶發消息,那么上面定義的sendMq方法就能被直接調用了。豈不是爽歪歪?
換句話說,把大方法按功能模塊拆分成N個小方法,更有利于代碼的復用。
順便說一句,Hotspot對字節碼超過8000字節的大方法有JIT編譯限制,超過了限制不會被編譯。
6.參數過多
我們平常在定義某個方法時,可能并沒注意參數個數的問題(其實是我猜的)。我的建議是方法的參數不要超過5
個。
先一起看看下面的例子:
public void fun(String a,String b,String c,String d,String e,String f) {...
}public void client() {fun("a","b","c","d",null,"f");
}
上面的fun方法中定義了6個參數,這樣在調用該方面的所有地方都需要思考一下,這些參數該怎么傳值,哪些參數可以為空,哪些參數不能為空。
方法的入參太多,也會導致該方法的職責不單一,方法存在風險的概率更大。
那么,如何優化參數過多問題呢?
答:可以將一部分參數遷移到新方法中。
這個例子中,可以把參數d,e,f遷移到otherFun方法。例如:
public Result fun(String a,String b,String c) {...return result;
}public void otherFun(Result result,String d,String e,String f) {...
}public void client() {Result result = fun("a","b","c");otherFun(result, "d", null, "f");
}
這樣優化之后,每個方法的邏輯更單一一些,更有利于方法的復用。
如果fun中還需要返回參數a、b、c,給下個方法繼續使用,那么代碼可以改為:
public Result fun(String a,String b,String c) {...Result result = new Result();result.setA(a);result.setB(b);result.setC(c);return result;
}
在給Result對象賦值時,這里有個小技巧,可以使用lombok
的@Builder
注解,做成鏈式調用。例如:
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class Result {private String a;private String b;private String c;
}
這樣在調用的地方,可以這樣賦值:
Result result = Result.builder()
.a("a").b("b").c("c")
.build();
非常直觀明了。
此時,有人可能會說,ThreadPoolExecutor
不也提供了7個參數的方法?
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {...
}
沒錯,不過它是構造方法
,我們這里主要討論的是普通方法
。
7.代碼層級太深
不知道你有沒有見過類似這樣的代碼:
if (a == 1) {if(b == 2) {if(c == 3) {if(d == 4) {if(e == 5) {...}...}...}...}...
}
這段代碼中有很多層if判斷,是不是看得人有點眼花繚亂?
有同感的同學,請舉個手。
如果你沒啥感覺,那么接著往下看:
for(int i=0; i<100;i++) {for(int j=0; j<50;j++) {for(int m=0; m<200;m++) {for(int n=0; n<100;n++) {for(int k=0; k<50; k++) {...}}}}
}
看了這段代碼,你心中可能會一緊。這么多循環,代碼的性能真的好嗎?
這兩個例子中的代碼都犯了同一個錯誤,即:代碼層級太深
。
代碼層級太深導致的問題是代碼變得非常不好維護,不容易理清邏輯,有時候代碼的性能也可能因此變差。
那么關鍵問題來了,如何解決代碼層級較深的問題呢?
對于if判斷層級比較多的情況:
if(a!=1) {...return;
}doConditionB();
private void doConditionB() {if(b!=2) {...return;}doConditionC();
}
把不滿足條件(a1)的邏輯先執行,先返回。再把滿足條件(a1)的邏輯單獨抽取到一個方法(doConditionB)中。該doConditionB中也會把不滿足條件(b2)的邏輯先執行,先返回。再把滿足條件(b2)的邏輯單獨抽取到一個方法(doConditionC)中。后面邏輯以此類推。
這種做法是面向防御式編程
的一種,即先把不滿足條件的代碼先執行,然后才執行滿足條件的代碼。此外別忘了,把滿足條件的代碼抽取到一個新的方法中喔。
對于for循環層級太深的優化方案,一般推薦使用map
。
例如:
for(Order order:orderList) {for(OrderDetail detail: detailList) {if(order.getId().equals(detail.getOrderId())) {doSamething();}}
}
使用map優化之后:
Map<Long, List<OrderDetail>> detailMap = detailList.stream().collect(Collectors.groupingBy(OrderDetail::getOrderId));for(Order order:orderList) {List<OrderDetail> detailList = detailMap.get(order.getId());if(CollectionUtils.isNotEmpty) {doSamething();}
}
這個例子中使用map,少了一層循環,代碼效率提升一些。但不是所有的for循環都能用map替代,要根據自己實際情況選擇。
代碼層級太深,還有其他的場景,比如:方法中return的次數太多,也會降低代碼的可讀性。
這種情況,其實也可能通過面向防御式編程進行代碼優化。
8.判斷條件太多
我們在寫代碼的時候,判斷條件是必不可少的。不同的判斷條件,走的代碼邏輯通常會不一樣。
廢話不多說,先看看下面的代碼。
public interface IPay { void pay();
} @Service
public class AliaPay implements IPay { @Overridepublic void pay() { System.out.println("===發起支付寶支付==="); }
} @Service
public class WeixinPay implements IPay { @Overridepublic void pay() { System.out.println("===發起微信支付==="); }
} @Service
public class JingDongPay implements IPay { @Overridepublic void pay() { System.out.println("===發起京東支付==="); }
} @Service
public class PayService { @Autowiredprivate AliaPay aliaPay; @Autowiredprivate WeixinPay weixinPay; @Autowiredprivate JingDongPay jingDongPay; public void toPay(String code) { if ("alia".equals(code)) { aliaPay.pay(); } elseif ("weixin".equals(code)) { weixinPay.pay(); } elseif ("jingdong".equals(code)) { jingDongPay.pay(); } else { System.out.println("找不到支付方式"); } }
}
PayService類的toPay方法主要是為了發起支付,根據不同的code,決定調用用不同的支付類(比如:aliaPay)的pay方法進行支付。
這段代碼有什么問題呢?也許有些人就是這么干的。
試想一下,如果支付方式越來越多,比如:又加了百度支付、美團支付、銀聯支付等等,就需要改toPay方法的代碼,增加新的else...if判斷,判斷多了就會導致邏輯越來越多?
很明顯,這里違法了設計模式六大原則的:開閉原則 和 單一職責原則。
開閉原則:對擴展開放,對修改關閉。就是說增加新功能要盡量少改動已有代碼。
單一職責原則:顧名思義,要求邏輯盡量單一,不要太復雜,便于復用。
那么,如何優化if...else判斷呢?
答:使用?策略模式
+工廠模式
。
策略模式定義了一組算法,把它們一個個封裝起來, 并且使它們可相互替換。
工廠模式用于封裝和管理對象的創建,是一種創建型模式。
public interface IPay {void pay();
}@Service
public class AliaPay implements IPay {@PostConstructpublic void init() {PayStrategyFactory.register("aliaPay", this);}@Overridepublic void pay() {System.out.println("===發起支付寶支付===");}
}@Service
public class WeixinPay implements IPay {@PostConstructpublic void init() {PayStrategyFactory.register("weixinPay", this);}@Overridepublic void pay() {System.out.println("===發起微信支付===");}
}@Service
public class JingDongPay implements IPay {@PostConstructpublic void init() {PayStrategyFactory.register("jingDongPay", this);}@Overridepublic void pay() {System.out.println("===發起京東支付===");}
}public class PayStrategyFactory {private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();public static void register(String code, IPay iPay) {if (null != code && !"".equals(code)) {PAY_REGISTERS.put(code, iPay);}}public static IPay get(String code) {return PAY_REGISTERS.get(code);}
}@Service
public class PayService3 {public void toPay(String code) {PayStrategyFactory.get(code).pay();}
}
這段代碼的關鍵是PayStrategyFactory類,它是一個策略工廠,里面定義了一個全局的map,在所有IPay的實現類中注冊當前實例到map中,然后在調用的地方通過PayStrategyFactory類根據code從map獲取支付類實例即可。
如果加了一個新的支付方式,只需新加一個類實現IPay接口,定義init方法,并且重寫pay方法即可,其他代碼基本上可以不用動。
當然,消除又臭又長的if...else判斷,還有很多方法,比如:使用注解、動態拼接類名稱、模板方法、枚舉等等。由于篇幅有限,在這里我就不過多介紹了,更詳細的內容可以看看我的另一篇文章《替換if...else的錦囊妙計-CSDN博客》
9.硬編碼
不知道你有沒有遇到過這類需求:
- 限制批量訂單上傳接口,一次性只能上傳200條數據。
- 在job中分頁查詢用戶,一頁查詢100個用戶,然后計算用戶的等級。
上面例子中的200條數據和100個用戶,很容易硬編碼,即在代碼中把參數寫死了。
我們以上傳200條數據為例:
private static final int MAX_LIMIT = 200;public void upload(List<Order> orderList) {if(CollectionUtils.isEmpty(orderList)) {throw new BusinessException("訂單不能為空");} if(orderList.size() > MAX_LIMIT) {throw new BusinessException("超過單次請求的數量限制");}
}
其中MAX_LIMIT被定義成了靜態常量
。
上線之后,你發現上傳歷史數據時速度太慢了,需要把限制調大一點。
我擦。。。這種小小的參數改動,還需要改源代碼,重新編譯,重新打包,重新部署。。。
但如果你當初把這些公共參數,設置成可配置的,例如:
@Value("${com.susan.maxLimit:200}")
private int maxLimit = 200;public void upload(List<Order> orderList) {if(CollectionUtils.isEmpty(orderList)) {throw new BusinessException("訂單不能為空");} if(orderList.size() > maxLimit) {throw new BusinessException("超過單次請求的數量限制");}
}
這樣只需在配置中心(比如:apollo、nocas等)中修改一下配置即可,不用修改源代碼,不用重新編譯,不用重新打包,不用重新部署。
一個字:爽。
我們在前期開發的時候,寧可多花一分鐘思考一下,這個參數后面是否會被修改,是否可以定義成可配置的參數。也比后期修改代碼,重新編譯,重新打包,重新上線花的時間少得多。
10.事務過大
我們平時在使用spring框架開發項目時,喜歡用@Transactional
注解聲明事務。例如:
@Transactional(rollbackFor = Throwable.class)
public void updateUser(User user) {System.out.println("update");
}
只需在需要使用事務的方法上,使用@Transactional
注解聲明一下,該方法通過AOP就自動擁有了事務的功能。
沒錯,這種做法給我們帶來了極大的便利,開發效率更高了。
但也給我們帶來了很多隱患,比如大事務的問題。我們一起看看下面的這段代碼:
@Transactional(rollbackFor = Throwable.class)
public void updateUser(User user) {User oldUser = userMapper.getUserById(user.getId());if(null != oldUser) {userMapper.update(user);} else {userMapper.insert(user);}sendMq(user);
}
這段代碼中getUserById方法和sendMq方法,在這個案例中無需使用事務,只有update或insert方法才需要事務。
所以上面這段代碼的事務太大了,是整個方法級別的事務。假如sendMq方法是一個非常耗時的操作,則可能會導致整個updateUser方法的事務超時,從而出現大事務問題。
那么,如何解決這個問題呢?
答:可以使用TransactionTemplate
的編程式事務優化代碼。
@Autowired
private TransactionTemplate transactionTemplate;....public void updateUser(User user) {User oldUser = userMapper.getUserById(user.getId());transactionTemplate.execute((status) => {if(null != oldUser) {userMapper.update(user);} else {userMapper.insert(user);}return Boolean.TRUE;})sendMq(user);
}
只有在execute
方法中的代碼塊才真正需要事務,其余的方法,可以非事務執行,這樣就能縮小事務的范圍,避免大事務。
當然使用TransactionTemplate
這種編程式事務,縮小事務范圍,來解決大事務問題,只是其中一種手段。
如果你想對大事務問題,有更深入的了解,可以看看我的另一篇文章《事務問題的常用處理思路-CSDN博客》
11.在循環中遠程調用
有時候,我們需要在某個接口中,遠程調用第三方的某個接口。
比如:在注冊企業時,需要調用天眼查接口,查一下該企業的名稱和統一社會信用代碼是否正確。
這時候在企業注冊接口中,不得不先調用天眼查接口校驗數據。如果校驗失敗,則直接返回。如果校驗成功,才允許注冊。
如果只是一個企業還好,但如果某個請求有10個企業需要注冊,是不是要在企業注冊接口中,循環調用10次天眼查接口才能判斷所有企業是否正常呢?
public void register(List<Corp> corpList) {for(Corp corp: corpList) {CorpInfo info = tianyanchaService.query(corp); if(null == info) {throw new RuntimeException("企業名稱或統一社會信用代碼不正確");}}doRegister(corpList);
}
這樣做可以,但會導致整個企業注冊接口性能很差,極容易出現接口超時問題。
那么,如何解決這類在循環中調用遠程接口的問題呢?
11.1 批量操作
遠程接口支持批量操作,比如天眼查支持一次性查詢多個企業的數據,這樣就無需在循環中查詢該接口了。
但實際場景中,有些第三方不愿意提供第三方接口。
11.2 并發操作
java8以后通過CompleteFuture
類,實現多個線程查天眼查接口,并且把查詢結果統一匯總到一起。
具體用法我就不展開了,有興趣的朋友可以看看我的另一篇文章《接口性能優化的小技巧-CSDN博客》
12.頻繁捕獲異常
通常情況下,為了在程序中拋出異常時,任然能夠繼續運行,不至于中斷整個程序,我們可以選擇手動捕獲異常
。例如:
public void run() {try {doSameThing();} catch (Exception e) {//ignore}doOtherThing();
}
這段代碼可以手動捕獲異常,保證即使doSameThing方法出現了異常,run方法也能繼續執行完。
但有些場景下,手動捕獲異常被濫用了。
12.1 濫用場景1
不知道你在打印異常日志時,有沒有寫過類似這樣的代碼:
public void run() throws Exception {try {doSameThing();} catch (Exception e) {log.error(e.getMessage(), e);throw e;}doOtherThing();
}
通過try/catch關鍵字,手動捕獲異常的目的,僅僅是為了記錄錯誤日志,在接下來的代碼中,還是會把該異常拋出。
在每個拋出異常的地方,都捕獲一下異常,打印日志。
12.2 濫用場景2
在寫controller層接口方法時,為了保證接口有統一的返回值,你有沒有寫過類似這樣的代碼:
@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {try {List<User> userList = userService.query(ids);return Result.ok(userList);} catch (Exception e) {log.error(e.getMessage(), e);return Result.fature(500, "服務器內部錯誤");}
}
在每個controller層的接口方法中,都加上了上面這種捕獲異常的邏輯。
上述兩種場景中,頻繁的捕獲異常,會讓代碼性能降低,因為捕獲異常是會消耗性能的。
此外,這么多重復的捕獲異常代碼,看得讓人頭疼。
其實,我們還有更好的選擇。在網關層(比如:zuul或gateway),有個統一的異常處理代碼,既可以打印異常日志,也能統一封裝接口返回值,這樣可以減少很多異常被濫用的情況。
13.不正確的日志打印
在我們寫代碼的時候,打印日志是必不可少的工作之一。
因為日志可以幫我們快速定位問題,判斷代碼當時真正的執行邏輯。
但打印日志的時候也需要注意,不是說任何時候都要打印日志,比如:
@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {log.info("request params:{}", ids);List<User> userList = userService.query(ids);log.info("response:{}", userList);return userList;
}
對于有些查詢接口,在日志中打印出了請求參數和接口返回值。
咋一看沒啥問題。
但如果ids中傳入值非常多,比如有1000個。而該接口被調用的頻次又很高,一下子就會打印大量的日志,用不了多久就可能把磁盤空間打滿。
如果真的想打印這些日志該怎么辦?
@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {if (log.isDebugEnabled()) {log.debug("request params:{}", ids);}List<User> userList = userService.query(ids);if (log.isDebugEnabled()) {log.debug("response:{}", userList);}return userList;
}
使用isDebugEnabled
判斷一下,如果當前的日志級別是debug才打印日志。生產環境默認日志級別是info,在有些緊急情況下,把某個接口或者方法的日志級別改成debug,打印完我們需要的日志后,又調整回去。
方便我們定位問題,又不會產生大量的垃圾日志,一舉兩得。
14.沒校驗入參
參數校驗是接口必不可少的功能之一,一般情況下,提供給第三方調用的接口,需要做嚴格的參數校驗。
以前我們是這樣校驗參數的:
@PostMapping("/add")
public void add(@RequestBody User user) {if(StringUtils.isEmpty(user.getName())) {throw new RuntimeException("name不能為空");}if(null != user.getAge()) {throw new RuntimeException("age不能為空");}if(StringUtils.isEmpty(user.getAddress())) {throw new RuntimeException("address不能為空");}userService.add(user);
}
需要手動寫校驗的代碼,如果作為入參的實體中字段非常多,光是寫校驗的代碼,都需要花費大量的時間。而且這些校驗代碼,很多都是重復的,會讓人覺得惡心。
好消息是使用了hibernate
的參數校驗框架validate
之后,參數校驗一下子變得簡單多了。
我們只需要校驗的實體類User中使用validation框架的相關注解,比如:@NotEmpty、@NotNull等,定義需要校驗的字段即可。
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {private Long id;@NotEmptyprivate String name;@NotNullprivate Integer age;@NotEmptyprivate String address;
}
然后在controller類上加上@Validated
注解,在接口方法上加上@Valid
注解。
@Slf4j
@Validated
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/add")public void add(@RequestBody @Valid User user) {userService.add(user);}
}
這樣就能自動實現參數校驗的功能。
然而,現在需求改了,需要在User類上增加了一個參數Role,它也是必填字段,并且它的roleName和tag字段都不能為空。
但如果我們在校驗參數時,不小心把代碼寫成這樣:
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {private Long id;@NotEmptyprivate String name;@NotNullprivate Integer age;@NotEmptyprivate String address;@NotNullprivate Role role;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {@NotEmptyprivate String roleName;@NotEmptyprivate String tag;
}
結果就悲劇了。
你心里可能還樂呵呵的認為寫的代碼不錯,但實際情況是,roleName和tag字段根本不會被校驗到。
如果傳入參數:
{"name": "tom","age":1,"address":"123","role":{}
}
即使role字段傳入的是空對象,但該接口也會返回成功。
那么如何解決這個問題呢?
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {private Long id;@NotEmptyprivate String name;@NotNullprivate Integer age;@NotEmptyprivate String address;@NotNull@Validprivate Role role;
}
需要在Role字段上也加上@Valid
注解。
溫馨的提醒一聲,使用validate框架校驗參數一定要自測,因為很容易踩坑。
15.返回值格式不統一
我之前對接某個第三方時,他們有部分接口的返回值結構是這樣的:
{"ret":0,"message":null,"data":[]
}
另一部分接口的返回值結構是這樣的:
{"code":0,"msg":null,"success":true,"result":[]
}
整得我有點懵逼。
為啥沒有一個統一的返回值?
我需要給他們的接口寫兩套返回值解析的代碼,后面其他人看到了這些代碼,可能也會心生疑問,為什么有兩種不同的返回值解析?
唯一的解釋是一些接口是新項目的,另外一些接口是老項目的。
但如果不管是新項目,還是老項目,如果都有一個統一的對外網關服務,由這個服務進行鑒權和統一封裝返回值。
{"code":0,"message":null,"data":[]
}
就不會有返回值結構不一致的問題。
溫馨的提醒一下,業務服務不要捕獲異常,直接把異常拋給網關服務,由它來統一全局捕獲異常,這樣就能統一異常的返回值結構。
16.提交到git的代碼不完整
我們寫完代碼之后,把代碼提交到gitlab
上,也有一些講究。
最最忌諱的是代碼還沒有寫完,因為趕時間(著急下班),就用git
把代碼提交了。例如:
public void test() {String userName="寒舞";String password=
}
這段代碼中的password變量都沒有定義好,項目一運行起來必定報錯。
這種錯誤的代碼提交方式,一般是新手會犯。但還有另一種情況,就是在多個分支merge代碼的時候,有時候會出問題,merge之后的代碼不能正常運行,就被提交了。
好的習慣是:用git提交代碼之前,一定要在本地運行一下,確保項目能正常啟動才能提交。
寧可不提交代碼到遠程倉庫,切勿因為一時趕時間,提交了不完整的代碼,導致團隊的隊友們項目都啟動不了。
17.不處理沒用的代碼
有些時候,我們為了偷懶,對有些沒用的代碼不做任何處理。
比如:
@Slf4j
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public void add(User user) {System.out.println("add");}public void update(User user) {System.out.println("update");}public void query(User user) {System.out.println("query");}
}
本來UserService類中的add、update、query方法都在用的。后來,某些功能砍掉了,現在只有add方法真正在用。
某一天,項目組來了一個新人,接到需求需要在user表加一個字段,這時候他是不是要把add、update、query方法都仔細看一遍,評估一下影響范圍?
后來發現只有add方法需要改,他心想前面的開發者為什么不把沒用的代碼刪掉,或者標記出來呢?
在java中可以使用@Deprecated
表示這個類或者方法沒在使用了,例如:
@Slf4j
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public void add(User user) {System.out.println("add");}@Deprecatedpublic void update(User user) {System.out.println("update");}@Deprecatedpublic void query(User user) {System.out.println("query");}
}
我們在閱讀代碼時,可以先忽略標記了@Deprecated
注解的方法。這樣一個看似簡單的舉手之勞,可以給自己,或者接手該代碼的人,節省很多重復查代碼的時間。
建議我們把沒用的代碼優先刪除掉,因為gitlab中是有歷史記錄的,可以找回。但如果有些為了兼容調用方老版本的代碼,不能刪除的情況,建議使用
@Deprecated
注解相關類或者接口。
18.隨意修改接口名和參數名
不知道你有沒有遇到過這種場景:你寫了一個接口,本來以為沒人使用,后來覺得接口名或參數名不對,偷偷把它們改了。比如:
@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {return userService.query(ids);
}
接口名改了:
@PostMapping("/queryUser")
public List<User> queryUser(@RequestBody List<Long> ids) {return userService.query(ids);
}
結果導致其他人的功能報錯,原來他已經在調用該接口了。
大意了。。。
所以在修改接口名、參數名、修改參數類型、修改參數個數時,一定要先詢問一下相關同事,有沒有使用該接口,免得以后出現不必要的麻煩。
對于已經在線上使用的接口,盡量不要修改接口名、參數名、修改參數類型、修改參數個數,還有請求方式,比如:get改成post等。寧可新加一個接口,也盡量不要影響線上功能。
19.使用map接收參數
我之前見過有些小伙伴,在代碼中使用map接收參數的。例如:
@PostMapping("/map")
public void map(@RequestBody Map<String, Object> mapParam){System.out.println(mapParam);
}
在map方法中使用mapParam對象接收參數,這種做法確實很方便,可以接收多種json格式的數據。
例如:
{"id":123,"name":"寒舞","age":18,"address":"成都"
}
或者:
{"id":123,"name":"寒舞","age":18,"address":"成都","role": {"roleName":"角色","tag":"t1"}
}
這段代碼可以毫不費勁的接收這兩種格式的參數,so cool。
但同時也帶來了一個問題,那就是:參數的數據結構你沒法控制,有可能你知道調用者傳的json數據格式是第一種,還是第二種。但如果你沒有寫好注釋,其他的同事看到這段代碼,可能會一臉懵逼,map接收的參數到底是什么東東?
項目后期,這樣的代碼變得非常不好維護。有些同學接手前人的代碼,時不時吐槽一下,是有原因的。
那么,如果優化這種代碼呢?
我們應該使用有明確含義的對象去接收參數,例如:
@PostMapping("/add")
public void add(@RequestBody @Valid User user){System.out.println(user);
}
其中的User對象是我們已經定義好的對象,就不會存在什么歧義了。
20.從不寫單元測試
因為項目時間實在太緊了,系統功能都開發不完,更何況是單元測試呢?
大部分人不寫單元測試的原因,可能也是這個吧。
但我想告訴你的是,不寫單元測試并不是個好習慣。
我見過有些編程高手是測試驅動開發
,他們會先把單元測試寫好,再寫具體的業務邏輯。
那么,我們為什么要寫單元測試呢?
-
我們寫的代碼大多數是可維護的代碼,很有可能在未來的某一天需要被重構。試想一下,如果有些業務邏輯非常復雜,你敢輕易重構不?如果有單元測試就不一樣了,每次重構完,跑一次單元測試,就知道新寫的代碼有沒有問題。
-
我們新寫的對外接口,測試同學不可能完全知道邏輯,只有開發自己最清楚。不像頁面功能,可以在頁面上操作。他們在測試接口時,很有可能覆蓋不到位,很多bug測不出來。
建議由于項目時間非常緊張,在開發時確實沒有寫單元測試,但在項目后期的空閑時間也建議補上。
原文:https://www.cnblogs.com/12lisu/p/15873055.html?