前言
逃離北上廣從廣州回老家南寧,入職這家公司用的技術是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