Jul 10, 2017 10:38:44 AM
作者:zozoh
什么是适配器?
将 HTTP 参数转换成一个函数参数的过程是一个典型适配过程,执行这个过程的对象被称为适配器了。Nutz.Mvc 提供了 org.nutz.mvc.HttpAdaptor 接口,隔离了这种行为。
在每一个入口函数上,你都可以通过注解 @AdaptBy 来声明如何适配 HTTP 参数。当然,你没必要在每一个入口函数上都声明,在子模块类上声明,或者在整个应用的主模块上声明均可。
如何使用适配器?
默认的,如果你什么也不写,Nutz.Mvc 会采用 org.nutz.mvc.adaptor.PairAdaptor (也就是名值对的方式)来适配你的 HTTP 参数。
你可以通过 @AdaptBy 注解来改变任何一个入口函数的适配方式。比如
某些时候,你需要对一个适配器做一些复杂的设置, 注解还支持一个属性 args,你可以通过这个属性为你的适配器设置构造函数参数
通过 Ioc 容器获得适配器
更复杂的情况是,如果你希望你的适配器是交由 Ioc 容器管理的,你可以:
即,如果你的参数数组长度为一,并且,由 "ioc:" 开始,那么这个适配器会交付 Ioc 容器管理,你可以在容器的配置文件中详细规定这个适配器的各个属性。当然,你需要在整个应用启用 Ioc 容器,详情,请参看
内置的适配器
Nutz.Mvc 为你内置了 4 个最常用的适配器,可以让支持用如下四种方式适配 HTTP 参数:
名值对 (默认) - PairAdaptor
@AdaptBy(type=PairAdaptor.class)
这种方式,是传统的 HTTP 参数方式。关键的问题是如何将 HTTP 的参数表同入口函数的参数对应起来。为此,它支持一个新的注解 @Param,你可以:
public String someFunc( @Param("pid") int petId,
@Param("pnm") String petName){
...
有些时候,你需要入口函数接受一个对象,比如一个表单对象
public String someFunc( @Param("..") Pet pet){
...
值 ".." 有特殊含义,表示当前的这个对象,需要对应整个的 HTTP 参数表。 所以, Nutz.Mvc 会将 HTTP 参数表中的参数一个个的按照名字设置到 Pet 对象的字段里。 但是如果 Pet 对象的字段名同 HTTP 参数不符怎么办? 你可以在Pet 字段上声明 。
进行比较复杂的 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){
...
关键就是这个 ("::user.")
符号 '::' 表示这个参数是一个表单对象,并且它有统一的前缀'user.' 表示前缀,Nutz.Mvc 会查看一下 User, Department 类所有的字段:
public class User {
private int id;
private String name;
private int age;
}
public class Department {
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.属性 = 值
从现在开始, nutz 参数的类型不再只支持单纯的 Object 对象注入了, 同时也提供了 List, Map, Set 以及对象数组. 亲, 还等什么? 赶快来试试吧, 不需要9998, 也不需要998, 只要98, 亲, 还等什么, 赶快拿起你手中的电话…额…请在参数前加上@Param(::前缀).
更更更强大的功能, 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 的,也可以写成这样
你的 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);
});
某些特殊的情况,你需要彻底控制输入流的解析,同时你又不想使用任何适配器,你可以
@AdaptBy(type=VoidAdaptor.class)
public String someFunc(HttpServletRequest req){
...
VoidAdaptor 什么都不会干,不会碰 HTTP 请求对象的输入流。
上传文件 - UploadAdaptor
NutzMvc 内置了 org.nutz.mvc.upload.UploadAdaptor。关于文件上传详细的说明,请参看:
特殊参数
某些时候,你可能需要得到 HttpSession,或者你需要得到 Ioc 容器的一个引用。因为你想做点更高级的事情,你想出搞掂小花样。Nutz.Mvc 完全支持你这样做。
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()的返回值
还有就是注解,可以用于获取req或session的attr
- 默认先查找Request,然后找Session
- 找不到就返回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*
如果你这么写了,匹配的时候很可能出一些奇奇怪怪的问题。因此你记住了,通配符如果在路径中出现:
- 左边一定有一个字符 '/'
- 右侧可能没有字符,但是如果有,也一定是 '/'
当然,通配符声明的路径参数仍然可以同 以及 特殊参数 混用,只是请记得,将入口函数中的路径参数排在前面
错误处理
这是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
本页面的文字允许在知识共享 署名-相同方式共享 3.0协议和下修改和再使用。