JFinal框架详细教程(二)高级用法


JFinal独创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,200);
    int count2=Db.update("update account set cash = cash + ? where id= ?",100,456);
    return count==1 && count2==1;
    }
});

以上两次数据库更新操作在一个事物中执行,如果执行过程中发生异常或者invoke方法返回false,则自动回滚事务。


paginate 分页支持

Model与Db中提供了四种paginate 分页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);

第二种是paginate(int pageNumber,int pageSize,boolean isGroupBySql,String select,String sqlExceptSelect,Object ... paras),相比第一种多了一个boolean isGroupBySql参数,以下是代码示例:

dao.paginate(1,10,true,"select *","from girl where age > ? group by age");

这种API用于具有group by的子句的sql,如果是嵌套型SQL,并且group by 不在最外层那么仍然可以不用该API,例如:

select * from (select x from t group by y) as t 

这种group by在内层可以不用该API

第三种是paginateByFullSql(int pageNumber,int pageSize,int totalRowSql,String findSql,Object... paras),相对于第一种是将查询总行数与查询数据的两条sql独立出来,这样处理主要是应对具有复杂order by 语句或者select 中带有distinct的情况,只有在使用第一种paginate出现一场时才需要使用该API,以下是示例代码:

String from ="from girl where age > ?";
String totalRowSql ="select count(*)"+from;
String findSql ="select * "+from + "order by age";
dao.paginateByFullSql(1,10,totalRowSql,findSql,18);

上面代码中order by子句并不复杂,所以仍然可以使用第一种API搞定。

第四种是paginate(int pageNumber,int pageSize,SqlPara sqlPara),用于配合sql管理功能使用,在后面介绍。


声明式事务

ActiceRecord 支持声明式事务,声明式事务需要使用ActiveRecordPlugin提供的拦截器来实现,以下代码为声明式事务示例:
//@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);
}

以上代码中,仅声明了一个Tx拦截器即为action添加了事务支持。除此之外ActiveRecord还配备了TxByActionKeys、TxByActionKeyRegex、TxByMethods、TxByMethodRegex,分别支持actionKeys、actionKey正则、actionMethods、actionMethod正则声明式事务,以下是示例代码

public void configInterceptor(Interceptors me) {
me.add(new TxByMethodRegex("(.*save.*|.*update.*)"));
me.add(new TxByMethods("save","update"));

me.add(new TxByActionKeyRegex("/trans.*"));
me.add(new TxByActionKeys("/tx/save","/tx/update"));
}

上例中TxByRegex拦截器可以通过传入正则对action进行拦截,当actionKey被正则匹配上将开启事务。


Cache

ActiveRecord可以使用缓存以大大提高性能,一下代码是Cache使用示例:

public void list() {
    List<Blog> blogList =Blog.dao.findByCache("cacheName","key","select * from blog");
    setAttr("blogList",blogList).render("list.html");
}

上例中findByCache方法中的cacheName需要在ehcache.xml中配置如:
<cachename="cacheName" ...>。此外Model.paginateByCache(...)、Db.findByCache(...)、Db.PaginateByCache(...)方法都提供了cache支持。在使用时,只需要传入cacheName、key以及在ehcache.xml中配置相对应的cacheName就可以了。


Dialect多数据库支持

目前ActiceRecordPlugin 提供了mysqlDialect、OracleDialect、AnsiSqlDialect实现类。
//方言基本还用不到,需要再添加
//TODO


表关联操作

JFinal ActiceRecord 天然支持表关联操作,并不需要学习新的东西。表关联操作主要有两种方式:一是直接使用sql得到关联数据;二是在Model中添加获取关联数据的方法。
假定现有两张数据库表:user、blog,并且user到blog是一对多的关系,blog表中使用user_id关联到user表。如下代码演示使用第一种方式得到username;

public void relation () {
 String sql="select b.*,u.user_name from blog b inner join user u on b.user_id=u.id where b.id=?";
 Blog blog =Blog.dao.findFirst(sql,123);
 String name = blog.getStr("user_name");
}

第二种

public class Blog extends Model<Blog> {
    public static final Blog dao = new Blog();
    public User getUser() {
    return User.dao.findById(get("user_id"));
    }
}
public class User extends Model<User> {
    public staitc final User dao =new User();
    public List<Blog> getBlogs() {
        return Blog.dao.find("select * from blog where user_id=?",get("id"));
    }
}

复合主键

JFinal ActiveRecord从2.0版本开始,采用极简设计支持复合主键,对于Model来说需要在映射时指定复合主键名称,以下是具体例子。

ActiveRecordPlugin arp = new ActiveRecordPlugin(druidPlugin);
//多数据源的配置仅仅是如下第二个参数是定一次复合主键的名称
arp.addMapping("user_role","userId,roleId",UserRole.class);

//同时指定复合主键即可查找记录
UserRole.dao.findById(123,456);

//同时指定复合主键即可删除记录
UserRole.dao.deleteById(123,456);

只需要在映射时指定复合主键名称即可开始使用复合主键,JFianl会对复合主键支持的个数进行检测,当复合主键数量不正确会报异常。
对于Db+Record模式来说,复合主键的使用不需要配置,直接用即可。


Sql管理与动态生成

Jfinal利用自带的Template Engine极为简洁的是现在Sql管理功能。一如既往的极简设计,仅有#sql、#para、#namespace 三个指令。


基本配置

在ActiveRecordPlugin 中使用sql管理功能示例代码如下:

ActiveRecordPlugin arp=new ActiceRecordPlugin(druidPlugin);
arp.setBaseSqlTemplatePath(PathKit.getRootClassPath());
arp.addSqlTemplate("demo.sql");
_MappingKit.mapping(arp);
plugin.add(arp);

如上例所示,arp.setBaseSqlTemplatePath(...)设置了sql文件存放的基础路径,注意上例代码将基础路径设置为了classpath的根,可以将sql文件放在maven项目下的resources之下,编译器会自动将其编译至classpath之下,该路径可自由设置。
arp.addSqlTemplate(...)添加外部sql模板文件,可以通过多次调用addSqlTemplate来添加任意多个外部sql文件,并且对于不同的ActiveRecordPlugin对象对视彼此独立配置,有利于多数据源下对sql进行模板化管理。


sql指令

通过#sql指令可以定义sql语句,如下是代码示例:

#sql("findPrettyGirl")
select * from girl where age > ? and age < ? and weight < 50 
#end

上例通过#sql指令 在模板文件中定义了key值为findPrettyGirl的sql语句,在java代码中的获取方式如下:

String sql =Db.getSql("findPrettyGirl");
Db.find(sql,16,23);

上例中第一行代码通过Db.getSql()方法获取到定义好的sql语句,第二行代码直接将sql用于查询。
还可以通过Model.getSql(key)方法来获取sql语句,功能与Db.getSql(key)基本一样,唯一不同的是为多数据源分别配置了sql模板的场景。

  • Model.getSql()在自身所对应的ActiveRecordPlugin的sql模板中去取sql
  • Db.getSql()在自身所对应的ActiveRecordPlugin的sql模板中去取
  • 可通过Db.use(...).getSql(...)实现Model.getSql()相同的功能

para指令

para指令用于生成sql中问号占位符以及占位符所对应的参数,两者分别生成在了SqlPara对象的sql和paraList对象之中。通过SqlPara.getSql()与SqlPara.getPara()可以分别获取到它们。#para指令需要与java后端的getSqlPara()协同工作。

para指令支持两种用法,一种是传入int 型常量参数的用法,如下示例展示的是int型常量参数的用法:

#sql("findPrettyGirl")
select * from girl where age > #para(0) and weight < #para(1)
#end

上例中#para指令传入了两个int型常量参数,所对应的java后端代码必须调用getSqlPara(String key,Object... paras),如下是代码示例:

SqlPara sqlPara =Db.getSqlPara("findPrettyGirl",18,50);
Db.find(sqlPara);

以上java代码中的18与50这两个参数将被前面#sql指令中定义的#para(0)与#para(1)所使用。为#para传入的int型常量值表示获取传入的参数的index下标值,上例中的#para(0)对应下标为0的参数值18,而#para(1)则对应下标为1的参数值50.

para指令的另一种用法是传入除了int型常量以外的任意类型参数,如下是代码示例:

#sql(findPrettyGirl)
 select * from gril where age > #para(age) and weight < #para(weight) 
#end

与上例模板配套的java代码

Kv cond=Kv.by("age",18).set("weight",50);
SqlPara sqlPara=Db.getSqlPara("findPrettyGirl",cond);
Db.find(sqlPara);

上例代码获取到的SqlPara对象sqlPara中封装的sql为:select * from gril where age > ? and weight < ? 封装的与sql问号占位符次序一致的参数列表值为【18,50】
以上两个示例,获取到的SqlPara对象中的值完全一样,其中的sql值都为:select * from gril where age > ? and weight < ? 其中的参数列表值也都为【18,50】.不同的是#para用法不同,以及它们对应的java代码传参方式不同,前者传入的是Object... paras参数,后者是Map data参数。
Notice:#para指令所在之处永远是生成一个问号占位符,并不是参数的值,参数值被生成在了SqlPara对象的paraList属性之中,通过sqlPara.getPara()可获取,如果想生成参数值用以下模板输出指令即可:#(value)


namespace指令

在#sql上可以增加#namespace("japan") 指定namespace为japan,在使用的时候,只需要在key前面添加namespace值前缀 + 句点符号 + key即可:

getSql("japan.findPrettyGirl");

分页用法

在使用#sql定义sql时,与普通查询完全一样,不需要使用额外的指令,在java代码中使用getSqlPara得到SqlPara对象以后,直接扔给Db或者Model的paginate方法就可以了,以下是代码示例:

SqlPara sqlPara =Db.getSqlPara("findPrettyGirl",18,50);
Db.paginate(1,10,sqlPara);

如以上代码所示,将sqlPara对象直接用于paginate方法即可,而#sql定义与普通的非分页sql未定义完全相同。


高级用法

除了#sql、#para、#namespace之外,还可以使用JFinal Template Engine中所有存在的指令,生成复杂条件的sql语句,以下是相对灵活的示例:

#sql("find")
select * from girl 
#for(x : cond)
 #(for.index == 0 ? "where" : "and") #(x.key) #para(x.value)
 #end
#end

以上代码#for指令 对于Map类型的cond参数进行迭代,动态生成自由的查询条件。上例中的三元表达式表示在第一次迭代时生成where,后续则生成and。#(x.key)输出参数key值,#para(x.value)输出一个问号占位符,以及将参数value值输出到SqlPara.paraList中去。
以上sql模板对应的java代码如下:

Kv cond=Kv.by("age >",16).set("age <",23).set("sex = ","female");
SqlPara sp=Db.getSqlPara("find",Kv.by());
Db.find(sp);

以上三个带有比较运算符的参数,可以同时生成sql查询条件名称、条件运算符号、参数列表,一石三鸟。甚至可以将此法用于 and or not 再搭配一个LinkedHashMap生成更加灵活的逻辑组合条件sql。
还可以用JFinal模板引擎#define指令将常用的sql定义成通用的模板函数,以便消除重复性的sql代码,下面是利用id数组删除数据的示例。

###定义模板函数 deleteByIdList
#define deleteByIdList(table,idList)
delete from #(table) where id in (
    #for (id:idList)
        #(for.index > 0 ? "," :"") #(id)
    #end
)
#end
#调用上面定义的模板函数
#sql("deleteUsers")
    #@deleteByIdList("user",idList)
#end

如上sql模板先是用template engine 所提供的#define 指令定义模板函数deleteByIdList,然后下发的#sql指令中对其调用,可以避免重复代码,在java中可以使用如下代码来生成sql:

List idList =Arrays.asList(1,2,3);
SqlPara sp =Db.getSqlPara("deleteUsers",Kv.by("idList",idLIst));
Db.update(sp.getSql());

Notice:上面的例子中的Kv是JFinal提供的用户体验更好的Map实现,使用任意的Map都可以,不限定为Kv。总之,利用sql模块专用的三个指令再结合模板引擎已有指令自由组合,可非常简洁地实现极为强大的sql管理功能。
Notice:sql管理模块使用的模板引擎并非在configEngine(Engine engine)配置,因此在配置shared method、directive等扩展时需要使用activeRecordPlugin.getEngine(),然后对该Engine对象进行配置。


最佳实践

在开发中,可以先创建一个总的sql模板文件,然后在此模板文件中,使用#namespace与#include指令对其他模板文件进行统一管理。以下是jfinal俱乐部专享项目中的做法。
第一步,创建名为all.sql的模板文件,内容如下:

    #namespace("index")
        #include("index.sql")
    #end
    #namespace("project")
        #include("project.sql")
    #end#
    namespace("share")
        #include("share.sql")
    #end
    #namespace("feedback")
        #include("feedback.sql")
    #end

以上每一个#namespace中使用#include指令包含了一个sql子模版文件,随后在子模版中就不再需要使用#namespace便可以使用子模版拥有了namespace。

第二步,对应于#include指令,分别创建index.sql、project.sql、share.sql、feedback.sql四个sql子模版,以下是project.sql子模版内容:

#sql("paginate")
    select p.id,
        substring(p.title,1,100) as titile,
        substring(p.content,1,100) as content,
        a.avatar,a.id as accountId
        from project p inner join account a on p.accountId =a.id 
        where report < #para(0)
#end

#sql("findById")
    select p.*,a.avatar,a.nickName
    from project p inner join account a on p.accountId=a.id
    where p.id =#para(0) and p.report < #para(1) limit 1
#end

#sql("findByIdWithColumns")
    select #(columns)
    from project
    where id=#para(id) and report < #para(report) limit 1
#end

以上每个#sql指令中的key值对应于java 代码中使用该sql的方法名称,例如#sql("paginate")中的paginate对应ProjectService.paginate()方法的方法名称。
第三步,在configPlugin()中对acticeRecordPlugin进行sql模板的添加

ActiceRecordPlugin arp=new ActiceRecordPlugin(druidPlugin);
arp.setBaseSqlTemplatePath(PathKit.getRootClassPath() + "/sql");
arp.addSqlTemplate("all.sql");

以上代码,只需要对arp添加一个all.sql即可,不需要再添加多个子模板。

以上最佳实践的主要优点:

  • 子模版中摆脱掉#namespace的使用,避免了书写时的#namespace与#sql嵌套
  • 避免了为书写美观而需要一个tab缩进,书写与生成sql更加美观
  • 在java代码中主需要配置添加all.sql这一个总的模板文件

多数据源支持

ActiveRecordPlugin 可同时支持多数据源、多方言、多缓存、多事务级别等特性,对于每个ActiceRecordPlugin可进行彼此独立的配置,简言之JFinal可以同时使用多数据源,并且可以针对这个多个eshju'yuan书院配置独立的方言、缓存、事务级别等。
当使用多数据源时,只需要对每个ActiveRecordPlugin指定一个configName即可,如下是代码示例:

public void configPlugin(Plugins plugin) {
    DruidPlugin dsMysql= new DruidPlugin(...);
    plugin.add(dsMysql);
    
    //Mysql ActiceRecordPlugin 示例,并制定configName为mysql
    ActiceRecordPlugin arpMysql=new ActiveRecordPlugin("mysql",dsMysql);
    plugin.add(a)rpMysql);
    arpMysql.setCache(new EhCache());
    arpMysql.addMapping("user",U)ser,class);
    
    //oracle
    DruidPlugin dsOracle = new DruidPlugin(...);
    plugin.add(dsOracle);
    
    //oracle ActiveRecordPlugin 实例,并指定configName为Oracle 
    ActiceRecordPlugin arpOracle=new ActiceReocrdPlugin("oralce",dsOracle);
    plugin.add(arpOracle);
    arpOracle.setDisalect(new OracleDialect());
    arpOralce.setTransactionLevel(8);
    arpOracle.addMapping("blog".Blog.class);
}

以上代码创建了两个ActiceRecordPlugin实例,arpMysql与arpOralce,特别注意创建实例的同时指定其configName分别为mysql与oracle arpMysql与arpOracle分别映射了不同的Model,配置了不同的方言。
对于Model的使用,不同的Model会自动找到其所属的ActiveRecordPlugin实例以及相关配置进行数据库操作。加入希望同一个Model能够切换到不同数据源上使用,也极度方便,这种用法非常适合不同数据源中的table拥有相同表结构的情况,开发者希望用同一个Model来操作这些相同表结构的table,以下是示例代码:

public void multiDsModel() {
    //默认使用arp.addMapping(...)时关联起来的数据源
    Blog blog =Blog.dao.findById(123);
    
    //只需调用一次use方法即可切换到另一台数据源上去
    blog.use("backupDatabase").save();
}

上例中的代码,blog.use("backupDatabase")方法切换数据源到backupDatabase并直接将数据保存起来。
Notice:只有在同一个Model希望对应到多个数据源的table时才需要使用use方法,如果同一个Model唯一对应一个数据源的一个table,那么数据源的切换是自动的,无需使用use方法。
对于Db+Record的使用,数据源的切换需要使用Db.use(configName)方法得到数据库操作对象,然后就可以进行数据库操作了,以下是代码示例。

//查询dsMysql数据源中的user
List<Record> user= Db.use("mysql").find("select * from user");
//查询dsOracle数据源中的blog
List<Record> blog=Db.use("oracle").find("select * from blog");

以上两行代码,分别通过configName为mysql、oracle得到各自的数据库操作对象,然后就可以如同单数据完全一样的方式来使用数据库操作API了。简言之,对于Db+Record来说,多数据源相比单数据源仅需多调用一下Db.use(configName),随后的API使用方式完全一样。
注意最先创建的ActiveRecordPlugin实例将会成为主数据源,可以省略configName。最先创建的ActiveRecordPlugin实例中的配置将默认成为住配置,此外还可以通过设置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);
    
    //与web环境唯一的不同是要手动调用一次相关插件的start()方法
    dp.start();
    arp.start();
    
    //通过上面简单几行代码即可立即开始使用
    new Blog().set("title","title").set("content","cxt text").save();
    Blog.dao.findById(123);
    }
}

Notice:ActiveRecordPlugin 所依赖的其他插件也必须手动调用一下start方法,如上例中的dp.start()。


Generator与javaBean

ActiveRecord 模块提供了ModelGenerator、BaseModelGenerator、MappingKitGeneator、DataDictionaryGeneator,可分别生成Model、BaseModel、MappingKit、DataDictionary四类文件。可根据数据表自动化生成这四类文件。
生成后的Model继承自BaseModel而非继承自Model,BaseModel中拥有getter、setter方法遵守传统java bean规范,Model继承自BaseModel即完成了JavaBean与Model合体,拥有了传统JavaBean所有的优势,并且所有的getter、setter方法完全无需人工干预,数据表有任何变动一键重新生成即可。
使用时通常只需要配置Geneator的四个参数即可:baseModelPackageName、baseModelOutputDir、modelPackageName、modelOutputDir。四个参数分别表示baseModel的包名,baseModel的输出路径,model的包名,model的输出路径,以下是示例代码:
//base model 所使用的包名
String baseModelPkg = "model.base";
//base model 文件保存路径
String baseModelDir = PathKit.getWebRootPath() + "/../src/model/base";
//model 所使用的包名
String modelPkg = "model";
//model 文件保存路径
String modelDir =baseModelDir + "/..";

Generator generator = new Generator(dataSource,baseModelPkg,baseModelDir,ModelPkg,modelDir);
generator.generate();

相关生成文件

BaseModel是用于被最终的Model继承的基类,所有的getter、setter方法都将生成在此文件内,这样就保障了最终给的Model清爽与干净,BaseModel不需要人工维护,在数据库有任何变化时重新生成一次即可。
MappingKit用于生成table到Model的映射关系,并且会生成主键/复合主键的配置,也即无需在configPlugin(Plugins plugin)方法中书写任何样板式的映射代码。
DataDictionary是指生成的数据字典,会生成数据表所有字段的名称、类型、长度、备注、是否主键等信息。


Model与Bean合体后主要优势

  • 充分利用海量的针对与Bean设计的第三方工具,例如:jackson、freemarker
  • 快速响应数据库表变动,极速重构,提升开发效率
  • 不用记数据表字段名,避免水榭字段名出现错误
  • BaseModel设计令Model中依然保持清爽,在表结构变化时极速重构。
  • 自动化table至Model映射。
  • 自动化主键、复合主键名称识别与映射
  • MappingKit承载映射代码,JFinalConfig保持清爽
  • 有利于分布式场景和无数据源时使用Model

Model与Bean合体后注意事项

  • 合体后Jsp模板输出Bean中数据将依赖其getter方法,输入的变量名即为getter方法去掉"get"前缀字符后剩下的字符首字母变小写,如果希望JSP仍然使用之前的输出方式,可以在系统启动时调用以下 ModelRecordElResolver.setResolveBeanAsModel(true);
  • Controller之中的getModel需要表单域名称对应于数据表字段名,而getBean()则依赖于setter方法,表单域名对应于setter方法去掉"set"前缀字符后剩下的字符串字母变小写。
  • 许多类似于jackson、fastjson的第三方工具依赖于Bean的getter方法进行操作,所以只有合体后才可以使用jackson、fastjson
  • JFinalJson 将Model转换为json数据时,json的keyName是原始的数据表字段名,而jackson、fastjson这类依赖于getter方法转化成的json的keyName的数据表字段名转换而成的驼峰命名。
  • 建议mysql数据表的字段名直接使用驼峰命名,这样可以令json的keyName完全一致,也可以使JSP在页面中取值时使用完全一致的属性名。注意:mysql数据表的名称仍然使用下划线明明方式并使用小写字母,方便在linux与windows系统之间移植。
  • 总之,合体后的Bean在使用时候要清楚使用的是其BaseModel中的getter、setter方法还是其Model中的get(String attrName)方法。

Template Engine

//TODO


EhCachePlugin

EhCachePlugin是JFinal集成的缓存插件,通过使用EhCachePlugin 可以提高系统的并发访问速度。


EhCachePlugin

EhCachePlugin 是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置EhcachePlugin,以下是Plugin配置示例代码:

public class DemoConfig extendsJ FinalConfig {
    public void configPlugin(Plugins plugin) {
    plugin.add(new EhCachePlugin());
    }
}

CacheInterceptor

CacheInterceptor 可以将action所需数据全部缓存起来,下次请求到来时如果擦车存在则直接使用数据并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(Model.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是支持Redis极速化插件,同时支持多Redis服务端。

RedisPlugin

RedisPlugin是作为JFinal的Plugin而存在的,所以使用时需要在JFinalConfig中配置RedisPlugin,以下是RedisPlugin配置示例代码:

public class DemoConfig extends JFinalConfig {
    public void configPlugin(Plugins plugin) {
        //用于缓存bbs模块的redis服务
        RedisPlugin bbsRedis = new RedisPlugin("bbs","localhost");
        plugin.add(bbsRedis);
        //用于缓存news模块的redis服务
        RedisPlugin newsRedis = new Redisplugin("news","192.168.3.9");
        plugin.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");
    
    bbsCache = Redis.use();
    bbsCache.set("jfinal","awesome");
}

以上代码中通过"bbs"、"news"作为use方法的参数获取到了两个Cache对象,使用这两个对象即可操作其所对应的Redis服务端。
通常情况下只会创建一个RedisPlugin连接一个redis服务端,使用Redis.user().set(key,value)即可。
Notice:使用incr、incrBy、decr、decrBy 方法操作的计数器,需要使用getCounter(key)进行读取而不能使用get(key),否则会抛反序列化异常。

非web环境使用RedisPlugin

RedisPlugin 也可以在非web环境下使用,只需引入jfinal.jar 然后多调用以下redisPlugin.start()即可,以下是代码示例:

public class RedisTest {
    public static void main(String args[]) {
    Redisplugin rp = new RedisPlugin("myRedis","localhost");
    //与web下唯一区别是需要在这里调用一次start()方法
    rp.start();
    
    Redis.use().set("key","value");
    Redis.use().get("key");
    }
}

Cron4jPlugin

Cron4j是Jfinal集成的任务调度插件,通过使用Cron4jPlugin可以使用通用的cron表达式极为便利的实现任务调度功能。

Cron4jPlugin

Cron4jPlugin是作为JFinal的Plugin而存在的,所有使用时需要在JFinalConfig中配置,如下时代码示例:

Cron4jPlugin cp=new Cron4jPlugin();
cp.addTask("* * * * *",new MyTask());
plugin.add(cp);

如上所示创建插件,addTask传入参数,并添加到JFInal即完成了基本配置,第一个参数"* * * * *"是用于任务调度的cron表达式,第二个参数是Runnable接口的一个实现类,Cron4jPlugin会根据cron表达式调用MyTask中的run方法。
请注意,cron表达式最多只允许5部分,每部分用空格来分隔开来,这五部分从左到右依次表示
分、时、天、月、周,其具体规则如下:

  • 分:从0到59
  • 时:从0到23
  • 天:从1到31,字母L可以表示月的最后一天
  • 月:从1到12,可以别名:"jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"。
  • 周:从0到6 0表示周日,6表示周6,可以使用别名:"sun","mon","tue","wed","thu","fri","sat"

如上五部分的分、时、天、月、周又分别支持如下字符,其用法如下:

  • 数字n:表示一个具体的时间点,例如:5 * * * * 表示5分这个时间点执行
  • 逗号,:表示指定多个数值,例如:3,5 * * * *表示3和5分这两个时间点执行
  • 减号 -:表示范围,例如:1-3 * * * * 表示1分、2分、3分这三个时间点执行
  • 星号 :表示每一个时间点,例如: * * * * 表示每分钟执行
  • 除号 /:表示指定一个值得增加幅度。例如:n/m表示从n开始,每次增加m的时间点执行
    以上规则不是JFinal创造的,是通过cron表达式规则。

使用外部配置文件

上一个示例仅展示了java硬编码式的配置,更多的应用场景是使用外部配置文件,灵活配置调度策略,以便于随时改变调度策略,如下是外部配置的代码示例:

    cron4j=task1,task2
    
    task1.cron=* * * * *
    task1.class=com.xxx.TaskAaa
    task1.daemon=true
    task1.enable=true
    
    task2.cron=* * * * *
    task2.class=com.xxx.TaskBbb
    task2.daemon=true
    task2.enable=false

上图中的cron4j是所谓的配置名称:configName,可以随便取名,这个名称在创建Cron4jPlugin对象时会被用到,如果创建Cron4jPlugin对象时不提供名称则默认值为cron4j。
上图中的configName后面紧跟着的是task1、task2,表示当前配置的两个task的名称,这两个名称规定了后续的配置将以其打头,例如后面的task1.cron、task2.cron都是以这两个task名称打头的。
上图中的task1.cron是指该task的cron表达式,task1.class是指该task要调度的目标java类,该java类需要实现Runnable接口,task1.daemon是指被调度的任务线程是否为守护线程,task1.enable是指该task是开启还是停用,这个配置不是必须的,可以省略,省略时默认表示开启。同理task2的配置与task1的意义相同,只是taskName不同。

假定配置文件名为config.txt 配置完成以后Cron4jPlugin的创建方式可以如下:

cp =new Cron4jPlugin("config.txt");
cp =new Cron4jPlugin("config.txt","cron4j");

cp =new Cron4jPlugin(PropKit.use("config.txt"));
cp =new Cron4jPlugin(PropKit.use("config.txt"),cron4j);

plugin.add(cp);

以上代码中前四行是利用配置文件创建Cron4jPlugin的四种方式。两种带configName 两种不带,两种使用PropKit 两种没有
Notice:这里所说的configName,就是前面示例中配置项 cron4j=task1,task2中的"cron4j",这个configName相当于就是Cron4jPlugin寻找的配置入口。

高级用法

除了可以对实现了Runnable接口的java类进行调度以外,还可以直接调度外部的应用程序,例如windows或linux下的某个可执行程序,如下是代码示例:

String[] command = {"C:\\tomcat\\bin\\catalina.bat","start"};
String[] envs = {"CATALINA_HOME=C:\\tomcat","JAVA_HOME=C:\\jdks\\jdk5"};
File directory = "C:\\MyDirectory";
ProcessTask task = new ProcessTask(comand,envs,directory);

cron4jPlugin.addTask(task);
plugin.add(cron4jPlugin);

如上所示,只需要创建一个ProcessTask对象,并让其指向某个应用程序,再通过addTask添加进来,就可以实现对其的调度,这种方式实现类似于每天半夜备份服务器数据库并打包成zip的功能,变得极为简单便捷。更加详细的用法,可以看一下Cron4jPlugin.java源代码中的注释。


Validator

Validator是JFinal校验组件,再Validator类中提供了非常方便的校验方法,学习简单使用方便。

Validator

Validator自身实现了Interceptor接口,所以它也是一个拦截器,配置方式与拦截器完全一样,以下是示例:

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)方法可以调用validate(...)系列方法进行后端校验
protected void handleError(Controller c)方法中可以调用c.keepPara(...)方法将提交的值再传回页面以便保持原先输入的值,还可以调用c.render(...)方法来返回相应的页面。注意handleError(Controller c)只有校验失败时才会调用。
以上代码handleError方法中的keepXxx方法用于将页面表单中的数据包吃住并传递回页,以便于用户无需再重复输入已经通过验证的表单域,如果传递过来的是model对象,可以使用keepModel方法来保持住用户输入过的数据。

Validator配置

配置方式与拦截器完全一样

public class UserController extends Controller {
    @Before(LoginValidator.class) 
    public void login() {
    }
}

国际化

国际化模块仅三个类文件,使用方式要比spring 容易得多。

I18n与Res

I18n对象可通过资源文件的baseName与locale参数获取到与值相对应的Res对象,Res对象提供了API用来获取国际化数据。
以下给出了具体使用步骤:
  • 创建i18n_en_US.properties、i18n_zh_CN.properties资源文件,i18n即为资源文件的baseName,可以是任意名称,在此示例中使用i18n作为baseName
  • i18n_en_US.properties文件中添加如下内容 msg=Hello{0},today is{1}.
  • i18n_zh_CN.properties文件中添加如下内容 msg=你好{0},今天是{1}
  • 在YourJFinalConfig中使用constant.setI18nDefaultBaseName("i18n")配置资源文件默认baseName
  • 特别注意:java国际化规范要求properties文件编辑需要使用专用的编辑器,否则会出乱码,常用的有Properties Editor,在此可以下载下载editor

以下是基于以上步骤以后的代码示例:

//通过locale参数en_US得到对应的Res对象
Res resEn=I18n.use("en_US");
//直接获取数据
String msgEn =resEn.get("msg");
//获取数据并使用参数格式化
String msgEnFormat =resEn.format("msg","james",new Date());

//通过locale参数zh_CN得到对应的Res对象
Res resZh = I18n.use("zh_CN");
//直接获取数据
String msgZh = resZh.get("msg");
//获取数据并使用参数格式化
String msgZhFormat = resZh.format("msg","詹波",new Date());

//另外,I18n还可以加载未使用me.setI18nDefaultBaseName()配置过的资源文件,唯一的不同是
//需要指定baseName参数,下面例子需要先创建otherRes_en_US.properties文件
Res otherRes = I18n.use("otherRes","en_US");
otherRes.get("msg");

I18nInterceptor

I18nInterceptor 拦截器是针对与web应用提供的一个国际化组件,以下是在freemarker模板中使用的例子

//先将I18nInterceptor配置成全局拦截器
public void configInterceptor(Interceptors me) {
    me.add(new I18nInterceptor());
}
//然后在freemarker中即可通过_res对象来获取国际化数据
${_res.get("msg")}

以上代码通过配置了I18nInterceptor拦截action请求,然后即可在freemarker模板文件中通过名为_res对象来获取国际化数据,I18nInterceptor的具体工作流程如下:

  • 试图从请求中通过controller.getPara("_locale")获取locale参数,如果获取到则将其保存到cookie之中
  • 如果没有获取到locale参数,则视图通过controller.getCookie("_locale")得到locale参数
  • 如果以上两步仍然没有获取到locale参数值,则使用I18n.defaultLocale的值作为locale值来使用。
  • 使用前三步中得到的locale值,通过I18n.use(locale)得到Res对象,并通过controller.setAttr("_res",res)将Res对象传递给页面使用。
  • 如果I18nInterceptor.isSwitchView为true值的话还会改变render的view值,实现整体模板文件的切换,详情看源码。

以上步骤I18nInterceptor中的变量名"_locale"、"_res"都可以在创建I18nInterceptor对象时进行指定,不指定将使用默认值,还可以通过继承I18nInterceptor并且覆盖getLocalPara、getResName、getBaseName来定制更加个性化的功能。

在有些web系统中,页面需要国际化的文本过多,并且css以及html也因为国际化而大不相同,对于这种应用场景先直接制作多套同名称的国际化视图,并将这些视图以locale为子目录分类存放,最后使用I18nInterceptor拦截器根据locale动态切换视图,而不必对视图中的文本逐个进行国际化切换,只需要将I18nInterceptor.isSwitchView设置为true即可,省时省力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容