jFinal 开发规范
概述
本文档用于说明使用jFinal进行项目开发时,应该遵循的规范与惯用法等内容。
开发流程
虽然我们期望数据库能够在设计阶段即可冻结,但往往要到开发的后期。这就势必涉及到数据库的更新与jFinal模型更新如何同步的问题。推荐做法如下:
数据库设计 => 生成sql => 更新数据库 => 更新模型
需要说明的是.sql文件也要纳入到版本管理系统中进行管理。
代码组织
jFinal是一个标准的MVC框架,在使用其进行开发时,也应该按照MVC的结构进行组织。
按照“先分功能,后分模块”的思路,给出了推荐的一个代码组织框架:
ProjectRoot
+----src
| +----package
| Config.java
| ModelGenerator.java
| +----package.models
| | +----package.models.base
| | +----packages.models.validators
| +----package.controllers.moduleName
| +----package.controllers.moduleName.admin
+----sql
+----web
| +----templates
| | +----include
| | +----global
| | +----modelName
| | +----admin
| | +----include
| | +----modelName
| | admin_base.ftl
| | base.ftl
| +----static
| | +----css
| | +----javascript
| | +----zui
| +----resources
| | db.properties
| | log4j.properties
| | config.properties
| +----WEB-INFO
| +----classes
- package :项目的顶层包
- packages.models 模型定义。模型定义包含整个项目的完整模型定义,未区分模块。
- packages.models.base 存放自动生成的模型基类
- packages.models.validators 模型验证定义
- packages.controllers.moduleName 存放指定模块的前台控制器。如用户验证与鉴权的模块auth。
- package.controllers.moduleName.admin 存放模块的后台管理功能
- web 前端文件的根目录
- web.templates 存放所有模板
- web.templates.moduleName 存放指定模块的模板
- web.templates.include 模板公用片段
- web.templates.global 全局静态文件。比如:关于我们,版权声明等公用性内容
- web.templates.base.ftl 前端模板的基模板
- web.templates.admin 后台管理模板。
- web.static 静态文件根目录
- web.resources 配置文件根目录
- web.resources 资源文件根目录
模型定义
虽然jFinal支持的ActiveRecord模式进行开发。但是其对ActiveRecord的模式非常基础,为了更好地规范化代码组织,我们需要更加细化的要求。
基本原则:
- 封装与内聚原则。与模型相关的所有操作,均要封装到模型中。原则上不允许在模型之外编写SQL。
- 模型"可导航"原则。一对多,多对多模型,需要在模型中导航到关联模型的方法,便于前台程序编写。详细内容可参阅下文的“关联”部分。
关联
一对多关联
- 在"一"的一方,创建getChildModelList方法。
- 在"多"的一方,创建getParent方法。
示例:
public class Column extends Model{
public finial static Column dao = new Column();
public List<Article> getArticleList(){
return Article.dao.findByColumnId(getId());
}
}
public class Article extends Model{
public final static Article dao = new Article();
public Column getColumn(){
return Column.dao.fineById(getColumnId());
}
public List<Article> findByColumnId(int columnId){
....
}
}
每个项目中都包含一个ModelGenerator类,用于根据数据库的变化更新模型。生成规则请参照上面的代码组织。
特别说明: 数据库字段名称,一定要采用驼峰形式。
参阅文献:《jFianl-2.2-manual.pdf》第0章 快速体验Model与Bean合体
模型验证
- 原则上说,应该为每一个模型创建一个模型验证类。
- 验证失败时,采用 “字段+Error” 作为errorKey,方便后续错误显示。参见后文。
一个验证器的例子:
/**
* 栏目校验
* Created by YuanXu on 2016/4/12.
*/
public class ColumnValidator extends Validator {
@Override
protected void validate(Controller c) {
validateRequired("column.title", "titleError", "请输入栏目名称");
validateRequired("column.slug", "slugError", "请输入Slug");
String slug = c.getPara("column.slug");
Integer id = c.getParaToInt("column.id");
if (slug != null && !slug.isEmpty()) {
if (id == null || id == 0) { //new
if (Column.dao.find("select id from cms_column where slug=?", slug).size() > 0) {
addError("slugError", "Slug不能重复!");
}
} else {
if (Column.dao.find("select id from cms_column where slug=? and id !=?", slug, id).size() > 0) {
addError("slugError", "Slug不能重复!");
}
}
}
}
@Override
protected void handleError(Controller c) {
c.keepModel(Column.class);
c.renderFreeMarker("/templates/cms/admin/column_form.ftl");
}
}
Contorller
常规的数据处理控制器应该包含index、create、update、save、delete几个方法。分别用于列表、创建的空白表单渲染、更新表单渲染、数据保存和删除动作。
代码示例
public class UserController extends Controller{
/**
* 显示数据列表
*/
public void index(){
int page = getParaToInt("page", 1);
Page<Model> page_obj = Article.dao.paginate(page, PAGE_SIZE,.....);
setAttr("page_obj",page_obj);
setAttr("object_list",page_obj.get_list());
renderFreemarker("/templates/module/model_list.ftl");
}
/**
* 新建数据
*/
public void create(){
Model obj = new Model();
setAttr("object",obj);
//TODO:新建模型所需其他数据
renderFreemarker("/templates/module/model_form.ftl");
}
/**
* 更新数据
*/
public void update(){
Model obj = Model.dao.findById(getParam("id"));
if (obj == null){
renderError(404);
return;
}
setAttr("object",obj);
//TODO:更新模型所需其他数据
renderFreemarker("/templates/module/model_form.ftl");
}
/**
* 保存数据
*/
public void save(){
Model obj = getModel(Model.class);
if (obj.getID() == null)
obj.save();
else
obj.update();
redirect(""); #TODO:转到列表页
}
}
模板
我们使用freemarker作为模板层,并且尽量避免在模板中书写逻辑代码。 通过定义一个基类模板,在基模板中定义可被重写的“片段”。子模版引入(include)基类模板,并且通过宏的形式重写/替换基类模板中的占位符。 为了方便区分html标记与模板标记,所有模板采用方括号形式。这就要求没个模板的第一行写入[#ftl]
base.ftl
这里给出一个基类模板的了例子:
[#ftl]
<html>
<head>
<title>[#if title??] [@title /][/#if]</title>
<link rel="stylesheet" href="/static/zui/css/zui.css">
<script src="/static/zui/lib/jquery/jquery.js"></script>
<script src="/static/zui/js/zui.js"></script>
[#if head_css??]
[@head_css][/@head_css]
[/#if]
[#if head_js??]
[@head_js][/@head_js]
[/#if]
</head>
<body class="[#if body_css??][@body_css][/@body_css] [/#if]">
<div class="container">
[#include '/templates/cms/admin/admin_nav.ftl' ]
[#if main??]
[@main][/@main]
[/#if]
</div>
[#if body_js??]
[@body_js][/@body_js]
[/#if]
</body>
</html>
子模板
同样给出一个子模版的例子。
[#ftl]
[#assign active='column' /]
[#include "/templates/admin_base.ftl" ]
[#macro body_css]column[/#macro]]
[#macro title]栏目管理[/#macro]
[#macro main]
<form action="" class="form form-horizontal" method="get">
<a href="/admin/cms/column/create">新建栏目</a>
</form>
<table class="table table-bordered table-hovered">
<tr>
<th>编号</th>
<th>slug</th>
<th>标题</th>
<th>创建时间</th>
<th>更新时间</th>
<th>动作</th>
</tr>
[#if column_list??]
[#list column_list as column ]
<tr>
<td>${column_index+1}</td>
<td>${column.slug!}</td>
<td>${column.title!}</td>
<td>${column.createAt!}</td>
<td>${column.updateAt!}</td>
<td><a href="/admin/cms/column/update?id=${column.id}" class="btn btn-default btn-sm"><i
class="icon-edit">修改</i></a>
<a href="javascript:void(0)" data-id="${column.id}" class="btn btn-delete btn-default btn-sm"><i
class="icon-remove">删除</i></a>
</td>
</tr>
[/#list]
[/#if]
</table>
[/#macro]
[#macro body_js]
<script src="/static/javascript/dibo.admin.article.js"></script>
[/#macro]
表单处理
推荐采用PRG模式进行处理。可参阅http://m.oschina.net/blog/143120 表单处理需要结合验证器中的错误处理组织。一般来说,需要采用如下模式:
...
<input type="hidden" name="column.id" value="${column.id!}">
...
<div class="form-group [#if titleError??]has-error[/#if]">
<label for="id_title" class="col-md-3 control-label">标题</label>
<div class="col-md-9 ">
<input type="text" class="form-control" name="column.title" id="id_title" required maxlength="50"
value="${column.title!}">
[#if titleError??]
<p class="help-block">${titleError}</p>
[/#if]
</div>
</div>
...
常用基础设施
zui自定义标签[开发中]
主要目的:根据模型和校验结果,生成模板。
- [@zui_form 表单]
- [@zui_field 字段名 cssClass="" fieldClass="" /]
- [@zui_pagination page_obj]
特别期望的特性
- 更加完善的ORM。
- 分离模型定义与操作。
- 避免SQL. 采用Django类似的方式,函数式风格,惰性计算,尽量避免直接编写。
- 通过模型维护数据库的scheme,模型变更纳入版本控制
- form
- url支持命名与命名空间。通过resolve_url函数解析地址,避免url硬编码。