開源界,本是技術愛好者百花齊放、各顯其能的地方。可是,無論什麼好東西,到了這塊奇葩的土地都能變了味。如今的開源界,真的是魚龍混雜,有些開源軟件,不知道是噱頭喊得高,仍是star刷得好,竟能憑藉一身垃圾代碼招搖撞騙,誤人子弟。垃圾不掃,這世界只能愈來愈臭。以iBase4J爲例,我來給你們分析一下,讓你們提升警戒,尤爲是編程新手,不要上了賊船,省得抱撼終身。html
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
話說,我本不是什麼路見不平,拔刀相助的俠客,而是魯迅筆下的一個小小的看客。對於這種垃圾項目,敬而遠之對我來講本是最佳選擇。可是,自稱iBase4J原做者的"萬明"欠了我五千多的工資不發(如此如此,這般這般),就跟我結下了樑子。程序員
iBase4J的做者叫「沈華傑」,河南人,萬明(南充巴蜀文化傳媒)的小弟。萬明曾向我得意地說他纔是iBase4J的原做者。我在這家公司負責先後端接口調試,後端是沈華傑用他的iBase4J寫的。我被剋扣工資憤而離職後,萬明藉口我什麼都沒作,交接工做沒作好(我寫了1萬字以上的交接文檔,能講的都講了,萬明說新同事看了個人文檔徹底看不懂,接手不了,我調試過的接口所有都重寫了(服,接口不都是你家沈華傑寫的?我只是來調試和維護的)),不發工資,並且是一毛不發。github
沆瀣一氣者,一路貨色也。當初維護iBase4J寫的項目,搞得我焦頭爛額。如今正好記錄一下,讓你們共賞。算法
首先,JAVA
四個字母用的是全大寫。衆所周知,Java 名字的由來是印尼的爪哇島,是地名,不是詞組的簡寫。做爲一個合格的 Java 程序員,對於給了咱飯碗的 Java 語言,至少要尊重人家的名字吧。全大寫的 JAVA
,由一個 Java 程序員拼寫出來,徹底是不三不四。spring
而後,看 README 中的這句話sql
持久層:mybatis持久化,使用MyBatis-Plus優化,減小sql開發量;aop切換數據庫實現讀寫分離。Transtraction註解事務。數據庫
文法內容先略過不表。單說Transtraction
,英文中徹底沒有這個詞彙。事務,做爲數據庫的核心概念之一,相信程序員們對於這個詞都熟悉得很。我固然也不例外,當初打開這個項目主頁,一眼就瞅到這個不倫不類的單詞,二話沒說Fork下來改正拼寫,而後提交Pull request
。我好心好意幫你修正這低級錯誤,爲了避免傷你自尊,提交信息我仍是用的純英文的Update readme.md
。結果,到如今都沒改過來。沒有任何反饋,就在二十多天後悄悄把這個Pull request
給關了。
首頁的其餘地方也有槽點,不過我不是來找碴的,先架起項目再說。
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 裏。
至於sortAsc
,selectIdPage
之類的命名風格,還須要細細品味。
因爲時間有限,我就不慢慢深刻了。在此再列出幾條突出的槽點,供你們品鑑:
修改配置的時候,尋找配置項所在的位置極其痛苦。甚至部分配置扔在jar包裏,部署後想要修改這些配置,還得解包jar,再從新打包。自定義的配置是經過org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources("classpath\*:config/\*.properties")
方法獲取。此方法依次從當前項目、依賴模塊、依賴jar的config目錄讀取properties文件,不搞懂優先規則,配置好的東西就被莫名其妙地覆蓋掉了。
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查詢數據,大家能看懂嗎?
用戶的Token不在服務端生成,反而要客戶端生成,尚未格式限制,你不嫌頭大?
正常狀況下,只要私鑰保存在服務器,一對密鑰就足夠安全了。但是這iBase4J的密鑰要客戶端調接口去申請,
服務端生成密鑰對保存在Redis裏。一個用戶一個密鑰,哥們,你真有苦!
iBase4J的簽名算法是:請求參數按key順序排序,組成URL查詢串,取前100各字符,MD5哈希成Base64格式,後綴一個"\r\n",最後用私鑰簽名。
哥們,你這九曲迴腸的簽名方法,把FBI都弄哭了!
如下是截取的最近的幾條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也要錢,能要錢的地方你一個都拉不下,哥們你想錢想瘋了,還有心思寫代碼嗎???
但願你們多多轉載,多多評價,還開源界一片淨土!