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