前天客服羣有一個用戶反饋,說是某個用戶進入直播間老是進不去。然後就查了查這塊的代碼。和接口業務使用 PHP不同,聊天業務用 Java 開發,而且是老年版本(不知道啥時候寫的,反正給人很古老的感覺)。然後就發現了一些問題,嘗試着做了一些解決方案,在此記錄一下😂。
繼續查,發現底層錯誤堆棧中有這麼一個 exception。
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:504)
at java.lang.Integer.parseInt(Integer.java:527)
at newtv.chatserver.model.json.UserInfo.<init>(UserInfo.java:56)
at newtv.chatserver.utils.RedisHelper.getUserInfo(RedisHelper.java:475)
at newtv.chatserver.WebSocketMessageHandler.onOpen(WebSocketMessageHandler.java:521)
原來是這樣,可以看出是chattvserver.utils.RedisHelper.getUserInfo
方法裏面報了錯了,具體的錯誤內容爲NumberFormatException
。看了下里面的代碼
Map<String, String> info = jedis.hgetAll(NEWTV_USERINFO_HSET_PREFIX + uid);
if( info != null && info.containsKey("userid") && !Strings.isNullOrEmpty(info.get("userid"))){
userinfo = new UserInfo(info);
Map<String, String> vip = jedis.hgetAll(VIP_INFO + uid);
if(vip != null && vip.containsKey("id") && !Strings.isNullOrEmpty(vip.get("id"))) {
int vipid = Integer.parseInt(vip.get("id"));
而這最後一行就是定位到出問題的代碼了。因爲聊天服務器是 netty 寫的,所以之前的調試方法一般有兩種:
- 硬調,靜態檢查(就是直接看代碼,不能保證健壯性,需要很通順的思路)
- 打日誌,上測試環境,看輸出,再回歸(流程較長,步驟繁瑣)
哪種我都不想碰,聯想到大一的時候導師(姜國海),一直在強調
- 少用 IDE,用 notepad++寫Java
- 用命令行編譯執行Java 程序
老薑號稱“東北 Java 第一人”,是我的本科導師,不知道他這個稱號是不是自封的,但是他本人技術水平那確實是沒的說,牛!哈哈,有點跑偏,回到今天的問題上:完善代碼測試流程。
說到這裏,不得不提一下 Java 的 classpath.
這張圖先做下鋪墊,待會還會用得到它。接着一點點來循序漸進,一步步完成一個 “差強人意” 的自動化腳本。
- hello world 版本
//MyTest.java
public class MyTest{
public static void main(String[] args){
System.out.println("Hello world!");
}
}
在 IDEA 或者 Eclipse 中直接 run 就能看到Hello world!
的輸出,但是在命令行裏,想必大家也都知道😜
javac MyTest.java
java MyTest
- 使用第三方包
光是輸出 helloworld,只能算開了個頭,涉及 Redis 的操作,所以不可避免的要引入一些第三方的 jar 包,這裏我用了 jedis。
// MyTest.java
import redis.clients.jedis.Jedis;
public class MyTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.32.103", 6379);
System.out.println(jedis.ping());
System.out.println("hello world");
}
}
依樣畫葫蘆,咱還按剛纔的步驟來編譯執行。
javac MyTest.java
MyTest.java:2: 錯誤: 程序包redis.clients.jedis不存在
import redis.clients.jedis.Jedis;
^
MyTest.java:5: 錯誤: 找不到符號
Jedis jedis = new Jedis("192.168.32.103", 6379);
^
符號: 類 Jedis
位置: 類 MyTest
MyTest.java:5: 錯誤: 找不到符號
Jedis jedis = new Jedis("192.168.32.103", 6379);
^
符號: 類 Jedis
位置: 類 MyTest
3 個錯誤
哈哈,編譯都通不過,看給出的錯誤原因,應該是找不到Redis的jar包了,回頭看看eclipse,依稀記得有把某個jar包Add to Build Path 的行爲,那是不是說eclipse幫我們做好了添加classpath的工作,所以IDE使用起來就方便很多,命令行下就得手動添加了呢?
root@ktv322:/home/wwwroot/chattvserver/src# javac -classpath /home/wwwroot/chattvserver/libs/jedis-2.1.0.jar MyTest.java
root@ktv322:/home/wwwroot/chattvserver/src#
發現編譯沒報錯,沒報錯說明就是個好消息。接着看下執行結果:
root@ktv322:/home/wwwroot/chattvserver/src# java MyTest
Exception in thread "main" java.lang.NoClassDefFoundError: redis/clients/jedis/Jedis
at MyTest.main(MyTest.java:5)
Caused by: java.lang.ClassNotFoundException: redis.clients.jedis.Jedis
at java.net.URLClassLoader$1.run(URLClassLoader.java:359)
at java.net.URLClassLoader$1.run(URLClassLoader.java:348)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:347)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:312)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
... 1 more
執行的結果不是很樂觀。但是提示的也很明顯,沒找到類的路徑,應該和編譯的時候是一樣的問題, 那就手動加上去再試試
root@ktv322:/home/wwwroot/chattvserver/src# java -classpath /home/wwwroot/chattvserver/libs/jedis-2.1.0.jar MyTest
錯誤: 找不到或無法加載主類 MyTest
root@ktv322:/home/wwwroot/chattvserver/src#
這裏我注意到一個問題,MyTest.class文件是剛纔手動編譯出來的,明明就在當前路徑下面,但是按下tab鍵就是不出來,是因爲路徑變掉了嗎?然後嘗試着把運行時的classpath改了一下,Windows上路徑之間的分隔符是; 而Linux上的分隔符是:
root@ktv322:/home/wwwroot/chattvserver/src# java -classpath /home/wwwroot/chattvserver/libs/jedis-2.1.0.jar:. MyTest
PONG
hello world
root@ktv322:/home/wwwroot/chattvserver/src#
回過頭看看剛纔那個圖,人家也說classpath 路徑之間用 :
進行分隔。
root@ktv322:/home/wwwroot/chattvserver/src# cat MyTest.java
import java.util.Map;
import redis.clients.jedis.Jedis;
public class MyTest {
public static void main(String[] args) {
String key = "uid:2614677";
Jedis jedis = new Jedis("192.168.32.103", 6379);
Map<String, String> userinfo = jedis.hgetAll(key);
System.out.println(userinfo);
}
}
root@ktv322:/home/wwwroot/chattvserver/src# javac -classpath /home/wwwroot/chattvserver/libs/jedis-2.1.0.jar:. MyTest.java
root@ktv322:/home/wwwroot/chattvserver/src# java -classpath /home/wwwroot/chattvserver/libs/jedis-2.1.0.jar:. MyTest
{valid=0, birthday=, phone=110, lastlivetime=1548064849, memberid=, truename=嘻嘻嘻, registertime=2017-11-09 10:16:57, audience_cnt=38390, userid=2614677, md5pwd=, level=7, lastlogintime=2019-01-23 14:23:19, invalid=0, lastsessionid=10120290, gender=1, experience=654168, popularcount=69891, signature=拒絕評論啦啦啦, userlevel=7, headphoto=http://aliimg.changbalive.com/photo/35/05c78d9124a78310.jpg, accesstoken=, nickname_blob=太陽宮李榮浩, nickname_md5=fc6af45fd4ca8d10135ad5acce50ce42, audience_cnt_inner=60, starlevel=, userexp=4983, coins=10674, anchorexp=69891, experiencelevel=4983, realname=2}
- 自動化腳本
#!/usr/bin bash
# 目標: 自配置classpath
ORIGIN_PATH=`pwd`
#echo $ORIGIN_PATH
# 項目的根目錄 一般不會怎麼變化,就每次都改着用吧,或者通過export的形式動態添加,會話內有效
echo "###########################################"
echo "# 基本上每個項目都只有一個依賴包目錄,所以#"
echo "# 所以有必要的話單獨設置下SEARCH_PATH即可 #"
echo "# TODO runtime_class_path 的自發現實現 #"
echo "###########################################"
SEARCH_PATH=/home/wwwroot/chattvserver/libs/
cd $SEARCH_PATH
RUNTIME_CLASS_PATH=`find ./ -name "*.jar" | sed "s#./#$SEARCH_PATH#" | tr "\n" ":"`.
cd $ORIGIN_PATH
#echo PWD:$ORIGIN_PATH
#echo "--------------------"
NAME=$1
PREFIX=`echo $NAME| echo ${NAME%.*}`
#echo name: $NAME
#echo prefix: $PREFIX
RUBBISH_FILE=$PWD/$PREFIX.class
javac -classpath $RUNTIME_CLASS_PATH $PWD/$1
java -classpath $RUNTIME_CLASS_PATH $PREFIX
rm $RUBBISH_FILE
cd $ORIGIN_PATH
配到alias別名裏面,然後source下,運行,完事,如果想再靈活點,可以(通過export會話內)修改下SEARCH_PATH,這樣更高效。
- 新的問題
本身一個類怎麼寫,無非是調類內方法,或者第三方庫。但是如果調用其他包的方法,就還是會失敗。但是可以猜測到依舊是 classpath 的問題。那麼解決方法應該還是類似的,
模擬一個目錄結構
- workspace
- libs
- fastjson-1.2.61.jar
- jedis-2.1.0.jar
- src
- defaultpackage
- MyTest.java
- utils
- IPUsage.java
核心代碼
// src/defaultpackage/MyTest.java
import redis.clients.jedis.Jedis;
import utils.IPUsage;
public class MyTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1");
String ipaddress = jedis.get("ipaddress");
System.out.println(IPUsage.getIPString(ipaddress));
}
}
// src/utils/IPUsage.java
package utils;
public class IPUsage {
// 測試外部 class 調用
public static String getIPString(String ipaddress) {
if ("192.168.1.1".equals(ipaddress)) {
return "1.1";
}else if("192.168.0.1".equals(ipaddress)) {
return "0.1";
}
return "0.0";
}
}
彙總 classpath 版本
javac -cp /Users/biao/eclipse-workspace/MyTest/libs/fastjson-1.2.61.jar:/Users/biao/eclipse-workspace/MyTest/libs/jedis-3.2.0.jar:/Users/biao/eclipse-workspace/MyTest/src:/Users/biao/eclipse-workspace/MyTest/src/utils MyTest.java
java -cp /Users/biao/eclipse-workspace/MyTest/libs/fastjson-1.2.61.jar:/Users/biao/eclipse-workspace/MyTest/libs/jedis-3.2.0.jar:/Users/biao/eclipse-workspace/MyTest/src:/Users/biao/eclipse-workspace/MyTest/src/utils MyTest
0.0
# 修改 Redis 中的內容
redis-cli
127.0.0.1:6379> set ipaddress 192.168.1.1
OK
127.0.0.1:6379> exit
# 重新執行
java -cp /Users/biao/eclipse-workspace/MyTest/libs/fastjson-1.2.61.jar:/Users/biao/eclipse-workspace/MyTest/libs/jedis-3.2.0.jar:/Users/biao/eclipse-workspace/MyTest/src:/Users/biao/eclipse-workspace/MyTest/src/utils MyTest
1.1
可以看出啊,這個命令行已經很臃腫了。而且這還是隻涉及幾個 java 文件,想想日常你見到的Java 項目,還是放棄命令行模式編譯執行吧。選擇 ant,gradle,換新框架去吧😂。