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」 進行獲取。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※新北清潔公司,居家、辦公、裝潢細清專業服務