开源界,本是技术爱好者百花齐放、各显其能的地方。但是,不管什么好东西,到了这块奇葩的土地都能变了味。现在的开源界,真的是鱼龙混杂,有些开源软件,不知道是噱头喊得高,还是star刷得好,竟能凭借一身垃圾代码招摇撞骗,误人子弟。垃圾不扫,这世界只能越来越臭。以iBase4J为例,我来给大家分析一下,让大家提高警惕,尤其是编程新手,不要上了贼船,免得抱撼终身。
1. iBase4J是什么东西
iBase4J,作者自称是一个JAVA
(原文如此)分布式快速开发平台。项目的Github地址是https://github.com/iBase4J/iBase4J。截至本文的撰写时间(2018年7月3日,自由日前夕),该项目已有943个Star。在码云https://gitee.com/iBase4J/iBase4J上,该项目甚至有6422个Star,而且竟然是GVP(码云最有价值开源项目),这就是所谓鸡犬升天?
项目的官方介绍:
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=iBase2J
2. 我与iBase4J的渊源
话说,我本不是什么路见不平,拔刀相助的侠客,而是鲁迅笔下的一个小小的看客。对于这种垃圾项目,敬而远之对我来说本是最佳选择。但是,自称iBase4J原作者的"万明"欠了我五千多的工资不发(如此如此,这般这般),就跟我结下了梁子。
iBase4J的作者叫“沈华杰”,河南人,万明(南充巴蜀文化传媒)的小弟。万明曾向我得意地说他才是iBase4J的原作者。我在这家公司负责前后端接口调试,后端是沈华杰用他的iBase4J写的。我被克扣工资愤而离职后,万明借口我什么都没做,交接工作没做好(我写了1万字以上的交接文档,能讲的都讲了,万明说新同事看了我的文档完全看不懂,接手不了,我调试过的接口全部都重写了(服,接口不都是你家沈华杰写的?我只是来调试和维护的)),不发工资,而且是一毛不发。
狼狈为奸者,一路货色也。当初维护iBase4J写的项目,搞得我焦头烂额。现在正好记录一下,让大家共赏。
3. 从项目主页看起
首先,JAVA
四个字母用的是全大写。众所周知,Java 名字的由来是印尼的爪哇岛,是地名,不是词组的简写。作为一个合格的 Java 程序员,对于给了咱饭碗的 Java 语言,至少要尊重人家的名字吧。全大写的 JAVA
,由一个 Java 程序员拼写出来,完全是不伦不类。
然后,看 README 中的这句话
持久层: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 里。
至于sortAsc
,selectIdPage
之类的命名风格,还需要细细品味。
由于时间有限,我就不慢慢深入了。在此再列出几条突出的槽点,供大家品鉴:
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也要钱,能要钱的地方你一个都拉不下,哥们你想钱想疯了,还有心思写代码吗???
希望大家多多转载,多多评价,还开源界一片净土!