Controller是JFinal核心类之一,该类作为MVC模式中的控制器。基于JFinal的Web应用的控制器需要继承该类。Controller是定义Action方法的地点,是组织Action的一种方式,一个Controller可以包含多个Action。Controller是线程安全的。
在 Controller 之中定义的 public 方法称为Action。Action 是请求的最小单位。Action 方法必须在 Controller 中定义,且必须是 public 可见性
public class HelloController extends Controller {
public void index() {
renderText("此方法是一个action");
}
public String test() {
return "index.html";
}
}
以上代码中定义了两个Action:HelloController.index()、HelloController.test()。
Action可以有返回值,返回值可在拦截器中通过invocation.getReturnValue() 获取到,以便进行render控制。
如果希望 controller 中的 public 方法不成为一个 action,可以使用 @NotAction 注解。@NotAction 注解通常用于引入了 BaseController 的中间 Controller,例如:
public class BaseController extends Controller {
// 不希望成为 action,仅供子类调用,或拦截器中调用
@NotAction
public void getLoginUser() {
}
}
自 jfinal 3.6 开始,控制器超类中的所有方法默认不会被映射为 action。(也就是自 jfinal 3.6 版本开始上例中 BaseController 中的 @NotAction 默认已经不需要了,因为 BaseController 是你最终控制器 XxxController 的超类)
如果希望超类中的方法也被映射为 action 只需添加一行配置:
public void configRoute(Routes me) {
me.setMappingSuperClass(true);
}
该功能属于性能优化,拥有大量路由的大型项目可加快启动速度。该配置如果配置在 “子Routes” 中,将只对该 “子Routes” 有效,例如:
public FrontRoutes extends Routes {
public void config() {
// 这里配置只对 FrontRoutes 下的路由有效,建议这样配置以提升性能
setMappingSuperClass(true);
add("/weixin", WeixinController.class);
}
}
“子Routes” 相关内容详见文档JFinal学习之JFinalConfig。
Action 参数注入是指为 action 方法传入参数,可以省去 getPara(…) 代码直接获得参数值,以下是代码示例:
public class ProjectController extends Controller {
public void index(Project project) {
project.save();
render("index.html");
}
}
ction 参数注入可以代替 getPara、getBean、getModel 系列方法获取参数,使用 File、UploadFile 参数时可以代替 getFile 方法实现文件上传。这种传参方式还有一个好处是便于与 swagger 这类第三方无缝集成,生成API文档。
重要用法:如果 action 形参是一个 model 或者 bean,原先通过 getBean(User.class, "") 获取时第二个参数为空字符串或null,那么与之等价的形参注入只需要用一下 @Para("") 注解即可:
public void action(@Para("")User user) { …. }
检查开发环境 jdk 一定为 8 !!!
以上截图中的红色箭头指向部分是配置关键,以下 XML 配置内容与上面截图完全一样,提供出来便于复制使用:
<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>
最后要注意:从 jfinal 3.5 版开始,已直接支持 action 参数注入功能,如果使用的 jfinal 3.2、3.3、3.4 这三个老版本希望支持该功能需要使用 jfinal 的 jfinal-java8 这个分支发行版,其 maven坐标如下:
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jfinal-java8</artifactId>
<version>3.4</version>
</dependency>
Controller提供了getPara系列方法用来从请求中获取参数。getPara系列方法分为两种类型。
该系列方法是对HttpServletRequest.getParameter(String name)的封装,这类方法都是转调了HttpServletRequest.getParameter(String name)。
getPara使用例子:
jfinal 3.6 重要更新:jfinal 3.6 针对 getPara 系以及 getParaToXxx 系统方法添加了更简短的替代方法,以下是部分使用示例:
// 替代 getPara 的 get 用法
String title = get("title");
// 替代 getParaToInt 的 getInt 用法
Integer age = getInt("age");
// 替代 setAttr 的 set 用法
set("article", article);
jfinal 3.5 重要更新:jfinal 3.5 版本新增了 getRawData() 方法,可以很方便地从 http 请求 body 中获取 String 型的数据,通常这类数据是 json 或 XML 数据,例如:
String json = getRawData();
User user = FastJson.getJson().parse(json, User.class);
以上代码通过 getRawData() 获取到了客户端传过来的 String 型的 json 数据库。 getRawData() 方法可以在一次请求交互中多次反复调用,不会抛出异常。
这里要注意一个问题:通过 forwardAction(…) 转发到另一个 action 时,getRawData() 无法获取到数据,此时需要使用 setAttr(“rawData”, getRawData()) 将数据传递给 forward 到的目标 action,然后在目标 action 通过 getAttr(“rawData”) 获取。一般这种情况很少见。
该系列方法是去获取urlPara中所带的参数值。getParaMap与getParaNames分别对应HttpServletRequest的getParameterMap与getParameterNames。
getModel 用来接收页面表单域传递过来的model对象,表单域名称以”modelName.attrName”方式命名,getModel使用的attrName必须与数据表字段名完全一样。
getBean 方法用于支持传统Java Bean,包括支持使用jfinal生成器生成了getter、setter方法的Model,页面表单传参时使用与setter方法相一致的attrName,而非数据表字段名。
getModel与getBean区别在于前者使用数据库表字段名而后者使用与setter方法一致的属性名进行数据注入。建议优先使用getBean方法。
以下是一个简单的示例:
// 定义Model,在此为Blog
public class Blog extends Model<Blog> {
}
// 在页面表单中采用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>
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(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;
}
jfinal 3.6 新增:为了进一步减少代码量、提升开发效率,jfinal 3.6 新增了 set 方法替代 setAttr,用法如下:
set("article", article);
// 链式用法
set("project", project).set("replyList", replyList).render("index.html");
jfinal 对于减少代码量、提升开发效率、降低学习成本的追求永不止步。
render(String view) 方法将对 view 所指向的模板进行渲染,view 参数最终指向的模板文件规则如下:
String template = baseViewPath + viewPath + view
其中 view 即为 render(String view) 方法所携带的参数值,而 baseViewPath、viewPath 则是在路由配置时指定的两个值,例如:
public void configRoute(Routes me) {
// baseViewPath 为 "/_view",该 Routes 对象之下映射的所有 Controller 都将取这个值
me.setBaseViewPath("/_view");
// basePath 为第三个参数 "/index"
me.add("/", IndexController.class, "/index");
// 第三个参数省略时, basePath 取第一个参数的值 : "/project"
me.add("/project", ProjectController.class);
}
注意看上面的代码,其中的 me.setBaseViewPath(…) 指定的值即为 baseViewPath,其中 me.add(…) 第三个参数即为 viewPath,当第三个参数省略时默认取第一个参数的值。
针对上述配置,在 IndexController 中使用 render 时的模板文件如下:
public class IndexController extends Controller {
public void demo() {
// 模板指向 : "/_view/index/abc.html"
render("abc.html");
}
}
上述的 render(“abc.html”) 将指向 webapp 目录下面的 “/_view/index/abc.html” 这个模板。
使用技巧:baseViewPath 配置为项目存放模板的总的根目录。viewPath 配置为当前被映射的 controller 的子目录。最终的效果就是 render(view) 的参数 view 永远是一个最终的文件名,例如: render(“index.html”),从而消除掉了 view 参数中的目录部分。
当需要打破 baseViewPath 与 viewPath 这两个参数的限制时,view 参数以 “/” 打头即可:
render("/other_path/my_path/index.html");
view 参数以 “/” 打头时,将勿略掉 baseViewPath 与 viewPath 这两个值:
String template = "/other_path/my_path/index.html";
render(String view) 将根据 configConstant(Constants me) 中配置的 me.setViewType(ViewType) 方法选择一种模板引进渲染模板文件,例如:
public void configConstant(Constants me) {
me.setViewType(ViewType.JFINAL_TEMPLATE);
}
以上配置将选择 jfinal 内置的 enjoy 模板引擎渲染模板,该配置是默认值,在使用时无需配置。注意该配置仅仅针对 Controller.render(String view) 方法,其它 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(RenderFactory)来设置Controller中所有render系列方法所使用的Render实现类。
以上是 render 系方法使用例子:
// 渲染名为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方法只有最后一次有效。
jfinal 提供了 RenderFactory 来定制 Controller.render 所有 render 方法的实现类,以下是定制 Controller.render(String view) 实现类的代码:
// 定制一个 MyRender
public class MyRender extends Render {
...
}
// 扩展 RenderFactory,用于将 Controller.render(String view)
// 切换到自己定制的 MyRender 上去
public class MyRenderFactory extends RenderFactory {
public Render getRender(String view) {
return new MyRender(view);
}
}
// 配置生效
public void configConstant(Constants me) {
me.setRenderFactory(new MyRenderFactory());
}
以上代码中 MyRenderFactory.getRender(…) 方法重写了父类 RenderFactory.getRender(…) 方法,将切换掉 Controller.render(String view) 的实现类。同理,可以通过覆盖掉 getJsonRender() 来切换掉 Controller.renderJson()。 Controller 中所有 render 方法的实现类都可以通过这种方式来切换到自己的实现类上去,极度方便灵活。
renderFile 系列方法用于下载文件。
renderFile 方法使用一个 baseDownloadPath 参数为基础路径去寻找文件。以标准的 maven 项目为例,该参数默认值指向目录:src/main/webapp/download
以下是在默认配置下的使用示例:
// 最终下载文件为:src/main/webapp/download/file.zip
renderFile("file.zip");
// 最终下载文件为:src/main/webapp/download/abc/def/file.zip
renderFile("abc/deb/file.zip");
如上所示,最终下载文件总是:baseDownloadPath + renderFile 传入的参数
baseDownloadPath 的存在相当于固定了一个基础路路径。renderFile 总是以该路径为基础路径去寻找文件。
baseDownloadPath 还可以在 configConstant(Constants me) 中自由配置,例如:
me.setBaseDownloadPath("files");
以标准的 maven 项目为例,以上配置的 baseDonwnloadPath 值将指向目录 src/main/webapp/files。
此外,还可以将 baseDownloadPath 配置为绝对路径,那么该路径将跳出项目之外,例如:
// linux、mac 系统以字符 "/" 打头是绝对路径
me.setBaseDownloadPath("/var/download");
// windows 系统以盘符打头也是绝对路径
me.setBaseDownloadPath("D:/download");
以上配置 Linux 下以 “/” 打头则表示是绝对路径,那么 renderFile 将去该路径 “/var/download” 之下去寻找下载文件。
这种配置可以跳出项目之外,便于项目资源与下载资源进行分离,也便于集群部署(单机多实例部署)时多个节点可以共享同一个目录,共享同一份下载文件。
renderFile(File file) 方法直接使用 File 参数去获取下载文件,可脱离 baseDownloadPath 的束缚,指向任意地点的文件,例如:
String file = "D:/my-project/share/files/jfinal-all.zip";
renderFile(new File(file));
如上所示,File 指向了一个任意地点的文件,跳出了 baseDownloadPath 的束缚。
如果不想使用下载文件原有的文件名,还可以指定新的下载文件名:
renderFile("老文件名.txt", "新文件名.txt");
renderQrCode 生成二维码极其简单方便,常见用法如下:
// 二维码携带的数据
String data = "weixin://wxpay/bizpayurl?appid=xx&mch_id=xx......";
// 渲染二维码图片,长度与宽度为 200 像素
renderQrCode(data, 200, 200);
上例代码中的 data 为该二维码所携带的数据,该数据将被二维码扫描程序读取到。
此外,renderQrCode 还可以指定二维码的 “纠错级别”,例如:
// 最后一个参数 'M' 为纠错级别
renderQrCode(data, 200, 200, 'M');
纠错参数可以在二维码图片被遮挡或者被损坏一部分时仍然可以正确读取其中的内容。
纠错级别从高到低可以指定为:‘H’、‘Q’、‘M’、‘L’,其纠错率分别为:30%、25%、15%、7%。 不指定该参数值默认为 ‘L’。
使用 renderQrCode 方法需要引入第三方依赖,其坐标如下:
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.2.1</version>
</dependency>
通过 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 范畴数据存放在数据库或者类似于 reids 的共享空间之中。
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磁盘盘符打头的绝对路径,即可将基础路径指向项目根径之外,方便单机多实例部署。当该路径参数设置为相对路径时,则是以项目根为基础的相对路径。
当页面提交表单请求到 action,如果提交过来的数据存在错误或者缺失,这时应该让用户继续修改或填写表单数据,这时可以使用 keepPara 方法将用户之前填写过的内容保持住:
// 保持住所有表单域
keepPara()
//指定保持住的表单域,如: nickName、email 等等
keepPara("nickName", "email", ...);
如上,不带参的 keepPara() 方法将保持住所有表单域的内容。以上两种用法保持住的参数返回页面时,无论是什么类型都将转换成 String 类型,所以,如果表单域的类型必须要保持住的话可以使用如下的方式:
// 指定 keep 后的类型为 Date
keepPara(Date.class, "createAt");
// 指定 keep 后的类型为 Integer
keepPara(Integer.class, "age");
由于上面的 createAt、age 两个表单域 keep 时指定了类型,所以在页中就可以利用其类型参与表达式求值,例如:
// 由于前面代码 keep 时指定 createAT 为 Date,所以 #date(...) 指令输出时不会抛异常
#date(createAt)
// 由于前面代码 keep 时指定 age 类型为 Integer,所以才可以进行 age > 18 操作
#if (age > 18)
...
#end
当然,如果类型为 Integer、Long、Float、Double、Byte、Short、String,还可以使用 enjoy 的 extension method 来解决类型问题,例如:
#if (age.toInt() > 18)
...
#end
如上所示,age 被 keepPara() 后为 String 型,那么 age.toInt() 会将其转化成 Integer 型。extension method 更多文档请见:https://www.jfinal.com/doc/6-9
keepPara 一般用在 Validator 或者拦截器之中,在本站首页右侧可以下载 jfinal demo for maven,里面有实际的例子。
keepModel 可以将以 modelName 前缀的表单域保持住内容与类型,例如:
<input name="blog.title" value="#(blog.title ??)"/>
<input name="blog.content" value="#(blog.content ??)" />
如上所示,表单域是以前缀为 blog 的 model,提交到后端是通过 getModel 来接收数据,如果提交的数据不完整或者有错误可以使用 keepModel 保持住内容返回给页面,让用户继续填写。
keepBean 与 keepModel 的功能相似,只不过 keepBean 针对的是传统 java bean,而不是 Model。当然,如果 Model 使用生成器生成了 setter 方法,使用 keepBean 也可以。
注意:keepModel 与 keepBean 都可以 keep 住表单域原有的类型,无需指定类型。