JFinal 框架入门
(本文均来自JFinal官方文档, 需要学习的同学请移步官方文档)
一.JFinal 是用来干什么的?
JFinal 是基于 Java 语言的极速 WEB + ORM 框架,其核心设计目标是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展、Restful。在拥有Java语言所有优势的同时再拥有ruby、python、php等动态语言的开发效率!
二.JFinal有什么特点?
以上来自http://www.jfinal.com/ JFinal官方网站,可以在这里找到快速使用JFinal的方法.
三.入门Demo
configRoute(…)
http://www.jfinal.com/upload/img/document/0/1_20180112225146.png
访问路径需要注意的问题:
http://localhost/hello/methodName 将访问到 HelloController.methodName() 方法。
路径中可以继续拼接参数.
urlPara是为了能在url中携带参数值,urlPara可以在一次请求中同时携带多个值,JFinal默认使用减号“-”来分隔多个值(可通过constants. setUrlParaSeparator(String)设置分隔符),在Controller中可以通过getPara(int index)分别取出这些值。controllerKey、method、urlPara这三部分必须使用正斜杠“/”分隔。
注意,controllerKey自身也可以包含正斜杠“/”,如“/admin/article”,这样实质上实现了struts2的namespace功能。
Routes.setBaseViewPath(baseViewPath)方法用于为该Routes 内部的所有Controller设置视图渲染时的基础路径,该基础路径与Routes.add(…, viewPath)方法传入的viewPath以及 Controller.render(view) 方法传入的 view 参数联合组成最终的视图路径,规则如下:
finalView = baseViewPath + viewPath + view
注意:当view以 “/” 字符打头时表示绝对路径,baseViewPath 与 viewPath 将被忽略。
如果JFinal默认路由规则不能满足需求,开发者还可以根据需要使用Handler定制更加个性化的路由,大体思路就是在Handler中改变第一个参数String target的值。
使用Handler定制更加个性化的路由的方法还有待深入了解…
PropKit 读取配置
public class AppConfig extends JFinalConfig {
public void configConstant(Constants me) {
// 第一次使用use加载的配置将成为主配置,可以通过PropKit.get(...)直接取值
PropKit.use("a_little_config.txt");
me.setDevMode(PropKit.getBoolean("devMode"));
}
public void configPlugin(Plugins me) {
// 非第一次使用use加载的配置,需要通过每次使用use来指定配置文件名再来取值
//加整redis的配置文件
String redisHost = PropKit.use("redis_config.txt").get("host");
int redisPort = PropKit.use("redis_config.txt").getInt("port");
RedisPlugin rp = new RedisPlugin("myRedis", redisHost, redisPort);
me.add(rp);
// 非第一次使用 use加载的配置,也可以先得到一个Prop对象,再通过该对象来获取值
Prop p = PropKit.use("db_config.txt");
DruidPlugin dp = new DruidPlugin(p.get("jdbcUrl"), p.get("user")…);
me.add(dp);
}
}
如上代码所示,PropKit可同时加载多个配置文件,第一个被加载的配置文件可以使用PorpKit.get(…)方法直接操作,非第一个被加载的配置文件则需要使用PropKit.use(…).get(…)来操作。
PropKit 的使用并不限于在 YourJFinalConfig 中,可以在项目的任何地方使用。此外PropKit.use(…)方法在加载配置文件内容以后会将数据缓存在内存之中,可以通过PropKit.useless(…)将缓存的内容进行清除。
Controller
Controller是JFinal核心类之一,该类作为MVC模式中的控制器。基于JFinal的Web应用的控制器需要继承该类。Controller是定义Action方法的地点,是组织Action的一种方式,一个Controller可以包含多个Action。Controller是线程安全的。
在Controller之中定义的public方法称为Action。Action是请求的最小单位。Action方法必须在Controller中定义,且必须是public可见性。
Action 参数注入可以代替 getPara、getBean、getModel 系列方法获取参数,使用 File、UploadFile 参数时可以代替 getFile 方法实现文件上传。这种传参方式还有一个好处是便于与 swagger 这类第三方无缝集成,生成API文档。
重要用法:如果 action 形参是一个 model 或者 bean,原先通过 getBean(User.class, “”) 获取时第二个参数为空字符串或null,那么与之等价的形参注入只需要用一下 @Para("") 注解即可:
public void action(@Para("")User user) { …. }
getPara系列方法
Controller提供了getPara系列方法用来从请求中获取参数。getPara系列方法分为两种类型。第一种类型为第一个形参为String的getPara系列方法。该系列方法是对HttpServletRequest.getParameter(String name)的封装,这类方法都是转调了HttpServletRequest.getParameter(String name)。
第二种类型为第一个形参为int或无形参的getPara系列方法。该系列方法是去获取urlPara中所带的参数值。getParaMap与getParaNames分别对应HttpServletRequest的getParameterMap与getParameterNames。
记忆技巧:第一个参数为String类型的将获取表单或者url中问号挂参的域值。第一个参数为int或无参数的将获取urlPara中的参数值。
getPara使用例子:
http://www.jfinal.com/upload/img/document/0/1_20180112235732.png
getBean与getModel系列
getModel用来接收页面表单域传递过来的model对象,表单域名称以”modelName.attrName”方式命名,getModel使用的attrName必须与数据表字段名完全一样。
getBean方法用于支持传统Java Bean,包括支持使用jfinal生成器生成了getter、setter方法的Model,页面表单传参时使用与setter方法相一致的attrName,而非数据表字段名。
getModel与getBean区别在于前者使用数据库表字段名而后者使用与setter方法一致的属性名进行数据注入。建议优先使用getBean方法。
上面代码中,表单域采用了 "blog.title"、"blog.content" 作为表单域的name属性,"blog" 是类文件名称 "Blog" 的首字母变小写, "title" 是blog数据库表的title字段,如果希望表单域使用任意的modelName,只需要在getModel时多添加一个参数来指定,例如:getModel(Blog.class, "otherName")。
如果希望传参时避免使用modelName前缀,可以使用空串作为modelName来实现:getModel(Blog.class, “”); 这对开发纯API项目非常有用。
如果希望在接收时跳过数据转换或者属性名错误异常可以传入true参:getBean(…, true)
getFile文件上传
Controller提供了getFile系列方法支持文件上传。
特别注意:如果客户端请求为multipart request(form表单使用了enctype="multipart/form-data"),那么必须先调用getFile系列方法才能使getPara系列方法正常工作,因为multipart request需要通过getFile系列方法解析请求体中的数据,包括参数。同样的道理在Interceptor、Validator中也需要先调用getFile。
文件默认上传至项目根路径下的upload子路径之下,该路径称为文件上传基础路径。可以在 JFinalConfig.configConstant(Constants me)方法中通过me.setBaseUploadPath(baseUploadPath) 设置文件上传基础路径,该路径参数接受以”/”打头或者以windows磁盘盘符打头的绝对路径,即可将基础路径指向项目根径之外,方便单机多实例部署。当该路径参数设置为相对路径时,则是以项目根为基础的相对路径。
renderFile文件下载
Controller提供了renderFile系列方法支持文件下载。
文件默认下载路径为项目根路径下的download子路径之下,该路径称为文件下载基础路径。可以在 JFinalConfig.configConstant(Constants me)方法中通过me.setBaseDownloadPath(path) 设置文件下载基础路径,该路径参数接受以”/”打头或者以windows磁盘盘符打头的绝对路径,即可将基础路径指向项目根径之外,方便单机多实例部署。当该路径参数设置为相对路径时,则是以项目根为基础的相对路径。
render系列方法
render系列方法将渲染不同类型的视图并返回给客户端。
还可以通过继承Render抽象类来无限扩展视图类型。
注意:
1:IE不支持contentType为application/json,在ajax上传文件完成后返回json时IE提示下载文件,解决办法是使用:render(new JsonRender().forIE())或者render(new JsonRender(params).forIE())。这种情况只出现在IE浏览器 ajax 文件上传,其它普通ajax请求不必理会。
2:除renderError方法以外,在调用render系列的方法后程序并不会立即返回,如果需要立即返回需要使用return语句。在一个action中多次调用render方法只有最后一次有效。
AOP
JFinal采用极速化的AOP设计,专注AOP最核心的目标,将概念减少到极致,仅有三个概念:Interceptor、Before、Clear,并且无需引入IOC也无需使用啰嗦的XML。
注意:必须调用 inv.invoke() 方法,才能将当前调用传递到后续的 Interceptor 与 Action。
以下为 Invocation 中的方法:
http://www.jfinal.com/upload/img/document/0/1_20180114204535.png
更正一下上面截图中倒数第三行的一处手误:setArg(int) 应该改为 setArg(int, Object)
Before
Before注解用来对拦截器进行配置,该注解可配置Class、Method级别的拦截器,以下是代码示例:
// 配置一个Class级别的拦截器,她将拦截本类中的所有方法
@Before(AaaInter.class)
public class BlogController extends Controller {
// 配置多个Method级别的拦截器,仅拦截本方法
@Before({BbbInter.class, CccInter.class})
public void index() {
}
// 未配置Method级别拦截器,但会被Class级别拦截器AaaInter所拦截
public void show() {
}
}
此外Before可以同时配置多个拦截器,只需用在大括号内用逗号将多个拦截器进行分隔即可。
除了Class与Method级别的拦截器以外,JFinal还支持全局拦截器以及Inject拦截器(Inject拦截将在后面介绍),全局拦截器分为控制层全局拦截器与业务层全局拦截器,前者拦截控制 层所有Action方法,后者拦截业务层所有方法。
全局拦截器需要在YourJFinalConfig进行配置,以下是配置示例:
public class AppConfig extends JFinalConfig {
public void configInterceptor(Interceptors me) {
// 添加控制层全局拦截器
me.addGlobalActionInterceptor(new GlobalActionInterceptor());
// 添加业务层全局拦截器
me.addGlobalServiceInterceptor(new GlobalServiceInterceptor());
// 为兼容老版本保留的方法,功能与addGlobalActionInterceptor完全一样
me.add(new GlobalActionInterceptor());
}
}
当某个Method被多个级别的拦截器所拦截,拦截器各级别执行的次序依次为:Global、Routes、Class、Method,如果同级中有多个拦截器,那么同级中的执行次序是:配置在前面的先执行。
注意 : 业务层与控制层中的拦截器 添加方法是不同的方法.
Clear
拦截器从上到下依次分为Global、Routes、Class、Method四个层次,Clear用于清除自身所处层次以上层的拦截器。
Clear声明在Method层时将针对Global、Routes、Class进行清除。Clear声明在Class层时将针对Global、Routes 进行清除。Clear注解携带参数时清除目标层中指定的拦截器。
Clear用法记忆技巧:
在某些应用场景之下,需要移除Global或Class拦截器。例如某个后台管理系统,配置了一个全局的权限拦截器,但是其登录action就必须清除掉她,否则无法完成登录操作,以下是代码示例:
// login方法需要移除该权限拦截器才能正常登录
@Before(AuthInterceptor.class)
public class UserController extends Controller {
// AuthInterceptor 已被Clear清除掉,不会被其拦截
@Clear
public void login() {
}
// 此方法将被AuthInterceptor拦截
public void show() {
}
}
Clear注解带有参数时,能清除指定的拦截器,以下是一个更加全面的示例:
@Before(AAA.class)
public class UserController extends Controller {
@Clear
@Before(BBB.class)
public void login() {
// Global、Class级别的拦截器将被清除,但本方法上声明的BBB不受影响
}
@Clear({AAA.class, CCC.class})// 清除指定的拦截器AAA与CCC
@Before(CCC.class)
public void show() {
// 虽然Clear注解中指定清除CCC,但她无法被清除,因为清除操作只针对于本层以上的各层
}
}
Inject 依赖注入
使用 @Inject 注解可以向 Controller 以及 Interceptor 中注入依赖对象,使用注入功能需要如下配置:
public void configConstant(Constants me) {
me.setInjectDependency(true);
}
配置完成以后就可以在控制器中使用了,例如:
public class AccountController {
@Inject
AccountService service; // 此处会注入依赖对象
public void index() {
service.justDoIt(); // 调用被注入对象的方法
}
}
如果需要创建的对象并不是 Controller 的属性,也不是 Interceptor 的属性,还可以使用 Aop.get(…) 方法进行依赖对象的创建以及注入,例如:
public class MyKit {
static Service service = Aop.get(Service.class);
public void doIt() {
service.justDoIt();
}
}
由于 MyKit 的创建并不是 jfinal 接管的,所以不能使用 @Inject 进行依赖注入。 而 Controller、Interceptor 的创建和组装是由 jfinal 接管的,所以可以使用 @Inject 注入依赖。
有了 Aop.get(…) 就可以在任何地方创建对象并且对创建的对象进行注入。此外还可以使用 Aop.inject(…) 仅仅向对象注入依赖但不创建对象。
依赖注入这部分还需要再学习!!!
ActiveRecord
ActiveRecord 是 JFinal 最核心的组成部分之一,通过 ActiveRecord 来操作数据库,将极大地减少代码量,极大地提升开发效率。
ActiveRecord 模式的核心是:一个 Model 对象唯一对应数据库表中的一条记录,而对应关系依靠的是数据库表的主键值。
因此,ActiveRecord 模式要求数据库表必须要有主键。当数据库表没有主键时,只能使用 Db + Record 模式来操作数据库。
ActiveRecord是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置ActiveRecordPlugin。
以下是Plugin配置示例代码:
public class DemoConfig extends JFinalConfig {
public void configPlugin(Plugins me) {
DruidPlugin dp = new DruidPlugin("jdbc:mysql://localhost/db_name", "userName", "password");
me.add(dp);
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
me.add(arp);
arp.addMapping("user", User.class);
arp.addMapping("article", "article_id", Article.class);
}
}
Model
Model是ActiveRecord中最重要的组件之一,它充当MVC模式中的Model部分。以下是Model定义示例代码:
public class User extends Model<User> {
public static final User dao = new User().dao();
}
以上代码中的User通过继承Model,便立即拥有的众多方便的操作数据库的方法。在User中声明的dao静态对象是为了方便查询操作而定义的,该对象并不是必须的。基于ActiveRecord的Model无需定义属性,无需定义getter、setter方法,无需XML配置,无需Annotation配置,极大降低了代码量
以下为Model的一些常见用法:
// 创建name属性为James,age属性为25的User对象并添加到数据库
new User().set("name", "James").set("age", 25).save();
// 删除id值为25的User
User.dao.deleteById(25);
// 查询id值为25的User将其name属性改为James并更新到数据库
User.dao.findById(25).set("name", "James").update();
// 查询id值为25的user, 且仅仅取name与age两个字段的值
User user = User.dao.findByIdLoadColumns(25, "name, age");
// 获取user的name属性
String userName = user.getStr("name");
// 获取user的age属性
Integer userAge = user.getInt("age");
// 查询所有年龄大于18岁的user
List<User> users = User.dao.find("select * from user where age>18");
// 分页查询年龄大于18的user,当前页号为1,每页10个user
Page<User> userPage = User.dao.paginate(1, 10, "select *", "from user where age > ?", 18);
特别注意:User中定义的 public static final User dao对象是全局共享的,只能用于数据库查询,不能用于数据承载对象。数据承载需要使用new User().set(…)来实现。
Generator与JavaBean
使用生成器通常只需配置Generator的四个参数即可,以下是具体使用示例:
// base model 所使用的包名
String baseModelPkg = "model.base";
// base model 文件保存路径
String baseModelDir = PathKit.getWebRootPath() + "/../src/model/base";
// model 所使用的包名
String modelPkg = "model";
// model 文件保存路径
String modelDir = baseModelDir + "/..";
Generator gernerator = new Generator(dataSource, baseModelPkg, baseModelDir, modelPkg, modelDir);
gernerator.generate();
MappingKit用于生成table到Model的映射关系,并且会生成主键/复合主键的配置,也即无需在configPlugin(Plugins me)方法中书写任何样板式的映射代码。
DataDictionary是指生成的数据字典,会生成数据表所有字段的名称、类型、长度、备注、是否主键等信息。
Model与Bean合体后主要优势
Model与Bean合体后注意事项
独创Db + Record模式
Db类及其配套的Record类,提供了在Model类之外更为丰富的数据库操作功能。使用Db与Record类时,无需对数据库表进行映射,Record相当于一个通用的Model。以下为Db + Record模式的一些常见用法:
// 创建name属性为James,age属性为25的record对象并添加到数据库
Record user = new Record().set("name", "James").set("age", 25);
Db.save("user", user);
// 删除id值为25的user表中的记录
Db.deleteById("user", 25);
// 查询id值为25的Record将其name属性改为James并更新到数据库
user = Db.findById("user", 25).set("name", "James");
Db.update("user", user);
// 获取user的name属性
String userName = user.getStr("name");
// 获取user的age属性
Integer userAge = user.getInt("age");
// 查询所有年龄大于18岁的user
List<Record> users = Db.find("select * from user where age > 18");
// 分页查询年龄大于18的user,当前页号为1,每页10个user
Page<Record> userPage = Db.paginate(1, 10, "select *", "from user where age > ?", 18);
以下为事务处理示例:
boolean succeed = Db.tx(new IAtom(){
public boolean run() throws SQLException {
int count = Db.update("update account set cash = cash - ? where id = ?", 100, 123);
int count2 = Db.update("update account set cash = cash + ? where id = ?", 100, 456);
return count == 1 && count2 == 1;
}});
以上两次数据库更新操作在一个事务中执行,如果执行过程中发生异常或者run()方法返回false,则自动回滚事务。
paginate 分页
Model 与 Db 中提供了最常用的分页API:paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras)
其中的参数含义分别为:当前页的页号、每页数据条数、sql语句的select部分、sql语句除了select以外的部分、查询参数。绝大多数情况下使用这个API即可。以下是使用示例:
dao.paginate(1, 10, "select *", "from girl where age > ? and weight < ?", 18, 50);
声明式事务
ActiveRecord支持声明式事务,声明式事务需要使用ActiveRecordPlugin提供的拦截器来实现,拦截器的配置方法见Interceptor有关章节。以下代码是声明式事务示例:
// 本例仅为示例, 并未严格考虑账户状态等业务逻辑
@Before(Tx.class)
public void trans_demo() {
// 获取转账金额
Integer transAmount = getParaToInt("transAmount");
// 获取转出账户id
Integer fromAccountId = getParaToInt("fromAccountId");
// 获取转入账户id
Integer toAccountId = getParaToInt("toAccountId");
// 转出操作
Db.update("update account set cash = cash - ? where id = ?",
transAmount, fromAccountId);
// 转入操作
Db.update("update account set cash = cash + ? where id = ?",
transAmount, toAccountId);
}
特别注意:声明式事务默认只针对主数据源进行回滚**,如果希望针对 “非主数据源” 进行回滚,需要使用注解进行配置,以下是示例:
@TxConfig("otherConfigName")
@Before(Tx.class)
public void doIt() {
...
}
与声明式事务一样,Db.tx 方法默认针对主数据源进行事务处理,如果希望对其它数据源开启事务,使用 Db.use(configName).tx(…) 即可。
使用任意缓存实现
除了要把使用默认的 ehcache 实现以外,还可以通过实现 ICache 接口切换到任意的缓存实现上去,下面是个简单提示意性代码实现:
public class MyCache implements ICache {
public <T>T get(String cacheName, Object key) {
}
public void put(String cacheName, Object key, Object value) {
}
public void remove(String cacheName, Object key) {
}
public void removeAll(String cacheName) {
}
}
如上代码所示,MyCache 需要实现 ICache 中的四个抽象方法,然后通过下面的配置方式即可切换到自己的 cache 实现上去:
ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
arp.setCache(new MyCache());
如上代码所示,通过调用 ActiveRecordPlugin.setCache(…) 便可切换 cache 实现。
复合主键
对于 Db + Record 模式来说,复合主键的使用不需要配置,直接用即可:
Db.findById("user_role", "roleId, userId", 123, 456);
Db.deleteById("user_role", "roleId, userId", 123, 456);
Oracle支持
Oracle数据库具有一定的特殊性,JFinal针对这些特殊性进行了一些额外的支持以方便广大的Oracle使用者。以下是一个完整的Oracle配置示例:
public class DemoConfig extends JFinalConfig {
public void configPlugin(Plugins me) {
DruidPlugin dp = new DruidPlugin(……);
me.add(dp);
//配置Oracle驱动
dp.setDriverClass("oracle.jdbc.driver.OracleDriver");
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
me.add(arp);
// 配置Oracle方言
arp.setDialect(new OracleDialect());
// 配置属性名(字段名)大小写不敏感容器工厂
arp.setContainerFactory(new CaseInsensitiveContainerFactory());
arp.addMapping("user", "user_id", User.class);
}
}
另外,Oracle并未直接支持自增主键,JFinal为此提供了便捷的解决方案。要让Oracle支持自动主键主要分为两步:一是创建序列,二是在model中使用这个序列,具体办法如下:
1:通过如下办法创建序列,本例中序列名为:MY_SEQ
CREATE SEQUENCE MY_SEQ
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999999999
START WITH 1
CACHE 20;
2:在YourModel.set(…)中使用上面创建的序列
// 创建User并使用序列
User user = new User().set("id", "MY_SEQ.nextval").set("age", 18);
user.save();
// 获取id值
Integer id = user.get("id");
序列的使用很简单,只需要 yourModel.set(主键名, 序列名 + “.nextval”)就可以了。特别注意这里的 “.nextval” 后缀一定要是小写,OracleDialect对该值的大小写敏感。
注意:Oracle下分页排序Sql语句必须满足2个条件:
多数据源支持
ActiveRecordPlugin可同时支持多数据源、多方言、多缓存、多事务级别等特性,对每个 ActiveRecordPlugin 可进行彼此独立的配置。简言之 JFinal 可以同时使用多数据源,并且可以针对这多个数据源配置独立的方言、缓存、事务级别等。
当使用多数据源时,只需要对每个 ActiveRecordPlugin指定一个 configName即可,如下是代码示例:
public void configPlugin(Plugins me) {
// mysql 数据源
DruidPlugin dsMysql = new DruidPlugin(…);
me.add(dsMysql);
// mysql ActiveRecrodPlugin 实例,并指定configName为 mysql
ActiveRecordPlugin arpMysql = new ActiveRecordPlugin("mysql", dsMysql);
me.add(arpMysql);
arpMysql.addMapping("user", User.class);
// oracle 数据源
DruidPlugin dsOracle = new DruidPlugin(…);
me.add(dsOracle);
// oracle ActiveRecrodPlugin 实例,并指定configName为 oracle
ActiveRecordPlugin arpOracle = new ActiveRecordPlugin("oracle", dsOracle);
me.add(arpOracle);
arpOracle.setDialect(new OracleDialect());
arpOracle.addMapping("blog", Blog.class);
}
对于Db + Record的使用,数据源的切换需要使用Db.use(configName)方法得到数据库操作对象,然后就可以进行数据库操作了,以下是代码示例:
// 查询 dsMysql数据源中的 user
List<Record> users = Db.use("mysql").find("select * from user");
// 查询 dsOracle数据源中的 blog
List<Record> blogs = Db.use("oracle").find("select * from blog");
以上两行代码,分别通过configName为mysql、oracle得到各自的数据库操作对象,然后就可以如同单数据完全一样的方式来使用数据库操作 API了。简言之,对于 Db + Record来说,多数据源相比单数据源仅需多调用一下Db.use(configName),随后的API使用方式完全一样。
注意最先创建的 ActiveRecrodPlugin实例将会成为主数据源,可以省略configName。最先创建的 ActiveRecrodPlugin实例中的配置将默认成为主配置,此外还可以通过设置configName为 DbKit.MAIN_CONFIG_NAME常量来设置主配置。
独立使用ActiveRecord
ActiveRecordPlugin可以独立于java web 环境运行在任何普通的java程序中,使用方式极度简单,相对于web项目只需要手动调用一下其start() 方法即可立即使用。以下是代码示例:
public class ActiveRecordTest {
public static void main(String[] args) {
DruidPlugin dp = new DruidPlugin("localhost", "userName", "password");
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
arp.addMapping("blog", Blog.class);
// 与 jfinal web 环境唯一的不同是要手动调用一次相关插件的start()方法
dp.start();
arp.start();
// 通过上面简单的几行代码,即可立即开始使用
new Blog().set("title", "title").set("content", "cxt text").save();
Blog.dao.findById(123);
}
}
注意:ActiveRecordPlugin所依赖的其它插件也必须手动调用一下start()方法,如上例中的dp.start()。
JFinal Template Engine模板引擎
jfinal 模板引擎核心概念只有指令与表达式这两个。而表达式是与 java 直接打通的,所以没有学习成本,剩下来只有 #if、#for、#define、#set、#include、#(…) 六个指令需要了解,而这六个指令的学习成本又极低。
共享模板函数配置
如果模板中通过 #define 指令定义了 template function,并且希望这些 template function 可以在其它模板中直接调用的话,可以进行如下配置:
// 添加共享函数,随后可在任意地方调用这些共享函数
me.addSharedFunction("/view/common/layout.html");
以上代码添加了一个共享函数模板文件 layout.html,这个文件中使用了#define指令定义了template function。通过上面的配置,可以在任意地方直接调用 layout.html 里头的 template function。
从 class path 和 jar 包加载模板配置
如果模板文件在项目的 class path 路径或者 jar 包之内,可以通过me.setSourceFactory(new ClassPathSourceFactory()) 以及 me.setBaseTemplatePath(null) 来实现,以下是代码示例:
public void configEngine(Engine me) {
me.setDevMode(true);
me.setBaseTemplatePath(null);
me.setSourceFactory(new ClassPathSourceFactory());
me.addSharedFunction("/view/common/layout.html");
}
通过上面的 me.setSourceFactory(…) 可以推测出来,还可以通过实现ISourceFactory、ISource 扩展出从任何地方加载模板文件的功能,目前已有用户实现 DbSource 来从数据库加载模板的功能。
sql 管理模块的 Engine 配置
JFinal Template Engine 被设计成为可以在单独的项目中同时使用多个 Engine 对象,这多个不同的 Engine 对象可分别进行不同的配置,用于不同的用途,独立配置、互不干扰。
例如,jfinal 中的 configEngine(Engine me) 中配置的 Engine 对象是用于 Controller.render(...) 方法的渲染,而 ActiveRecordPlugin.getEngine() 对象是用于 sql 管理功能模块,这两个 Engine 对象是两个不同的实例,互相之间没有干扰,配置方式也不同。
前面例子中的配置已介绍过了用于 Controller.render(...) 渲染的 Engine 对象的配置,而 ActiveRecordPlugin 的 sql 管理模块的 Engine 对象的配置方法如下:
public void configPlugin(Plugins me) {
ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
Engine engine = arp.getEngine();
// 上面的代码获取到了用于 sql 管理功能的 Engine 对象,接着就可以开始配置了
engine.setSourceFactory(new ClassPathSourceFactory());
engine.addSharedMethod(new StrKit());
me.add(arp);
}
上面代码中通过 arp.getEngine() 获取到了 sql 管理模块专用的 Engine 对象,并对其进行了两项配置。这两项配置仅对 sql 管理功能的 Engine 对象有效,与 configEngine(Engine me) 中配置的 Engine 对象完全无关,她们自各拥有独立的配置。
Spring整合
1、maven 坐标
Spring 整合JFinal Template Engine 可以在pom.xml中配置jfinal坐标,也可以配置JFinal Template Engine的独立发布版本坐标,其maven坐标如下:
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>enjoy</artifactId>
<version>3.4</version>
</dependency>
2、Spring MVC整合
在Spring mvc下整合JFinal Template Engine非常简单,只需要配置一个bean即可,如下是具体配置方式:
<bean id="viewResolver" class="com.jfinal.template.ext.spring.JFinalViewResolver">
<!-- 是否热加载模板文件 -->
<property name="devMode" value="true"/>
<!-- 配置shared function,多文件用逗号分隔 -->
<property name="sharedFunction" value="/view/_layout.html, /view/_paginate.html"/>
<!-- 是否支持以 #(session.value) 的方式访问 session -->
<property name="sessionInView" value="true"/>
<property name="prefix" value="/view/"/>
<property name="suffix" value=".html"/>
<property name="order" value="1"/>
<property name="contentType" value="text/html; charset=utf-8"/>
</bean>
EhCachePlugin
EhCachePlugin是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置EhCachePlugin,以下是Plugin配置示例代码:
public class DemoConfig extends JFinalConfig {
public void configPlugin(Plugins me) {
me.add(new EhCachePlugin());
}
}
CacheInterceptor
CacheInterceptor可以将action所需数据全部缓存起来,下次请求到来时如果cache存在则直接使用数据并render,而不会去调用action。此用法可使action完全不受cache相关代码所污染,即插即用,以下是示例代码:
@Before(CacheInterceptor.class)
public void list() {
List<Blog> blogList = Blog.dao.find("select * from blog");
User user = User.dao.findById(getParaToInt());
setAttr("blogList", blogList);
setAttr("user", user);
render("blog.html");
}
上例中的用法将使用actionKey作为cacheName,在使用之前需要在ehcache.xml中配置以actionKey命名的cache如:<cache name="/blog/list" …>,注意actionKey作为cacheName配置时斜杠”/”不能省略。此外CacheInterceptor还可以与CacheName 注解配合使用,以此来取代默认的actionKey作为cacheName,以下是示例代码:
@Before(CacheInterceptor.class)
@CacheName("blogList")
public void list() {
List<Blog> blogList = Blog.dao.find("select * from blog");
setAttr("blogList", blogList);
render("blog.html");
}
以上用法需要在ehcache.xml中配置名为blogList的cache如:<cache name=“blogList” …>。
EvictInterceptor
EvictInterceptor可以根据CacheName注解自动清除缓存。以下是示例代码:
@Before(EvictInterceptor.class)
@CacheName("blogList")
public void update() {
getModel(Blog.class).update();
redirect("blog.html");
}
上例中的用法将清除cacheName为blogList的缓存数据,与其配合的CacheInterceptor会自动更新cacheName为blogList的缓存数据。
CacheKit
CacheKit是缓存操作工具类,以下是示例代码:
public void list() {
List<Blog> blogList = CacheKit.get("blog", "blogList");
if (blogList == null) {
blogList = Blog.dao.find("select * from blog");
CacheKit.put("blog", "blogList", blogList);
}
setAttr("blogList", blogList);
render("blog.html");
}
CacheKit 中最重要的两个方法是get(String cacheName, Object key)与put(String cacheName, Object key, Object value)。get方法是从cache中取数据,put方法是将数据放入cache。参数cacheName与ehcache.xml中的<cache name=“blog” …>name属性值对应;参数key是指取值用到的key;参数value是被缓存的数据。
以下代码是CacheKit中重载的CacheKit.get(String, String, IDataLoader)方法使用示例:
public void list() {
List<Blog> blogList = CacheKit.get("blog", "blogList", new IDataLoader(){
public Object load() {
return Blog.dao.find("select * from blog");
}});
setAttr("blogList", blogList);
render("blog.html");
}
CacheKit.get方法提供了一个IDataLoader接口,该接口中的load()方法在缓存值不存在时才会被调用。该方法的具体操作流程是:首先以cacheName=blog以及key=blogList为参数去缓存取数据,如果缓存中数据存在就直接返回该数据,不存在则调用IDataLoader.load()方法来获取数据。
ehcache.xml简介
EhCache的使用需要有ehcache.xml配置文件支持,该配置文件中配置了很多cache节点,每个cache节点会配置一个name属性,例如:<cache name="blog" …>,该属性是CacheKit取值所必须的。其它配置项如eternal、overflowToDisk、timeToIdleSeconds、timeToLiveSeconds详见EhCache官方文档。
RedisPlugin
RedisPlugin是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置RedisPlugin,以下是RedisPlugin配置示例代码:
public class DemoConfig extends JFinalConfig {
public void configPlugin(Plugins me) {
// 用于缓存bbs模块的redis服务
RedisPlugin bbsRedis = new RedisPlugin("bbs", "localhost");
me.add(bbsRedis);
// 用于缓存news模块的redis服务
RedisPlugin newsRedis = new RedisPlugin("news", "192.168.3.9");
me.add(newsRedis);
}
}
以上代码创建了两个RedisPlugin对象,分别为bbsRedis和newsRedis。最先创建的RedisPlugin对象所持有的Cache对象将成为主缓存对象,主缓存对象可通过Redis.use()直接获取,否则需要提供cacheName参数才能获取,例如:Redis.use(“news”)
Redis与Cache
Redis与Cache联合起来可以非常方便地使用Redis服务,Redis对象通过use()方法来获取到Cache对象,Cache对象提供了丰富的API用于使用Redis服务,下面是具体使用示例:
public void redisDemo() {
// 获取名称为bbs的Redis Cache对象
Cache bbsCache = Redis.use("bbs");
bbsCache.set("key", "value");
bbsCache.get("key");
// 获取名称为news的Redis Cache对象
Cache newsCache = Redis.use("news");
newsCache.set("k", "v");
newsCache.get("k");
// 最先创建的Cache将成为主Cache,所以可以省去cacheName参数来获取
bbsCache = Redis.use(); // 主缓存可以省去cacheName参数
bbsCache.set("jfinal", "awesome");
}
以上代码中通过”bbs”、”news”做为use方法的参数分别获取到了两个Cache对象,使用这两个对象即可操作其所对应的Redis服务端。
通常情况下只会创建一个RedisPlugin连接一个redis服务端,使用Redis.use().set(key,value)即可。
注意:使用 incr、incrBy、decr、decrBy 方法操作的计数器,需要使用 getCounter(key) 进行读取而不能使用 get(key),否则会抛反序列化异常
Cron4jPlugin
Cron4jPlugin是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置,如下是代码示例:
Cron4jPlugin cp = new Cron4jPlugin();
cp.addTask("* * * * *", new MyTask());
me.add(cp);
请注意,cron 表达式最多只允许五部分,每部分用空格分隔开来,这五部分从左到右依次表示分、时、天、月、周,其具体规则如下:
分 :从 0 到 59
时 :从 0 到 23
天 :从 1 到 31,字母 L 可以表示月的最后一天
月 :从 1 到 12,可以别名:jan", “feb”, “mar”, “apr”, “may”, “jun”, “jul”, “aug”, “sep”, “oct”, “nov” and “dec”
周 :从 0 到 6,0 表示周日,6 表示周六,可以使用别名: “sun”, “mon”, “tue”, “wed”, “thu”, “fri” and “sat”
两大疑问:第一个疑问是当某个任务调度抛出了异常,那么这个任务在下次被调度的时间点上还会不会被调度,答案是肯定的,不管什么时候出现异常,时间一到调度仍然会被执行。
第二个疑问是假如某个任务执行时间很长,如果这个任务上次调度后直到本次调度到来的时候还没执行完,那么本次调度是否还会进行,答案也是肯定的。
总结一句话就是:每次调度都是独立的,上次调度是否抛出异常、是否执行完,都与本次调度无关。
特别提醒:Cron4jPlugin的cron表达式与linux一样只有5个部分,与quartz这个项目的7个部分不一样,但凡在网上搜索到的7部分cron表达式都不要试图应用在Cron4jPlugin之中。
Validator
1、基本用法
Validator自身实现了Interceptor接口,所以它也是一个拦截器,配置方式与拦截器完全一样。以下是Validator示例:
public class LoginValidator extends Validator {
protected void validate(Controller c) {
validateRequiredString("name", "nameMsg", "请输入用户名");
validateRequiredString("pass", "passMsg", "请输入密码");
}
protected void handleError(Controller c) {
c.keepPara("name");
c.render("login.html");
}
}
protected void validator(Controller c)方法中可以调用validateXxx(…)系列方法进行后端校验,protected void handleError(Controller c)方法中可以调用c.keepPara(…)方法将提交的值再传回页面以便保持原先输入的值,还可以调用c.render(…)方法来返回相应的页面。注意handleError(Controller c)只有在校验失败时才会调用。
以上代码handleError方法中的keepXxx方法用于将页面表单中的数据保持住并传递回页,以便于用户无需再重复输入已经通过验证的表单域。
如果传递过来的是 model 对象,可以使用keepModel(...) 方法来保持住用户输入过的数据。同理,如果传递过来的是传统 java bean 对象,可以使用 keepBean(...) 方法来保持住用户输入过的数据。
keepPara(…) 方法默认将所有数据keep成String类型传给客户端,如果希望keep成为特定的类型,使用keepPara(Class, …) 即可,例如:keepPara(Integer.class, “age”)。
注意:如果keepPara() 造成模板中出现类型相关异常,解决方法参见Template Engine这章的Extension Method小节。
Validator配置
Validator配置方式与拦截器完全一样,见如下代码:
public class UserController extends Controller {
@Before(LoginValidator.class) // 配置方式与拦截器完全一样
public void login() {
}
}
Json转换
jfinal 的 json 模块以抽象类 Json 为核心,方便扩展第三方实现,jfinal 官方给出了三个 Json 实现,分别是 JFinalJson、FastJson、Jackson,这三个实现继承自抽象类 Json。
抽象类 Json 的核心抽象如下:
public abstract class Json {
public abstract String toJson(Object object);
public abstract <T> T parse(String jsonString, Class <T> type);
}
如上代码可以看出 Json 抽象就是 Object 与 json string 互转的两个方法,toJson(…)将任意 java 类型转成 json string,而 parse 将 json string 再反向转成范型指定的对象。
Json 配置
jfinal 官方提供了 Json 抽象类的三个实现:JFinalJson、FastJson、Jackson,如果不进行配置,那么默认使用 JFinalJson 实现,指定为其它实现需要在 configConstant 进行如下配置:
public void configConstant(Constants me) {
me.setJsonFactory(new FastJsonFactory());
}
上面配置将系统默认使用的 JFinalJson 切换到了 FastJson。还可以通过扩展 Json 抽象类以及 JsonFactory 来实现定制的 Json 实现。
假定用户扩展出了一个 MyJson 与 MyJsonFactory ,那么可以通过如下的方式切换到自己的实现上去:
public void configConstant(Constants me) {
me.setJsonFactory(new MyJsonFactory());
}
还可以配置 Date 类型转 json 后的格式:
public void configConstant(Constants me) {
me.setJsonDatePattern("yyyy-MM-dd");
}
Json 转换用法
json 转换在 jfinal 中的使用分为两类用法,第一类是使用配置的 json 转换,第二类是指定某个实现进行 json 转换。
1、使用配置的 json 实现转换
如下代码将使用前面章节中介绍的配置的 json 实现进行转换:
// 在 Controller 中使用 renderJson 进行 json 转换,并渲染给客户端
renderJson();
renderJson(key, object);
renderJson(new String[]{...});
// 使用 JsonKit 工具类进行 json 转换
JsonKit.toJson(...);
JsonKit.parse(...);
2、使用指定的 json 实现转换
如果下代码将使用指定的 json 实现去转换:
// 临时指定使用 FastJson 实现
FastJson.getJson().toJson(...);
FastJson.getJson().parse(...);
// 为 Controller.renderJson(..) 方法直接传入转换好的 json string
renderJson(FastJson.getJson().toJson(...));
上面这种用法可以临时摆脱配置的 json 实现,从而使用指定的 json 实现。
项目启动步骤
1: 使用 blog.sql 中的 sql 语句创建数据库与数据库表
2: 修改 res/a_little_config.txt 文件,填入正确的数据库连接用户名、密码
3: 将项目导入 eclipse。推荐使用 Eclipse IDE for Java EE Developers
4: 打开 com.demo.common包下的 DemoConfig 文件,右键单击该文件并选择 Debug As —> Java Application。
其它启动项目的方式见 《JFinal手册》。除此之外,项目还可以与其它普通java web 项目一样使用 tomcat
jetty 等 web server 来启动,启动方式与非jfinal项目完全一样。
5: 打开浏览器输入 localhost 即可查看运行效果
注意: 请确保您安装了 JDK 1.8 或更高版本,tomcat下运行项目需要先删除 jetty-server-xxx.jar,否则会有冲突