1. 程式人生 > >JFinal 開箱評測,這次我是認真的

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

![](https://cdn.geekdigging.com/technique-sharing/20200621/jfinal_header.png) ## 引言 昨天在看伺服器容器的時候意外的遇到了 JFinal ,之前我對 JFinal 的印象僅停留在這是一款國人開發的整合 Spring 全家桶的一個框架。 後來我查了一下,好像事情並沒有這麼簡單。 JFinal 連續好多年獲得 OSChina 最佳開源專案,並不是我之前理解的整合 Spring 全家桶,而是自己開發了一套 WEB + ORM + AOP + Template Engine 框架,大寫的牛逼! 先看下官方倉庫對自己的介紹: ![](https://cdn.geekdigging.com/technique-sharing/20200621/jfinal_1.png) 這介紹寫的,簡直是深的我心 `為您節約更多時間,去陪戀人、家人和朋友 :)` 。 做碼農這一行,誰不想早點把活做完,能正常下班,而不是天天 996 的福報。 介於這麼優秀的框架自己從來沒了解過,這絕對是一個 Java 老司機梭不能容忍的。 那麼今天我就做一次框架的開箱評測,看看到底能不能做到宣傳語上說的 `節約更多的時間` ,到底好不好用。 這可能是業界第一個做框架評測的文章的吧,還是先低調一把:本人能力有限,以下內容如有不對的地方還請各位海涵。 接下來的目的是簡單做一個 Demo ,完成最簡單的 CRUD 操作來體驗下 JFinal 。 ## 構建專案 我懷揣著崇敬的心態打開了 JFinal 的官方文件。 * 文件地址:https://jfinal.com/doc 在官網還看到了示例專案,這個必須 down 下來看一眼,這時一件讓我完全沒想到的事兒發生了,竟然還要我註冊登入,天啊,這都 2020 年了,下載一個 demo 竟然還要登入,我是瞎了麼。 ![](https://cdn.geekdigging.com/technique-sharing/20200621/login_jfinal.png) 好吧好吧,你是老大你說了算,誰讓我饞你身子呢。 官方對專案的構建演示是使用的 eclipse ,好吧,你又贏了,我用 idea 照著你的步驟來。 ![](https://cdn.geekdigging.com/technique-sharing/20200621/step_1.png) 過程其實很簡單,就是建立了一個 maven 專案,然後把依賴引入進去,核心依賴就下面這兩個: ```xml com.jfinal jfinal-undertow 2.1 com.jfinal jfinal 4.9 ``` 全量程式碼我就不貼了(畢竟太長),程式碼都會提交到程式碼倉庫,有興趣的同學可以訪問程式碼倉庫獲取。 其實用慣了 SpringBoot 的建立專案的過程,已經非常不習慣用這種方式來構建專案了,排除 IDEA 對 SpringBoot 專案構建的支援,直接訪問 https://start.spring.io/ ,直接勾勾選選把自己需要的依賴選上直接下載匯入 IDE 就好了。 ![](https://cdn.geekdigging.com/technique-sharing/20200621/springboot_init.png) 不過這個沒啥好說的, SpringBoot 畢竟後面是有一個大團隊在支援的,而 JFinal 貌似開發者只有一個人,能做成這樣基本上也可以說是在開源領域國人的驕傲了。 ## 專案啟動 專案依賴搞好了,接下來第一件事兒就是要想辦法啟動專案了,在 JFinal 中,有一個全域性配置類,而啟動專案的程式碼也在這裡。 這個類需要繼承 `JFinalConfig` ,而繼承這個類需要實現下面 6 個抽象方法: ```java 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 等等。 下面是我選用的官方文件給出來的一些配置: ```java 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 這個方法是配置訪問路由資訊,我的示例是這麼寫的: ```java public void configRoute(Routes me) { me.add("/user", UserController.class); } ``` 看到這裡我想到一個問題,每次我新增一個 `Controller` 都要來這裡配置下路由資訊的話,這也太傻了。 如果是小型專案還好,路由資訊不回很多,有個十幾條几十條足夠用了,如果是一些中大型專案,上百或者上千個 `Controller` ,我要是都配置在這裡,能找得到麼,這裡打個問號。 這裡在實際應用中存在一個致命的問題,在釋出版本的時候,做過專案的同學都知道,最少四套環境:開發,測試,UAT,生產。每個環境的程式碼功能版本都不一樣,難道我釋出之前需要手動人工修改這裡麼,這怎麼可能管理的過來。 ### configEngine 這個是用來配置 Template Engine ,也就是頁面模版的,介於我只想單純的簡單的寫兩個 Restful 介面,這裡我就不做配置了,下面是官方提供的示例: ```java 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 ,也就是一些外掛資訊的,我的程式碼如下: ```java 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 這個方法是用來配置全域性攔截器的,全域性攔截器分為兩類:控制層、業務層,我的示例程式碼是這樣的: ```java 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 : ```java public void configHandler(Handlers me) { me.add(new ResourceHandler()); } ``` ## 配置檔案 我看官方的配置檔案,結尾竟然是 txt ,這讓我第一眼就開始懷疑人生,為啥配置檔案要選用 txt 格式的,而裡面的配置格式,卻和 property 檔案一模一樣,難道是為了彰顯個性麼,這讓我產生了深深的懷疑。 ![](https://cdn.geekdigging.com/technique-sharing/20200621/conf.png) 在前面的那個 DemoConfig 配置類中,是可以通過 Prop 來直接獲取配置檔案的內容: ```java static Prop p; /** * PropKit.useFirstFound(...) 使用引數中從左到右最先被找到的配置檔案 * 從左到右依次去找配置,找到則立即載入並立即返回,後續配置將被忽略 */ static void loadConfig() { if (p == null) { p = PropKit.useFirstFound("demo-config-pro.txt", "demo-config-dev.txt"); } } ``` 在配置檔案這裡雖然引入了環境配置的概念,但是還是略顯粗糙,很多需要配置的內容都沒法配置,而這裡能配置的暫時看下來只有資料庫、快取服務等有限的內容。 ## Model 配置 說實話,剛開始看到 Model 這一部分的使用的時候驚呆我了,完全沒想到這麼簡單: ```java public class User extends Model { } ``` 就這樣,就可以了,裡面什麼都不用寫,完全顛覆了我之前的認知,難道這個框架會動態的去資料庫找欄位麼,倒不是智慧不智慧的問題,如果兩個人一起開發同一個專案,我光看程式碼都不知道這個 Model 裡面的屬性有啥,必須要對著資料庫一起看,這個會讓人崩潰的。 後來事實證明我年輕了,程式碼還是需要的,只是不用自己寫了, JFinal 提供了一個程式碼生成器,相關程式碼根據資料庫表自動生成的,生成的程式碼就不看了,簡單看下這個自動生成器的程式碼: ```java 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 類: ```java public class UserService { private static final User dao = new User().dao(); // 分頁查詢 public Page 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 先上程式碼吧,就著程式碼嘮: ```java 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」 進行