引言
昨天在看服务器容器的时候意外的遇到了 JFinal ,之前我对 JFinal 的印象仅停留在这是一款国人开发的集成 Spring 全家桶的一个框架。
后来我查了一下,好像事情并没有这么简单。
JFinal 连续好多年获得 OSChina 最佳开源项目,并不是我之前理解的集成 Spring 全家桶,而是自己开发了一套 WEB + ORM + AOP + Template Engine 框架,大写的牛逼!
先看下官方仓库对自己的介绍:
这介绍写的,简直是深的我心 为您节约更多时间,去陪恋人、家人和朋友 。
做码农这一行,谁不想早点把活做完,能正常下班,而不是天天 996 的福报。
介于这么优秀的框架自己从来没了解过,这绝对是一个 Java 老司机梭不能容忍的。
那么今天我就做一次框架的开箱评测,看看到底能不能做到宣传语上说的 节约更多的时间 ,到底好不好用。
这可能是业界第一个做框架评测的文章的吧,还是先低调一把:本人能力有限,以下内容如有不对的地方还请各位海涵。
接下来的目的是简单做一个 Demo ,完成最简单的 CRUD 操作来体验下 JFinal 。
构建项目
我怀揣着崇敬的心态打开了 JFinal 的官方文档。
文档地址:https://jfinal.com/doc
在官网还看到了示例项目,这个必须 down 下来看一眼,这时一件让我完全没想到的事儿发生了,竟然还要我注册登录,天啊,这都 2020 年了,下载一个 demo 竟然还要登录,我是瞎了么。
好吧好吧,你是老大你说了算,谁让我馋你身子呢。
官方对项目的构建演示是使用的 eclipse ,好吧,你又赢了,我用 idea 照着你的步骤来。
过程其实很简单,就是创建了一个 maven 项目,然后把依赖引入进去,核心依赖就下面这两个:
com.jfinal jfinal-undertow 2.1 com.jfinal jfinal 4.9 全量代码我就不贴了(毕竟太长),代码都会提交到代码仓库,有兴趣的同学可以访问代码仓库获取。其实用惯了 SpringBoot 的创建项目的过程,已经非常不习惯用这种方式来构建项目了,排除 IDEA 对 SpringBoot 项目构建的支持,直接访问 https://start.spring.io/ ,直接勾勾选选把自己需要的依赖选上直接下载导入 IDE 就好了。
不过这个没啥好说的, SpringBoot 毕竟后面是有一个大团队在支持的,而 JFinal 貌似开发者只有一个人,能做成这样基本上也可以说是在开源领域国人的骄傲了。
项目启动
项目依赖搞好了,接下来第一件事儿就是要想办法启动项目了,在 JFinal 中,有一个全局配置类,而启动项目的代码也在这里。
这个类需要继承 JFinalConfig ,而继承这个类需要实现下面 6 个抽象方法:
public class DemoConfig extends JFinalConfig {
public void configConstant(Constants me) {}
public void configRoute(Routes me) {}
public void configEngine(Engine me) {}
public void configPlugin(Plugins me) {}
public void configInterceptor(Interceptors me) {}
public void configHandler(Handlers me) {}
}
configConstant
这个方法主要是用来配置 JFinal 的一些常量值,比如:设置 aop 代理使用 cglib,设置日志使用 slf4j 日志系统,默认编码格式为 UTF-8 等等。
下面是我选用的官方文档给出来的一些配置:
public void configConstant(Constants me) {
// 配置开发模式,true 值为开发模式
me.setDevMode(true);
// 配置 aop 代理使用 cglib,否则将使用 jfinal 默认的动态编译代理方案
me.setToCglibProxyFactory();
// 配置依赖注入
me.setInjectDependency(true);
// 配置依赖注入时,是否对被注入类的超类进行注入
me.setInjectSuperClass(false);
// 配置为 slf4j 日志系统,否则默认将使用 log4j
// 还可以通过 me.setLogFactory(…) 配置为自行扩展的日志系统实现类
me.setToSlf4jLogFactory();
// 设置 Json 转换工厂实现类,更多说明见第 12 章
me.setJsonFactory(new MixedJsonFactory());
// 配置视图类型,默认使用 jfinal enjoy 模板引擎
me.setViewType(ViewType.JFINAL_TEMPLATE);
// 配置 404、500 页面
me.setError404View("/common/404.html");
me.setError500View("/common/500.html");
// 配置 encoding,默认为 UTF8
me.setEncoding(“UTF8”);
// 配置 json 转换 Date 类型时使用的 data parttern
me.setJsonDatePattern(“yyyy-MM-dd HH:mm”);
// 配置是否拒绝访问 JSP,是指直接访问 .jsp 文件,与 renderJsp(xxx.jsp) 无关
me.setDenyAccessJsp(true);
// 配置上传文件最大数据量,默认 10M
me.setMaxPostSize(10 * 1024 * 1024);
// 配置 urlPara 参数分隔字符,默认为 “-”
me.setUrlParaSeparator("-");
}
这里是一些项目的通用配置信息,在 SpringBoot 中这种配置信息一般是写在 yaml 或者 property 配置文件里面,不过这里这么配置我个人感觉无所谓,只是稍微有点不适应。
configRoute
这个方法是配置访问路由信息,我的示例是这么写的:
public void configRoute(Routes me) {
me.add("/user", UserController.class);
}
看到这里我想到一个问题,每次我新增一个 Controller 都要来这里配置下路由信息的话,这也太傻了。
如果是小型项目还好,路由信息不回很多,有个十几条几十条足够用了,如果是一些中大型项目,上百或者上千个 Controller ,我要是都配置在这里,能找得到么,这里打个问号。
这里在实际应用中存在一个致命的问题,在发布版本的时候,做过项目的同学都知道,最少四套环境:开发,测试,UAT,生产。每个环境的代码功能版本都不一样,难道我发布之前需要手动人工修改这里么,这怎么可能管理的过来。
configEngine
这个是用来配置 Template Engine ,也就是页面模版的,介于我只想单纯的简单的写两个 Restful 接口,这里我就不做配置了,下面是官方提供的示例:
public void configEngine(Engine me) {
me.addSharedFunction("/view/common/layout.html");
me.addSharedFunction("/view/common/paginate.html");
me.addSharedFunction("/view/admin/common/layout.html");
}
configPlugin
这里是用来配置 JFinal 的 Plugin ,也就是一些插件信息的,我的代码如下:
public void configPlugin(Plugins me) {
DruidPlugin dp = new DruidPlugin(p.get(“jdbcUrl”), p.get(“user”), p.get(“password”).trim());
me.add(dp);
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
arp.addMapping("user", User.class);
me.add(arp);
}
我的配置很简单,前面配置了 Druid 的数据库连接池插件,后面配置了 ActiveRecord 数据库访问插件。
让我觉得有点傻的地方是我如果要增加 ActiveRecord 数据库访问的映射关系,需要手动在这里增加代码,比如 arp.addMapping(“aaa”, Aaa.class); ,还是回到上面的问题,不同的环境之间发布系统需要手动修改这里,项目不大还能人工管理,项目大的话这里会成为噩梦。
configInterceptor
这个方法是用来配置全局拦截器的,全局拦截器分为两类:控制层、业务层,我的示例代码是这样的:
public void configInterceptor(Interceptors me) {
me.add(new AuthInterceptor());
me.addGlobalActionInterceptor(new ActionInterceptor());
me.addGlobalServiceInterceptor(new ServiceInterceptor());
}
这里 me.add(…) 与 me.addGlobalActionInterceptor(…) 两个方法是完全等价的,都是配置拦截所有 Controller 中 action 方法的拦截器。而 me.addGlobalServiceInterceptor(…) 配置的拦截器将拦截业务层所有 public 方法。
拦截器没什么好说的,这么配置感觉和 SpringBoot 里面完全一致。
configHandler
这个方法用来配置 JFinal 的 Handler , Handler 可以接管所有 Web 请求,并对应用拥有完全的控制权。
这个方法是一个高阶的扩展方法,我只是想写一个简单的 CRUD 操作,完全用不着,这里还是摘抄一个官方的 Demo :
public void configHandler(Handlers me) {
me.add(new ResourceHandler());
}
配置文件
我看官方的配置文件,结尾竟然是 txt ,这让我第一眼就开始怀疑人生,为啥配置文件要选用 txt 格式的,而里面的配置格式,却和 property 文件一模一样,难道是为了彰显个性么,这让我产生了深深的怀疑。
在前面的那个 DemoConfig 配置类中,是可以通过 Prop 来直接获取配置文件的内容:
static Prop p;
/**
* PropKit.useFirstFound(…) 使用参数中从左到右最先被找到的配置文件
* 从左到右依次去找配置,找到则立即加载并立即返回,后续配置将被忽略
*/
static void loadConfig() {
if (p == null) {
p = PropKit.useFirstFound(“demo-config-pro.txt”, “demo-config-dev.txt”);
}
}
在配置文件这里虽然引入了环境配置的概念,但是还是略显粗糙,很多需要配置的内容都没法配置,而这里能配置的暂时看下来只有数据库、缓存服务等有限的内容。
Model 配置
说实话,刚开始看到 Model 这一部分的使用的时候惊呆我了,完全没想到这么简单:
public class User extends Model {
}
就这样,就可以了,里面什么都不用写,完全颠覆了我之前的认知,难道这个框架会动态的去数据库找字段么,倒不是智能不智能的问题,如果两个人一起开发同一个项目,我光看代码都不知道这个 Model 里面的属性有啥,必须要对着数据库一起看,这个会让人崩溃的。
后来事实证明我年轻了,代码还是需要的,只是不用自己写了, JFinal 提供了一个代码生成器,相关代码根据数据库表自动生成的,生成的代码就不看了,简单看下这个自动生成器的代码:
public static void main(String[] args) {
// base model 所使用的包名
String baseModelPackageName = “com.geekdigging.demo.model.base”;
// base model 文件保存路径
String baseModelOutputDir = PathKit.getWebRootPath() + “/src/main/java/com/geekdigging/demo/model/base”;
// model 所使用的包名 (MappingKit 默认使用的包名)
String modelPackageName = “com.geekdigging.demo.model”;
// model 文件保存路径 (MappingKit 与 DataDictionary 文件默认保存路径)
String modelOutputDir = baseModelOutputDir + “/…”;
// 创建生成器
Generator generator = new Generator(getDataSource(), baseModelPackageName, baseModelOutputDir, modelPackageName, modelOutputDir);
// 配置是否生成备注
generator.setGenerateRemarks(true);
// 设置数据库方言
generator.setDialect(new MysqlDialect());
// 设置是否生成链式 setter 方法
generator.setGenerateChainSetter(false);
// 添加不需要生成的表名
generator.addExcludedTable(“adv”, “data”, “rate”, “douban2019”);
// 设置是否在 Model 中生成 dao 对象
generator.setGenerateDaoInModel(false);
// 设置是否生成字典文件
generator.setGenerateDataDictionary(false);
// 设置需要被移除的表名前缀用于生成modelName。例如表名 “osc_user”,移除前缀 "osc_"后生成的model名为 "User"而非 OscUser
generator.setRemovedTableNamePrefixes(“t_”);
// 生成
http://v.qq.com/x/search/?q=%E7%BC%85%E7%94%B8%E5%8D%8E%E7%BA%B3%E5%BC%80%E6%88%B7%E7%94%B5%E8%AF%9D_18183615678
generator.generate();
}
看到这段代码我心都凉了,居然是整个数据库做扫描的,还好是用的 MySQL ,开源免费的,如果是 Oracle ,一个项目就需要一台数据库或者是一个数据库集群,这个太有钱了。
当然,这段代码也提供了排除不需要生成的表名 addExcludedTable() 方法,其实没什么使用价值,一个 Oracle 集群上可能有 N 多个项目一起跑,上面的表成百上千张,一个小项目如果只用到十来张表,addExcludedTable() 这个方法光把表名 copy 进去估计一两天都搞不完。
数据库 CRUD 操作
JFinal 把数据的 CRUD 操作集成在了 Model 上,这种做法如何我不做评价,看下我写的一个样例 Service 类:
http://v.qq.com/x/search/?q=%E5%8D%8E%E7%BA%B3%E5%AE%A2%E6%9C%8D%E7%94%B5%E8%AF%9D_18183615678
public class UserService {
private static final User dao = new User().dao();
// 分页查询
public Page userPage() {
return dao.paginate(1, 10, “select *”, “from user where age > ?”, 18);
}
public User findById(String id) {
System.out.println(">>>>>>>>>>>>>>>>UserService.findById()>>>>>>>>>>>>>>>>>>>>>>>>>");
return dao.findById(id);
}
public void save(User user) {
System.out.println(">>>>>>>>>>>>>>>>UserService.save()>>>>>>>>>>>>>>>>>>>>>>>>>");
user.save();
}
public void update(User user) {
System.out.println(">>>>>>>>>>>>>>>>UserService.update()>>>>>>>>>>>>>>>>>>>>>>>>>");
user.update();
}
public void deleteById(String id) {
System.out.println(">>>>>>>>>>>>>>>>UserService.deleteById()>>>>>>>>>>>>>>>>>>>>>>>>>");
dao.deleteById(id);
}
}
这里的分页查询看的我有点懵逼,为啥一句 SQL 非要拆成两半,总感觉后面那半 from user where age > ? 是 Hibernate 的 HQL ,难道这两者之间有啥不可告人的秘密么。
其他的普通 CRUD 操作写法倒是蛮正常的,无任何槽点。
Controller
先上代码吧,就着代码唠:
http://v.qq.com/x/search/?q=%E5%8D%8E%E7%BA%B3%E5%B9%B3%E5%8F%B0%E6%B3%A8%E5%86%8C_18183615678
public class UserController extends Controller {
@Inject
UserService service;
public void findById() {
renderJson(service.findById("1"));
}
public void save() {
User user = new User();
user.set("id", "2");
user.set("create_date", new Date());
user.set("name", "小红");
user.set("age", 24);
service.save(user);
renderNull();
}
public void update() {
User user = new User();
user.set("id", "2");
user.set("create_date", new Date());
user.set("name", "小红");
user.set("age", 19);
service.update(user);
renderNull();
}
public void deleteById() {
service.deleteById(getPara("id"));
renderNull();
}
}
首先 Service 使用 @Inject 进行注入,这个没啥好说的,和 Spring 里面的 @Autowaire 一样。
这个类里面所有实际方法的返回类型都是 void 空类型,返回的内容全靠 render() 进行控制,可以返回 json 也可以返回页面视图,也罢,只是稍微有点不适应,这个没啥问题。
但是接下来这个问题就让我有点方了,感觉都不是问题,成了缺陷了,获取参数只提供了两种方法:
一种是 getPara() 系列方法,这种方法只能获取到表单提交的数据,基本上类似于 Spring 中的 request.getParameter() 。
另一种是 getModel / getBean ,首先,这两个方法接受通过表单提交过来的参数,其次是一定要转成一个 Model 类。
我就想知道一件事情,如果一个请求的类型不是表单提交,而是 application/json ,怎么去接受参数,我把文档翻了好几遍,都没找到我想要的 request 对象。
可能只是我没找到,一个成熟的框架,不应该不支持这种常见的 application/json 的数据提交方式,这不可能的。
还有就是,getModel / getBean 这种方式一定要直接转化成 Model 类,有时候并不是一件好事,如果当前这个接口的入参格式比较复杂,这种 Model 构造起来还是有一定难度的,尤其是有时候只需要获取其中的少量数据做解析预处理,完全没必要解析整个请求数据。
小结
通过一个简单的 CRUD 操作看下来, JFinal 整体上完成了一个 WEB + ORM 框架该有的东西,只是有些地方做的不是那么好的,当然,这是和 SpringBoot 做比较。
如果是拿来做一些小东西感觉还是可以值得尝试的,如果是要做一些企业级的应用,就显得有些捉襟见肘了。
不过这个项目出来的年代是比较早了,从 2012 年至今已经走过了 8 年的时间了,如果是和当年的 SpringMCV + Spring + ORM 这种框架做比较,我觉得我选的话肯定是会选 JFinal 的。
如果是和现在的 SpringBoot 做比较,我觉得我还是倾向于选择 SpringBoot ,一个是因为熟悉,另一个是因为 JFinal 很多地方,为了方便开发者使用,把相当多的代码都封装起来了,这种做法不能说不好,对于初学者而言肯定是好的,文档简单看看,基本上半天到一天就能开始上手干活的,但是对于一些老司机而言,这样做会让人觉得束手束脚的,这也不能做那也不能做。
我自己的示例代码和官方的 Demo 我一起提交到代码仓库了,有需要的同学可以回复 「JFinal」 进行获取。