警戒掛着開源的招牌處處坑蒙拐騙的垃圾項目,好比iBase4J

開源界,本是技術愛好者百花齊放、各顯其能的地方。可是,無論什麼好東西,到了這塊奇葩的土地都能變了味。如今的開源界,真的是魚龍混雜,有些開源軟件,不知道是噱頭喊得高,仍是star刷得好,竟能憑藉一身垃圾代碼招搖撞騙,誤人子弟。垃圾不掃,這世界只能愈來愈臭。以iBase4J爲例,我來給你們分析一下,讓你們提升警戒,尤爲是編程新手,不要上了賊船,省得抱撼終身。html

1. iBase4J是什麼東西

iBase4J,做者自稱是一個JAVA(原文如此)分佈式快速開發平臺。項目的Github地址是https://github.com/iBase4J/iBase4J。截至本文的撰寫時間(2018年7月3日,自由日前夕),該項目已有943個Star。在碼雲https://gitee.com/iBase4J/iBase4J上,該項目甚至有6422個Star,並且居然是GVP(碼雲最有價值開源項目),這就是所謂雞犬升天?java

項目的官方介紹:mysql

JAVA分佈式快速開發平臺:Spring,SpringBoot 2.0,SpringMVC,Mybatis,mybatis-plus,motan/dubbo分佈式,Redis緩存,Shiro權限管理,Spring-Session單點登陸,Quartz分佈式集羣調度,Restful服務,QQ/微信登陸,App token登陸,微信/支付寶支付;日期轉換、數據類型轉換、序列化、漢字轉拼音、身份證號碼驗證、數字轉人民幣、發送短信、發送郵件、加密解密、圖片處理、excel導入導出、FTP/SFTP/fastDFS上傳下載、二維碼、XML讀寫、高精度計算、系統配置工具類等等。SpringBoot版本:https://github.com/iBase4J/iBase4J-SpringBoot http://gitee.com/signup?inviter=iBase2Jgit

2. 我與iBase4J的淵源

話說,我本不是什麼路見不平,拔刀相助的俠客,而是魯迅筆下的一個小小的看客。對於這種垃圾項目,敬而遠之對我來講本是最佳選擇。可是,自稱iBase4J原做者的"萬明"欠了我五千多的工資不發(如此如此這般這般),就跟我結下了樑子。程序員

iBase4J的做者叫「沈華傑」,河南人,萬明(南充巴蜀文化傳媒)的小弟。萬明曾向我得意地說他纔是iBase4J的原做者。我在這家公司負責先後端接口調試,後端是沈華傑用他的iBase4J寫的。我被剋扣工資憤而離職後,萬明藉口我什麼都沒作,交接工做沒作好(我寫了1萬字以上的交接文檔,能講的都講了,萬明說新同事看了個人文檔徹底看不懂,接手不了,我調試過的接口所有都重寫了(服,接口不都是你家沈華傑寫的?我只是來調試和維護的)),不發工資,並且是一毛不發。github

沆瀣一氣者,一路貨色也。當初維護iBase4J寫的項目,搞得我焦頭爛額。如今正好記錄一下,讓你們共賞。算法

3. 從項目主頁看起

首先,JAVA 四個字母用的是全大寫。衆所周知,Java 名字的由來是印尼的爪哇島,是地名,不是詞組的簡寫。做爲一個合格的 Java 程序員,對於給了咱飯碗的 Java 語言,至少要尊重人家的名字吧。全大寫的 JAVA,由一個 Java 程序員拼寫出來,徹底是不三不四。spring

而後,看 README 中的這句話sql

持久層:mybatis持久化,使用MyBatis-Plus優化,減小sql開發量;aop切換數據庫實現讀寫分離。Transtraction註解事務。數據庫

文法內容先略過不表。單說Transtraction,英文中徹底沒有這個詞彙。事務,做爲數據庫的核心概念之一,相信程序員們對於這個詞都熟悉得很。我固然也不例外,當初打開這個項目主頁,一眼就瞅到這個不倫不類的單詞,二話沒說Fork下來改正拼寫,而後提交Pull request。我好心好意幫你修正這低級錯誤,爲了避免傷你自尊,提交信息我仍是用的純英文的Update readme.md。結果,到如今都沒改過來。沒有任何反饋,就在二十多天後悄悄把這個Pull request給關了。

首頁的其餘地方也有槽點,不過我不是來找碴的,先架起項目再說。

4. 搭建環境與運行項目

iBase4J有SpringBoot版,是在另外一個git倉庫(https://github.com/iBase4J/iBase4J-SpringBoot)。既然有SpringBoot版,就優先使用SpringBoot版吧。

先查查文檔怎麼介紹的。結果只能在 README 裏頭找到這兩句有點用的:

啓動方法:
SysServiceApplication.java
SysWebApplication.java

具體的文檔還須要加QQ羣才能下載:

加入QQ羣538240548
交流技術問題,下載項目文檔和一鍵啓動依賴服務工具。

既然沒有文檔,就直接導入IDE執行吧。

先把項目源碼克隆到本地,再用 Jetbrains IDEA 打開。IDEA 會在後臺自動下載 Maven 依賴。

依賴下載完成後,先啓動 SysServiceApplication.java ,控制檯一屏的報錯。

先不說這報錯,先看看日誌的打印。SpringBoot默認狀況下,打印的是彩色的日誌,報錯信息紅色顯示,十分醒目。但這個 ibase4J 放着 SpringBoot 精心設計好的日誌格式不用,非要在 resources 目錄建立一個 log4j2.xml ,打印了滿屏的黑色。還有一堆亂碼日誌 [main] DEBUG [DefaultVFS:102] - Reader entry: ����4? 不知道是怎麼搞出來的。

接下來看具體的報錯。

報的第一個錯是

main ERROR Unable to create file /output/logs/iBase4J-SYS-Service-dev/iBase4J-SYS-Service.log java.io.IOException: Could not create directory /output/logs/iBase4J-SYS-Service-dev
    at org.apache.logging.log4j.core.util.FileUtils.mkdir(FileUtils.java:127)
    at org.apache.logging.log4j.core.util.FileUtils.makeParentDirs(FileUtils.java:144)
    at org.apache.logging.log4j.core.appender.rolling.RollingFileManager$RollingFileManagerFactory.createManager(RollingFileManager.java:627)

顯然,是 log4j 建立日誌文件失敗,日誌的根目錄居然是 /output。你日誌文件默認不寫到工做目錄,非要在系統根目錄建立一個文件夾 output ?在類UNIX系統上,普通用戶必然沒有權限在系統根目錄建立文件夾。好吧,那就先把這個日誌文件夾創出來。執行命令 sudo mkdir /output && sudo chmod 777 /output,建立文件夾 /output 並把權限開到最大,否則普通用戶也沒有這個 /output 目錄的寫權限。

目錄建好後,再次運行,第二個報錯是:

[main] ERROR [DruidDataSource:870] - init datasource error, url: jdbc:mysql://127.0.0.1:3306/ibase4j?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&allowMultiQueries=true&serverTimezone=PRC&useSSL=false
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:127) ~[mysql-connector-java-8.0.11.jar:8.0.11]`

數據庫鏈接失敗,訪問被拒絕。無可厚非,畢竟我還沒配數據庫呢。理論上,數據庫應該配在 Spring Boot 的標準配置文件 application.yml 裏頭。可是裏頭沒有。找着一個 resources/config/dev/jdbc.properties ,看名字數據庫應該就在這裏配置了。

在這裏配置也算不錯了,ibase4J 以前的數據庫但是配置在 pom.xml 中的。2018年5月18日,我離職6天后,沈華傑鍋沒地方甩了,估計也被本身這奇葩的配置方式繞暈了,老老實實把數據庫配置放到了 properties 文件裏。

數據庫鏈接的默認配置以下:

druid.reader.url=jdbc:mysql://127.0.0.1:3306/ibase4j\u003fuseUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&allowMultiQueries=true&serverTimezone=PRC&useSSL=false
druid.reader.username=root
druid.reader.password=68NKG7n1mN8rErEfbag2qM==
druid.writer.url=jdbc:mysql://127.0.0.1:3306/ibase4j\u003fuseUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&allowMultiQueries=true&serverTimezone=PRC&useSSL=false
druid.writer.username=root
druid.writer.password=68NKG7n1mN8rErEfbag2qM==

居然把半角問號(?)用 UNICODE 碼(\u003f)表示,這個逼裝的,我給打99分,不打100分是怕你驕傲。

數據庫配置好後,再啓動應用。這下報的錯是:

[main] ERROR [SpringApplication:842] - Application run failed
java.lang.RuntimeException: 解密錯誤,錯誤信息:
    at top.ibase4j.core.util.SecurityUtil.decryptDes(SecurityUtil.java:134) ~[ibase4j-common-3.4.4.jar:?]
    at top.ibase4j.core.config.Configs.postProcessEnvironment(Configs.java:53) ~[ibase4j-common-3.4.4.jar:?]
    at org.springframework.boot.context.config.ConfigFileApplicationListener.onApplicationEnvironmentPreparedEvent(ConfigFileApplicationListener.java:183) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]

好吧,數據庫密碼居然要加密處理。找到解密邏輯,代碼以下:

if ("druid.password,druid.writer.password,druid.reader.password".contains(keyStr)) {
    String dkey = (String)map.get("druid.key");
    dkey = DataUtil.isEmpty(dkey) ? Constants.DB_KEY : dkey;
    value = SecurityUtil.decryptDes(value.toString(), dkey.getBytes());
    map.put(key, value);
}

因爲默認狀況下 druid.key 是空值,加密的密鑰取了默認值 90139119 ,這又是什麼鬼?

調用top.ibase4j.core.util.SecurityUtil#encryptDes(java.lang.String, byte[])算出加密後的本機數據庫密碼後,更新數據庫配置,再次啓動應用。此次報的錯是

[main] ERROR [JobStoreSupport$ClusterManager:3926] - ClusterManager: Error managing cluster: Failure obtaining db row lock: Table 'foo.qrtz_locks' doesn't exist
org.quartz.impl.jdbcjobstore.LockException: Failure obtaining db row lock: Table 'foo.qrtz_locks' doesn't exist
    at org.quartz.impl.jdbcjobstore.StdRowLockSemaphore.executeSQL(StdRowLockSemaphore.java:157) ~[quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.DBSemaphore.obtainLock(DBSemaphore.java:113) ~[quartz-2.3.0.jar:?]

顯然,缺乏 Quartz 相關的數據表。導入sqls/3.quartz.mysql.sql後,再次啓動應用。此次報錯是:

2018-07-04 11:03:30.287 [main] DEBUG [RetryLoop:171] - Retry-able exception received
org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /dubbo/org.ibase4j.service.SchedulerService/providers
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:102) ~[zookeeper-3.4.12.jar:3.4.12--1]

顯然,Zookeeper沒啓動。啓動Zookeeper,在啓動應用,接下來的報錯是:

[main] ERROR [SpringApplication:842] - Application run failed
org.springframework.jdbc.BadSqlGrammarException:
### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: Table 'foo.sys_user' doesn't exist

顯然,缺用戶表。導入 sqls/1.iBase4J.sql,報錯ERROR 1044 (42000) at line 16: Access denied for user 'foo'@'%' to database 'ibase4j'。iBase4J 居然把數據庫名寫死在 SQL 裏。

去掉數據庫選擇的 SQL , 再次導入,此次報錯 ERROR 1273 (HY000) at line 232: Unknown collation: 'utf8' 。不懂,應該是我機器上沒有叫utf8的字符集吧,可能新版本MySQL把這個字符集更名了。

去掉字符集設置,此次終於導入成功。啓動應用,此次終於算是啓動完成吧:

[main] INFO [ApplicationReadyListener:35] - =================================
[main] INFO [ApplicationReadyListener:38] - 系統[SysServiceApplication]啓動完成!!!
[main] INFO [ApplicationReadyListener:39] - =================================

因爲這個應用 "SysServiceApplication" 啓動的是 Java RPC 服務,所以得啓動一個 RPC 的客戶端才能驗證可否正常工做。

啓動 org.ibase4j.SysWebApplication,一次啓動完成。這個應用提供的是系統管理的 HTTP 接口。接下來測試一下。

訪問 http://localhost:8088,返回一個302,跳轉到 /index.html ,這個 /index.html 又302跳轉到 /unauthorized,返回以下的 JSON:

{
  "code": "401",
  "msg": "您尚未登陸",
  "timestamp": "1530675700497"
}

一個接口的首頁,居然用跳轉,還跳了兩次,也是沒誰了。。。

訪問 http://localhost:8088/swagger-ui.html,Swagger能進去。那就先看看這個接口的設計吧。

HTTP方法 URI Swagger描述 個人備註
PUT /user/read/list 查詢用戶 查詢全部用戶
PUT /user/read/detail 用戶詳細信息 查詢單個用戶
POST /user 修改用戶信息 新增及更新用戶
DELETE /user 刪除用戶 刪除用戶

顯然,這接口的設計跟 REST 規範相去甚遠,可是用 "PUT" 來進行查詢操做也太匪夷所思了吧。新增與修改共用一個接口,這 iBase4J 不只開源還會節流呢

找到登陸接口 POST /login,空參先調用一下,接口報錯:

{
    "code": "500",
    "msg": "系統走神了,請稍候再試.",
    "timestamp": "1530698198011"
}

全部的系統錯誤,所有返回「系統走神了,請稍候再試.」。。。

控制檯報錯:

[http-nio-8088-exec-19] ERROR [WebUtil:142] - java.lang.IllegalStateException: getInputStream() has already been called for this request
    at org.apache.catalina.connector.Request.getReader(Request.java:1232)
    at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504)
    at javax.servlet.ServletRequestWrapper.getReader(ServletRequestWrapper.java:225)
    at top.ibase4j.core.util.WebUtil.getRequestBody(WebUtil.java:135)
    at top.ibase4j.core.util.WebUtil.getParameter(WebUtil.java:164)
    at top.ibase4j.core.interceptor.EventInterceptor.afterCompletion(EventInterceptor.java:82)

意思是輸入流已經打開過了,不能再次打開。作Java居然不知道流只能打開一回。。。

這個bug之因此頑固到如今,是由於只要請求體不爲空,就不復現。

登陸的具體邏輯在 org.ibase4j.core.shiro.AuthorizeRealm 中,代碼以下:

// 登陸驗證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
        throws AuthenticationException {
    UsernamePasswordToken token = (UsernamePasswordToken)authcToken;
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("enable", 1);
    params.put("account", token.getUsername());
    List<?> list = sysUserService.queryList(params);
    if (list.size() == 1) {
        SysUser user = (SysUser)list.get(0);
        StringBuilder sb = new StringBuilder(100);
        for (int i = 0; i < token.getPassword().length; i++) {
            sb.append(token.getPassword()[i]);
        }
        if (user.getPassword().equals(SecurityUtil.encryptPassword(sb.toString()))) {
            ShiroUtil.saveCurrentUser(user.getId());
            saveSession(user.getAccount(), token.getHost());
            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getAccount(), sb.toString(),
                user.getUserName());
            return authcInfo;
        }
        logger.warn("USER [{}] PASSWORD IS WRONG: {}", token.getUsername(), sb.toString());
        return null;
    } else {
        logger.warn("No user: {}", token.getUsername());
        return null;
    }
}

其中 sysUserService 是一個 RPC 服務。如今這種調用比之前可強太多了,之前我在職的時候,所有RPC調用都走的是同一個接口,
代碼以下:

// 權限
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    Long userId = (Long)ShiroUtil.getCurrentUser();
    Parameter parameter = new Parameter("sysAuthorizeService", "queryPermissionByUserId", userId);
    logger.info("{} execute queryPermissionByUserId start...", parameter.getNo());
    List<?> list = sysProvider.execute(parameter).getResultList();
    logger.info("{} execute queryPermissionByUserId end.", parameter.getNo());
    for (Object permission : list) {
        if (StringUtils.isNotBlank((String)permission)) {
            // 添加基於Permission的權限信息
            info.addStringPermission((String)permission);
        }
    }
    // 添加用戶權限
    info.addStringPermission("user");
    return info;
}

看這一行Parameter parameter = new Parameter("sysAuthorizeService", "queryPermissionByUserId", userId);,調用的服務、
調用的方法所有經過字符串來傳遞,返回的結果再從Object向下強轉。廣義上說,應該算是自定義了一套協議了吧。
經過字符串調用服務,PHP和Ruby都不這麼幹吧?牛!

具體的調用邏輯:

@Override
public Parameter execute(Parameter parameter) {
    String no = parameter.getNo();
    logger.info("{} request:{}", no, JSON.toJSONString(parameter));
    Object service = applicationContext.getBean(parameter.getService());
    try {
        String method = parameter.getMethod();
        Object[] param = parameter.getParam();
        Object result = InstanceUtil.invokeMethod(service, method, param);
        Parameter response = new Parameter(result);
        logger.info("{} response:{}", no, JSON.toJSONString(response));
        return response;
    } catch (Exception e) {
        logger.error(no + " " + Constants.Exception_Head, e);
        throw e;
    }
}

這個方法參數用Parameter,返回類型還用Parameter,這就是傳說中的惜碼如金吧?

rpc調用經過top.ibase4j.core.base.provider.IBaseProvider#execute(top.ibase4j.core.base.provider.Parameter)方法,方法的參數和返回值均是一個Parameter對象。作參數時,調用構造函數Parameter(beanName, methodName, argList)。作返回結果時,getResult取對象,getResultList取列表,getResultPage取分頁,getResultLong取整數

具體的查詢數據庫代碼以下

@Override /** 根據參數查詢 */
public List<T> queryList(Map<String, Object> params) {
    if (DataUtil.isEmpty(params.get("orderBy"))) {
        params.put("orderBy", "id_");
    }
    if (DataUtil.isEmpty(params.get("sortAsc"))) {
        params.put("sortAsc", "desc");
    }
    List<Long> ids = mapper.selectIdPage(params);
    List<T> list = queryList(ids);
    return list;
}

做爲一套框架,字符串不傳參數,不傳枚舉,不傳常量,也不寫文檔,硬生生地就寫在 Map 裏。
至於sortAscselectIdPage之類的命名風格,還須要細細品味。

因爲時間有限,我就不慢慢深刻了。在此再列出幾條突出的槽點,供你們品鑑:

1. 項目的配置文件處處都是

修改配置的時候,尋找配置項所在的位置極其痛苦。甚至部分配置扔在jar包裏,部署後想要修改這些配置,還得解包jar,再從新打包。自定義的配置是經過org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources("classpath\*:config/\*.properties")方法獲取。此方法依次從當前項目、依賴模塊、依賴jar的config目錄讀取properties文件,不搞懂優先規則,配置好的東西就被莫名其妙地覆蓋掉了。

2. 數據庫查詢的queryById的代碼匪夷所思

private T queryById(Long id, int times) {
    CacheKey key = CacheKey.getInstance(getClass());
    T record = null;
    if (key != null) {
        try {
            record = (T)CacheUtil.getCache().get(key.getValue() + ":" + id, key.getTimeToLive());
        } catch (Exception e) {
            logger.error(Constants.Exception_Head, e);
        }
    }
    if (record == null) {
        String lockKey = getLockKey(id);
        String requestId = Sequence.next().toString();
        if (CacheUtil.getLock(lockKey, "根據ID查詢數據", requestId)) {
            try {
                record = mapper.selectById(id);
                saveCache(record);
            } finally {
                CacheUtil.unLock(lockKey, requestId);
            }
        } else {
            if (times > 3) {
                record = mapper.selectById(id);
                saveCache(record);
            } else {
                logger.debug(getClass().getSimpleName() + ":" + id + " retry getById.");
                sleep(100);
                return queryById(id, times + 1);
            }
        }
    }
    return record;
}

遞歸調用、睡眠100毫秒、計次。。。

queryById 先讀緩存,若是緩存有效,直接返回。
不然,獲取鎖(60秒超時)並調用com.baomidou.mybatisplus.mapper.BaseMapper#selectById。
沒獲取到鎖,則繼續獲取兩次,若是還沒獲取到鎖,則直接#selectById。
查詢到的數據繼續加入緩存。

最基本的用ID查詢數據,大家能看懂嗎?

3. 用戶的Token由客戶端生成

用戶的Token不在服務端生成,反而要客戶端生成,尚未格式限制,你不嫌頭大?

4. 一個用戶一個密鑰

正常狀況下,只要私鑰保存在服務器,一對密鑰就足夠安全了。但是這iBase4J的密鑰要客戶端調接口去申請,
服務端生成密鑰對保存在Redis裏。一個用戶一個密鑰,哥們,你真有苦!

5. 簽名算法把FBI都弄哭了

iBase4J的簽名算法是:請求參數按key順序排序,組成URL查詢串,取前100各字符,MD5哈希成Base64格式,後綴一個"\r\n",最後用私鑰簽名。
哥們,你這九曲迴腸的簽名方法,把FBI都弄哭了!

6. Git日誌幾乎清一色的「優化」

如下是截取的最近的幾條Git提交日誌(git log --oneline | cat

e110a6da 優化
654db900 優化
1daba720 優化
20834312 優化緩存管理
f595b17f 優化
f4d9683d 修改bug
73593c9b 優化
ab0d465e 優化讀取request.body
fb5f300c 優化
5e3d5e4d SQL
bf8a44d9 慶祝國人加入JCP
ad3b0047 慶祝國人加入JCP
603fb11f 慶祝國人加入JCP
ae7e968f 優化-慶祝國人加入JCP
c44d888c 優化
5228bce1 優化
4cd3257d 優化
7973d0b9 優化配置
6fb59931 優化配置
6393156c 優化配置
17de6d23 優化FDFS
c6bb4e70 優化
b5ea3662 JSTL
c619c366 省-市-區縣
443a6b60 優化郵件模塊
101c0f0c 優化發送郵件
0c87fd5c 優化
929d7a07 發送郵件
93c66a68 優化配置

不知道這位「Git優化大師」是在解釋什麼,仍是在掩飾什麼。。。

這iBase4J的槽點太多,我實在吐不過來了,JavaScript代碼還沒提到。軟件寫成這樣,本身用就得了,還出來招搖撞騙、誤人子弟就太過度了。

捐贈要錢、文檔要錢、加羣交流要錢、後臺UI也要錢,能要錢的地方你一個都拉不下,哥們你想錢想瘋了,還有心思寫代碼嗎???

但願你們多多轉載,多多評價,還開源界一片淨土!

文章首發:https://baijifeilong.github.io/2018/07/03/ibase4j

相關文章
相關標籤/搜索