T O P

[资源分享]     JFinal 开箱评测,这次我是认真的

  • By - 楼主

  • 2021-01-07 22:11:58
  • 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」 进行获取。

    本帖子中包含资源

    您需要 登录 才可以下载,没有帐号?立即注册