适配器
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 注解来改变任何一个入口函数的适配方式。比如
@AdaptBy(type=JsonAdaptor.class)
某些时候,你需要对一个适配器做一些复杂的设置,@AdaptBy 注解还支持一个属性 args,你可以通过这个属性为你的适配器设置构造函数参数
通过 Ioc 容器获得适配器
更复杂的情况是,如果你希望你的适配器是交由 Ioc 容器管理的,你可以:
@AdaptBy(type=JsonAdaptor.class, args={"ioc:objName"}) // 一定不可以是单例
即,如果你的参数数组长度为一,并且,由 "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){
...
表单方式 - 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集合以及对象数组进行注入. 在此我们提供了两种书写方式:
对象.map(key) = 值对象.map(key).属性 = 值
- 对象.list:索引 = 值
对象.list:索引.属性 = 值
对象.map.key = 值对象.map.key.属性 = 值
两种方式是完全等价的(小声透露一下, 其实代码里面就是把第一种方式转换成第二种方式实现的哦…). 并且都可以包含多层集合. 同时需要注意的是, 在进行 list 注入的时候需要注意, 出于内存方面的考虑, 所提供的 "索引" 只做为一个组装对象的参考字段(必需,不然不能组装对象), 不做为真实list的索引使用. 因此, list 的索引可以是任意大小的数字, 以及字符, 出字符串组成.
从现在开始, 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 的,也可以写成这样
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*
如果你这么写了,匹配的时候很可能出一些奇奇怪怪的问题。因此你记住了,通配符如果在路径中出现:
错误处理
这是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协议和GNU自由文档许可证下修改和再使用。