Nutz.Mvc 根据 @At 注解,将一个 HTTP 请求映射到了一个函数,函数只有一个参数,会被路径参数 (1045) 填充。
- Nutz.Mvc 最基本的想法,就是通过注解 @At 将一个HTTP请求路径同一个 Java 函数关联起来。
- 并且,@At 支持你写多个路径
- 声明了 @At 的函数被称为 入口函数
- 任何一个请求,都会经过四道工序
- A - 过滤: 你通过 @Filters 注解可以为你的入口函数定义任意多的过滤器
- B - 适配: 这个过程将 HTTP 输入流转换成入口函数的参数数组
- 默认的,它认为输入流是传统的名值对方式
- 更多的适配方式请参看 关于适配器
- 文件上传也是一种适配方式,请参看 UploadAdpto
什么是适配器?
将 HTTP 参数转换成一个函数参数的过程是一个典型适配过程,执行这个过程的对象被称为适配器了。 Nutz.Mvc 提供了 org.nutz.mvc.HttpAdaptor 接口,隔离了这种行为。
在每一个入口函数上,你都可以通过注解 @AdaptBy 来声明如何适配 HTTP 参数。当然,你 没必要在每一个入口函数上都声明,在子模块类上声明,或者在整个应用的主模块上声明均可。
如何使用适配器?
1.默认:Nutz.Mvc 会采用 org.nutz.mvc.adaptor.PairAdaptor (也就是名值对的方式) 来适配你的 HTTP 参数。 2.通过构造函数获得适配器:通过 @AdaptBy 注解来改变任何一个入口函数的适配方式。
@AdaptBy 注解还支持一个属性 args,你可以通过这个属性 为你的适配器设置构造函数参数 3.通过 Ioc 容器获得适配器:如果你希望你的适配器是交由 Ioc 容器管理的@AdaptBy(type=JsonAdaptor.class)
如果你的参数数组长度为一,并且,由 "ioc:" 开始,那么这个适配器会交付 Ioc 容器管理,你可以在容器的 配置文件中详细规定这个适配器的各个属性。当然,你需要在整个应用启用 Ioc 容器,@AdaptBy(type=JsonAdaptor.class, args={"ioc:objName"}) // 一定不可以是单例
- 一个 Mvc 框架可以通过 Ioc 接口同一个 Ioc 容器挂接,挂接的方法很简单: 在主模块上声明 @IocBy
@IocBy(args={"*js", "ioc/", "*anno", "net.wendal.nutzbook", "*async", "*tx" }) public class MainModule { ...
复合加载器的使用方法
推荐使用ComboIocProvider,组合使用各种IocLoader.
注意: IocBy的默认type就是ComboIocProvider,可以不写.ComboIocLoader loader = new ComboIocLoader("*js", "ioc/dao.js", "ioc/service.js", "*anno", "com.myapp.module", "com.myapp.service", "*tx", // @Async注解,异步执行. "*async");
由 Ioc 容器管理子模块
通过 @IocBy 为整个应用声明了 Ioc 容器,那么如何使用呢。 实际上,你的每一个模块都可以来自容器,只要 你在模块上声明 @Inject。 当然,在主模块声明这个注解是没有意义的。
@IocBean("petM") public class PetModule { ...
内置的适配器
Nutz.Mvc 为你内置了 4 个最常用的适配器,可以让支持用如下四种方式适配 HTTP 参数:
名值对
(默认)
- PairAdaptor一般方式
@AdaptBy(type=PairAdaptor.class)
这种方式,是传统的 HTTP 参数方式。关键的问题是如何将 HTTP 的参数表同入口函数的参数对应起来。 为此,它支持一个新的注解 @Param,你可以:
public String someFunc( @Param("pid") int petId, @Param("pnm") String petName){ ...
表单方式 - Form Bean
有些时候,你需要入口函数接受一个对象,比如一个表单对象
public String someFunc( @Param("..") Pet pet){ ...
值 ".." 有特殊含义,表示当前的这个对象,需要对应整个的 HTTP 参数表。 所以, Nutz.Mvc 会将 HTTP 参数表中的 参数一个个的按照名字设置到 Pet 对象的字段里。 但是如果 Pet 对象的字段名同 HTTP 参数不符怎么办? 你可以在 Pet 字段上声明 @Param。
前缀表单方式
进行比较复杂的 HTTP 交互时,大家都比较偏爱名值对的方式提交数据,可能是因为数据组织比较方便 -- 通过
<form>
即可。 但是如果在一个表单里混合上两个甚至多个表单项,那么 HTTP 的参数就会有点复杂,虽然这种情况下我更推荐采用 Json 输入流,但是并不是所有人都那么喜欢它,对吗?比如有一个表单,它希望提交两个对象的数据, User 以及 Department,这HTTP 请求的参数格式可能是这样的:
user.id = 23 user.name = abc user.age = 56 dep.id = 15 dep.name = QA dep.users[1].id = 23 dep.users[1].name = abc dep.users[1].age = 56 dep.users[10001].id = 22 dep.users[10001].name = abcd dep.users[10001].age = 26 dep.users:50001.id = 22 dep.users:50001.name = abcd dep.users:50001.age = 26 dep.children(abc).id = 13 dep.children(abc).name = ABC dep.children(jk).id = 25 dep.children(jk).name = JK dep.children.nutz.id = 1 dep.children.nutz.name = NUTZ
怎样在入口函数内声明这样的表单项呢?我们可以采用前缀方式:
public String someFunc( @Param("::user.") User user, @Param("::dep.") Department dept){ ... }
关键就是这个 `@Param("::user.")` 符号 '::' 表示这个参数是一个表单对象,并且它有统一的前缀 'user.' 表示前缀,Nutz.Mvc 会查看一下 User, Department 类所有的字段:
public class User { private int id; private String name; private int age; } public class Department { private List<User> users; private Map<String, User> children; }
那么, id 会对应到 HTTP 参数中的 'user.id', 其他的字段同理. 眼尖的你肯定发现了有点异样的地方, 对了, 那就是我们 nutz 对集合的支持. 在此, 你不仅可以对一般的属性进行注入, 还能对list, set, map集合以及对象数组进行注入. 在此我们提供了两种书写方式:
- 对象.list索引 = 值
对象.list索引.属性 = 值
对象.map(key) = 值 对象.map(key).属性 = 值
- 对象.list:索引 = 值
对象.list:索引.属性 = 值
对象.map.key = 值 对象.map.key.属性 = 值
两种方式是完全等价的(小声透露一下, 其实代码里面就是把第一种方式转换成第二种方式实现的哦...). 并且都可以包含多层集合. 同时需要注意的是, 在进行 list 注入的时候需要注意, 出于内存方面的考虑, 所提供的 "索引" 只做为一个组装对象的参考字段(必需,不然不能组装对象), 不做为真实list的索引使用. 因此, list 的索引可以是任意大小的数字, 以及字符, 或字符串组成.
从现在开始, nutz 参数的类型不再只支持单纯的 Object 对象注入了, 同时也提供了 List, Map, Set 以及对象数组.
更更更强大的功能, nutz开始支持泛型了, 直接来例子, 懒得解释:
class Abc<T>{ T obj; } class jk{ String name; } public void test(@Param("::abc.")Abc<jk> abc){}
如果要写test的参数, 你可以直接写 abc.obj.name = "nutz" , 我们的nutz就会非常智能的生成jk对象.
混合方式
值得一说的是,按照这个约定,实际上,一个入口函数,是可以支持多个 POJO 的,也可以写成这样
public String someFunc( @Param("pid") int petId, @Param("..") Pet pet, @Param("..") Food food){ ...
JSON 的支持
你的 HTTP 参数也可以是一个 JSON 字符串
public String someFunc( @Param("pid") int petId, @Param("pet") Pet pet, @Param("foods") Food[] food){ ...
HTTP 参数的值都是字符串,比如上例的第二个参数,Nutz.Mvc 会看看 HTTP 参数表中的 "pet" 的值,如果它用 "
{
" 和 "}
" 包裹,则会试图将其按照 JSON 的方式解析成 Pet 对象。当然,如果你传入的参数格式有问题,会解析失败,抛出异常。第三个参数,是一个数组,Nutz.Mvc 会看看 HTTP 参数表中的 "foods" 的值,如果用 "
[
" 和 "]
" 包裹,则会试图将其 转换成一个数组。 如果你 JSON 字符串的格式有问题,它也会抛出异常。参数类型如果是列表(java.util.List),同数组的处理方式相同。但是它不知道列表元素的类型,所以转换出的元素只可能是
- 布尔
- 数字
- 字符串
- 列表
- Map
JSON 输入流 - JsonAdaptor
如果你要通过 HTTP 传给服务器一个比较复杂的对象,通过名值对的方式可能有点不方便。因为它很难同时传两个对象。 并且一个对象如果还嵌入了另外一个对象,也很难传入,你必须要自己定义一些奇奇怪怪的格式,在 JS 里组织字符串, 在服务器端,手工解析这些字符串。
针对这个问题, JSON 流是一个比 XML 流更好的解决方案,它足够用,并且它更短小。
如果你的 HTTP 输入流就是一个 JSON 串,你可以这样:
@AdaptBy(type=JsonAdaptor.class) public String someFunc( Pet pet ){ ...
如果你的 JSON 流是一个数组
@AdaptBy(type=JsonAdaptor.class) public String someFunc( Pet[] pet ){ ...
如果你的 JSON 流类似:
{ fox : { name : "Fox", arg : 30 }, fox_food : { type : "Fish" , price : 1.3 } }
你希望有两个 POJO (Pet 和 Food) 分别表示这两个对象,你可以:
@AdaptBy(type=JsonAdaptor.class) public String someFunc( @Param("fox") Pet pet, @Param("fox_food") Food food){ ...
实际上,Nutz.Mvc 会将 HTTP 输入流解析成一个 Map,然后从 Map 里取出 "fox" 和 "fox_food" 这 两个子 Map,分别转换成 Pet 对象和 Food 对象。
js通常这样写
var data = {id:1,name:'陆离',age:19,sex:'女',relation:'媳妇'}; $.ajax({ url:'/HelloNutz/jsonAdapter', "data": JSON.stringify(data), // 注意要转为json,除非data本身就是json字符串 dataType:'json', type : 'POST', success:function(re){ console.log(re); } });
什么都不干 - VoidAdaptor
某些特殊的情况,你需要彻底控制输入流的解析,同时你又不想使用任何适配器,你可以
@AdaptBy(type=VoidAdaptor.class) public String someFunc(HttpServletRequest req){ ...
VoidAdaptor 什么都不会干,不会碰 HTTP 请求对象的输入流。
上传文件 - UploadAdaptor
NutzMvc 内置了 org.nutz.mvc.upload.UploadAdaptor。关于文件上传详细的说明,请参看: 文件上传
特殊参数
某些时候,你可能需要得到 HttpSession,或者你需要得到 Ioc 容器的一个引用。因为你想做点更高级的 事情,你想出搞点小花样。Nutz.Mvc 完全支持你这样做。
你只要在你的入口函数里声明你希望得到的对象类型即可,比如:
@At("/myfunc") public String someFunc( @Param("pid") int petId, Ioc ioc, HttpServletRequest req){ ...
- 第一个参数会从 HTTP 参数表中取出赋给入口函数
- 第二个参数,Nutz.Mvc 会把自身使用的 Ioc 容器赋给入口函数,
- 第三个参数,当前请求对象也会直接赋给入口函数。
那么 Nutz.Mvc 到底支持多少类似这样的特殊参数类型呢?
Nutz.Mvc 支持的特殊参数类型
- ServletRequest & HttpServletRequest
- ServletResponse * HttpServletResponse
- HttpSession
- ServletContext
- Ioc & Ioc2
- Map ServletRequest.getParameterMap()的返回值
还有就是@Attr注解,可以用于获取req或session的attr
- 默认先查找Request,然后找Session
- 找不到就返回null
示例代码:
@Ok("json") public Object listAllUser(@Attr("me")User user) { if (user == null || !user.isAdmin()) return new HttpStatusView(500); return dao.query(User.class, null); }
如果你还想支持更多的类型,那么你就需要定制你自己的适配器了,稍后会有详细描述。
路径参数
某些时候,你可能觉得这样的 URL 很酷
/my/article/1056.nut
起码比
/my/article.nut?id=1056
看起来要顺眼一些。
Nutz.Mvc 支持将路径作为参数吗? 你可以在路径中增加通配符,在运行时,Nutz.Mvc 会将路径对应的内容依次变成你的 入口函数的调用参数。通配符有两种:
- '?' - 单层通配符,后面你可以继续写路径和其他的通配符
- '*' - 多层通配符,后面个不能再有任何内容
单层通配符
@At("/topic/?/comment/?") public String getComment(int topicId, int commentId){ // 如果输入的 URL 是: /topic/35/comment/171 // 那么 topicId 就是 35 // 而 commentId 就是 171 }
如果你有这种需求,我想不用我废话了,不解释,你懂的。
多层通配符
@At("/article/*") public String getArticle(String author, int articleId){ // 如果输入的 URL 是: /article/zozoh/1352 // 那么 author 就是 "zozoh" // 而 articleId 就是 1352 }
Nutz.Mvc 在一层一层解析路径的时候,碰到了 '*', 它就会将这个路径从此处截断, 后面的字串按照字符 '/' 拆分成一个字符串数组。 为入口函数填充参数的时候,会优先将这个路径参数数组按照顺序填充成参数。 之后,如果它发现入口函数还有参数没有被填充完全,它才应用适配器的内部逻辑,填充其余的参数。
单层多层通配符混用
@At("/user/?/topic/?/comment/*") public String getComment(String author, int topicId, int commentId){ // 如果输入的 URL 是: /user/zozoh/topic/35/comment/171 // 那么 author 就是 "zozoh" // 而 topicId 就是 35 // 而 commentId 就是 171 }
通配符的限制
总之,在 @At 注解中通过通配符,你可以声明你的路径参数,但是你的通配符必须是一层路径,但是它们有限制:
你不能这么写 /article/a?/topic/* 也不能这么写 /article/y*
如果你这么写了,匹配的时候很可能出一些奇奇怪怪的问题。因此你记住了,通配符如果在路径中出现:
- 左边一定有一个字符 '/'
- 右侧可能没有字符,但是如果有,也一定是 '/'
当然,通配符声明的路径参数仍然可以同 @Param 以及 特殊参数 混用,只是请记得,将入口函数 中的路径参数排在前面
错误处理
这是1.b.45及之后的版本才有的功能
在以前的版本中,由用户输入导致的类型转换错误(例如字符串转数字,非法日期),都只能通过@Fail处理
故,现在引入了AdaptorErrorContext,用于解决这一直以来被骂的缺陷
仅当入口方法的最后一个参数为AdaptorErrorContext(其子类也行),才会触发这个错误处理机制
看以下代码:
// 传入的id,会是一个非法的字符串!! @At({"/err/param", "/err/param/?"}) @Fail("http:500") public void errParam(@Param("id") long id, AdaptorErrorContext errCtx) { TestCase.assertNotNull(errCtx); // 当没有异常产生时, errCtx为null TestCase.assertNotNull(errCtx.getErrors()[0]); }
当用户输入的参数id,为"Nutz"时,自然会导致异常, 而这个方法的最后一个参数是AdaptorErrorContext, 所以,仍将进入这个方法, 且errCtx参数不为null
AdaptorErrorContext类本身很简单, 但它也是一个很不错的扩展点. 因为最后一个参数只要求是AdaptorErrorContext 或其子类,所以,你可以自定义一个AdaptorErrorContext,覆盖其核心方法 setError,以实现你需要的纠错逻辑
定制自己的适配器
先来看看适配器的接口:
public interface HttpAdaptor { void init(Method method); Object[] adapt( HttpServletRequest request, HttpServletResponse response, String[] pathArgs); }
你如果实现自己的适配器,你需要知道:
- 你的适配器,对每个入口函数,只会有一份实例 -- Nutz.Mvc 只会创建一遍
- 如果你的适配器是从 Ioc 容器中取得的,那么也只会被取出一次
- init 函数是 Nutz.Mvc 在创建你的适配器以后,马上就要调用的一个方法,你可以在这个方法里初始化一些逻辑
- adapt 方法的第三个参数,是 Nutz.Mvc 为你准备好的路径参数,它有可能为 null。 你的适配器 将决定是不是应用这个路径参数
- 推荐继承 AbstractAdaptor
- 对象.list索引 = 值
- C - 调用: 调用入口函数,你在里面需要调用相关的业务层代码。
- 如果你的业务比较复杂,为了解耦合,你可能需要 Ioc 容器的帮助,请参看 同 Ioc 容器一起工作 一节
- D - 渲染: 根据入口函数的返回,渲染 HTTP Response。
- 如果返回是个 View,则用这个 View 来渲染 null (null? 是的,你没看错,这种情况 View 接口第三个参数会是 null)
- 你可以用 org.nutz.mvc.view.ViewWrapper 将你的返回对象以及要返回的视图组合在一起返回,ViewWrapper 也是一个 View
- 否则用函数的 @Ok 注解声明的 View 来渲染入口函数的返回对象
- 如果你的函数处理过程中抛出了异常,用 @Fail 注解声明的 View 来渲染异常对象
- 返回值会保存在request的attr中,名字是obj
主模块
任何一个类都可以作为主模块,只要你将其配置在 web.xml 中,这样的设计主要是为了方便 一些非 JSP/Servlet 标注的 web 服务器整合 Nutz.mvc 框架。
在主模块中,你可以声明如下的注解:
- @Modules - 声明应用的所有子模块
- @IocBy - 设置应用所采用的 Ioc 容器
- @SetupBy - 应用启动以及关闭时的额外处理
- @Views - 自定义的扩展视图
- @Localization - 应用的本地化字符串设定
- - 自定义Session实现
- 所有入口函数上支持的注解
在 web.xml 中,一个比较典型的例子(推荐使用):
<filter> <filter-name>nutz</filter-name> <filter-class>org.nutz.mvc.NutFilter</filter-class> <init-param> <param-name>modules</param-name> <param-value>net.wendal.nutzbook.MainModule</param-value> </init-param> </filter> <filter-mapping> <filter-name>nutz</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping>
- 如果你没有声明 'modules' 参数,它会抛异常!!
- 默认的,只要被正则表达式 `"^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|swf|ico|map)$"` 匹配上的 URL 都不会被 Nutz 过滤器处理
- 你可以为 NutFilter 设置参数 "ignore",来重新设置这个正则表达式
- 如果你的值是 "null" (不区分大小写),所有的请求都会转发到 Nutz.mvc 进行处理
- 如果 Nutz.mvc 没有找到合适入口函数处理,将会继续 chain.doFilter
- 如果返回是个 View,则用这个 View 来渲染 null (null? 是的,你没看错,这种情况 View 接口第三个参数会是 null)