

JFinal 开箱评测,这次我是认真的
source link: http://www.cnblogs.com/babycomeon/p/13193956.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引言
昨天在看服务器容器的时候意外的遇到了 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 项目,然后把依赖引入进去,核心依赖就下面这两个:
<dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal-undertow</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal</artifactId> <version>4.9</version> </dependency>
全量代码我就不贴了(毕竟太长),代码都会提交到代码仓库,有兴趣的同学可以访问代码仓库获取。
其实用惯了 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<User> { }
就这样,就可以了,里面什么都不用写,完全颠覆了我之前的认知,难道这个框架会动态的去数据库找字段么,倒不是智能不智能的问题,如果两个人一起开发同一个项目,我光看代码都不知道这个 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_"); // 生成 generator.generate(); }
看到这段代码我心都凉了,居然是整个数据库做扫描的,还好是用的 MySQL ,开源免费的,如果是 Oracle ,一个项目就需要一台数据库或者是一个数据库集群,这个太有钱了。
当然,这段代码也提供了排除不需要生成的表名 addExcludedTable()
方法,其实没什么使用价值,一个 Oracle 集群上可能有 N 多个项目一起跑,上面的表成百上千张,一个小项目如果只用到十来张表, addExcludedTable()
这个方法光把表名 copy 进去估计一两天都搞不完。
数据库 CRUD 操作
JFinal 把数据的 CRUD 操作集成在了 Model 上,这种做法如何我不做评价,看下我写的一个样例 Service 类:
public class UserService { private static final User dao = new User().dao(); // 分页查询 public Page<User> 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
先上代码吧,就着代码唠:
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」 进行获取。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK