前言
逃離北上廣從廣州回老家南寧,入職這家公司用的技術是JFinal,藉此機會得以學習這個國產的MVC框架,經過一段時間的學習,基於之前的經驗搭建一個通用專案jfinal-demo
jfinal-demo是基於JFinal封裝的一個簡單通用專案,一套通用程式碼,實現增刪改查分頁等基礎功能,單表模組通過繼承通用模組實現該基礎功能,通過程式碼生成器可快速生成全套單表程式碼。
技術棧:JFinal + MySql
JFinal介紹
JFinal已連續多次獲得GVP Gitee最有價值開源專案,gitee地址:https://gitee.com/jfinal/jfinal
JFinal官方文件:https://jfinal.com/doc
JFinal官方簡介:
JFinal 是基於 Java 語言的極速 WEB + ORM + AOP + Template Engine 框架,其核心設計目標是開發迅速、程式碼量少、學習簡單、功能強大、輕量級、易擴充套件、Restful。在擁有Java語言所有優勢的同時再擁有ruby、python、php等動態語言的開發效率!為您節約更多時間,去陪戀人、家人和朋友 :)
JFinal有如下主要特點:
MVC架構,設計精巧,使用簡單
遵循COC原則,支援零配置,無xml
獨創Db + Record模式,靈活便利
ActiveRecord支援,使資料庫開發極致快速
自動載入修改後的java檔案,開發過程中無需重啟web server
AOP支援,攔截器配置靈活,功能強大
Plugin體系結構,擴充套件性強
多檢視支援,支援FreeMarker、JSP、Velocity
強大的Validator後端校驗功能
功能齊全,擁有struts2的絕大部分功能
體積小僅 723 KB,且無第三方依賴
程式碼編寫
專案結構
jfinal.bat、jfinal.sh是啟動指令碼
通用程式碼包括統一返回物件Result,分頁條件PageCondition,控制層CommonController,業務層CommonService/Impl
資料庫表與實體類的關係對映需要在_MappingKit中手動進行維護(其實也可以做成自動維護,只是我們的程式碼生成器還不支援)
/** * 資料表、主鍵、實體類關係對映 * 需要手動維護 */ public class _MappingKit { /** * 表、實體、主鍵關係集合 * 方便SqlUtil工具類拼接查詢sql */ public static HashMap<String,String> tableMapping = new HashMap<>(); public static HashMap<String,String> primaryKeyMapping = new HashMap<>(); public static void mapping(ActiveRecordPlugin arp) { arp.addMapping("blog", "id", Blog.class); tableMapping.put(Blog.class.getName(),"blog"); primaryKeyMapping.put(Blog.class.getName(),"id"); arp.addMapping("user", "user_id", User.class); tableMapping.put(User.class.getName(),"user"); primaryKeyMapping.put(User.class.getName(),"user_id"); } }
表欄位全部在BaseModel中(禁止改動)
/** * 部落格表 BaseModel * * 作者:Auto Generator By 'huanzi-qch' * 生成日期:2021-07-26 09:31:41 */ @SuppressWarnings("serial") public abstract class BaseBlog<M extends BaseBlog<M>> extends Model<M> implements IBean { //部落格id private Integer id; public void setId(Integer id) { this.id = id; set("id", this.id); } public Integer getId() { this.id = get("id"); return this.id; } //部落格標題 private String title; public void setTitle(String title) { this.title = title; set("title", this.title); } public String getTitle() { this.title = get("title"); return this.title; } //部落格內容 private String content; public void setContent(String content) { this.content = content; set("content", this.content); } public String getContent() { this.content = get("content"); return this.content; } //使用者id private String userId; public void setUserId(String userId) { this.userId = userId; set("user_id", this.userId); } public String getUserId() { this.userId = get("user_id"); return this.userId; } }
如果需要加與資料庫表無關屬性(例如方便介面接參,新增其他屬性),在Model新增,另外,表關聯也可以在這裡維護
/** * 部落格表 Model * * 作者:Auto Generator By 'huanzi-qch' * 生成日期:2021-07-26 09:31:41 */ @SuppressWarnings("serial") public class Blog extends BaseBlog<Blog> { public static final Blog dao = new Blog().dao(); /** * 表關聯操作在這裡維護 * User.userId = Blog.userId */ public Result<User> getUser(String userId){ UserServiceImpl userService = Aop.get(UserServiceImpl.class); return userService.get(userId); } }
攔截器實現Controller層全域性異常處理
/** * Controller層全域性異常處理 * 特殊情況外,禁止捕獲異常,所有異常都應交給這裡處理 */ public class GlobalExceptionInterceptor implements Interceptor{ private static Log log = Log.getLog(GlobalExceptionInterceptor.class); public void intercept(Invocation inv) { Result result = null; try { inv.invoke(); } //業務異常 catch (ServiceException e){ e.printStackTrace(); result = Result.error(e.getErrorEnum()); } //空指標、非法引數 catch (NullPointerException | IllegalArgumentException e){ e.printStackTrace(); result = Result.error(ErrorEnum.INTERNAL_SERVER_ERROR); } //... //未知異常(放在最後) catch (Exception e){ e.printStackTrace(); result = Result.error(ErrorEnum.UNKNOWN); } if(StrKit.notNull(result)){ inv.getController().renderJson(result); } } }
需要在AppConfig中配置Routes級別全域性攔截器
/** * 配置路由 */ public void configRoute(Routes me) { // 掃描僅會在該包以及該包的子包下進行 me.scan("cn.huanzi.qch."); //該方法用於配置是否要將控制器父類中的 public方法對映成 action me.setMappingSuperClass(true); // 此處配置 Routes 級別的攔截器,可配置多個 me.addInterceptor(new GlobalExceptionInterceptor()); }
所有的異常資訊都應該在ErrorEnum中維護
/** * 自定義異常列舉類 */ public enum ErrorEnum { //自定義系列 USER_NAME_IS_NOT_NULL(10001,"【引數校驗】使用者名稱不能為空"), PWD_IS_NOT_NULL(10002,"【引數校驗】密碼不能為空"), //400系列 BAD_REQUEST(400,"請求的資料格式不符!"), UNAUTHORIZED(401,"登入憑證過期!"), FORBIDDEN(403,"抱歉,你無許可權訪問!"), NOT_FOUND(404, "請求的資源找不到!"), //500系列 INTERNAL_SERVER_ERROR(500, "伺服器內部錯誤!"), SERVICE_UNAVAILABLE(503,"伺服器正忙,請稍後再試!"), //未知異常 UNKNOWN(10000,"未知異常!"); /** 錯誤碼 */ private Integer code; /** 錯誤描述 */ private String msg; ErrorEnum(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public String getMsg() { return msg; } }
測試介面
Controller public void errorTest(){ throw new ServiceException(ErrorEnum.USER_NAME_IS_NOT_NULL); } public void errorTest2(){ renderJson(blogService.errorTest2()); } public void errorTest3(){ renderJson(blogService.errorTest3()); } ServiceImpl @Override public String errorTest2() { int i = 1/0; return "失敗乃成功之母!"; } @Override public String errorTest3() { throw new NullPointerException(); }
自定義請求處理器
/** * 自定義處理器 */ public class MyActionHandler extends Handler { public MyActionHandler() { } @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { //應用路徑 request.setAttribute("ctx", request.getContextPath()); Action action = JFinal.me().getAction(target, new String[]{null}); boolean flag = false; List<String> allActionKeys = JFinal.me().getAllActionKeys(); if(!allActionKeys.contains(target)){ int i = target.lastIndexOf(47); if (i != -1) { String substring = target.substring(0, i); if (!allActionKeys.contains(substring) || action.getControllerPath().equals(substring)) { flag = true; } } } /* 404 其他靜態資源可直接訪問,但.html頁面禁止直接訪問 */ if ((target.contains(".html") || !target.contains(".")) && flag) { try { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = response.getWriter(); out.print(JsonKit.toJson(Result.error(ErrorEnum.NOT_FOUND))); out.flush(); out.close(); response.flushBuffer(); } catch (IOException e) { e.printStackTrace(); } }else{ this.next.handle(target, request, response, isHandled); } } }
效果演示
get
page
list
save
id不存在新增
id存在則更新
delete
一個簡單頁面,包括CRUD、分頁
異常處理
統一Controller層介面異常處理
非controller介面錯誤,會跳轉去配置好的500.html頁面
後記
習慣了Spring全家桶,一時可能接受不了JFinal的風格,經過改造封裝,jfinal-demo專案的程式設計風格儘量與我們之前的習慣一致
JFinal的生態遠沒有SpringBoot的好,碰到問題基本上靠百度是搜不到什麼解決方案的,好在這個框架並不複雜,依賴的東西也很少,大部分都可以按照需要進行魔改、擴充套件
程式碼開源
程式碼已經開源、託管到我的GitHub、碼雲:
GitHub:https://github.com/huanzi-qch/jfinal-demo
碼雲:https://gitee.com/huanzi-qch/jfinal-demo