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 的几个设计决定
- URL 是否使用后缀名, 如 ".html"? 不使用.
URL 应该体现业务, 而 ".html" 等后缀是技术细节类的东西, 应该去掉. 对动态页面来说, 对应 handler 的处理逻辑是确定的, 并不关心后缀. 而且这样在地址栏修改末尾 URL 也方便许多.
但对一些静态内容, 如 js, css, 图片, 后缀名可以帮助服务器确定 mime 类型, 通常继续保留后缀. 还有一些非展示页面内容, 如 ".json", 后缀可帮助用户识别返回内容, 保存文件时保留正确的扩展名, 可以选择保留后缀.
- 是否使用 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".
- 有子页面的页面, 根页存在父文件夹下,
但通常页面的视图 view 文件存在磁盘文件系统上, 跟 url 结构同构, 这时 "/" 对应的视图文件还是要占用名字空间的, 我们叫做根页. 这有几个解决办法:
(1) 存在父目录下, 根目录没有父目录, 要特殊处理. (2) 把所有根页视图存在另外一个独立的文件夹下, 就不受任何限制了, 但这样根页 handler 要特殊处理, 管理较复杂, 不建议使用. (3) 存在子目录下, 要占用一个名字, 可以就用 "index" 好了, 子页面不能再用这个名字, 这没什么问题, 我们已经把这个名字当着一个特殊名字, 不允许使用, 也符合传统规范.
其中 (1) 符合 spring 默认的查找视图方式, 只有根目录根页一个视图需要特殊处理, (3) 虽然最终目录结构上相同业务放在一起比较好, 但对根页查找视图的逻辑要特殊处理, 不如 (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.