基于JFinal的web项目需要创建一个继承自JFinalConfig类的子类,该类用于对整个web项目进行配置。
一. JFinalConfig子类需要实现六个抽象方法,如下所示:
public class DemoConfig extends JFinalConfig {
public void configConstant(Constants me) {}
public void configRoute(Routes me) {}
public void configEngine(Engine me) {}
public void configPlugin(Plugins me) {}
public void configInterceptor(Interceptors me) {}
public void configHandler(Handlers me) {}
}
1. configConstant(..)
此方法用来配置JFinal常量值,如开发模式常量devMode的配置,如下代码配置了JFinal运行在开发模式
public void configConstant(Constants me) {
me.setDevMode(true);
}
//此方法用来配置JFinal常量值,如开发模式常量devMode的配置,如下代码配置了JFinal运行在开发模式
// 在开发模式下,JFinal会对每次请求输出报告,如输出本次请求的URL、Controller、Method以及请求所携带的参数。
2. configRoute(..) 此方法用来配置访问路由
public void configRoute(Routes me) {
me.setBaseViewPath("/view");
me.addInterceptor(new FrontInterceptor());
me.add("/hello", HelloController.class);
}
//此方法用来配置访问路由,如下代码配置了将 "/hello" 映射到HelloController这个控制器,
//通过以下的配置,http://localhost/hello 将访问 HelloController.index() 方法,
//而http://localhost/hello/methodName 将访问到 HelloController.methodName() 方法。
//Routes.setBaseViewPath(baseViewPath)方法用于为该Routes 内部的所有Controller设置视图渲染时的基础路径,
//该基础路径与Routes.add(…, viewPath)方法传入的viewPath以及
//Controller.render(view) 方法传入的 view 参数联合组成最终的视图路径,规则如下:
// finalView = baseViewPath + viewPath + view
// 注意:当view以 “/” 字符打头时表示绝对路径,baseViewPath 与 viewPath 将被忽略。
//Routes 类中添加路由的方法有两个:
public Routes add(String controllerKey, Class<? extends Controller> controllerClass, String viewPath)
public Routes add(String controllerKey, Class<? extends Controller> controllerClass)
//第一个参数controllerKey是指访问某个Controller所需要的一个字符串,该字符串唯一对应一个Controller,controllerKey仅能定位到Controller。
//第二个参数controllerClass是该controllerKey所对应到的Controller。
//第三个参数viewPath是指该Controller返回的视图的相对路径。当viewPath未指定时默认值为controllerKey。
从表中可以看出,JFinal访问一个确切的Action(Controller内的方法)需要使用controllerKey与method来精确定位,
当method省略时默认值为index。
urlPara是为了能在url中携带参数值,urlPara可以在一次请求中同时携带多个值,
JFinal默认使用减号“-”来分隔多个值(可通过constants. setUrlParaSeparator(String)设置分隔符),
在Controller中可以通过getPara(int index)分别取出这些值。
controllerKey、method、urlPara这三部分必须使用正斜杠“/”分隔。
注意,controllerKey自身也可以包含正斜杠“/”,如“/admin/article”,这样实质上实现了struts2的namespace功能。
JFinal在以上路由规则之外还提供了ActionKey注解,可以打破原有规则,以下是代码示例:
public class UserController extends Controller {
@ActionKey("/login")
public void login() {
render("login.html");
}
}
假定 UserController 的 controllerKey值为“/user”,
在使用了@ActionKey(“/login”)注解以后,
actionKey由原来的“/user/login”变为了“/login”。
该注解还可以让actionKey中使用减号或数字等字符,如“/user/123-456”。
如果JFinal默认路由规则不能满足需求,开发者还可以根据需要使用Handler定制更加个性化的路由,大体思路就是在Handler中改变第一个参数String target的值。
JFinal路由还可以进行拆分配置,这对大规模团队开发十分有用,以下是代码示例:
public class FrontRoutes extends Routes {
public void config() {
setBaseViewPath("/view/front");
add("/", IndexController.class);
add("/blog", BlogController.class);
}
}
public class AdminRoutes extends Routes {
public void config() {
setBaseViewPath("/view/admin");
addInterceptor(new AdminInterceptor());
add("/admin", AdminController.class);
add("/admin/user", UserController.class);
}
}
public class MyJFinalConfig extends JFinalConfig {
public void configRoute(Routes me) {
me.add(new FrontRoutes()); // 前端路由
me.add(new AdminRoutes()); // 后端路由
}
public void configConstant(Constants me) {}
public void configEngine(Engine me) {}
public void configPlugin(Plugins me) {}
public void configInterceptor(Interceptors me) {}
public void configHandler(Handlers me) {}
}
FrontRoutes类中配置了系统前端路由,
AdminRoutes配置了系统后端路由,
MyJFinalConfig.configRoute(…)方法将拆分后的这两个路由合并起来。
使用这种拆分配置不仅可以让MyJFinalConfig文件更简洁,
而且有利于大规模团队开发,避免多人同时修改MyJFinalConfig时的版本冲突。
FrontRoutes与AdminRoutes中分别使用setBaseViewPath(…)
设置了各自Controller.render(view)时使用的baseViewPath。
AdminRoutes 还通过addInterceptor(new AdminInterceptor())添加了 Routes 级别的拦截器,
该拦截器将拦截 AdminRoutes 中添加的所有 Controller,
相当于业务层的inject拦截器,会在class拦截器之前被调用。
这种用法可以避免在后台管理这样的模块中的所有class上使用
@Before(AdminInterceptor.class),减少代码冗余。
3.configEngine(..)此方法用来配置Template Engine
public void configEngine(Engine me) {
me.addSharedFunction("/view/common/layout.html");
me.addSharedFunction("/view/common/paginate.html");
me.addSharedFunction("/view/admin/common/layout.html");
}
上面的方法向模板引擎中添加了三个定义了 template function 的模板文件
4. configPlugin(..) 此方法用来配置JFinal的Plugin(插件),JFinal插件架构是其主要扩展方式之一,可以方便地创建插件并应用到项目中去。
ublic void configPlugin(Plugins me) {
DruidPlugin dp = new DruidPlugin(jdbcUrl, userName, password);
me.add(dp);
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
arp.addMapping("user", User.class);
me.add(arp);
}
如下代码配置了Druid数据库连接池插件
与ActiveRecord数据库访问插件。
通过以下的配置,可以在应用中使用ActiveRecord非常方便地操作数据库。
5. configInterceptor(..) 此方法用来配置JFinal的全局拦截器,全局拦截器将拦截所有 action 请求,除非使用@Clear在Controller中清除
public void configInterceptor(Interceptors me) {
me.add(new AuthInterceptor());
}
全局拦截器将拦截所有 action 请求,除非使用@Clear在Controller中清除,
如下代码配置了名为AuthInterceptor的拦截器。
JFinal 的 Interceptor 非常类似于 Struts2,但使用起来更方便,
Interceptor 配置粒度分为 Global、Inject、Class、Method四个层次,
其中以上代码配置粒度为全局。
Inject、Class与Method级的Interceptor配置将在后续章节中详细介绍。
6.configHandler(..) 此方法用来配置JFinal的Handler
public void configHandler(Handlers me) {
me.add(new ResourceHandler());
}
如下代码配置了名为ResourceHandler的处理器,
Handler可以接管所有web请求,并对应用拥有完全的控制权,可以很方便地实现更高层的功能性扩展。
7.回调方法配置
在 JFinalConfig 继承类中可以添加 afterJFinalStart() 与 beforeJFinalStop(),JFinal 会在系统启动完成之后以及系统关闭之前分别回调这两个方法:
// 系统启动完成后回调
public void afterJFinalStart() {
}
// 系统关闭之前回调
public void beforeJFinalStop() {
}
在 JFinalConfig 继承类中可以添加 afterJFinalStart() 与 beforeJFinalStop(),
JFinal 会在系统启动完成之后以及系统关闭之前分别回调这两个方法:
这两个方法可以很方便地在项目启动后与关闭前让开发者有机会进行额外操作
,如在系统启动后创建调度线程
或在系统关闭前写回缓存。
8. PropKit 读取配置
PropKit工具类用来读取外部键值对配置文件,
PropKit可以极度方便地在系统任意时空使用,
配置文件的格式如下:
userName=james
[email protected]
devMode=true
如下是 PropKit 代码示例:
PropKit.use("config.txt");
String userName = PropKit.get("userName");
String email = PropKit.get("email");
// Prop 配合用法
Prop p = PropKit.use("config.txt");
Boolean devMode = p.getBoolean("devMode");
如下是在项目中具体的使用示例:
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来指定配置文件名再来取值
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是线程安全的。
1.Action
在Controller之中定义的public方法称为Action。Action是请求的最小单位。Action方法必须在Controller中定义,且必须是public可见性。
public class HelloController extends Controller {
public void index() {
renderText("此方法是一个action");
}
public String test() {
return "index.html";
}
}
在Controller之中定义的public方法称为Action。
Action是请求的最小单位。
Action方法必须在Controller中定义,且必须是public可见性。
以上代码中定义了两个Action:HelloController.index()、HelloController.test()。
Action可以有返回值,返回值可在拦截器中通过invocation.getReturnValue() 获取到,以便进行render控制。
从 JFinal 3.2 版本开始,其 jfinal-java8 编译版本已支持Action携带形参
public class ProjectController extends Controller {
public void index(Project project) {
project.save();
render("index.html");
}
}
Action带参可以代替getPara、getBean、getModel系列方法获取参数,
使用UploadFile参数时可以代替getFile方法实现文件上传。
这种传参方式还有一个好处是便于与swagger这类第三方无缝集成,生成API文档。
小技巧:如果action形参是一个model或者bean,
原先通过getBean(User.class, "") 获取时第二个参数为空字符串或null,那么与之等价的形参注入只需要用一下 @Para("") 注解即可:
public void action(@Para("")User user) { …. }
使用带参Action需要进行两步操作,第一步需要在 java8 下使用 jfinal-java8 版本,其maven坐标如下:
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jfinal-java8</artifactId>
<version>3.4</version>
</dependency>
第二步是在开发工具中配置打开编译参数保留住方法参数名称。注意过于老旧的eclipse版本不支持java8和该配置项,建议至少使用eclipse mars版本,以下是eclipse中的设置:
检查项目属性配置的Java Build Path菜单下的Libraries下的java版本是否为1.8:
检查项目属性配置的Project Facets菜单下的 java版本配置确定是否为1.8:
如果要使用maven插件进行编译,为maven-compiler-plugin编译插件配置一个<compilerArgument>-parameters</compilerArgument> 属性即可:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!-- java8 保留参数名编译参数 -->
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
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中的参数值。
getBean与getModel系列
getModel用来接收页面表单域传递过来的model对象,
表单域名称以”modelName.attrName”方式命名,getModel使用的attrName必须与数据表字段名完全一样。
getBean方法用于支持传统Java Bean,
包括支持使用jfinal生成器生成了getter、setter方法的Model,
页面表单传参时使用与setter方法相一致的attrName,而非数据表字段名。
getModel与getBean区别在于前者使用数据库表字段名而后者使用与setter方法一致的属性名进行数据注入。建议优先使用getBean方法。
以下是一个简单的示例:
// 在页面表单中采用modelName.attrName形式为作为表单域的name
<form action="/blog/save" method="post">
<input name="blog.title" type="text">
<input name="blog.content" type="text">
<input value="提交" type="submit">
</form>
// 定义Model,在此为Blog
public class Blog extends Model<Blog> {
}
public class BlogController extends Controller {
public void save() {
// 页面的modelName正好是Blog类名的首字母小写
Blog blog = getModel(Blog.class);
// 如果表单域的名称为 "otherName.title"可加上一个参数来获取
blog = getModel(Blog.class, "otherName");
}
}
上面代码中,表单域采用了 "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)
setAttr方法
setAttr(String, Object)转调了HttpServletRequest.setAttribute(String, Object),
该方法可以将各种数据传递给View并在View中显示出来。
通过查看 jfinal 源码 Controller 可知 setAttr(String, Object) 方法在底层仅仅转调了底层的 HttpServletRequest 方法:
private HttpServletRequest request;
public Controller setAttr(String name, Object value) {
request.setAttribute(name, value);
return this;
}
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磁盘盘符打头的绝对路径,即可将基础路径指向项目根径之外,方便单机多实例部署。当该路径参数设置为相对路径时,则是以项目根为基础的相对路径。
session操作方法
通过setSessionAttr(key, value)可以向session中存放数据,
getSessionAttr(key)可以从session中读取数据。
还可以通过getSession()得到session对象从而使用全面的session API。
public void login() {
User user = loginService.login(...);
if (user != null) {
setSessionAttr("loginUser", user);
}
}
为了便于项目在未来方便支持集群与分布式,
已不建议使用 session 存放数据,
建议将 session 范畴数据存放在数据库中。
render系列方法
render系列方法将渲染不同类型的视图并返回给客户端。
JFinal目前支持的视图类型有:JFinal Template、FreeMarker、JSP、Velocity、JSON、File、Text、Html、QrCode 二维码 等等。除了JFinal支持的视图型以外,还可以通过继承Render抽象类来无限扩展视图类型。
通常情况下使用Controller.render(String)方法来渲染视图,
使用Controller.render(String)时的视图类型由JFinalConfig.configConstant(Constants constants)
配置中的constants. setViewType(ViewType)来决定,该设置方法支持的ViewType有:JFINAL_TEMPLATE、FreeMarker、JSP、Velocity,不进行配置时的缺省配置为JFINAL_TEMPLATE。
此外,还可以通过 constants.setRenderFactory(IRenderFactory)
来设置Controller中所有render系列方法所使用的Render实现类。
假设在JFinalConfig.configRoute(Routes routes)中有如下
Controller映射配置:routes.add(“/user”, UserController.class, “/path”),
render(String view)使用例子:
// 渲染名为test.html的视图,且视图类型为 JFinal Template
renderTemplate(”test.html”);
// 生成二维码
renderQrCode("content");
// 渲染名为test.html的视图,且视图类型为FreeMarker
renderFreeMarker(”test.html”);
// 渲染名为test.html的视图,且视图类型为Velocity
renderVelocity(“test.html”);
// 将所有setAttr(..)设置的变量转换成 json 并渲染
renderJson();
// 以 "users" 为根,仅将 userList 中的数据转换成 json 并渲染
renderJson(“users”, userList);
// 将user对象转换成 json 并渲染
renderJson(user);
// 直接渲染 json 字符串
renderJson("{\"age\":18}" );
// 仅将setAttr(“user”, user)与setAttr(“blog”, blog)设置的属性转换成json并渲染
renderJson(new String[]{"user", "blog"});
// 渲染名为test.zip的文件,一般用于文件下载
renderFile("test.zip");
// 渲染纯文本内容 "Hello JFinal"
renderText("Hello JFinal");
// 渲染 Html 内容 "Hello Html"
renderHtml("Hello Html");
// 渲染名为 test.html 的文件,且状态为 404
renderError(404 , "test.html");
// 渲染名为 test.html 的文件,且状态为 500
renderError(500 , "test.html");
// 不渲染,即不向客户端返回数据
renderNull();
// 使用自定义的MyRender来渲染
render(new MyRender());
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
传统AOP实现需要引入大量繁杂而多余的概念,例如:Aspect、Advice、Joinpoint、Poincut、Introduction、Weaving、Around等等,并且需要引入IOC容器并配合大量的XML或者annotation来进行组件装配。
传统AOP不但学习成本极高,开发效率极低,开发体验极差,而且还影响系统性能,尤其是在开发阶段造成项目启动缓慢,极大影响开发效率。
JFinal采用极速化的AOP设计,专注AOP最核心的目标,将概念减少到极致,仅有三个概念:Interceptor、Before、Clear,并且无需引入IOC也无需使用啰嗦的XML。
Interceptor
Interceptor 可以对方法进行拦截,并提供机会在方法的前后添加切面代码,实现 AOP 的核心目标。Interceptor 接口仅仅定义了一个方法 public void intercept(Invocation inv)。
public class DemoInterceptor implements Interceptor {
public void intercept(Invocation inv) {
System.out.println("Before method invoking");
inv.invoke();
System.out.println("After method invoking");
}
}
Interceptor 可以对方法进行拦截,并提供机会在方法的前后添加切面代码,实现 AOP 的核心目标。
Interceptor 接口仅仅定义了一个方法 public void intercept(Invocation inv)。
以上代码中的 DemoInterceptor 将拦截目标方法,并且在目标方法调用前后向控制台输出文本。
inv.invoke() 这一行代码是对目标方法的调用,在这一行代码的前后插入切面代码可以很方便地实现AOP。
注意:必须调用 inv.invoke() 方法,才能将当前调用传递到后续的 Interceptor 与 Action。
Invocation 作为 Interceptor 接口 intercept 方法中的唯一参数,
提供了很多便利的方法在拦截器中使用。以下为 Invocation 中的方法:
更正一下上面截图中倒数第三行的一处手误: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级别,前者将拦截本类中所有方法,后者仅拦截本方法。
此外Before可以同时配置多个拦截器,只需用在大括号内用逗号将多个拦截器进行分隔即可。
除了Class与Method级别的拦截器以外,JFinal还支持全局拦截器以及Inject拦截器(Inject拦截将在后面介绍),
全局拦截器分为控制层全局拦截器与业务层全局拦截器,
前者拦截控制 层所有Action方法,后者拦截业务层所有方法。
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、Inject、Class、Method,
如果同级中有多个拦截器,那么同级中的执行次序是:配置在前面的先执行。
Clear
拦截器从上到下依次分为Global、Inject、Class、Method四个层次,
Clear用于清除自身所处层次以上 层的拦截器。
Clear声明在Method层时将针对Global、Inject、Class进行清除。
Clear声明在Class层时将针对Global、Inject进行清除。
Clear注解携带参数时清除目标层中指定的拦截器。
Clear用法记忆技巧:
-
共有Global、Inject、Class、Method四层拦截器
-
清除只针对Clear本身所处层的向上所有层,本层与下层不清除
-
不带参数时清除所有拦截器,带参时清除参数指定的拦截器
// login方法需要移除该权限拦截器才能正常登录
@Before(AuthInterceptor.class)
public class UserController extends Controller {
// AuthInterceptor 已被Clear清除掉,不会被其拦截
@Clear
public void login() {
}
// 此方法将被AuthInterceptor拦截
public void show() {
}
}
在某些应用场景之下,需要移除Global或Class拦截器。
例如某个后台管理系统,配置了一个全局的权限拦截器,
但是其登录action就必须清除掉她,否则无法完成登录操作,
@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,但她无法被清除,因为清除操作只针对于本层以上的各层
}
}
Interceptor的触发
JFinal中的AOP被划分为控制层AOP以及业务层AOP,
严格来说业务层AOP并非仅限于在业务层使用,因为JFinal AOP可以应用于其它任何地方。
控制层拦截器的触发,只需发起action请求即可。
业务层拦截器的触发需要先使用enhance方法对目标对象进行增强,然后调用目标方法即可。
以下是业务层AOP使用的例子:
// 定义需要使用AOP的业务层类
public class OrderService {
// 配置事务拦截器
@Before(Tx.class)
public void payment(int orderId, int userId) {
// service code here
}
}
// 定义控制器,控制器提供了enhance系列方法可对目标进行AOP增强
public class OrderController extends Controller {
public void payment() {
// 使用 enhance方法对业务层进行增强,使其具有AOP能力
OrderService service = enhance(OrderService.class);
// 调用payment方法时将会触发拦截器
service.payment(getParaToInt("orderId"), getParaToInt("userId"));
}
}
以上代码中OrderService是业务层类,
其中的payment方法之上配置了Tx事务拦截器,
OrderController是控制器,在其中使用了enhance方法对OrderSevice进行了增强,
随后调用其payment方法便可触发Tx拦截器。
简言之,业务层AOP的触发相对于控制层仅需多调用一次enhance方法即可,
而Interceptor、Before、Clear的使用方法完全一样。
Enhancer、Duang
Enhancer、Duang 用来对目标进行增强,让其拥有AOP的能力。
public class TestMain{
public void main(String[] args) {
// 使用Duang.duang方法在任何地方对目标进行增强
OrderService service = Duang.duang(OrderService.class);
// 调用payment方法时将会触发拦截器
service.payment(…);
// 使用Enhancer.enhance方法在任何地方对目标进行增强
OrderService service = Enhancer.enhance(OrderService.class);
}
}
Duang.duang()、
Enhancer.enhance()
与Controller.enhance()系方法在功能上完全一样,
她们除了支持类增强以外,还支持对象增强,
例如duang(new OrderService())以对象为参数的用法,功能本质上是一样的,
使用Duang、Enhancer类可以对任意目标在任何地方增强,
所以JFinal的AOP可以应用于非web项目,
只需要引入jfinal.jar包,然后使用Enhancer.enhance()
或Duang.duang()即可极速使用JFinal的AOP功能。
Routes级别拦截器
Routes级别拦截器是指在Routes中添加的拦截器
/**
* 后端路由
*/
public class AdminRoutes extends Routes {
public void config() {
// 此处配置 Routes 级别的拦截器,可配置多个
addInterceptor(new AdminAuthInterceptor());
add("/admin", IndexAdminController.class, "/index");
add("/admin/project", ProjectAdminController.class, "/project");
add("/admin/share", ShareAdminController.class, "/share");
}
}
Routes级别拦截器是指在Routes中添加的拦截器
AdminAuthInterceptor 将拦截IndexAdminController、ProjectAdminController、
ShareAdminController 中所有的 action 方法。
Routes 拦截器在功能上通过一行代码,
同时为多个 Controller 配置好相同的拦截器,减少了代码冗余。
Routes 级别拦截器将在 Class 级别拦截器之前被调用。
Inject拦截器
Inject拦截器是指在使用enhance或duang方法增强时使用参数传入的拦截器。
Inject可以对目标完全无侵入地应用AOP。
假如需要增强的目标在jar包之中,无法使用Before注解对其配置拦截器,
此时使用Inject拦截器可以对jar包中的目标进行增强。
如下是Inject拦截器示例:
public void injectDemo() {
// 为enhance方法传入的拦截器称为Inject拦截器,下面代码中的Tx称为Inject拦截器
OrderService service = Enhancer.enhance(OrderService.class, Tx.class);
service.payment(…);
}
如上代码中Enhance.enhance()方法的第二个参数Tx.class被称之为Inject拦截器,
使用此方法便可完全无侵入地对目标进行AOP增强。
与Class级拦截器一样,Inject拦截器将拦截被增强目标中的所有方法。
Inject拦截器可以被认为就是Class级拦截器,
只不过执行次序在Class级拦截器之前而已。
四.ActiveRecord 概述
ActiveRecord 是 JFinal 最核心的组成部分之一,
通过 ActiveRecord 来操作数据库,将极大地减少代码量,极大地提升开发效率。
ActiveRecord 模式的核心是:
一个 Model 对象唯一对应数据库表中的一条记录,而对应关系依靠的是数据库表的主键值。
因此,ActiveRecord 模式要求数据库表必须要有主键。当数据库表没有主键时,只能使用 Db + Record 模式来操作数据库。
ActiveRecordPlugin
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);
}
}
以上代码配置了两个插件:
DruidPlugin
与ActiveRecordPlugin,
前者是druid数据源插件,
后者是ActiveRecrod支持插件。
ActiveReceord中定义了addMapping(String tableName, Class<? extends Model> modelClass>)
方法,该方法建立了数据库表名到Model的映射关系。
另外,以上代码中arp.addMapping("user", User.class),
表的主键名为默认为 "id",
如果主键名称为 "user_id" 则需要手动指定,
如:arp.addMapping("user", "user_id", User.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
生成器的使用
ActiveRecord 模块的 com.jfinal.plugin.activerecord.generator 包下,
提供了一个 Generator 工具类,
可自动生成 Model、BaseModel、MappingKit、DataDictionary 四类文件。
生成后的 Model 与 java bean 合体,立即拥有了 getter、setter 方法,
使之遵守传统的 java bean 规范,立即拥有了传统 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();
baseModelPackageName、baseModelOutputDir、modelPackageName、modelOutputDir。
四个参数分别表示baseMode的包名,baseModel的输出路径,modle的包名,model的输出路径。
相关生成文件
BaseModel是用于被最终的Model继承的基类,所有的getter、setter方法都将生成在此文件内
,这样就保障了最终Model的清爽与干净,BaseModel不需要人工维护,在数据库有任何变化时重新生成一次即可。
MappingKit用于生成table到Model的映射关系,
并且会生成主键/复合主键的配置,
也即无需在configPlugin(Plugins me)方法中书写任何样板式的映射代码。
DataDictionary是指生成的数据字典,会生成数据表所有字段的名称、类型、长度、备注、是否主键等信息。
Model与Bean合体后主要优势
-
充分利用海量的针对于Bean设计的第三方工具,例如jackson、freemarker
-
快速响应数据库表变动,极速重构,提升开发效率,提升代码质量
-
拥有IDE代码提示不用记忆数据表字段名,消除记忆负担,避免手写字段名出现手误
-
BaseModel设计令Model中依然保持清爽,在表结构变化时极速重构关联代码
-
自动化table至Model映射
-
自动化主键、复合主键名称识别与映射
-
MappingKit承载映射代码,JFinalConfig保持干净清爽
-
有利于分布式场景和无数据源时使用Model
-
新设计避免了以往自动扫描映射设计的若干缺点:引入新概念(如注解)增加学习成本、性能低、jar包扫描可靠性与安全性低
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)方法
独创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,则自动回滚事务。
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,则自动回滚事务。
匿名内部类由于没有名字,所以它的创建方式有点儿奇怪。创建格式如下:
new 父类构造器(参数列表)|实现接口()
{
//匿名内部类的类体部分
}
其作用就是临时实例化一个对象
Cache 缓存
ActiveRecord 可以使用缓存以大大提高性能,默认的缓存实现是 ehcache,使用时需要引入 ehcache 的 jar 包及其配置文件
public void list() {
List<Blog> blogList = Blog.dao.findByCache("cacheName", "key", "select * from blog");
setAttr("blogList", blogList).render("list.html");
}
上例findByCache方法中的cacheName需要在ehcache.xml中配置
如:<cache name="cacheName" …>。
此外Model.paginateByCache(…)、Db.findByCache(…)、Db.paginateByCache(…)方法都提供了cache支持。
在使用时,只需传入cacheName、key以及在ehccache.xml中配置相对应的cacheName就可以了。
除了要把使用默认的 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 实现。
Dialect多数据库支持
目前ActiveRecordPlugin提供了MysqlDialect、
OracleDialect、PostgresqlDialect、
SqlServerDialect、Sqlite3Dialect、
AnsiSqlDialect实现类。
MysqlDialect与OracleDialect分别实现对Mysql与Oracle的支持,
AnsiSqlDialect实现对遵守ANSI SQL数据库的支持。
以下是数据库Dialect的配置代码:
public class DemoConfig extends JFinalConfig {
public void configPlugin(Plugins me) {
ActiveRecordPlugin arp = new ActiveRecordPlugin(…);
me.add(arp);
// 配置Postgresql方言
arp.setDialect(new PostgresqlDialect());
}
}
表关联操作
JFinal ActiveRecord 天然支持表关联操作,
表关联操作主要有两种方式:一是直接使用sql得到关联数据;
二是在Model中添加获取关联数据的方法。
假定现有两张数据库表:user、blog,并且user到blog是一对多关系,
blog表中使用user_id关联到user表。如下代码演示使用第一种方式得到user_name:
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");
}
Enjoy模板
jfinal 模板引擎核心概念只有指令与表达式这两个。而表达式是与 java 直接打通的,所以没有学习成本,剩下来只有 #if、#for、#define、#set、#include、#(...) 六个指令需要了解
如果在web环境下使用,可以通过JFinalConfig中的configEngine(Engine me)抽象方法对其进行配置,如下是代码示例:
public void configEngine(Engine me) {
// devMode 配置为 true,将支持模板实时热加载
me.setDevMode(true);
me.setBaseTemplatePath(null);
me.setSourceFactory(new ClassPathSourceFactory());
// 添加共享函数,随后可在任意地方调用这些共享函数
me.addSharedFunction("/view/common/layout.html");
}
将engine设置为 devMode 模式,在该模式下模板文件的修改会及时生效,相当于热加载功能,这在开发环境下是提升开发效率必须的配置。
如果模板文件在项目的 class path 路径或者 jar 包之内,可以通过me.setSourceFactory(new ClassPathSourceFactory()) 以及 me.setBaseTemplatePath(null) 来实现
#define layout()
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xml:lang="zh-CN" xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link href="/css/manage.css" media="screen" rel="stylesheet" type="text/css" />
<script src="/js/jquery-1.4.4.min.js" type="text/javascript" ></script>
</head>
<body>
<div class="manage_container">
<div class="manage_head">
<div class="manage_logo">
<a href="http://www.jfinal.com" target="_blank">JFinal web framework</a>
</div>
<div id="nav">
<ul>
<li><a href="/"><b>首页</b></a></li>
<li><a href="/blog"><b>Blog管理</b></a></li>
</ul>
</div>
</div>
<div class="main">
#@main()
</div>
</div>
</body>
</html>
#end
以上代码添加了一个共享函数模板文件 layout.html,
这个文件中使用了#define指令定义了template function。
通过上面的配置,可以在任意地方直接调用 layout.html 里头的 template function。