前言

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