JFinal 開箱評測,這次我是認真的

極客挖掘機發表於2020-06-26

引言

昨天在看伺服器容器的時候意外的遇到了 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) {}
}

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」 進行獲取。

相關文章