1. 首页
  2. 杂谈

JFinal 开箱评测,这次我是认真的

JFinal 开箱评测,这次我是认真的

引言

昨天在看服务器容器的时候意外的遇到了 JFinal ,之前我对 JFinal 的印象仅停留在这是一款国人开发的集成 Spring 全家桶的一个框架。

后来我查了一下,好像事情并没有这么简单。

JFinal 连续好多年获得 OSChina 最佳开源项目,并不是我之前理解的集成 Spring 全家桶,而是自己开发了一套 WEB + ORM + AOP + Template Engine 框架,大写的牛逼!

先看下官方仓库对自己的介绍:

JFinal 开箱评测,这次我是认真的

这介绍写的,简直是深的我心 为您节约更多时间,去陪恋人、家人和朋友 :)

做码农这一行,谁不想早点把活做完,能正常下班,而不是天天 996 的福报。

介于这么优秀的框架自己从来没了解过,这绝对是一个 Java 老司机梭不能容忍的。

那么今天我就做一次框架的开箱评测,看看到底能不能做到宣传语上说的 节约更多的时间 ,到底好不好用。

这可能是业界第一个做框架评测的文章的吧,还是先低调一把:本人能力有限,以下内容如有不对的地方还请各位海涵。

接下来的目的是简单做一个 Demo ,完成最简单的 CRUD 操作来体验下 JFinal 。

构建项目

我怀揣着崇敬的心态打开了 JFinal 的官方文档。

在官网还看到了示例项目,这个必须 down 下来看一眼,这时一件让我完全没想到的事儿发生了,竟然还要我注册登录,天啊,这都 2020 年了,下载一个 demo 竟然还要登录,我是瞎了么。

JFinal 开箱评测,这次我是认真的

好吧好吧,你是老大你说了算,谁让我馋你身子呢。

官方对项目的构建演示是使用的 eclipse ,好吧,你又赢了,我用 idea 照着你的步骤来。

JFinal 开箱评测,这次我是认真的

过程其实很简单,就是创建了一个 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 就好了。

JFinal 开箱评测,这次我是认真的

不过这个没啥好说的, 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 文件一模一样,难道是为了彰显个性么,这让我产生了深深的怀疑。

JFinal 开箱评测,这次我是认真的

在前面的那个 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」 进行获取。

转载声明:本博客由极客挖掘机创作,采用 CC BY 3.0 CN 许可协议。可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code