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

  1. @AdaptBy(type=PairAdaptor.class)

这种方式,是传统的 HTTP 参数方式。关键的问题是如何将 HTTP 的参数表同入口函数的参数对应起来。为此,它支持一个新的注解 @Param,你可以:

  1. public String someFunc( @Param("pid") int petId,
  2. @Param("pnm") String petName){
  3. ...

有些时候,你需要入口函数接受一个对象,比如一个表单对象

  1. public String someFunc( @Param("..") Pet pet){
  2. ...

值 ".." 有特殊含义,表示当前的这个对象,需要对应整个的 HTTP 参数表。 所以, Nutz.Mvc 会将 HTTP 参数表中的参数一个个的按照名字设置到 Pet 对象的字段里。 但是如果 Pet 对象的字段名同 HTTP 参数不符怎么办? 你可以在Pet 字段上声明 。

进行比较复杂的 HTTP 交互是,大家都比较偏爱名值对的方式提交数据,可能是因为数据组织比较方便 — 通过<form> 即可。但是如果在一个表单里混合上两个甚至多个表单项,那么 HTTP 的参数就会有点复杂,虽然这种情况下我更推荐采用Json 输入流,但是并不是所有人都那么喜欢它,对吗?

比如有一个表单,它希望提交两个对象的数据, User 以及 Department,这HTTP 请求的参数格式可能是这样的:

  1. user.id = 23
  2. user.name = abc
  3. user.age = 56
  4. dep.id = 15
  5. dep.name = QA
  6. dep.users[1].id = 23
  7. dep.users[1].name = abc
  8. dep.users[1].age = 56
  9. dep.users[10001].id = 22
  10. dep.users[10001].name = abcd
  11. dep.users[10001].age = 26
  12. dep.users:50001.id = 22
  13. dep.users:50001.name = abcd
  14. dep.users:50001.age = 26
  15. dep.children(abc).id = 13
  16. dep.children(abc).name = ABC
  17. dep.children(jk).id = 25
  18. dep.children(jk).name = JK
  19. dep.children.nutz.id = 1
  20. dep.children.nutz.name = NUTZ

怎样在入口函数内声明这样的表单项呢?我们可以采用前缀方式:

  1. public String someFunc( @Param("::user.") User user,
  2. @Param("::dep.") Department dept){
  3. ...

关键就是这个 (&#34;::user.&#34;) 符号 '::' 表示这个参数是一个表单对象,并且它有统一的前缀'user.' 表示前缀,Nutz.Mvc 会查看一下 User, Department 类所有的字段:

  1. public class User {
  2. private int id;
  3. private String name;
  4. private int age;
  5. }
  6. public class Department {
  7. private Map<String, User> children;
  8. }

那么, 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开始支持泛型了, 直接来例子, 懒得解释:

  1. class Abc<T>{
  2. T obj;
  3. }
  4. class jk{
  5. String name;
  6. }
  7. public void test(@Param("::abc.")Abc<jk> abc){}

如果要写test的参数, 你可以直接写 abc.obj.name = "nutz" , 我们的nutz就会非常智能的生成jk对象.

值得一说的是,按照这个约定,实际上,一个入口函数,是可以支持多个 POJO 的,也可以写成这样

你的 HTTP 参数也可以是一个 JSON 字符串

  1. public String someFunc( @Param("pid") int petId,
  2. @Param("pet") Pet pet,
  3. @Param("foods") Food[] food){
  4. ...

HTTP 参数的值都是字符串,比如上例的第二个参数,Nutz.Mvc 会看看 HTTP 参数表中的 "pet" 的值,如果它用 "{" 和 "}"包裹,则会试图将其按照 JSON 的方式解析成 Pet 对象。当然,如果你传入的参数格式有问题,会解析失败,抛出异常。

第三个参数,是一个数组,Nutz.Mvc 会看看 HTTP 参数表中的 "foods" 的值,如果用 "[" 和 "]" 包裹,则会视试图将其转换成一个数组。 如果你 JSON 字符串的格式有问题,它也会抛出异常。

参数类型如果是列表(java.util.List),同数组的处理方式相同。但是它不知道列表元素的类型,所以转换出的元素只可能是

  • 布尔
  • 数字
  • 字符串
  • 列表
  • Map

JSON 输入流 - JsonAdaptor

如果你要通过 HTTP 传给服务器一个比较复杂的对象,通过名值对的方式可能有点不方便。因为它很难同时传两个对象。并且一个对象如果还嵌入了另外一个对象,也很难传入,你必须要自己定义一些奇奇怪怪的格式,在 JS 里组织字符串,在服务器端,手工解析这些字符串。

针对这个问题, JSON 流是一个比 XML 流更好的解决方案,它足够用,并且它更短小。

如果你的 HTTP 输入流就是一个 JSON 串,你可以这样:

  1. @AdaptBy(type=JsonAdaptor.class)
  2. public String someFunc( Pet pet ){
  3. ...

如果你的 JSON 流是一个数组

  1. @AdaptBy(type=JsonAdaptor.class)
  2. public String someFunc( Pet[] pet ){
  3. ...

如果你的 JSON 流类似:

  1. {
  2. fox : {
  3. name : "Fox",
  4. arg : 30
  5. },
  6. fox_food : {
  7. type : "Fish" ,
  8. price : 1.3
  9. }
  10. }

你希望有两个 POJO (Pet 和 Food) 分别表示这两个对象,你可以:

  1. @AdaptBy(type=JsonAdaptor.class)
  2. public String someFunc( @Param("fox") Pet pet,
  3. @Param("fox_food") Food food){
  4. ...

实际上,Nutz.Mvc 会将 HTTP 输入流解析成一个 Map,然后从 Map 里取出 "fox" 和 "fox_food" 这两个子 Map,分别转换成 Pet 对象和 Food 对象。

js通常这样写

  1. var data = {id:1,name:'陆离',age:19,sex:'女',relation:'媳妇'};
  2. $.ajax({
  3. url:'/HelloNutz/jsonAdapter',
  4. "data": JSON.stringify(data), // 注意要转为json,除非data本身就是json字符串
  5. dataType:'json',
  6. type : 'POST',
  7. success:function(re){
  8. console.log(re);
  9. });

某些特殊的情况,你需要彻底控制输入流的解析,同时你又不想使用任何适配器,你可以

  1. @AdaptBy(type=VoidAdaptor.class)
  2. public String someFunc(HttpServletRequest req){
  3. ...

VoidAdaptor 什么都不会干,不会碰 HTTP 请求对象的输入流。

上传文件 - UploadAdaptor

NutzMvc 内置了 org.nutz.mvc.upload.UploadAdaptor。关于文件上传详细的说明,请参看:

特殊参数

某些时候,你可能需要得到 HttpSession,或者你需要得到 Ioc 容器的一个引用。因为你想做点更高级的事情,你想出搞掂小花样。Nutz.Mvc 完全支持你这样做。

  1. public String someFunc( @Param("pid") int petId,
  2. Ioc ioc,
  3. HttpServletRequest req){
  4. ...
  • 第一个参数会从 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 很酷

  1. /my/article/1056.nut

起码比

  1. /my/article.nut?id=1056

看起来要顺眼一些。

Nutz.Mvc 支持将路径作为参数吗? 你可以在路径中增加通配符,在运行时,Nutz.Mvc 会将路径对应的内容依次变成你的入口函数的调用参数。通配符有两种:

  • '?' - 单层通配符,后面你可以继续写路径和其他的通配符
  • '*' - 多层通配符,后面个不能再有任何内容
  1. @At("/topic/?/comment/?")
  2. public String getComment(int topicId, int commentId){
  3. // 如果输入的 URL 是: /topic/35/comment/171
  4. // 那么 topicId 就是 35
  5. // 而 commentId 就是 171
  6. }

如果你有这种需求,我想不用我废话了,不解释,你懂的。

多层通配符

  1. @At("/article/*")
  2. public String getArticle(String author, int articleId){
  3. // 如果输入的 URL 是: /article/zozoh/1352
  4. // 那么 author 就是 "zozoh"
  5. // 而 articleId 就是 1352
  6. }

Nutz.Mvc 在一层一层解析路径的时候,碰到了 '*',它就会将这个路径从此处截断,后面的字串按照字符 '/' 拆分成一个字符串数组。为入口函数填充参数的时候,会优先将这个路径参数数组按照顺序填充成参数。之后,如果它发现入口函数还有参数没有被填充完全,它才应用适配器的内部逻辑,填充其余的参数。

单层多层通配符混用

  1. @At("/user/?/topic/?/comment/*")
  2. public String getComment(String author, int topicId, int commentId){
  3. // 如果输入的 URL 是: /user/zozoh/topic/35/comment/171
  4. // 那么 author 就是 "zozoh"
  5. // 而 topicId 就是 35
  6. // 而 commentId 就是 171
  7. }

通配符的限制

总之,在 @At 注解中通过通配符,你可以声明你的路径参数,但是你的通配符必须是一层路径,但是它们有限制:

  1. 你不能这么写
  2. /article/a?/topic/*
  3. 也不能这么写
  4. /article/y*

如果你这么写了,匹配的时候很可能出一些奇奇怪怪的问题。因此你记住了,通配符如果在路径中出现:

  • 左边一定有一个字符 '/'
  • 右侧可能没有字符,但是如果有,也一定是 '/'
    当然,通配符声明的路径参数仍然可以同 以及 特殊参数 混用,只是请记得,将入口函数中的路径参数排在前面

错误处理

这是1.b.45及之后的版本才有的功能

在以前的版本中,由用户输入导致的类型转换错误(例如字符串转数字,非法日期),都只能通过@Fail处理

故,现在引入了AdaptorErrorContext,用于解决这一直以来被骂的缺陷

仅当入口方法的最后一个参数为AdaptorErrorContext(其子类也行),才会触发这个错误处理机制

看以下代码:

  1. // 传入的id,会是一个非法的字符串!!
  2. @At({"/err/param", "/err/param/?"})
  3. @Fail("http:500")
  4. public void errParam(@Param("id") long id, AdaptorErrorContext errCtx) {
  5. TestCase.assertNotNull(errCtx); // 当没有异常产生时, errCtx为null
  6. TestCase.assertNotNull(errCtx.getErrors()[0]);
  7. }

当用户输入的参数id,为"Nutz"时,自然会导致异常, 而这个方法的最后一个参数是AdaptorErrorContext,所以,仍将进入这个方法, 且errCtx参数不为null

AdaptorErrorContext类本身很简单, 但它也是一个很不错的扩展点. 因为最后一个参数只要求是AdaptorErrorContext或其子类,所以,你可以自定义一个AdaptorErrorContext,覆盖其核心方法 setError,以实现你需要的纠错逻辑

定制自己的适配器

先来看看适配器的接口:

  1. public interface HttpAdaptor {
  2. void init(Method method);
  3. Object[] adapt( HttpServletRequest request, HttpServletResponse response, String[] pathArgs);
  4. }

你如果实现自己的适配器,你需要知道:

  • 你的适配器,对每个入口函数,只会有一份实例 — Nutz.Mvc 只会创建一遍
    • 如果你的适配器是从 Ioc 容器中取得的,那么也只会被取出一次
  • init 函数是 Nutz.Mvc 在创建你的适配器以后,马上就要调用的一个方法,你可以在这个方法里初始化一些逻辑
  • adapt 方法的第三个参数,是 Nutz.Mvc 为你准备好的路径参数,它有可能为 null。 你的适配器 将决定是不是应用这个路径参数
  • 推荐继承 AbstractAdaptor

本页面的文字允许在知识共享 署名-相同方式共享 3.0协议和下修改和再使用。