2015 11 15 spring 3 mvc study - hanyong/note GitHub Wiki

spring 3 mvc 学习与最佳实践

spring 是一个高度设计的框架, 它有很清晰的架构设计和职责划分, 但是当我们尝试用 spring 来做一件事, 比如搭一套 mvc 流程, 发现需要创建很多组件, 如 pathMatcher, handlerMapping(urlMapping), requestHandler, viewResolver 等, 它们之间有很多复杂的依赖协作关系. 要 "亲手" 创建每一个组件, 并恰当的组装好, 需要对整套体系和流程有很完善的理解和把握, 这相当复杂和困难.

所幸 spring 为了简化入门者的使用, 提供了许多预定义好的框架。 如我们只需要新建一个类继承 WebMvcConfigurerAdapter, 加上 @Configuration @EnableWebMvc 注解, 它就自动搭好一套已经可以运行的 mvc 框架, 自动注册了很多默认的 handlerMapping 和 viewResolver, 并预留了一些接口给我们添加自定义配置。 不幸的是, 它在哪些场景下会自动做哪些事? 怎么做的? 哪些东西我们可以改, 哪些东西我们不能改, 如何改? 有哪些默认约定? 需要我们对这些预定义的规则和框架比较了解才行。

handler mapping 的几个设计决定

  1. URL 是否使用后缀名, 如 ".html"? 不使用.

URL 应该体现业务, 而 ".html" 等后缀是技术细节类的东西, 应该去掉. 对动态页面来说, 对应 handler 的处理逻辑是确定的, 并不关心后缀. 而且这样在地址栏修改末尾 URL 也方便许多.

但对一些静态内容, 如 js, css, 图片, 后缀名可以帮助服务器确定 mime 类型, 通常继续保留后缀. 还有一些非展示页面内容, 如 ".json", 后缀可帮助用户识别返回内容, 保存文件时保留正确的扩展名, 可以选择保留后缀.

  1. 是否使用 welcome 页? 支持, "/" 直接映射到 handler 上.

传统上, 如果使用 "/" 访问页面, 默认会显示 "/index.html" 的内容, 即 "/" 等同于 "/index.html", 这里等同的意思是直接显示不跳转. "index.html" 叫做 welcome 页或默认页.

这是因为对静态网站而言, "/" 对应文件系统目录, 不能存储数据, 不能对应实体内容, 虽然可以选择显示目录列表, 但这对内容网站而言没什么意义. 于是服务器就显示了 "index.html" 的内容. 当直接访问 "index.html" 时, 这本来就是它真正的地址, 所以显示内容是一样的.

因为我们对动态页面省略了后缀, 那么 welcome 应该变成 "index". 如果允许 welcome 文件, 即 "/" 与 "/index" 等价. 那么需要考虑一个问题, "/x" 与 "/index/x" 是否等价? 即附加子页面的情况. 回到静态网站的例子, 这两者明确定义了不同的地址, 显然是不等价的. 即对静态网站的情况来说, welcome 页等价仅对叶子节点而言, 我们把 "/" 看着叶子节点时他才与 "/index" 等价. "/" 的叶子节点角色是我们新增捏造出来的, 常规情况下它是一个目录不是叶子.

回到动态页面, 要让 "/" 和 "/index" 等价似乎很简单, 我们只需要把他们匹配到同一个 handler 即可. 然而经常会有一些 handler 同时处理一个页面即其子页面. 如果 "index" 的 handler 正是这样, 那么 "/x" 与 "/index/x" 就是等价的. 这时 URL 匹配 handler 通常会使用前缀匹配. 如果 "/" 前缀匹配到一个 handler, 那么所有请求都会匹配到这个 handler, 这显然不科学. 可以想到尝试用最长匹配优先策略来匹配其他 handler, 如我们给 "/x" 分配另一个 handler, 这会导致 "/x" 与 "/index/x" 不等价. 即 "/x" 与 "/index/x" 有时等价有时不等价, 这很奇怪, 规则不清晰容易引发错误. 如为什么 "/index/a" 可以简写成 "/a", "/index/b" 简写成 "/b" 却引发一个非预期的行为, 用户和我们会很迷茫. 要避免这种误操作, 除非 "index" 子页面使用的名字, 其他同级页面就不能使用, 反之亦然. 包括 "index" 这个名字在根目录下已经使用了, 在 "index" 下就不能再使用了, 嵌套是肯定不行的. 本质上来说, "/" 与 "/index" 应该共享同一个名字空间, "/index" 也是一个目录, 我们应该把它写作 "/index/". 实践上我们在 RequestMapping 注解里使用完整匹配而不是前缀匹配, 我们把 "/index/" 下的名字会被直接带到 "/" 下, 如果两者间有冲突直接会被检查出来报错. 所以说, 这是一个使用限制, 但使用是相对安全的.

从另一个角度思考, "/index/" 下可以访问到的, "/" 下可以访问到, 反之, "/" 下可以访问到的, "/index/" 也要可以访问到, 这时 "index" 相当于文件系统中的 ".", 即当前目录, "/index/" 无论重复多少次都是自己. 即 index 下重复可以访问自己, 甚至可以访问根目录下的其他名字, 这个工作显然不适合让 "index" 的 handler 来做, 应该由外部的路径映射来做. 参考文件系统目录 "./" 的经验, "/" 应该属于 "/index/" 的规范简写形式. URL 匹配 handler 时, 应该先做一个规范化, 去掉冗余的当前目录嵌套, 再查找匹配. 当然, welcome 页与根共享名字空间的事实还是不能改变的, 如规范化后的 URL 还是不能有自身嵌套.

简单来说, 我们可以把 "/" 直接映射到 handler 上. 这里 welcome 页的概念已经发生变化了, "/" 自己就是 welcome 页, 它甚至不占用子名字空间. 我们可以直接把 "/index" 页去掉, 这样还不用费力的维护 "/" 与 "/index" 的等价关系. 这也是与静态网站不一样的地方, 静态网站 "/" 不能对应实体资源, 而动态网站 "/" 也是可以直接对应 handler 做任何定制处理的, 跟其他 URL 没什么区别. 理论上, "/" 作为一个独立的名字, 不占用其子页面的名字空间.

虽然页面层面可以去掉 welcome 项, 试图文件存储还是要存 welcome 页, 并且也在文件系统上占用了名字空间. 我们可以在 URL 映射上也保留 "/index", 用户访问 "/index" 时应直接重定向到 "/". 而 "/index/x" 这种方式则不支持, 这破坏了 URL 规范形式, 用户应该直接访问 "/x".

  1. 有子页面的页面, 根页存在父文件夹下,

但通常页面的视图 view 文件存在磁盘文件系统上, 跟 url 结构同构, 这时 "/" 对应的视图文件还是要占用名字空间的, 我们叫做根页. 这有几个解决办法:

(1) 存在父目录下, 根目录没有父目录, 要特殊处理. (2) 把所有根页视图存在另外一个独立的文件夹下, 就不受任何限制了, 但这样根页 handler 要特殊处理, 管理较复杂, 不建议使用. (3) 存在子目录下, 要占用一个名字, 可以就用 "index" 好了, 子页面不能再用这个名字, 这没什么问题, 我们已经把这个名字当着一个特殊名字, 不允许使用, 也符合传统规范.

其中 (1) 符合 spring 默认的查找视图方式, 只有根目录根页一个视图需要特殊处理, (3) 虽然最终目录结构上相同业务放在一起比较好, 但对根页查找视图的逻辑要特殊处理, 不如 (1) 方便. 因此我们可以采用方案 (1).

  1. 使用 "/a/" 作为有子页面 URL 的规范形式, "/a" 和 "/a/" 映射到相同的 handler.

对有子页面的页面 "a" 来说, "/a" 与 "/a/" 是等价的. 其实问题的本质是 URL 可以区分 "/a" 和 "/a/" , 但是文件系统不能区分, 用户也经常不加区分的使用这两者, 只好将这两者合并成一个东西来看待. 有些网站为了简单和统一, 干脆所有页面都以 "/" 结尾, 如 http://openid.net/connect/ . 但是这样对静态资源处理和传统网站风格不太一致.

我们可以约定有子页面的页面, URL 应该以 "/" 结尾作为规范形式. handler 收到不以 "/" 结尾的 URL, 可以考虑跳转, 但还要处理参数传递, 通常可直接处理. 即 "/a" 和 "/a/" 映射到相同的 handler. 由于我们的 URL 不带后缀, spring 实际上已经自动将两者同等对待.

规范化的 URL 能够帮助我们自动从 URL 找到视图文件位置等关联资源. 单用户不会有效的遵守 "/tree/", "/sub/leaf" 约定, 我们只好选择兼容. 或许可以采取通过的 redirect 办法, 通过 handler 声明自己是 tree 还是 leaf. 但对非 GET 请求还是不好 redirect.