开源界,本是技术爱好者百花齐放、各显其能的地方。可是,无论什么好东西,到了这块奇葩的土地都能变了味。如今的开源界,真的是鱼龙混杂,有些开源软件,不知道是噱头喊得高,仍是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也要钱,能要钱的地方你一个都拉不下,哥们你想钱想疯了,还有心思写代码吗???
但愿你们多多转载,多多评价,还开源界一片净土!