前言

  逃离北上广从广州回老家南宁,入职这家公司用的技术是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

不想用Spring全家桶?试试这个国产JFinal框架的更多相关文章

  1. 10分钟详解Spring全家桶7大知识点

    Spring框架自2002年诞生以来一直备受开发者青睐,它包括SpringMVC.SpringBoot.Spring Cloud.Spring Cloud Dataflow等解决方案.有人亲切的称之为 ...

  2. Spring全家桶系列–SpringBoot之AOP详解

    //本文作者:cuifuan //本文将收录到菜单栏:<Spring全家桶>专栏中 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP). OOP中模块化的关 ...

  3. Java秋招面试复习大纲(二):Spring全家桶+MyBatis+MongDB+微服务

    前言 对于那些想面试高级 Java 岗位的同学来说,除了算法属于比较「天方夜谭」的题目外,剩下针对实际工作的题目就属于真正的本事了,热门技术的细节和难点成为了面试时主要考察的内容. 这里说「天方夜谭」 ...

  4. 一文解读Spring全家桶 (转)

    Spring框架自2002年诞生以来一直备受开发者青睐,它包括SpringMVC.SpringBoot.Spring Cloud.Spring Cloud Dataflow等解决方案.有人亲切的称之为 ...

  5. 【转】Spring全家桶

    Spring框架自诞生以来一直备受开发者青睐,有人亲切的称之为:Spring 全家桶.它包括SpringMVC.SpringBoot.Spring Cloud.Spring Cloud Dataflo ...

  6. Spring全家桶注解一览(精选)

    废话 最近想整理一波Spring注解相关的文章,虽然写CURD就只涉及到那些常用的注解.但是笔者我也想去了解一下其他注解,丰富下自己的知识面(提升一下逼格!). 就想在网上搜了半天,好像大家的都差不多 ...

  7. Spring全家桶——SpringBoot之AOP详解

    Spring全家桶--SpringBoot之AOP详解 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP). OOP中模块化的关键单元是类,而在AOP中,模块化单元是方 ...

  8. Spring全家桶–SpringBoot Rest API

    Spring Boot通过提供开箱即用的默认依赖或者转换来补充Spring REST支持.在Spring Boot中编写RESTful服务与SpringMVC没有什么不同.总而言之,基于Spring ...

  9. Spring全家桶系列–SpringBoot渐入佳境

    //本文作者:cuifuan //本文将收录到菜单栏:<Spring全家桶>专栏中 首发地址:https://www.javazhiyin.com/20913.html 萌新:小哥,我在实 ...

随机推荐

  1. WPF添加外边框,添加外边框虚线

    <Border Background="LightBlue" BorderBrush="Black"  BorderThickness="2&q ...

  2. 『无为则无心』Python基础 — 8、Python中的数据类型(数值、布尔、字符串)

    目录 1.数据类型介绍 2.数值型(Number) 3.布尔型(bool) 4.None(空值) 5.常量 6.字符串(String) 1.数据类型介绍 (1)什么是数据类型 在生活中,我们日常使用的 ...

  3. beego搭建api服务

    beego介绍 beego是一个Golang实现的开源Go应用开发框架,他可以用来快速开发 API.Web 及后端服务等各种应用,是一个 RESTful的框架,主要设计灵感来源于tornado.sin ...

  4. 春风十里不如你,全新Windows UI 3(WinUI 3) 的第一个实现Project Reunion 0.5

    什么是WinUI Windows UI库 (WinUI) 是适用于 Windows 桌面应用程序和 UWP 应用程序的本机用户体验 (UX) 框架. WinUI is a user interface ...

  5. 详解C++中继承的基本内容

    有些类与类之间存在特殊的关系,有共性也有特性,比如动物类可以细分为猫,狗等.下级别的成员除了拥有上一级的共性,还有自己的特性,这个时候就可以考虑继承的技术,减少重复代码. 一.继承中的对象模型 1.1 ...

  6. Ubuntu 更换内核

    Ubuntu 更换内核步骤: 下载内核源码,例如wget https://git.kernel.org/torvalds/t/linux-4.17-rc2.tar.gz 按照需要的环境,sudo ap ...

  7. Mysql:报错message from server: "Too many connections"(连接太多)

    报错信息 Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Data source re ...

  8. 结构型模式 -- 代理模式(静态代理&动态代理)

    静态代理: 真实角色和代理角色实现相同的接口,代理角色拥有真实角色的引用.代理角色去执行方法,对于某些"真正"需要真实角色自己执行的方法时,在代理角色内部就调用真实角色的方法,其他 ...

  9. 5shell中的数组

    0.理解数组 (1)shell不限制数组的大小,数组元素的下标从0开始计数 (2)获取数组中的元素要使用下标[ ],下标可以是一个整数,也可以是一个结果为整数的表达式,但是下标必须大于等于0 (3)b ...

  10. Android布局方式总结

    Android的布局分别是:线性布局LinearLayout.相对布局RelativeLayout.帧布局FrameLayout.网格布局GridLayout.约束布局ConstraintLayout ...