昨天在看服务器容器的时候意外的遇到了 JFinal ,之前我对 JFinal 的印象仅停留在这是一款国人开发的集成 Spring 全家桶的一个框架。
后来我查了一下,好像事情并没有这么简单。
JFinal 连续好多年获得 OSChina 最佳开源项目,并不是我之前理解的集成 Spring 全家桶,而是自己开发了一套 WEB + ORM + AOP + Template Engine 框架,大写的牛逼!
先看下官方仓库对自己的介绍:
这介绍写的,简直是深的我心 为您节约更多时间,去陪恋人、家人和朋友 :)
。
做码农这一行,谁不想早点把活做完,能正常下班,而不是天天 996 的福报。
介于这么优秀的框架自己从来没了解过,这绝对是一个 Java 老司机梭不能容忍的。
那么今天我就做一次框架的开箱评测,看看到底能不能做到宣传语上说的 节约更多的时间
,到底好不好用。
这可能是业界第一个做框架评测的文章的吧,还是先低调一把:本人能力有限,以下内容如有不对的地方还请各位海涵。
接下来的目的是简单做一个 Demo ,完成最简单的 CRUD 操作来体验下 JFinal 。
我怀揣着崇敬的心态打开了 JFinal 的官方文档。
在官网还看到了示例项目,这个必须 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) {} }
这个方法主要是用来配置 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
配置文件里面,不过这里这么配置我个人感觉无所谓,只是稍微有点不适应。
这个方法是配置访问路由信息,我的示例是这么写的:
public void configRoute(Routes me) { me.add("/user", UserController.class); }
看到这里我想到一个问题,每次我新增一个 Controller
都要来这里配置下路由信息的话,这也太傻了。
如果是小型项目还好,路由信息不回很多,有个十几条几十条足够用了,如果是一些中大型项目,上百或者上千个 Controller
,我要是都配置在这里,能找得到么,这里打个问号。
这里在实际应用中存在一个致命的问题,在发布版本的时候,做过项目的同学都知道,最少四套环境:开发,测试,UAT,生产。每个环境的代码功能版本都不一样,难道我发布之前需要手动人工修改这里么,这怎么可能管理的过来。
这个是用来配置 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"); }
这里是用来配置 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);
,还是回到上面的问题,不同的环境之间发布系统需要手动修改这里,项目不大还能人工管理,项目大的话这里会成为噩梦。
这个方法是用来配置全局拦截器的,全局拦截器分为两类:控制层、业务层,我的示例代码是这样的:
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 里面完全一致。
这个方法用来配置 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 这一部分的使用的时候惊呆我了,完全没想到这么简单:
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 进去估计一两天都搞不完。
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 操作写法倒是蛮正常的,无任何槽点。
先上代码吧,就着代码唠:
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」 进行获取。