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

引言

昨天在看服務器容器的時候意外的遇到了 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」 進行獲取。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

您可能也會喜歡…