私塾在线学习网原创内容

Size: px
Start display at page:

Download "私塾在线学习网原创内容"

Transcription

1 私塾在线学习网 跟开涛学 SpringMVC 开源电子书, 请勿用于商业目的 版权归作者所有 作者博客 :

2 第一章 Web MVC 简介 1.1 Web 开发中的请求 - 响应模型 : 在 Web 世界里, 具体步骤如下 : 1 Web 浏览器 ( 如 IE) 发起请求, 如访问 2 Web 服务器 ( 如 Tomcat) 接收请求, 处理请求 ( 比如用户新增, 则将把用户保存一下 ), 最后产生响应 ( 一般为 html) 3 web 服务器处理完成后, 返回内容给 web 客户端 ( 一般就是我们的浏览器 ), 客户端对接收的内容进行处理 ( 如 web 浏览器将会对接收到的 html 内容进行渲染以展示给客户 ) 因此, 在 Web 世界里 : 都是 Web 客户端发起请求,Web 服务器接收 处理并产生响应 一般 Web 服务器是不能主动通知 Web 客户端更新内容 虽然现在有些技术如服务器推 ( 如 Comet) 还有现在的 HTML5 websocket 可以实现 Web 服务器主动通知 Web 客户端 到此我们了解了在 web 开发时的请求 / 响应模型, 接下来我们看一下标准的 MVC 模型是什么 1.2 标准 MVC 模型概述 MVC 模型 : 是一种架构型的模式, 本身不引入新功能, 只是帮助我们将开发的结构组织的更加合理, 使展示与模型分 离 流程控制逻辑 业务逻辑调用与展示逻辑分离 如图 1-2

3 图 1-2 首先让我们了解下 MVC(Model-View-Controller) 三元组的概念 : Model( 模型 ): 数据模型, 提供要展示的数据, 因此包含数据和行为, 可以认为是领域模型或 JavaBean 组件 ( 包含数据和行为 ), 不过现在一般都分离开来 :Value Object( 数据 ) 和服务层 ( 行为 ) 也就是模型提供了模型数据查询和模型数据的状态更新等功能, 包括数据和业务 View( 视图 ): 负责进行模型的展示, 一般就是我们见到的用户界面, 客户想看到的东西 Controller( 控制器 ): 接收用户请求, 委托给模型进行处理 ( 状态改变 ), 处理完毕后把返回的模型数据返回给视图, 由视图负责展示 也就是说控制器做了个调度员的工作, 从图 1-1 我们还看到, 在标准的 MVC 中模型能主动推数据给视图进行更新 ( 观察者设计模式, 在模型上注册视图, 当 模型更新时自动更新视图 ), 但在 Web 开发中模型是无法主动推给视图 ( 无法主动更新用户界面 ), 因为在 Web 开发是 请求 - 响应模型 那接下来我们看一下在 Web 里 MVC 是什么样子, 我们称其为 Web MVC 来区别标准的 MVC 1.3 Web MVC 概述 模型 - 视图 - 控制器概念和标准 MVC 概念一样, 请参考 1.2, 我们再看一下 Web MVC 标准架构, 如图 1-3: 如图 1-3 在 Web MVC 模式下, 模型无法主动推数据给视图, 如果用户想要视图更新, 需要再发送一次请求 ( 即请求 - 响应模型 ) 概念差不多了, 我们接下来了解下 Web 端开发的发展历程, 和使用代码来演示一下 Web MVC 是如何实现的, 还有为 什么要使用 MVC 这个模式呢? 1.4 Web 端开发发展历程 此处我们只是简单的叙述比较核心的历程, 如图 1-4

4 图 CGI:(Common Gateway Interface) 公共网关接口, 一种在 web 服务端使用的脚本技术, 使用 C 或 Perl 语言编 写, 用于接收 web 用户请求并处理, 最后动态产生响应给用户, 但每次请求将产生一个进程, 重量级 Servlet: 一种 JavaEE web 组件技术, 是一种在服务器端执行的 web 组件, 用于接收 web 用户请求并处理, 最后 动态产生响应给用户 但每次请求只产生一个线程 ( 而且有线程池 ), 轻量级 而且能利用许多 JavaEE 技术 ( 如 JDBC 等 ) 本质就是在 java 代码里面输出 html 流 但表现逻辑 控制逻辑 业务逻辑调用混杂 如图 1-5 图 1-5 如图 1-5, 这种做法是绝对不可取的, 控制逻辑 表现代码 业务逻辑对象调用混杂在一起, 最大的问题是直接在 Java 代码里面输出 Html, 这样前端开发人员无法进行页面风格等的设计与修改, 即使修改也是很麻烦, 因此实际项目这种做法不可取 JSP:(Java Server Page): 一种在服务器端执行的 web 组件, 是一种运行在标准的 HTML 页面中嵌入脚本语言 ( 现在只支持 Java) 的模板页面技术 本质就是在 html 代码中嵌入 java 代码 JSP 最终还是会被编译为 Servlet, 只不 过比纯 Servlet 开发页面更简单 方便 但表现逻辑 控制逻辑 业务逻辑调用还是混杂 如图 1-6

5 图 1-6 如图 1-6, 这种做法也是绝对不可取的, 控制逻辑 表现代码 业务逻辑对象调用混杂在一起, 但比直接在 servlet 里输 出 html 要好一点, 前端开发人员可以进行简单的页面风格等的设计与修改 ( 但如果嵌入的 java 脚本太多也是很难修改 的 ), 因此实际项目这种做法不可取 JSP 本质还是 Servlet, 最终在运行时会生成一个 Servlet( 如 tomcat, 将在 tomcat\work\catalina\web 应用名 \org\apache\jsp 下生成 ), 但这种使得写 html 简单点, 但仍是控制逻辑 表现代码 业务逻辑对象调用混杂在一起 Model1: 可以认为是 JSP 的增强版, 可以认为是 jsp+javabean 如图 1-7 特点 : 使用 <jsp:usebean> 标准动作, 自动将请求参数封装为 JavaBean 组件 ; 还必须使用 java 脚本执行控制逻辑

6 图 1-7 此处我们可以看出, 使用 <jsp:usebean> 标准动作可以简化 javabean 的获取 / 创建, 及将请求参数封装到 javabean, 再看 一下 Model1 架构, 如图 1-8 图 1-8 Model1 架构 Model1 架构中,JSP 负责控制逻辑 表现逻辑 业务对象 (javabean) 的调用, 只是比纯 JSP 简化了获取请求参数和封 装请求参数 同样是不好的, 在项目中应该严禁使用 ( 或最多再 demo 里使用 ) Model2: 在 JavaEE 世界里, 它可以认为就是 Web MVC 模型 Model2 架构其实可以认为就是我们所说的 Web MVC 模型, 只是控制器采用 Servlet 模型采用 JavaBean 视图采用 JSP,

7 如图 1-9 图 1-9 Model2 架构 具体代码事例如下 :

8 从 Model2 架构可以看出, 视图和模型分离了, 控制逻辑和展示逻辑分离了 但我们也看到严重的缺点 : 1. 1 控制器: 控制逻辑可能比较复杂, 其实我们可以按照规约, 如请求参数 submitflag=toadd, 我们其实可以直接调用 toadd 方法, 来简化控制逻辑 ; 而且每个模块基本需要一个控制器, 造成控制逻辑可能很复杂 ; 请求参数到模型的封装比较麻烦, 如果能交给框架来做这件事情, 我们可以从中得到解放 ; 选择下一个视图, 严重依赖 Servlet API, 这样很难或基本不可能更换视图 ;

9 1.1.4 给视图传输要展示的模型数据, 使用 Servlet API, 更换视图技术也要一起更换, 很麻烦 1.2 模型 : 此处模型使用 JavaBean, 可能造成 JavaBean 组件类很庞大, 一般现在项目都是采用三层架构, 而不采用 JavaBean 1.3 视图 现在被绑定在 JSP, 很难更换视图, 比如 Velocity FreeMarker; 比如我要支持 Excel PDF 视图等等 服务到工作者 :Front Controller + Application Controller + Page Controller + Context 即, 前端控制器 + 应用控制器 + 页面控制器 ( 也有称其为动作 )+ 上下文, 也是 Web MVC, 只是责任更加明确, 详情请 参考 核心 J2EE 设计模式 和 企业应用架构模式 如图 1-10: 发送请求用户返回响应 前端控制器 应用控制器 页面控制器 / 命令 视图 业务对象 图 1-10 运行流程如下 :

10 职责 : Front Controller: 前端控制器, 负责为表现层提供统一访问点, 从而避免 Model2 中出现的重复的控制逻辑 ( 由前端控制器统一回调相应的功能方法, 如前边的根据 submitflag=login 转调 login 方法 ); 并且可以为多个请求提供共用的逻辑 ( 如准备上下文等等 ), 将选择具体视图和具体的功能处理 ( 如 login 里边封装请求参数到模型, 并调用业务逻辑对象 ) 分离 Application Controller: 应用控制器, 前端控制器分离选择具体视图和具体的功能处理之后, 需要有人来管理, 应用控 制器就是用来选择具体视图技术 ( 视图的管理 ) 和具体的功能处理 ( 页面控制器 / 命令对象 / 动作管理 ), 一种策略设计 模式的应用, 可以很容易的切换视图 / 页面控制器, 相互不产生影响 Page Controller(Command): 页面控制器 / 动作 / 处理器 : 功能处理代码, 收集参数 封装参数到模型, 转调业务对象处 理模型, 返回逻辑视图名交给前端控制器 ( 和具体的视图技术解耦 ), 由前端控制器委托给应用控制器选择具体的视图 来展示, 可以是命令设计模式的实现 页面控制器也被称为处理器或动作 Context: 上下文, 还记得 Model2 中为视图准备要展示的模型数据吗, 我们直接放在 request 中 (Servlet API 相关 ), 有 了上下文之后, 我们就可以将相关数据放置在上下文, 从而与协议无关 ( 如 Servlet API) 的访问 / 设置模型数据, 一般 通过 ThreadLocal 模式实现 到此, 我们回顾了整个 web 开发架构的发展历程, 可能不同的 web 层框架在细节处理方面不同, 但的目的是一样的 : 干净的 web 表现层 : 模型和视图的分离 ; 控制器中的控制逻辑与功能处理分离 ( 收集并封装参数到模型对象 业务对象调用 ); 控制器中的视图选择与具体视图技术分离 轻薄的 web 表现层 : 做的事情越少越好, 薄薄的, 不应该包含无关代码 ; 只负责收集并组织参数到模型对象, 启动业务对象的调用 ; 控制器只返回逻辑视图名并由相应的应用控制器来选择具体使用的视图策略 ; 尽量少使用框架特定 API, 保证容易测试 到此我们了解 Web MVC 的发展历程, 接下来让我们了解下 Spring MVC 到底是什么 架构及来个 HelloWorld 了解下具 体怎么使用吧 本章具体代码请参考 springmvc-chapter1 工程

11 第二章 Spring MVC 入门 2.1 Spring Web MVC 是什么 Spring Web MVC 是一种基于 Java 的实现了 Web MVC 设计模式的请求驱动类型的轻量级 Web 框架, 即使用了 MVC 架 构模式的思想, 将 web 层进行职责解耦, 基于请求驱动指的就是使用请求 - 响应模型, 框架的目的就是帮助我们简化开 发,Spring Web MVC 也是要简化我们日常 Web 开发的 另外还有一种基于组件的 事件驱动的 Web 框架在此就不介绍了, 如 Tapestry JSF 等 Spring Web MVC 也是服务到工作者模式的实现, 但进行可优化 前端控制器是 DispatcherServlet; 应用控制器其实拆为处理器映射器 (Handler Mapping) 进行处理器管理和视图解析器 (View Resolver) 进行视图管理 ; 页面控制器 / 动作 / 处理器为 Controller 接口 ( 仅包含 ModelAndView handlerequest(request, response) 方法 ) 的实现 ( 也可以是任何的 POJO 类 ); 支持本地化 (Locale) 解析 主题 (Theme) 解析及文件上传等 ; 提供了非常灵活的数据验证 格式化和数据绑定机制 ; 提供了强大的约定大于配置 ( 惯例优先原则 ) 的契约式编程支持 2.2 Spring Web MVC 能帮我们做什么 让我们能非常简单的设计出干净的 Web 层和薄薄的 Web 层 ; 进行更简洁的 Web 层的开发 ; 天生与 Spring 框架集成 ( 如 IoC 容器 AOP 等 ); 提供强大的约定大于配置的契约式编程支持 ; 能简单的进行 Web 层的单元测试 ; 支持灵活的 URL 到页面控制器的映射 ; 非常容易与其他视图技术集成, 如 Velocity FreeMarker 等等, 因为模型数据不放在特定的 API 里, 而是放在一个 Model 里 (Map 数据结构实现, 因此很容易被其他框架使用 ); 非常灵活的数据验证 格式化和数据绑定机制, 能使用任何对象进行数据绑定, 不必实现特定框架的 API; 提供一套强大的 JSP 标签库, 简化 JSP 开发 ; 支持灵活的本地化 主题等解析 ; 更加简单的异常处理 ; 对静态资源的支持 ; 支持 Restful 风格 2.3 Spring Web MVC 架构 Spring Web MVC 框架也是一个基于请求驱动的 Web 框架, 并且也使用了前端控制器模式来进行设计, 再根据请求映射 规则分发给相应的页面控制器 ( 动作 / 处理器 ) 进行处理 首先让我们整体看一下 Spring Web MVC 处理请求的流程 : Spring Web MVC 处理请求的流程 如图 2-1

12 图 2-1 具体执行步骤如下 : 1 首先用户发送请求 > 前端控制器, 前端控制器根据请求信息 ( 如 URL) 来决定选择哪一个页面控制器进行处理并把请求委托给它, 即以前的控制器的控制逻辑部分 ; 图 2-1 中的 1 2 步骤 ; 2 页面控制器接收到请求后, 进行功能处理, 首先需要收集和绑定请求参数到一个对象, 这个对象在 Spring Web MVC 中叫命令对象, 并进行验证, 然后将命令对象委托给业务对象进行处理 ; 处理完毕后返回一个 ModelAndView( 模型数据和逻辑视图名 ); 图 2-1 中的 步骤 ; 3 前端控制器收回控制权, 然后根据返回的逻辑视图名, 选择相应的视图进行渲染, 并把模型数据传入以便视图渲染 ; 图 2-1 中的步骤 6 7; 4 前端控制器再次收回控制权, 将响应返回给用户, 图 2-1 中的步骤 8; 至此整个结束 问题 : 1 请求如何给前端控制器? 2 前端控制器如何根据请求信息选择页面控制器进行功能处理? 3 如何支持多种页面控制器呢? 4 如何页面控制器如何使用业务对象? 5 页面控制器如何返回模型数据? 6 前端控制器如何根据页面控制器返回的逻辑视图名选择具体的视图进行渲染? 7 不同的视图技术如何使用相应的模型数据? 首先我们知道有如上问题, 那这些问题如何解决呢? 请让我们先继续, 在后边依次回答 Spring Web MVC 架构 1 Spring Web MVC 核心架构图, 如图 2-2

13 6 2 HandlerExecutionChain HandlerMapping 用户 1 7 Dispatcher Servlet 3 HandlerInterceptor HandlerInterceptor Handler Handler 如 UserController View 5 ModelAndView Handler ModelAndView 4 Model View ViewResolver HandlerAdapter 图 2-2 架构图对应的 DispatcherServlet 核心代码如下 : // 前端控制器分派方法 protected void dodispatch(httpservletrequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedrequest = request; HandlerExecutionChain mappedhandler = null; int interceptorindex = -1; try { ModelAndView mv; boolean errorview = false; try { // 检查是否是请求是否是 multipart( 如文件上传 ), 如果是将通过 MultipartResolver 解析 processedrequest = checkmultipart(request); // 步骤 2 请求到处理器( 页面控制器 ) 的映射, 通过 HandlerMapping 进行映射 mappedhandler = gethandler(processedrequest, false); if (mappedhandler == null mappedhandler.gethandler() == null) { nohandlerfound(processedrequest, response); return; // 步骤 3 处理器适配, 即将我们的处理器包装成相应的适配器 ( 从而支持多种类型的处理器 ) HandlerAdapter ha = gethandleradapter(mappedhandler.gethandler());

14 // 304 Not Modified 缓存支持 // 此处省略具体代码 // 执行处理器相关的拦截器的预处理 (HandlerInterceptor.preHandle) // 此处省略具体代码 // 步骤 4 由适配器执行处理器 ( 调用处理器相应功能处理方法 ) mv = ha.handle(processedrequest, response, mappedhandler.gethandler()); // Do we need view name translation? if (mv!= null &&!mv.hasview()) { mv.setviewname(getdefaultviewname(request)); // 执行处理器相关的拦截器的后处理 (HandlerInterceptor.postHandle) // 此处省略具体代码 catch (ModelAndViewDefiningException ex) { logger.debug("modelandviewdefiningexception encountered", ex); mv = ex.getmodelandview(); catch (Exception ex) { Object handler = (mappedhandler!= null? mappedhandler.gethandler() : null); mv = processhandlerexception(processedrequest, response, handler, ex); errorview = (mv!= null); // 步骤 5 步骤 6 解析视图并进行视图的渲染 // 步骤 5 由 ViewResolver 解析 View(viewResolver.resolveViewName(viewName, locale)) // 步骤 6 视图在渲染时会把 Model 传入 (view.render(mv.getmodelinternal(), request, response);) if (mv!= null &&!mv.wascleared()) { render(mv, processedrequest, response); if (errorview) { WebUtils.clearErrorRequestAttributes(request); else { if (logger.isdebugenabled()) { logger.debug("null ModelAndView returned to DispatcherServlet with name '" + getservletname() + "': assuming HandlerAdapter completed request handling"); // 执行处理器相关的拦截器的完成后处理 (HandlerInterceptor.afterCompletion) // 此处省略具体代码

15 catch (Exception ex) { // Trigger after-completion for thrown exception. triggeraftercompletion(mappedhandler, interceptorindex, processedrequest, response, ex); throw ex; catch (Error err) { ServletException ex = new NestedServletException("Handler processing failed", err); // Trigger after-completion for thrown exception. triggeraftercompletion(mappedhandler, interceptorindex, processedrequest, response, ex); throw ex; finally { // Clean up any resources used by a multipart request. if (processedrequest!= request) { cleanupmultipart(processedrequest); 核心架构的具体流程步骤如下 : 1 首先用户发送请求 >DispatcherServlet, 前端控制器收到请求后自己不进行处理, 而是委托给其他的解析器进行处理, 作为统一访问点, 进行全局的流程控制 ; 2 DispatcherServlet >HandlerMapping, HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象 ( 包含一个 Handler 处理器 ( 页面控制器 ) 对象 多个 HandlerInterceptor 拦截器 ) 对象, 通过这种策略模式, 很容易添加新的映射策略 ; 3 DispatcherServlet >HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器, 从而支持多种类型的处理器, 即适配器设计模式的应用, 从而很容易支持很多类型的处理器 ; 4 HandlerAdapter > 处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法, 完成功能处理 ; 并返回一个 ModelAndView 对象 ( 包含模型数据 逻辑视图名 ); 5 ModelAndView 的逻辑视图名 > ViewResolver, ViewResolver 将把逻辑视图名解析为具体的 View, 通过这种策略模式, 很容易更换其他视图技术 ; 6 View > 渲染,View 会根据传进来的 Model 模型数据进行渲染, 此处的 Model 实际是一个 Map 数据结构, 因此很容易支持其他视图技术 ; 7 返回控制权给 DispatcherServlet, 由 DispatcherServlet 返回响应给用户, 到此一个流程结束 此处我们只是讲了核心流程, 没有考虑拦截器 本地解析 文件上传解析等, 后边再细述 到此, 再来看我们前边提出的问题 : 1 请求如何给前端控制器? 这个应该在 web.xml 中进行部署描述, 在 HelloWorld 中详细讲解 2 前端控制器如何根据请求信息选择页面控制器进行功能处理? 我们需要配置 HandlerMapping 进行映射 3 如何支持多种页面控制器呢? 配置 HandlerAdapter 从而支持多种类型的页面控制器 4 如何页面控制器如何使用业务对象? 可以预料到, 肯定利用 Spring IoC 容器的依赖注入功能

16 5 页面控制器如何返回模型数据? 使用 ModelAndView 返回 6 前端控制器如何根据页面控制器返回的逻辑视图名选择具体的视图进行渲染? 使用 ViewResolver 进行解析 7 不同的视图技术如何使用相应的模型数据? 因为 Model 是一个 Map 数据结构, 很容易支持其他视图技术 在此我们可以看出具体的核心开发步骤 : 1 DispatcherServlet 在 web.xml 中的部署描述, 从而拦截请求到 Spring Web MVC 2 HandlerMapping 的配置, 从而将请求映射到处理器 3 HandlerAdapter 的配置, 从而支持多种类型的处理器 4 ViewResolver 的配置, 从而将逻辑视图名解析为具体视图技术 5 处理器( 页面控制器 ) 的配置, 从而进行功能处理 上边的开发步骤我们会在 Hello World 中详细验证 2.4 Spring Web MVC 优势 1 清晰的角色划分 : 前端控制器 (DispatcherServlet) 请求到处理器映射 (HandlerMapping) 处理器适配器 (HandlerAdapter) 视图解析器 (ViewResolver) 处理器或页面控制器 (Controller) 验证器 ( Validator) 命令对象 (Command 请求参数绑定到的对象就叫命令对象 ) 表单对象 (Form Object 提供给表单展示和提交到的对象就叫表 单对象 ) 2 分工明确, 而且扩展点相当灵活, 可以很容易扩展, 虽然几乎不需要 ; 3 由于命令对象就是一个 POJO, 无需继承框架特定 API, 可以使用命令对象直接作为业务对象 ; 4 和 Spring 其他框架无缝集成, 是其它 Web 框架所不具备的 ; 5 可适配, 通过 HandlerAdapter 可以支持任意的类作为处理器 ; 6 可定制性,HandlerMapping ViewResolver 等能够非常简单的定制 ; 7 功能强大的数据验证 格式化 绑定机制 ; 8 利用 Spring 提供的 Mock 对象能够非常简单的进行 Web 层单元测试 ; 9 本地化 主题的解析的支持, 使我们更容易进行国际化和主题的切换 10 强大的 JSP 标签库, 使 JSP 编写更容易 还有比如 RESTful 风格的支持 简单的文件上传 约定大于配置的契约式编程支持 基于注解的零配置 支持等等 到此我们已经简单的了解了 Spring Web MVC, 接下来让我们来个实例来具体使用下这个框架 2.5 Hello World 入门 准备开发环境和运行环境 : 开发工具 :eclipse 运行环境 :tomcat 工程 : 动态 web 工程 (springmvc-chapter2) spring 框架下载 : spring-framework release-with-docs.zip 依赖 jar 包 : 1 Spring 框架 jar 包 : 为了简单, 将 spring-framework release-with-docs.zip/dist/ 下的所有 jar 包拷贝到项目的 WEB-INF/lib 目录下 ; 2 Spring 框架依赖的 jar 包 : 需要添加 Apache commons logging 日志, 此处使用的是 commons.logging jar;

17 需要添加 jstl 标签库支持, 此处使用的是 jstl jar 和 standard jar; 前端控制器的配置 在我们的 web.xml 中添加如下配置 : <servlet> <servlet-name>chapter2</servlet-name> <servlet-class>org.springframework.web.servlet.dispatcherservlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>chapter2</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> load-on-startup: 表示启动容器时初始化该 Servlet; url-pattern: 表示哪些请求交给 Spring Web MVC 处理, / 是用来定义默认 servlet 映射的 也可以如 *.html 表示拦截所有以 html 为扩展名的请求 自此请求已交给 Spring Web MVC 框架处理, 因此我们需要配置 Spring 的配置文件, 默认 DispatcherServlet 会加 载 WEB-INF/[DispatcherServlet 的 Servlet 名字 ]-servlet.xml 配置文件 本示例为 WEB-INF/ chapter2-servlet.xml 在 Spring 配置文件中配置 HandlerMapping HandlerAdapter 具体配置在 WEB-INF/ chapter2-servlet.xml 文件中 : <!-- HandlerMapping --> <bean class="org.springframework.web.servlet.handler.beannameurlhandlermapping"/> <!-- HandlerAdapter --> <bean class="org.springframework.web.servlet.mvc.simplecontrollerhandleradapter"/> BeanNameUrlHandlerMapping: 表示将请求的 URL 和 Bean 名字映射, 如 URL 为 上下文 /hello, 则 Spring 配置文件必须有一个名字为 /hello 的 Bean, 上下文默认忽略 SimpleControllerHandlerAdapter: 表示所有实现了 org.springframework.web.servlet.mvc.controller 接口的 Bean 可以作为 Spring Web MVC 中的处理器 如果需要其他类型的处理器可以通过实现 HadlerAdapter 来解决 在 Spring 配置文件中配置 ViewResolver 具体配置在 WEB-INF/ chapter2-servlet.xml 文件中 : <!-- ViewResolver --> <bean class="org.springframework.web.servlet.view.internalresourceviewresolver"> <property name="viewclass" value="org.springframework.web.servlet.view.jstlview"/> <property name="prefix" value="/web-inf/jsp/"/> <property name="suffix" value=".jsp"/> </bean>

18 InternalResourceViewResolver: 用于支持 Servlet JSP 视图解析 ; viewclass:jstlview 表示 JSP 模板页面需要使用 JSTL 标签库,classpath 中必须包含 jstl 的相关 jar 包 ; prefix 和 suffix: 查找视图页面的前缀和后缀 ( 前缀 [ 逻辑视图名 ] 后缀 ), 比如传进来的逻辑视图名为 hello, 则该该 jsp 视图页面应该存放在 WEB-INF/jsp/hello.jsp ; 开发处理器 / 页面控制器 package cn.javass.chapter2.web.controller; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.modelandview; import org.springframework.web.servlet.mvc.controller; public class HelloWorldController implements Controller publicmodelandviewhandlerequest(httpservletrequestreq,httpservletresponseresp) throws Exception { //1 收集参数 验证参数 //2 绑定参数到命令对象 //3 将命令对象传入业务对象进行业务处理 //4 选择下一个页面 ModelAndView mv = new ModelAndView(); // 添加模型数据可以是任意的 POJO 对象 mv.addobject("message", "Hello World!"); // 设置逻辑视图名, 视图解析器会根据该名字解析到具体的视图页面 mv.setviewname("hello"); return mv; org.springframework.web.servlet.mvc.controller: 页面控制器 / 处理器必须实现 Controller 接口, 注意别选错了 ; 后边我们会学习其他的处理器实现方式 ; public ModelAndView handlerequest(httpservletrequest req, HttpServletResponse resp) : 功能处理方法, 实现相应的功能处理, 比如收集参数 验证参数 绑定参数到命令对象 将命令对象传入业务对象进行业务处理 最后返回 ModelAndView 对象 ; ModelAndView: 包含了视图要实现的模型数据和逻辑视图名 ; mv.addobject("message", "Hello World!"); 表示添加模型数据, 此处可以是任意 POJO 对象 ; mv.setviewname("hello"); 表示设置逻辑视图名为 hello, 视图解析器会将其解析为具体的视图, 如前边的视图解析器 InternalResourceVi wresolver 会将其解析为 WEB-INF/jsp/hello.jsp 我们需要将其添加到 Spring 配置文件 (WEB-INF/chapter2-servlet.xml), 让其接受 Spring IoC 容器管理 : <!-- 处理器 --> <bean name="/hello" class="cn.javass.chapter2.web.controller.helloworldcontroller"/> name="/hello": 前边配置的 BeanNameUrlHandlerMapping, 表示如过请求的 URL 为 上下文 /hello, 则将会交给该 Bean 进行处理

19 2.5.6 开发视图页面 创建 /WEB-INF/jsp/hello.jsp 视图页面 : <%@ page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <title>hello World</title> </head> <body> ${message </body> </html> ${message: 表示显示由 HelloWorldController 处理器传过来的模型数据 启动服务器运行测试 通过请求 : 如果页面输出 Hello World! 就表明我们成功了! 运行流程分析 如图 2-3

20 图 2-3 运行步骤 : 1 首先用户发送请求 >web 容器,web 容器根据 /hello 路径映射到 DispatcherServlet(url-pattern 为 /) 进行处理 ; 2 DispatcherServlet >BeanNameUrlHandlerMapping 进行请求到处理的映射,BeanNameUrlHandlerMapping 将 /hello 路径直接映射到名字为 /hello 的 Bean 进行处理, 即 HelloWorldController,BeanNameUrlHandlerMapping 将其包装为 HandlerExecutionChain( 只包括 HelloWorldController 处理器, 没有拦截器 ); 3 DispatcherServlet > SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter 将 HandlerExecutionChain 中的处理器 (HelloWorldController) 适配为 SimpleControllerHandlerAdapter; 4 SimpleControllerHandlerAdapter > HelloWorldController 处理器功能处理方法的调用, SimpleControllerHandlerAdapter 将会调用处理器的 handlerequest 方法进行功能处理, 该处理方法返回一个 ModelAndView 给 DispatcherServlet; 5 hello(modelandview 的逻辑视图名 ) >InternalResourceViewResolver, InternalResourceViewResolver 使用 JstlView, 具体视图页面在 /WEB-INF/jsp/hello.jsp; 6 JstlView(/WEB-INF/jsp/hello.jsp) > 渲染, 将在处理器传入的模型数据 (message=helloworld!) 在视图中展示出来 ; 7 返回控制权给 DispatcherServlet, 由 DispatcherServlet 返回响应给用户, 到此一个流程结束 到此 HelloWorld 就完成了, 步骤是不是有点多? 而且回忆下我们主要进行了如下配置 : 1 前端控制器 DispatcherServlet; 2 HandlerMapping 3 HandlerAdapter 4 ViewResolver 5 处理器 / 页面控制器 6 视图 因此, 接下来几章让我们详细看看这些配置, 先从 DispatcherServlet 开始吧 2.6 POST 中文乱码解决方案 spring Web MVC 框架提供了 org.springframework.web.filter.characterencodingfilter 用于解决 POST 方式造成的中文乱码问题, 具体配置如下 : <filter> <filter-name>characterencodingfilter</filter-name> <filter-class>org.springframework.web.filter.characterencodingfilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterencodingfilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 以后我们项目及所有页面的编码均为 UTF-8

21 2.7 Spring3.1 新特性 一 Spring2.5 之前, 我们都是通过实现 Controller 接口或其实现来定义我们的处理器类 二 Spring2.5 引入注解式处理器支持, 注解定义我们的处理器类 并且提供了一组强大的注解 : 需要通过处理器映射 DefaultAnnotationHandlerMapping 和处理器适配器 AnnotationMethodHandlerAdapter 来开启支 用于标识是处理器类 请求到处理器功能方法的映射规则 请求参数到处理器功能处理方法的方法参数上的绑定 请求参数到命令对象的绑定 用于声明 session 级别存储的属性, 放置在处理器类上, 通常列出模型属性 ( 对应的名称, 则这些属性会透明的保存到 session 中 自定义数据绑定注册支持, 用于将请求参数转换到命令对象属性的对应类型 ; 三 Spring3.0 引入 RESTful 架构风格支持 ( 注解和一些其他特性支持 ), 且又引入了更多的注解支持 数据到处理器功能处理方法的方法参数上的绑定 请求头 (header) 数据到处理器功能处理方法的方法参数上的绑定 请求的 body 体的绑定 ( 通过 HttpMessageConverter 进行类型转换 处理器功能处理方法的返回值作为响应体 ( 通过 HttpMessageConverter 进行类型转换 定义处理器功能处理方法 / 异常处理器返回的状态码和原因 注解式声明异常处理器 请求 URI 中的模板变量部分到处理器功能处理方法的方法参数上的绑定, 从而支持 RESTful 架构风格的 URI; 四 还有比如 : JSR-303 验证框架的无缝支持 ( 注解定义验证元数据 ); 使用 Spring 3 开始的 ConversionService 进行类型转换 (PropertyEditor 依然有效 ), 来进行数字和日期的格式化 ; HttpMessageConverter(Http 输入 / 输出转换器, 比如 JSON XML 等的数据输出转换器 ); ContentNegotiatingViewResolver, 内容协商视图解析器, 它还是视图解析器, 只是它支持根据请求信息将同一模型 数据以不同的视图方式展示 ( 如 json xml html 等 ),RESTful 架构风格中很重要的概念 ( 同一资源, 多种表现形式 ); Spring 3 引入一个 mvc XML 的命名空间用于支持 mvc 配置, 包括如 : <mvc:annotation-driven>: 自动注册基于注解风格的处理器需要的 DefaultAnnotationHandlerMapping AnnotationMethodHandlerAdapter 支持 Spring3 的 ConversionService 自动注册 支持 JSR-303 验证框架的自动探测并注册 ( 只需把 JSR-303 实现放置到 classpath) 自动注册相应的 HttpMessageConverter( 如 XML 输入输出转换器 ( 只需将 JAXP 实现放置到 classpath) JSON 输入输出转换器 ( 只需将 Jackson 实现放置到 classpath)) 等 <mvc:interceptors>: 注册自定义的处理器拦截器 ; <mvc:view-controller>: 和 ParameterizableViewController 类似, 收到相应请求后直接选择相应的视图 ; <mvc:resources>: 逻辑静态资源路径到物理静态资源路径的支持 ; <mvc:default-servlet-handler>: 当在 web.xml 中 DispatcherServlet 使用 <url-pattern>/</url-pattern> 映射时, 能映射静

22 态资源 ( 当 Spring Web MVC 框架没有处理请求对应的控制器时 ( 如一些静态资源 ), 转交给默认的 Servlet 来响应静态 文件, 否则报 404 找不到资源错误,) 等等 五 Spring3.1 新特性 : 对 Servlet 3.0 用于在基于 Java 类定义 Bean 配置中开启 MVC 支持, 和 XML 中的 <mvc:annotation-driven> 功能一样 ; 注解支持类 : 处理器映射 RequestMappingHandlerMapping 和处理器适配器 RequestMappingHandlerAdapter 组合来代替 Spring2.5 开始的处理器映射 DefaultAnnotationHandlerMapping 和处理 器适配器 AnnotationMethodHandlerAdapter, 提供更多的扩展点, 它们之间的区别我们在处理器映射一章介绍 注解支持类 : ExceptionHandlerExceptionResolver 来代替 Spring3.0 的 AnnotationMethodHandlerExceptionResolver, 的 "consumes" 和 "produces" 条件支持 : 1 consumes 指定请求的内容是什么类型的内容, 即本处理方法消费什么类型的数据, 如 consumes="application/json" 表示 JSON 类型的内容,Spring 会根据相应的 HttpMessageConverter 注解的命令对象的转换 ; 2 produces 指定生产什么类型的内容, 如 produces="application/json" 表示 JSON 类型的内容,Spring 的根据相应的 HttpMessageConverter 注解的命令对象的转换,Spring 会根据相应的 HttpMessageConverter 进行模型数据 ( 返回值 ) 到 JSON 响应内容的转换 3 以上内容, 本章第 节详述 URI 模板变量增强 :URI 方法参数在视图 渲染之前被合并到模型数据中 ( 除 JSON 序列化 XML 混搭场景下 的 javax.validation.valid 一种变体 ( 非 JSR-303 规范定义的, 而是 Spring 自定义的 ), 用于 提供对 Spring 的验证器 (org.springframework.validation.validator) 支持, 需要 Hibernate Validator 4.2 及更 高版本支持 : 提供对 multipart/form-data 请求的全面支持, 支持 Servlet 3.0 文件上传 (javax.servlet.http.part) 支持内容的 HttpMessageConverter( 即根据请求头的 Content-Type, 来判断内容区数据是什么类型, 如 JSON XML, 能自动转换为命令对象 ), 更强大 ( 只能对请求参数数据绑定,key-alue 格式 ), 支持如 JSON XML 内容区数据的绑定 ; 详见本章的第 节 ; Flash 属性和 RedirectAttribute: 通过 FlashMap 存储一个请求的输出, 当进入另一个请求时作为该请求的输入, 典型场景如重定向 (POST-REDIRECT-GET 模式,1 POST 时将下一次需要的数据放在 FlashMap;2 重定向;3 通过 GET 访问重定向的地址, 此时 FlashMap 会把 1 放到 FlashMap 的数据取出放到请求中, 并从 FlashMap 中删除 ; 从而支持在两次请求之间保存数据并防止了重复表单提交 ) Spring Web MVC 提供 FlashMapManager 用于管理 FlashMap, 默认使用 SessionFlashMapManager, 即数据默认存储在 session 中

23 第三章 DispatcherServlet 详解 3.1 DispatcherServlet 作用 DispatcherServlet 是前端控制器设计模式的实现, 提供 Spring Web MVC 的集中访问点, 而且负责职责的分派, 而且与 Spring IoC 容器无缝集成, 从而可以获得 Spring 的所有好处 具体请参考第二章的图 2-1 DispatcherServlet 主要用作职责调度工作, 本身主要用于控制流程, 主要职责如下 : 1 文件上传解析, 如果请求类型是 multipart 将通过 MultipartResolver 进行文件上传解析 ; 2 通过 HandlerMapping, 将请求映射到处理器 ( 返回一个 HandlerExecutionChain, 它包括一个处理器 多个 HandlerInterceptor 拦截器 ); 3 通过 HandlerAdapter 支持多种类型的处理器 (HandlerExecutionChain 中的处理器 ); 4 通过 ViewResolver 解析逻辑视图名到具体视图实现 ; 5 本地化解析; 6 渲染具体的视图等; 7 如果执行过程中遇到异常将交给 HandlerExceptionResolver 来解析 从以上我们可以看出 DispatcherServlet 主要负责流程的控制 ( 而且在流程中的每个关键点都是很容易扩展的 ) 3.2 DispatcherServlet 在 web.xml 中的配置 <servlet> <servlet-name>chapter2</servlet-name> <servlet-class>org.springframework.web.servlet.dispatcherservlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>chapter2</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> load-on-startup: 表示启动容器时初始化该 Servlet; url-pattern: 表示哪些请求交给 Spring Web MVC 处理, / 是用来定义默认 servlet 映射的 也可以如 *.html 表示拦截所有以 html 为扩展名的请求 该 DispatcherServlet 默认使用 WebApplicationContext 作为上下文,Spring 默认配置文件为 /WEB-INF/[servlet 名字 ]-servlet.xml DispatcherServlet 也可以配置自己的初始化参数, 覆盖默认配置 : 摘自 Spring Reference 参数描述 contextclass 实现 WebApplicationContext 接口的类, 当前的 servlet 用它来创建上下文

24 参数 描述 contextconfiglocation 如果这个参数没有指定, 默认使用 XmlWebApplicationContext 传给上下文实例 ( 由 contextclass 指定 ) 的字符串, 用来指定上下文的位置 这个字符串可以被分成多个字符串 ( 使用逗号作为分隔符 ) 来支持多个上下文 ( 在多上下文的情况下, 如果同一个 bean 被定义两次, 后面一个优先 ) namespace WebApplicationContext 命名空间 默认值是 [server-name]-servlet 因此我们可以通过添加初始化参数 <servlet> <servlet-name>chapter2</servlet-name> <servlet-class>org.springframework.web.servlet.dispatcherservlet</servlet-class> <load-on-startup>1</load-on-startup> <init-param> <param-name>contextconfiglocation</param-name> <param-value>classpath:spring-servlet-config.xml</param-value> </init-param> </servlet> 如果使用如上配置,Spring Web MVC 框架将加载 classpath:spring-servlet-config.xml 来进行初始化上下文而不是 /WEB-INF/[servlet 名字 ]-servlet.xml 3.3 上下文关系 集成 Web 环境的通用配置 : <context-param> <param-name>contextconfiglocation</param-name> <param-value> classpath:spring-common-config.xml, classpath:spring-budget-config.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.contextloaderlistener</listener-class> </listener> 如上配置是 Spring 集成 Web 环境的通用配置 ; 一般用于加载除 Web 层的 Bean( 如 DAO Service 等 ), 以便于与其他任何 Web 框架集成 contextconfiglocation: 表示用于加载 Bean 的配置文件 ; contextclass: 表示用于加载 Bean 的 ApplicationContext 实现类, 默认 WebApplicationContext 创建完毕后会将该上下文放在 ServletContext: servletcontext.setattribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ContextLoaderListener 初始化的上下文和 DispatcherServlet 初始化的上下文关系, 如图 3-1

25 图 3-1 从图中可以看出 : ContextLoaderListener 初始化的上下文加载的 Bean 是对于整个应用程序共享的, 不管是使用什么表现层技术, 一般如 DAO 层 Service 层 Bean; DispatcherServlet 初始化的上下文加载的 Bean 是只对 Spring Web MVC 有效的 Bean, 如 Controller HandlerMapping HandlerAdapter 等等, 该初始化上下文应该只加载 Web 相关组件 3.4 DispatcherServlet 初始化顺序 继承体系结构如下所示 : 1 HttpServletBean 继承 HttpServlet, 因此在 Web 容器启动时将调用它的 init 方法, 该初始化方法的主要作用 ::: 将 Servlet 初始化参数 (init-param) 设置到该组件上 ( 如 contextattribute contextclass namespace contextconfiglocation), 通过 BeanWrapper 简化设值过程, 方便后续使用 ; ::: 提供给子类初始化扩展点,initServletBean(), 该方法由 FrameworkServlet 覆盖

26 public abstract class HttpServletBean extends HttpServlet implements public final void init() throws ServletException { // 省略部分代码 //1 如下代码的作用是将 Servlet 初始化参数设置到该组件上 // 如 contextattribute contextclass namespace contextconfiglocation; try { PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredproperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceloader = new ServletContextResourceLoader(getServletContext()); bw.registercustomeditor(resource.class, new ResourceEditor(resourceLoader, this.environment)); initbeanwrapper(bw); bw.setpropertyvalues(pvs, true); catch (BeansException ex) { // 省略其他代码 //2 提供给子类初始化的扩展点, 该方法由 FrameworkServlet 覆盖 initservletbean(); if (logger.isdebugenabled()) { logger.debug("servlet '" + getservletname() + "' configured successfully"); // 省略其他代码 2 FrameworkServlet 继承 HttpServletBean, 通过 initservletbean() 进行 Web 上下文初始化, 该方法主要覆盖一下两件事情 : 初始化 web 上下文 ; 提供给子类初始化扩展点 ; public abstract class FrameworkServlet extends HttpServletBean protected final void initservletbean() throws ServletException { // 省略部分代码 try { //1 初始化 Web 上下文 this.webapplicationcontext = initwebapplicationcontext(); //2 提供给子类初始化的扩展点 initframeworkservlet(); // 省略部分代码

27 protected WebApplicationContext initwebapplicationcontext() { wac; //ROOT 上下文 (ContextLoaderListener 加载的 ) WebApplicationContext rootcontext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webapplicationcontext!= null) { // 1 在创建该 Servlet 注入的上下文 wac = this.webapplicationcontext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) if (!cwac.isactive()) { if (cwac.getparent() == null) { cwac.setparent(rootcontext); configureandrefreshwebapplicationcontext(cwac); if (wac == null) { //2 查找已经绑定的上下文 wac = findwebapplicationcontext(); if (wac == null) { //3 如果没有找到相应的上下文, 并指定父亲为 ContextLoaderListener wac = createwebapplicationcontext(rootcontext); if (!this.refresheventreceived) { //4 刷新上下文( 执行一些初始化 ) onrefresh(wac); if (this.publishcontext) { // Publish the context as a servlet context attribute. String attrname = getservletcontextattributename(); getservletcontext().setattribute(attrname, wac); // 省略部分代码 return wac; 从 initwebapplicationcontext() 方法可以看出, 基本上如果 ContextLoaderListener 加载了上下文将作为根 上下文 (DispatcherServlet 的父容器 ) 最后调用了 onrefresh() 方法执行容器的一些初始化, 这个方法由子类实现, 来进行扩展 3 DispatcherServlet 继承 FrameworkServlet, 并实现了 onrefresh() 方法提供一些前端控制器相关的配置 :

28 public class DispatcherServlet extends FrameworkServlet { // 实现子类的 onrefresh() 方法, 该方法委托为 initstrategies() protected void onrefresh(applicationcontext context) { initstrategies(context); // 初始化默认的 Spring Web MVC 框架使用的策略 ( 如 HandlerMapping) protected void initstrategies(applicationcontext context) { initmultipartresolver(context); initlocaleresolver(context); initthemeresolver(context); inithandlermappings(context); inithandleradapters(context); inithandlerexceptionresolvers(context); initrequesttoviewnametranslator(context); initviewresolvers(context); initflashmapmanager(context); 从如上代码可以看出,DispatcherServlet 启动时会进行我们需要的 Web 层 Bean 的配置, 如 HandlerMapping HandlerAdapter 等, 而且如果我们没有配置, 还会给我们提供默认的配置 从如上代码我们可以看出, 整个 DispatcherServlet 初始化的过程和做了些什么事情, 具体主要做了如下两件事情 : 1 初始化 Spring Web MVC 使用的 Web 上下文, 并且可能指定父容器为 ( ContextLoaderListener 加载了根上下文 ); 2 初始化 DispatcherServlet 使用的策略, 如 HandlerMapping HandlerAdapter 等 服务器启动时的日志分析 ( 此处加上了 ContextLoaderListener 从而启动 ROOT 上下文容器 ):

29 信息 : Initializing Spring root WebApplicationContext // 由 ContextLoaderListener 启动 ROOT 上下文 :33:55 [main] INFO org.springframework.web.context.contextloader - Root WebApplicationContext: initialization started :33:55 [main] INFO org.springframework.web.context.support.xmlwebapplicationcontext - Refreshing Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy :33:55 [main] DEBUG org.springframework.beans.factory.xml.defaultbeandefinitiondocumentreader - Loading bean definitions :33:55 [main] DEBUG org.springframework.beans.factory.xml.xmlbeandefinitionreader - Loaded 0 bean definitions from location pattern [/WEB-INF/ContextLoaderListener.xml] :33:55 [main] DEBUG org.springframework.web.context.support.xmlwebapplicationcontext - Bean factory for Root WebApplicationContext: org.springframework.beans.factory.support.defaultlistablebeanfactory@1c05ffd:definingbeans [];root of factory hierarchy :33:55 [main] DEBUG org.springframework.web.context.support.xmlwebapplicationcontext - Bean factory for Root WebApplicationContext: :33:55 [main] DEBUG org.springframework.web.context.contextloader - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.webapplicationcontext.root] // 将 ROOT 上下文绑定到 ServletContext :33:55 [main] INFO org.springframework.web.context.contextloader - Root WebApplicationContext: initialization completed in 438 ms // 到此 ROOT 上下文启动完毕

30 :33:55 [main] DEBUG org.springframework.web.servlet.dispatcherservlet - Initializing servlet 'chapter2' 信息 : Initializing Spring FrameworkServlet 'chapter2' // 开始初始化 FrameworkServlet 对应的 Web 上下文 :33:55 [main] INFO org.springframework.web.servlet.dispatcherservlet - FrameworkServlet 'chapter2': initialization started :33:55 [main]debug org.springframework.web.servlet.dispatcherservlet - Servletwithname 'chapter2' will try to create custom WebApplicationContext context of class 'org.springframework.web.context.support.xmlwebapplicationcontext', using parent context [Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy] // 此处使用 Root WebApplicationContext 作为父容器 :33:55 [main] INFO org.springframework.web.context.support.xmlwebapplicationcontext - Refreshing WebApplicationContext for namespace 'chapter2-servlet': startup date [Mon Mar 12 13:33:55 CST 2012]; parent: Root WebApplicationContext :33:55 [main] INFO org.springframework.beans.factory.xml.xmlbeandefinitionreader - Loading XML bean definitions from ServletContext resource [/WEB-INF/chapter2-servlet.xml] :33:55 [main] DEBUG org.springframework.beans.factory.xml.defaultbeandefinitiondocumentreader - Loading bean definitions :33:55 [main] DEBUG org.springframework.beans.factory.xml.beandefinitionparserdelegate - Neither XML 'id' nor 'name' specified - using generated bean name [org.springframework.web.servlet.handler.beannameurlhandlermapping#0] // 我们配置的 HandlerMapping :33:55 [main] DEBUG org.springframework.beans.factory.xml.beandefinitionparserdelegate - Neither XML 'id' nor 'name' specified - using generated bean name [org.springframework.web.servlet.mvc.simplecontrollerhandleradapter#0] // 我们配置的 HandlerAdapter :33:55 [main] DEBUG org.springframework.beans.factory.xml.beandefinitionparserdelegate - Neither XML 'id' nor 'name' specified - using generated bean name [org.springframework.web.servlet.view.internalresourceviewresolver#0] // 我们配置的 ViewResolver :33:55 [main] DEBUG org.springframework.beans.factory.xml.beandefinitionparserdelegate - No XML 'id' specified - using '/hello' as bean name and [] as aliases // 我们的处理器 (HelloWorldController) :33:55 [main] DEBUG org.springframework.beans.factory.xml.xmlbeandefinitionreader - Loaded 4 bean definitions from location pattern [/WEB-INF/chapter2-servlet.xml] :33:55 [main] DEBUG org.springframework.web.context.support.xmlwebapplicationcontext - Bean factory for WebApplicationContext for namespace 'chapter2-servlet': org.springframework.beans.factory.support.defaultlistablebeanfactory@ : defining beans [org.springframework.web.servlet.handler.beannameurlhandlermapping#0,org.springframework.web.servl et.mvc.simplecontrollerhandleradapter#0,org.springframework.web.servlet.view.internalresourceviewr esolver#0,/hello]; parent: org.springframework.beans.factory.support.defaultlistablebeanfactory@1c05ffd // 到此容器注册的 Bean 初始化完毕

31 :33:56 [main] DEBUG org.springframework.web.servlet.dispatcherservlet - Unable to locate MultipartResolver with name 'multipartresolver': no multipart request handling provided :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Creating instance of bean 'org.springframework.web.servlet.i18n.acceptheaderlocaleresolver' // 默认的 LocaleResolver 注册 :33:56 [main] DEBUGorg.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'org.springframework.web.servlet.theme.fixedthemeresolver' // 默认的 ThemeResolver 注册 :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.handler.beannameurlhandlermapping#0' // 发现我们定义的 HandlerMapping 不再使用默认的 HandlerMapping :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.mvc.simplecontrollerhandleradapter#0' // 发现我们定义的 HandlerAdapter 不再使用默认的 HandlerAdapter :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Creating instance of bean 'org.springframework.web.servlet.mvc.annotation.annotationmethodhandlerexceptionresolver' // 异常处理解析器 ExceptionResolver :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Creating instance of bean 'org.springframework.web.servlet.mvc.annotation.annotationmethodhandlerexceptionresolver' :33:56 [main]debugorg.springframework.beans.factory.support.defaultlistablebeanfactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.view.internalresourceviewresolver#0' :33:56 [main] DEBUG org.springframework.web.servlet.dispatcherservlet - Published WebApplicationContext of servlet 'chapter2' as ServletContext attribute with name [org.springframework.web.servlet.frameworkservlet.context.chapter2] // 绑定 FrameworkServlet 初始化的 Web 上下文到 ServletContext :33:56 [main] INFO org.springframework.web.servlet.dispatcherservlet - FrameworkServlet 'chapter2': initialization completed in 297 ms :33:56 [main] DEBUG org.springframework.web.servlet.dispatcherservlet - Servlet 'chapter2' configured successfully // 到此完整流程结束 从如上日志我们也可以看出,DispatcherServlet 会进行一些默认的配置 接下来我们看一下默认配置吧

32 3.5 DispatcherServlet 默认配置 DispatcherServlet 的默认配置在 DispatcherServlet.properties( 和 DispatcherServlet 类在一个包下 ) 中, 而且是当 Spring 配置文件中没有指定配置时使用的默认策略 :

33 org.springframework.web.servlet.localeresolver=org.springframework.web.servlet.i18n.acce ptheaderlocaleresolver org.springframework.web.servlet.themeresolver=org.springframework.web.servlet.theme.fixe dthemeresolver org.springframework.web.servlet.handlermapping=org.springframework.web.servlet.handler.b eannameurlhandlermapping,\ org.springframework.web.servlet.mvc.annotation.defaultannotationhandlermapping org.springframework.web.servlet.handleradapter=org.springframework.web.servlet.mvc.httpr equesthandleradapter,\ org.springframework.web.servlet.mvc.simplecontrollerhandleradapter,\ org.springframework.web.servlet.mvc.annotation.annotationmethodhandleradapter org.springframework.web.servlet.handlerexceptionresolver=org.springframework.web.servlet.mvc.annotation.annotationmethodhandlerexceptionresolver,\ org.springframework.web.servlet.mvc.annotation.responsestatusexceptionresolver,\ org.springframework.web.servlet.mvc.support.defaulthandlerexceptionresolver org.springframework.web.servlet.requesttoviewnametranslator=org.springframework.web.serv let.view.defaultrequesttoviewnametranslator org.springframework.web.servlet.viewresolver=org.springframework.web.servlet.view.intern alresourceviewresolver org.springframework.web.servlet.flashmapmanager=org.springframework.web.servlet.support. SessionFlashMapManager 从如上配置可以看出 DispatcherServlet 在启动时会自动注册这些特殊的 Bean, 无需我们注册, 如果我们注册了, 默认的将不会注册 因此如第二章的 BeanNameUrlHandlerMapping SimpleControllerHandlerAdapter 是不需要注册的,DispatcherServlet 默 认会注册这两个 Bean 从 DispatcherServlet.properties 可以看出有许多特殊的 Bean, 那接下来我们就看看 Spring Web MVC 主要有哪些特殊的 Bean 3.6 DispatcherServlet 中使用的特殊的 Bean DispatcherServlet 默认使用 WebApplicationContext 作为上下文, 因此我们来看一下该上下文中有哪些特殊的 Bean: 1 Controller: 处理器 / 页面控制器, 做的是 MVC 中的 C 的事情, 但控制逻辑转移到前端控制器了, 用于对请求进行处理 ; 2 HandlerMapping: 请求到处理器的映射, 如果映射成功返回一个 HandlerExecutionChain 对象 ( 包含一个 Handler 处理器 ( 页面控制器 ) 对象 多个 HandlerInterceptor 拦截器 ) 对象 ; 如 BeanNameUrlHandlerMapping 将 URL 与 Bean 名字映射, 映射成功的 Bean 就是此处的处理器 ; 3 HandlerAdapter:HandlerAdapter 将会把处理器包装为适配器, 从而支持多种类型的处理器, 即适配器设计模式的应用, 从而很容易支持很多类型的处理器 ; 如 SimpleControllerHandlerAdapter 将对实现了 Controller 接口的 Bean 进行适配, 并且掉处理器的 handlerequest 方法进行功能处理 ;

34 4 ViewResolver:ViewResolver 将把逻辑视图名解析为具体的 View, 通过这种策略模式, 很容易更换其他视图技术 ; 如 InternalResourceViewResolver 将逻辑视图名映射为 jsp 视图 ; 5 LocalResover: 本地化解析, 因为 Spring 支持国际化, 因此 LocalResover 解析客户端的 Locale 信息从而方便进行国际化 ; 6 ThemeResovler: 主题解析, 通过它来实现一个页面多套风格, 即常见的类似于软件皮肤效果 ; 7 MultipartResolver: 文件上传解析, 用于支持文件上传 ; 8 HandlerExceptionResolver: 处理器异常解析, 可以将异常映射到相应的统一错误界面, 从而显示用户友好的界面 ( 而不是给用户看到具体的错误信息 ); 9 RequestToViewNameTranslator: 当处理器没有返回逻辑视图名等相关信息时, 自动将请求 URL 映射为逻辑视图名 ; 10 FlashMapManager: 用于管理 FlashMap 的策略接口,FlashMap 用于存储一个请求的输出, 当进入另一个请求时作为该请求的输入, 通常用于重定向场景, 后边会细述 到此 DispatcherServlet 我们已经了解了, 接下来我们就需要把上边提到的特殊 Bean 挨个击破, 那首先从控制器开始吧

35 第四章 Controller 接口控制器详解 4.1 Controller 简介 Controller 控制器, 是 MVC 中的部分 C, 为什么是部分呢? 因为此处的控制器主要负责功能处理部分 : 1 收集 验证请求参数并绑定到命令对象; 2 将命令对象交给业务对象, 由业务对象处理并返回模型数据 ; 3 返回 ModelAndView(Model 部分是业务对象返回的模型数据, 视图部分为逻辑视图名 ) 还记得 DispatcherServlet 吗? 主要负责整体的控制流程的调度部分 : 1 负责将请求委托给控制器进行处理 ; 2 根据控制器返回的逻辑视图名选择具体的视图进行渲染 ( 并把模型数据传入 ) 因此 MVC 中完整的 C( 包含控制逻辑 + 功能处理 ) 由 (DispatcherServlet + Controller) 组成 因此此处的控制器是 Web MVC 中部分, 也可以称为页面控制器 动作 处理器 Spring Web MVC 支持多种类型的控制器, 比如实现 Controller 接口, 从 Spring2.5 开始支持注解方式的控制器 等 ), 我们也可以自己实现相应的控制器 ( 只需要 定义相应的 HandlerMapping 和 HandlerAdapter 即可 ) 因为考虑到还有部分公司使用继承 Controller 接口实现方式, 因此我们也学习一下, 虽然已经不推荐使用了 对于注解方式的控制器, 后边会详细讲, 在此我们先学习 Spring2.5 以前的 Controller 接口实现方式 首先我们将项目 springmvc-chapter2 复制一份改为项目 springmvc-chapter4, 本章示例将放置在 springmvc-chapter4 中 大家需要将项目 springmvc-chapter4/.settings/ org.eclipse.wst.common.component 下的 chapter2 改为 chapter4, 否则上下 文还是 springmvc-chapter2 以后的每一个章节都需要这么做 4.2 Controller 接口 package org.springframework.web.servlet.mvc; public interface Controller { ModelAndView handlerequest(httpservletrequest request, HttpServletResponse response) throws Exception; 这是控制器接口, 此处只有一个方法 handlerequest, 用于进行请求的功能处理, 处理完请求后返回 ModelAndView(Model 模型数据部分和 View 视图部分 ) 还记得第二章的 HelloWorld 吗? 我们的 HelloWorldController 实现 Controller 接口,Spring 默认提供了一些 Controller 接口的实现以方便我们使用, 具体继承体系如图 4-1:

36 图 WebContentGenerator 用于提供如浏览器缓存控制 是否必须有 session 开启 支持的请求方法类型 (GET POST 等 ) 等, 该类主要有如下 属性 : Set<String> supportedmethods: 设置支持的请求方法类型, 默认支持 GET POST HEAD, 如果我们想支持 PUT, 则可以加入该集合 PUT boolean requiresession = false: 是否当前请求必须有 session, 如果此属性为 true, 但当前请求没有打开 session 将抛出 HttpSessionRequiredException 异常 ; boolean useexpiresheader = true: 是否使用 HTTP1.0 协议过期响应头 : 如果 true 则会在响应头添加 : Expires: ; 需 要配合 cacheseconds 使用 ; boolean usecachecontrolheader = true: 是否使用 HTTP1.1 协议的缓存控制响应头, 如果 true 则会在响应头添加 ; 需 要配合 cacheseconds 使用 ; boolean usecachecontrolnostore = true: 是否使用 HTTP 1.1 协议的缓存控制响应头, 如果 true 则会在响应头添加 ; 需 要配合 cacheseconds 使用 ; private int cacheseconds = -1: 缓存过期时间, 正数表示需要缓存, 负数表示不做任何事情 ( 也就是说保留上次的缓存设置 ), 1 cacheseconds =0 时, 则将设置如下响应头数据 : Pragma:no-cache // HTTP 1.0 的不缓存响应头 Expires:1L // useexpiresheader=true 时,HTTP 1.0 Cache-Control :no-cache // usecachecontrolheader=true 时,HTTP 1.1 Cache-Control :no-store // usecachecontrolnostore=true 时, 该设置是防止 Firefox 缓存

37 2 cacheseconds>0 时, 则将设置如下响应头数据 : Expires:System.currentTimeMillis() + cacheseconds * 1000L // useexpiresheader=true 时,HTTP 1.0 Cache-Control :max-age=cacheseconds // usecachecontrolheader=true 时,HTTP cacheseconds<0 时, 则什么都不设置, 即保留上次的缓存设置 此处简单说一下以上响应头的作用, 缓存控制已超出本书内容 : HTTP1.0 缓存控制响应头 Pragma:no-cache: 表示防止客户端缓存, 需要强制从服务器获取最新的数据 ; Expires:HTTP1.0 响应头, 本地副本缓存过期时间, 如果客户端发现缓存文件没有过期则不发送请求,HTTP 的日期时间必须是格林威治时间 (GMT), 如 Expires:Wed, 14 Mar :38:32 GMT ; HTTP1.1 缓存控制响应头 Cache-Control :no-cache 强制客户端每次请求获取服务器的最新版本, 不经过本地缓存的副本验证 ; Cache-Control :no-store 强制客户端不保存请求的副本, 该设置是防止 Firefox 缓存 Cache-Control:max-age=[ 秒 ] 客户端副本缓存的最长时间, 类似于 HTTP1.0 的 Expires, 只是此处是基于请求的相对时间间隔来计算, 而非绝对时间 还有相关缓存控制机制如 Last-Modified( 最后修改时间验证, 客户端的上一次请求时间在服务器的最后修改时间之 后, 说明服务器数据没有发生变化返回 304 状态码 ) ETag( 没有变化时不重新下载数据, 返回 304) 该抽象类默认被 AbstractController 和 WebContentInterceptor 继承 4.4 AbstractController 该抽象类实现了 Controller, 并继承了 WebContentGenerator( 具有该类的特性, 具体请看 4.3), 该类有如下属性 : boolean synchronizeonsession = false: 表示该控制器是否在执行时同步 session, 从而保证该会话的用户串行访问该控制器 public ModelAndView handlerequest(httpservletrequest request, HttpServletResponse response) throws Exception { // 委托给 WebContentGenerator 进行缓存控制 checkandprepare(request, response, this instanceof LastModified); // 当前会话是否应串行化访问. if (this.synchronizeonsession) { HttpSession session = request.getsession(false); if (session!= null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { return handlerequestinternal(request, response); return handlerequestinternal(request, response);

38 可以看出 AbstractController 实现了一些特殊功能, 如继承了 WebContentGenerator 缓存控制功能, 并提供了可选的会话 的串行化访问功能 而且提供了 handlerequestinternal 方法, 因此我们应该在具体的控制器类中实现 handlerequestinternal 方法, 而不再是 handlerequest AbstractController 使用方法 : 首先让我们使用 AbstractController 来重写第二章的 HelloWorldController: public class HelloWorldController extends AbstractController protected ModelAndView handlerequestinternal(httpservletrequest req, HttpServletResponse resp) throws Exception { //1 收集参数 //2 绑定参数到命令对象 //3 调用业务对象 //4 选择下一个页面 ModelAndView mv = new ModelAndView(); // 添加模型数据可以是任意的 POJO 对象 mv.addobject("message", "Hello World!"); // 设置逻辑视图名, 视图解析器会根据该名字解析到具体的视图页面 mv.setviewname("hello"); return mv; <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hello" class="cn.javass.chapter4.web.controller.helloworldcontroller"/> 从如上代码我们可以看出 : 1 继承 AbstractController 2 实现 handlerequestinternal 方法即可 直接通过 response 写响应如果我们想直接在控制器通过 response 写出响应呢, 以下代码帮我们阐述 : public class HelloWorldWithoutReturnModelAndViewController extends AbstractController protected ModelAndView handlerequestinternal(httpservletrequest req, HttpServletResponse resp) throws Exception { resp.getwriter().write("hello World!!"); // 如果想直接在该处理器 / 控制器写响应可以通过返回 null 告诉 DispatcherServlet 自己已经写出响应了, 不需要它进行视图解析 return null; <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellowithoutreturnmodelandview" class="cn.javass.chapter4.web.controller.helloworldwithoutreturnmodelandviewcontroller"/>

39 从如上代码可以看出如果想直接在控制器写出响应, 只需要通过 response 写出, 并返回 null 即可 强制请求方法类型 : <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellowithpost" class="cn.javass.chapter4.web.controller.helloworldcontroller"> <property name="supportedmethods" value="post"></property> </bean> 以上配置表示只支持 POST 请求, 如果是 GET 请求客户端将收到 HTTP Status Request method 'GET' not supported 比如注册 / 登录可能只允许 POST 请求 当前请求的 session 前置条件检查, 如果当前请求无 session 将抛出 HttpSessionRequiredException 异常 : <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellorequiresession" class="cn.javass.chapter4.web.controller.helloworldcontroller"> <property name="requiresession" value="true"/> </bean> 在进入该控制器时, 一定要有 session 存在, 否则抛出 HttpSessionRequiredException 异常 Session 同步 : 即同一会话只能串行访问该控制器 客户端端缓存控制 : 1 缓存 5 秒,cacheSeconds=5 package cn.javass.chapter4.web.controller; // 省略 import public class HelloWorldCacheController extends AbstractController protected ModelAndView handlerequestinternal(httpservletrequest req, HttpServletResponse resp) throws Exception { // 点击后再次请求当前页面 resp.getwriter().write("<a href=''>this</a>"); return null; <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellocache" class="cn.javass.chapter4.web.controller.helloworldcachecontroller"> <property name="cacheseconds" value="5"/> </bean> 如上配置表示告诉浏览器缓存 5 秒钟 : 开启 chrome 浏览器调试工具 :

40 服务器返回的响应头如下所示 : 添加了 Expires:Wed, 14 Mar :38:32 GMT 和 Cache-Control:max-age=5 表示允许客户端缓存 5 秒, 当你点 this 链接时, 会发现如下 : 而且服务器也没有收到请求, 当过了 5 秒后, 你再点 this 链接会发现又重新请求服务器下载新数据

41 注 : 下面提到一些关于缓存控制的一些特殊情况 : 1 对于一般的页面跳转( 如超链接点击跳转 通过 js 调用 window.open 打开新页面都是会使用浏览器缓存的, 在未过期情况下会直接使用浏览器缓存的副本, 在未过期情况下一次请求也不发送 ); 2 对于刷新页面( 如按 F5 键刷新 ), 会再次发送一次请求到服务器的 ; 2 不缓存,cacheSeconds=0 <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellonocache" class="cn.javass.chapter4.web.controller.helloworldcachecontroller"> <property name="cacheseconds" value="0"/> </bean> 以上配置会要求浏览器每次都去请求服务器下载最新的数据 : 3 cacheseconds<0, 将不添加任何数据 响应头什么缓存控制信息也不加 4 Last-Modified 缓存机制 (1 在客户端第一次输入 url 时, 服务器端会返回内容和状态码 200 表示请求成功并返回了内容 ; 同时会添加一个 Last-Modified 的响应头表示此文件在服务器上的最后更新时间, 如 Last-Modified:Wed, 14 Mar :22:42 GMT 表示最后更新时间为 ( :22); (2 客户端第二次请求此 URL 时, 客户端会向服务器发送请求头 If-Modified-Since, 询问服务器该时间之后当前请求内容是否有被修改过, 如 If-Modified-Since: Wed, 14 Mar :22:42 GMT, 如果服务器端的内容没有变化, 则自动返回 HTTP 304 状态码 ( 只要响应头, 内容为空, 这样就节省了网络带宽 ) 客户端强制缓存过期 : (1 可以按 ctrl+f5 强制刷新 ( 会添加请求头 HTTP1.0 Pragma:no-cache 和 HTTP1.1 Cache-Control:no-cache If-Modified-Since 请求头被删除 ) 表示强制获取服务器内容, 不缓存 (2 在请求的 url 后边加上时间戳来重新获取内容, 加上时间戳后浏览器就认为不是同一份内容 : 和 是两次不同的请求 Spring 也提供了 Last-Modified 机制的支持, 只需要实现 LastModified 接口, 如下所示 :

42 package cn.javass.chapter4.web.controller; public class HelloWorldLastModifiedCacheController extends AbstractController implements LastModified { private long lastmodified; protected ModelAndView handlerequestinternal(httpservletrequest req, HttpServletResponse resp) throws Exception { // 点击后再次请求当前页面 resp.getwriter().write("<a href=''>this</a>"); return null; public long getlastmodified(httpservletrequest request) { if(lastmodified == 0L) { //TODO 此处更新的条件 : 如果内容有更新, 应该重新返回内容最新修改的时间戳 lastmodified = System.currentTimeMillis(); return lastmodified; <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/hellolastmodified" class="cn.javass.chapter4.web.controller.helloworldlastmodifiedcachecontroller"/> HelloWorldLastModifiedCacheController 只需要实现 LastModified 接口的 getlastmodified 方法, 保证当内容发生改变时返回最新的修改时间即可 分析 : (1 发送请求到服务器, 如 ( 则服务器返回的响应为 : (2 再次按 F5 刷新客户端, 返回状态码 304 表示服务器没有更新过 :

43 (3 重启服务器, 再次刷新, 会看到 200 状态码 ( 因为服务器的 lastmodified 时间变了 ) Spring 判断是否过期, 通过如下代码, 即请求的 If-Modified-Since 大于等于当前的 getlastmodified 方法的时间 戳, 则认为没有修改 : this.notmodified = (ifmodifiedsince >= (lastmodifiedtimestamp / 1000 * 1000)); 5 ETag( 实体标记 ) 缓存机制 (1: 浏览器第一次请求, 服务器在响应时给请求 URL 标记, 并在 HTTP 响应头中将其传送到客户端, 类似服务器端返回的格式 : ETag:"0f8b0c86fe2c0c7a67791e53d660208e3" (2: 浏览器第二次请求, 客户端的查询更新格式是这样的 : If-None-Match:"0f8b0c86fe2c0c7a67791e53d660208e3", 如果 ETag 没改变, 表示内容没有发生改变, 则返回状态 304 Spring 也提供了对 ETag 的支持, 具体需要在 web.xml 中配置如下代码 : <filter> <filter-name>etagfilter</filter-name> <filter-class>org.springframework.web.filter.shallowetagheaderfilter</filter-class> </filter> <filter-mapping> <filter-name>etagfilter</filter-name> <servlet-name>chapter4</servlet-name> </filter-mapping> 此过滤器只过滤到我们 DispatcherServlet 的请求 分析 : 1): 发送请求到服务器 : 服务器返回的响应头中添加 了 (ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"): 2): 浏览器再次发送请求到服务器 ( 按 F5 刷新 ), 请求头中添加了 If-None-Match: "0f8b0c86fe2c0c7a67791e53d660208e3", 响应返回 304 代码, 表示服务器没有修改, 并且响应头再次添加了 ETag:"0f8b0c86fe2c0c7a67791e53d660208e3" ( 每次都需要计算 ):

44 那服务器端是如何计算 ETag 的呢? protected String generateetagheadervalue(byte[] bytes) { StringBuilder builder = new StringBuilder("\"0"); DigestUtils.appendMd5DigestAsHex(bytes, builder); builder.append('"'); return builder.tostring(); bytes 是 response 要写回到客户端的响应体 ( 即响应的内容数据 ), 是通过 MD5 算法计算的内容的摘要信息 也就是说如果服务器内容不发生改变, 则 ETag 每次都是一样的, 即服务器端的内容没有发生改变 此处只列举了部分缓存控制, 详细介绍超出了本书的范围, 强烈推荐 : 中文版 详细了解 HTTP 缓存控制及为什么要缓存 缓存的目的是减少相应延迟和减少网络带宽消耗, 比如 css js 图片这类静态资源应该进行缓存 实际项目一般使用反向代理服务器 ( 如 nginx apache 等 ) 进行缓存 4.5 ServletForwardingController 将接收到的请求转发到一个命名的 servlet, 具体示例如下 : package cn.javass.chapter4.web.servlet; public class ForwardingServlet extends HttpServlet protected void doget(httpservletrequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getwriter().write("controller forward to Servlet");

45 <servlet> <servlet-name>forwarding</servlet-name> <servlet-class>cn.javass.chapter4.web.servlet.forwardingservlet</servlet-class> </servlet> <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/forwardtoservlet" class="org.springframework.web.servlet.mvc.servletforwardingcontroller"> <property name="servletname" value="forwarding"></property> </bean> 当我们请求 /forwardtoservlet 时, 会被转发到名字为 forwarding 的 servlet 处理, 该 sevlet 的 servlet-mapping 标签配置是可选的 4.6 BaseCommandController 命令控制器通用基类, 提供了以下功能支持 : 1 数据绑定: 请求参数绑定到一个 command object( 命令对象, 非 GoF 里的命令设计模式 ), 这里的命令对象是指绑定请求参数的任何 POJO 对象 ; commandclass: 表示命令对象实现类, 如 UserModel; commandname: 表示放入请求的命令对象名字 ( 默认 command),request.setattribute(commandname, commandobject); 2 验证功能 : 提供 Validator 注册功能, 注册的验证器会验证命令对象属性数据是否合法 ; validators: 通过该属性注入验证器, 验证器用来验证命令对象属性是否合法 ; 该抽象类没有没有提供流程功能, 只是提供了一些公共的功能, 实际使用时需要使用它的子类 4.7 AbstractCommandController 命令控制器之一, 可以实现该控制器来创建命令控制器, 该控制器能把自动封装请求参数到一个命令对象, 而且提供 了验证功能 1 创建命令类( 就是普通的 JavaBean 类 /POJO) package cn.javass.chapter4.model; public class UserModel { private String username; private String password; // 省略 setter/getter 2 实现控制器

46 package cn.javass.chapter4.web.controller; // 省略 import public class MyAbstractCommandController extends AbstractCommandController { public MyAbstractCommandController() { // 设置命令对象实现类 protected ModelAndView handle(httpservletrequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { // 将命令对象转换为实际类型 UserModel user = (UserModel) command; ModelAndView mv = new ModelAndView(); mv.setviewname("abstractcommand"); mv.addobject("user", user); return mv; <! 在 chapter4-servlet.xml 配置处理器 --> <bean name="/abstractcommand" class="cn.javass.chapter4.web.controller.myabstractcommandcontroller"> <!-- 也可以通过依赖注入注入命令实现类 --> <!-- property name="commandclass" value="cn.javass.chapter4.model.usermodel"/--> </bean> <! WEB-INF/jsp/abstractCommand.jsp 视图下的主要内容 --> ${user.username -${user.password 当我们在浏览器中输入 会自 动将请求参数 username 和 password 绑定到命令对象 ; 绑定时按照 JavaBean 命名规范绑定 ; 4.8 AbstractFormController 用于支持带步骤的表单提交的命令控制器基类, 使用该控制器可以完成 : 1 定义表单处理( 表单的渲染 ), 并从控制器获取命令对象构建表单 ; 2 提交表单处理, 当用户提交表单内容后,AbstractFormController 可以将用户请求的数据绑定到命令对象, 并可以验证表单内容 对命令对象进行处理

47 @Override protected ModelAndView handlerequestinternal(httpservletrequest request, HttpServletResponse response) throws Exception { //1 是否是表单提交? 该方法实现为 ("POST".equals(request.getMethod())), 即 POST 表示表单提交 if (isformsubmission(request)) { try { Object command = getcommand(request); ServletRequestDataBinder binder = bindandvalidate(request, command); BindException errors = new BindException(binder.getBindingResult()); // 表单提交应该放到该方法实现 return processformsubmission(request, response, command, errors); catch (HttpSessionRequiredException ex) { // 省略部分代码 return handleinvalidsubmit(request, response); else { //2 表示是表单展示, 该方法又转调 showform 方法, 因此我们需要覆盖 showform 来完成表单展示 return shownewform(request, response); bindonnewform: 是否在进行表单展示时绑定请求参数到表单对象, 默认 false, 不绑定 ; sessionform:session 表单模式, 如果开启 (true) 则会将表单对象放置到 session 中, 从而可以跨越多次请求保证数据不丢失 ( 多步骤表单常使用该方式, 详解 AbstractWizardFormController), 默认 false; Object formbackingobject(httpservletrequest request) : 提供给表单展示时使用的表单对象 (form object 表单要展示的默认数据 ), 默认通过 commandname 暴露到请求给展示表单 ; Map referencedata(httpservletrequest request, Object command, Errors errors): 展示表单时需要的一些引用数据 ( 比如用户注册, 可能需要选择工作地点, 这些数据可以通过该方法提供 ), 如 : protected Map referencedata(httpservletrequest request) throws Exception { Map model = new HashMap(); model.put("citylist", citylist); return model; 这样就可以在表单展示页面获取 citylist 数据 SimpleFormController 继承该类, 而且提供了更简单的表单流程控制 4.9 SimpleFormController 提供了更好的两步表单支持 : 1 准备要展示的数据, 并到表单展示页面 ; 2 提交数据数据进行处理

48 第一步, 展示 : 第二步, 提交表单 : 接下来咱们写一个用户注册的例子学习一下 : (1 控制器 package cn.javass.chapter4.web.controller; // 省略 import public class RegisterSimpleFormController extends SimpleFormController { public RegisterSimpleFormController() { setcommandclass(usermodel.class); // 设置命令对象实现类 setcommandname("user");// 设置命令对象的名字 //form object 表单对象, 提供展示表单时的表单数据 ( 使用 commandname 放入请求 ) protected Object formbackingobject(httpservletrequest request) throws Exception { UserModel user = new UserModel(); user.setusername(" 请输入用户名 "); return user; // 提供展示表单时需要的一些其他数据 protected Map referencedata(httpservletrequest request) throws Exception { Map map = new HashMap(); map.put("citylist", Arrays.asList(" 山东 ", " 北京 ", " 上海 ")); return map; protected void dosubmitaction(object command) throws Exception { UserModel user = (UserModel) command; //TODO 调用业务对象处理 System.out.println(user);

49 setcommandclass 和 setcommandname: 分别设置了命令对象的实现类和名字 ; formbackingobject 和 referencedata: 提供了表单展示需要的视图 ; dosubmitaction: 用于执行表单提交动作, 由 onsubmit 方法调用, 如果不需要请求 / 响应对象或进行数据验证, 可以直接使用 dosubmitaction 方法进行功能处理 (2 spring 配置 (chapter4-servlet.xml) < bean name="/simpleform" class="cn.javass.chapter4.web.controller.registersimpleformcontroller"> <property name="formview" value="register"/> <property name="successview" value="redirect:/success"/> </bean> <bean name="/success" class="cn.javass.chapter4.web.controller.successcontroller"/> formview: 表示展示表单时显示的页面 ; successview: 表示处理成功时显示的页面 ; redirect:/success 表示成功处理后重定向到 /success 控制器 ; 防止表单重复提交 ; /success bean 的作用是显示成功页面, 此处就不列举了 (3 视图页面 <!-- register.jsp 注册展示页面 --> <form method="post"> username:<input type="text" name="username" value="${user.username"><br/> password:<input type="password" name="username"><br/> city:<select> <c:foreach items="${citylist " var="city"> <option>${city</option> </c:foreach> </select><br/> <input type="submit" value=" 注册 "/> </form> 此处可以使用 ${user.username 获取到 formbackingobject 设置的表单对象 使用 ${citylist 获取 referencedata 设置的表单支持数据 ; 到此一个简单的两步表单到此结束, 但这个表单有重复提交表单的问题, 而且表单对象到页面的绑定是通过手工绑定 的, 后边我们会学习 spring 标签库 ( 提供自动绑定表单对象到页面 ) 4.10 CancellableFormController 一个可取消的表单控制器, 继承 SimpleFormController, 额外提供取消表单功能 1 表单展示: 和 SimpleFormController 一样 ; 2 表单取消: 和 SimpleFormController 一样 ; 3 表单成功提交: 取消功能处理方法为 :oncancel(object command), 而且默认返回 cancelview 属性指定的逻辑视图名 那如何判断是取消呢? 如果请求中有参数名为 _cancel 的参数, 则表示表单取消 也可以通过 cancelparamkey 来修改参数名 ( 如 _cancel.x 等 )

50 示例 : (1 控制器 复制 RegisterSimpleFormController 一份命名为 CanCancelRegisterSimpleFormController, 添加取消功能处理方法实现 protected ModelAndView oncancel(object command) throws Exception { UserModel user = (UserModel) command; //TODO 调用业务对象处理 System.out.println(user); return super.oncancel(command); oncancel: 在该功能方法内实现取消逻辑, 父类的 oncancel 方法默认返回 cancelview 属性指定的逻辑视图名 (2 spring 配置 (chapter4-servlet.xml) <bean name="/cancancelform" class="cn.javass.chapter4.web.controller.cancancelregistersimpleformcontroller"> <property name="formview" value="register"/> <property name="successview" value="redirect:/success"/> <property name="cancelview" value="redirect:/cancel"/> </bean> <bean name="/cancel" class="cn.javass.chapter4.web.controller.cancelcontroller"/> cancelparamkey: 用于判断是否是取消的请求参数名, 默认是 _cancel, 即如果请求参数数据中含有名字 _cancel 则表示是取消, 将调用 oncancel 功能处理方法 ; cancelview: 表示取消时时显示的页面 ; redirect:/cancel 表示成功处理后重定向到 /cancel 控制器 ; 防止表单重复提交 ; /cancel bean 的作用是显示取消页面, 此处就不列举了 ( 详见代码 ) (3 视图页面( 修改 register.jsp) <input type="submit" name="_cancel" value=" 取消 "/> 该提交按钮的作用是取消, 因为 name="_cancel", 即请求后会有一个名字为 _cancel 的参数, 因此会执行 oncancel 功能处理方法 (4 测试: 在浏览器输入 则首先到展示视图页面, 点击 取消按钮 将重定向到 说明取消成功了 实际项目可能会出现比如一些网站的完善个人资料都是多个页面 ( 即多步 ), 那应该怎么实现呢? 接下来让我们看一下

51 spring Web MVC 提供的对多步表单的支持类 AbstractWizardFormController 4.11 AbstractWizardFormController 向导控制器类提供了多步骤 ( 向导 ) 表单的支持 ( 如完善个人资料时分步骤填写基本信息 工作信息 学校信息等 ) 假设现在做一个完善个人信息的功能, 分三个页面展示 : 1 页面 1 完善基本信息 ; 2 页面 2 完善学校信息 ; 3 页面 3 完善工作信息 这里我们要注意的是当用户跳转到页面 2 时页面 1 的信息是需要保存起来的, 还记得 AbstractFormController 中的 sessionform 吗? 如果为 true 则表单数据存放到 session 中, 哈哈,AbstractWizardFormController 就是使用了这个特性 向导中的页码从 0 开始 ; PARAM_TARGET = "_target": 用于选择向导中的要使用的页面参数名前缀, 如 _target0 则选择第 0 个页面显示, 即图中的 wizard/baseinfo, 以此类推, 如 _target1 将选择第 1 页面, 要得到的页码为去除前缀 _target 后的数字即是 ; PARAM_FINISH = "_finish": 如果请求参数中有名为 _finish 的参数, 表示向导成功结束, 将会调用 processfinish 方法进行完成时的功能处理 ; PARAM_CANCEL = "_cancel": 如果请求参数中有名为 _cancel 的参数, 表示向导被取消, 将会调用 processcancel 方法进行取消时的功能处理 ; 向导中的命令对象 : 向导中的每一个步骤都会把相关的参数绑定到命令对象, 该表单对象默认放置在 session 中, 从而可以跨越多次请求得到该命令对象 接下来具体看一下如何使用吧 (1 修改我们的模型数据以支持多步骤提交: public class UserModel { private String username; private String password; private String realname; // 真实姓名 private WorkInfoModel workinfo; private SchoolInfoModel schoolinfo; // 省略 getter/setter

52 public class SchoolInfoModel { private String schooltype; // 学校类型 : 高中 中专 大学 private String schoolname; // 学校名称 private String specialty; // 专业 // 省略 getter/setter public class WorkInfoModel { private String city; // 所在城市 private String job; // 职位 private String year; // 工作年限 // 省略 getter/setter (2 控制器 package cn.javass.chapter4.web.controller; // 省略 import public class InfoFillWizardFormController extends AbstractWizardFormController { public InfoFillWizardFormController() { setcommandclass(usermodel.class); setcommandname("user"); protected Map referencedata(httpservletrequest request, int page) throws Exception { Map map = new HashMap(); if(page==1) { // 如果是填写学校信息页需要学校类型信息 map.put("schooltypelist", Arrays.asList(" 高中 ", " 中专 ", " 大学 ")); if(page==2) {// 如果是填写工作信息页需要工作城市信息 map.put("citylist", Arrays.asList(" 济南 ", " 北京 ", " 上海 ")); return map; protected void validatepage(object command, Errors errors, int page) { // 提供每一页数据的验证处理方法 protected void postprocesspage(httpservletrequest request, Object command, Errors errors, int page) throws Exception { // 提供给每一页完成时的后处理方法 protected ModelAndView processfinish(httpservletrequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { // 成功后的处理方法 System.out.println(command); return new ModelAndView("redirect:/success"); protected ModelAndView processcancel(httpservletrequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { // 取消后的处理方法 System.out.println(command); return new ModelAndView("redirect:/cancel");

53 page 页码 : 是根据请求中以 _target 开头的参数名来确定的, 如 _target0, 则页码为 0; referencedata: 提供每一页需要的表单支持对象, 如完善学校信息需要学校类型,page 页码从 0 开始 ( 而且根据请求参数中以 _target 开头的参数来确定当前页码, 如 _target1, 则 page=1); validatepage: 验证当前页的命令对象数据, 验证应根据 page 页码来分步骤验证 ; postprocesspage: 验证成功后的后处理 ; processfinish: 成功时执行的方法, 此处直接重定向到 /success 控制器 ( 详见 CancelController); processcancel: 取消时执行的方法, 此处直接重定向到 /cancel 控制器 ( 详见 SuccessController); 其他需要了解 : allowdirtyback 和 allowdirtyforward: 决定在当前页面验证失败时, 是否允许向导前移和后退, 默认 false 不允许 ; onbindandvalidate(httpservletrequest request, Object command, BindException errors, int page): 允许覆盖默认的绑定参数到命令对象和验证流程 (3 spring 配置文件 (chapter4-servlet.xml) <bean name="/infofillwizard" class="cn.javass.chapter4.web.controller.infofillwizardformcontroller"> <property name="pages"> <list> <value>wizard/baseinfo</value> <value>wizard/schoolinfo</value> <value>wizard/workinfo</value> </list> </property> </bean> pages : 表示向导中每一个步骤的逻辑视图名, 当 InfoFillWizardFormController 的 wizard/baseinfo, 以此类推, 从而可以按步骤选择要展示的视图 page=0, 则将会选择 (4 向导中的每一步视图 (4.1 基本信息页面( 第一步 ) baseinfo.jsp: <form method="post"> 真实姓名 :<input type="text" name="realname" value="${user.realname"><br/> <input type="submit" name="_target1" value=" 下一步 "/> </form> 当前页码为 0; name="_target1": 表示向导下一步要显示的页面的页码为 1; (4.2 学校信息页面 ( 第二步 ) schoolinfo.jsp:

54 <form method="post"> 学校类型 :<select name="schoolinfo.schooltype"> <c:foreach items="${schooltypelist " var="schooltype"> <option value="${schooltype " <c:if test="${user.schoolinfo.schooltype eq schooltype"> selected="selected" </c:if> > ${schooltype </option> </c:foreach> </select><br/> 学校名称 :<input type="text" name="schoolinfo.schoolname" value="${user.schoolinfo.schoolname"/><br/> 专业 :<input type="text" name="schoolinfo.specialty" 当前页码为 value="${user.schoolinfo.specialty"/><br/> 1; name="_target0": <input type="submit" 上一步, name="_target0" 表示向导上一步要显示的页面的页码为 value=" "/> 0; name="_target2": <input type="submit" 下一步, name="_target2" 表示向导上一步要显示的页面的页码为 value=" 下一步 "/> 2; </form> (4.3 工作信息页面( 第三步 ) workinfo.jsp: <form method="post"> 所在城市 :<select name="workinfo.city"> <c:foreach items="${citylist " var="city"> <option value="${city " <c:if test="${user.workinfo.city eq city">selected="selected"</c:if> > ${city </option> </c:foreach> </select><br/> 职位 :<input type="text" name="workinfo.job" value="${user.workinfo.job"/><br/> 工作年限 :<input type="text" name="workinfo.year" value="${user.workinfo.year"/><br/> <input type="submit" name="_target1" value=" 上一步 "/> <input type="submit" name="_finish" value=" 完成 "/> <input type="submit" name="_cancel" value=" 取消 "/> </form> 当前页码为 2; name="_target1": 上一步, 表示向导上一步要显示的页面的页码为 1; name="_finish": 向导完成, 表示向导成功, 将会调用向导控制器的 processfinish 方法 ; name="_cancel": 向导取消, 表示向导被取消, 将会调用向导控制器的 processcancel 方法 ; 到此向导控制器完成, 此处的向导流程比较简单, 如果需要更复杂的页面流程控制, 可以选择使用 Spring Web Flow 框 架 4.12 ParameterizableViewController 参数化视图控制器, 不进行功能处理 ( 即静态视图 ), 根据参数的逻辑视图名直接选择需要展示的视图

55 <bean name="/parameterizableview" class="org.springframework.web.servlet.mvc.parameterizableviewcontroller"> <property name="viewname" value="success"/> </bean> 该控制器接收到请求后直接选择参数化的视图, 这样的好处是在配置文件中配置, 从而避免程序的硬编码, 比如像帮 助页面等不需要进行功能处理, 因此直接使用该控制器映射到视图 4.13 AbstractUrlViewController 提供根据请求 URL 路径直接转化为逻辑视图名的支持基类, 即不需要功能处理, 直接根据 URL 计算出逻辑视图名, 并选择具体视图进行展示 : urldecode: 是否进行 url 解码, 不指定则默认使用服务器编码进行解码 ( 如 Tomcat 默认 ISO ); urlpathhelper: 用于解析请求路径的工具类, 默认为 org.springframework.web.util.urlpathhelper UrlFilenameViewController 是它的一个实现者, 因此我们应该使用 UrlFilenameViewController 4.14 UrlFilenameViewController 将请求的 URL 路径转换为逻辑视图名并返回的转换控制器, 即不需要功能处理, 直接根据 URL 计算出逻辑视图名, 并选择具体视图进行展示 : 根据请求 URL 路径计算逻辑视图名 ; <bean name="/index1/*" class="org.springframework.web.servlet.mvc.urlfilenameviewcontroller"/> <bean name="/index2/**" class="org.springframework.web.servlet.mvc.urlfilenameviewcontroller"/> <bean name="/*.html" class="org.springframework.web.servlet.mvc.urlfilenameviewcontroller"/> <bean name="/index3/*.html" class="org.springframework.web.servlet.mvc.urlfilenameviewcontroller"/> /index1/*: 可以匹配 /index1/demo, 但不匹配 /index1/demo/demo, 如 /index1/demo 逻辑视图名为 demo; /index2/** : 可以匹配 /index2 路径下的所有子路径, 如匹配 /index2/demo, 或 /index2/demo/demo, /index2/demo 的逻辑视图名为 demo, 而 /index2/demo/demo 逻辑视图名为 demo/demo; /*.html: 可以匹配如 /abc.html, 逻辑视图名为 abc, 后缀会被删除 ( 不仅仅可以是 html); /index3/*.html: 可以匹配 /index3/abc.html, 逻辑视图名也是 abc; 上述模式为 Spring Web MVC 使用的 Ant-style 模式进行匹配的 :

56 ? 匹配一个字符, 如 /index? 可以匹配 /index1, 但不能匹配 /index 或 /index12 * 匹配零个或多个字符, 如 /index1/*, 可以匹配 /index1/demo, 但不匹配 /index1/demo/demo ** 匹配零个或多个路径, 如 /index2/**: 可以匹配 /index2 路径下的所有子路径, 如匹配 /index2/demo, 或 /index2/demo/demo 如果我有如下模式, 那 Spring 该选择哪一个执行呢? 当我的请求为 /long/long 时如下所示 : /long/long /long/**/abc /long/** /** Spring 的 AbstractUrlHandlerMapping 使用 : 最长匹配优先 ; 如请求为 /long/long 将匹配第一个 /long/long, 但请求 /long/acd 则将匹配 /long/**, 如请求 /long/aa/abc 则匹配 /long/**/abc, 如请求 /abc 则将匹配 /** UrlFilenameViewController 还提供了如下属性 : prefix: 生成逻辑视图名的前缀 ; suffix: 生成逻辑视图名的后缀 ; protected String postprocessviewname(string viewname) { return getprefix() + viewname + getsuffix(); <bean name="/*.htm" class="org.springframework.web.servlet.mvc.urlfilenameviewcontroller"> <property name="prefix" value="test"/> <property name="suffix" value="test"/> </bean> 当 prefix= test,suffix= test, 如上所示的 /*.htm: 可以匹配如 /abc.htm, 但逻辑视图名将变为 testabctest 4.15 MultiActionController 之前学过的控制器如 AbstractCommandController SimpleFormController 等一般对应一个功能处理方法 ( 如新增 ), 如果 我要实现比如最简单的用户增删改查 (CRUD Create-Read-Update-Delete), 那该怎么办呢? 解决方案 1 每一个功能对应一个控制器, 如果是 CRUD 则需要四个控制器, 但这样我们的控制器会暴增, 肯定不可取 ; 2 使用 Spring Web MVC 提供的 MultiActionController, 用于支持在一个控制器里添加多个功能处理方法, 即将多个请 求的处理方法放置到一个控制器里, 这种方式不错 问题 1 MultiActionController 如何将不同的请求映射不同的请求的功能处理方法呢?

57 Spring Web MVC 提供了 MethodNameResolver( 方法名解析器 ) 用于解析当前请求到需要执行的功能处理方法的方法名 默认使用 InternalPathMethodNameResolver 实现类, 另外还提供了 ParameterMethodNameResolver 和 PropertiesMethodNameResolver, 当然我们也可以自己来实现, 稍候我们仔细研究下它们是如何工作的 2 那我们的功能处理方法应该怎么写呢? public (ModelAndView Map String void) actionname(httpservletrequest request, HttpServletResponse response, [,HttpSession session] [,AnyObject]); 哦, 原来如此, 我们只需要按照如上格式写我们的功能处理方法即可 ; 此处需要注意一下几点 : 1 返回值: 即模型和视图部分 ; ModelAndView: 模型和视图部分, 之前已经见过了 ; Map: 只返回模型数据, 逻辑视图名会根据 RequestToViewNameTranslator 实现类来计算, 稍候讨论 ; String: 只返回逻辑视图名 ; void: 表示该功能方法直接写出 response 响应 ( 如果其他返回值类型 ( 如 Map) 返回 null 则和 void 进行相同的处理 ); 2 actionname: 功能方法名字 ; 由 methodnameresolver 根据请求信息解析功能方法名, 通过反射调用 ; 3 形参列表: 顺序固定, [] 表示可选, 我们来看看几个示例吧 : // 表示到新增页面 public ModelAndView toadd(httpservletrequest request, HttpServletResponse response); // 表示新增表单提交, 在最后可以带着命令对象 public ModelAndView add(httpservletrequest request, HttpServletResponse response, UserModel user); // 列表, 但只返回模型数据, 视图名会通过 RequestToViewNameTranslator 实现来计算 public Map list(httpservletrequest request, HttpServletResponse response); // 文件下载, 返回值类型为 void, 表示该功能方法直接写响应 public void filedownload(httpservletrequest request, HttpServletResponse response) // 第三个参数可以是 session public ModelAndView sessionwith(httpservletrequest request, HttpServletResponse response, HttpSession session); // 如果第三个参数是 session, 那么第四个可以是命令对象, 顺序必须是如下顺序 public void sessionandcommandwith(httpservletrequest request, HttpServletResponse response, HttpSession session, UserModel user) 4 异常处理方法,MultiActionController 提供了简单的异常处理, 即在请求的功能处理过程中遇到异常会交给异常处理方法进行处理, 式如下所示 : public ModelAndView anymeaningfulname(httpservletrequest request, HttpServletResponse response, ExceptionClass exception) MultiActionController 会使用最接近的异常类型来匹配对应的异常处理方法, 示例如下所示 : // 处理 PayException public ModelAndView processpayexception(httpservletrequest request, HttpServletResponse response, PayException ex) // 处理 Exception public ModelAndView processexception(httpservletrequest request, HttpServletResponse response, Exception ex) MultiActionController 类实现 类定义 :public class MultiActionController extends AbstractController implements LastModified, 继承了 AbstractController, 并实现了 LastModified 接口, 默认返回 -1; 核心属性 : delegate: 功能处理的委托对象, 即我们要调用请求处理方法所在的对象, 默认是 this; methodnameresolver: 功能处理方法名解析器, 即根据请求信息来解析需要执行的 delegate 的功能处理方法的方法名 核心方法 :

58 // 判断方法是否是功能处理方法 private boolean ishandlermethod(method method) { // 得到方法返回值类型 Class returntype = method.getreturntype(); // 返回值类型必须是 ModelAndView Map String void 中的一种, 否则不是功能处理方法 if (ModelAndView.class.equals(returnType) Map.class.equals(returnType) String.class.equals(returnType) void.class.equals(returntype)) { Class[] parametertypes = method.getparametertypes(); // 功能处理方法参数个数必须 >=2, 且第一个是 HttpServletRequest 类型 第二个是 HttpServletResponse // 且不能 Controller 接口的 handlerequest(httpservletrequest request, HttpServletResponse response), 这个方法是由系统调用 return (parametertypes.length >= 2 && HttpServletRequest.class.equals(parameterTypes[0]) && HttpServletResponse.class.equals(parameterTypes[1]) &&!("handlerequest".equals(method.getname()) && parametertypes.length == 2)); return false; // 是否是异常处理方法 private boolean isexceptionhandlermethod(method method) { // 异常处理方法必须是功能处理方法且参数长度为 3 第三个参数类型是 Throwable 子类 return (ishandlermethod(method) && method.getparametertypes().length == 3 && Throwable.class.isAssignableFrom(method.getParameterTypes()[2])); private void registerhandlermethods(object delegate) { // 缓存 Map 清空 this.handlermethodmap.clear(); this.lastmodifiedmethodmap.clear(); this.exceptionhandlermap.clear(); // 得到委托对象的所有 public 方法 Method[] methods = delegate.getclass().getmethods(); for (Method method : methods) { // 验证是否是异常处理方法, 如果是放入 exceptionhandlermap 缓存 map if (isexceptionhandlermethod(method)) { registerexceptionhandlermethod(method); // 验证是否是功能处理方法, 如果是放入 handlermethodmap 缓存 map else if (ishandlermethod(method)) { registerhandlermethod(method); registerlastmodifiedmethodifexists(delegate, method);

59 protected ModelAndView handlerequestinternal(httpservletrequest request, HttpServletResponse response) throws Exception { try { //1 使用 methodnameresolver 方法名解析器根据请求解析到要执行的功能方法的方法名 String methodname = this.methodnameresolver.gethandlermethodname(request); //2 调用功能方法( 通过反射调用, 此处就粘贴代码了 ) return invokenamedmethod(methodname, request, response); catch (NoSuchRequestHandlingMethodException ex) { return handlenosuchrequesthandlingmethod(ex, request, response); 接下来, 我们看一下 MultiActionController 如何使用 MethodNameResolver 来解析请求到功能处理方法的方法名 MethodNameResolver 1 InternalPathMethodNameResolver:MultiActionController 的默认实现, 提供从请求 URL 路径解析功能方法的方法名, 从请求的最后一个路径 (/) 开始, 并忽略扩展名 ; 如请求 URL 是 /user/list.html, 则解析的功能处 理方法名为 list, 即调用 list 方法 该解析器还可以指定前缀和后缀, 通过 prefix 和 suffix 属性, 如指定 prefix= test_, 则功能方法名将变为 test_list; 2 ParameterMethodNameResolver: 提供从请求参数解析功能处理方法的方法名, 并按照如下顺序进行解析 : (1 methodparamnames: 根据请求的参数名解析功能方法名 ( 功能方法名和参数名同名 ); <property name="methodparamnames" value="list,create,update"/> 如上配置时, 如果请求中含有参数名 list create update 时, 则功能处理方法名为 list create update, 这种 方式的可以在当一个表单有多个提交按钮时使用, 不同的提交按钮名字不一样即可 ParameterMethodNameResolver 也考虑到图片提交按钮提交问题 : <input type="image" name="list"> 和 submit 类似可以提交表单, 单击该图片后会发送两个参数 list.x=x 轴 坐标 和 list.y=y 轴坐标 ( 如提交后会变为 list.x=7&list.y=5); 因此我们配置的参数名 ( 如 list) 在会加上.x 和.y 进行匹配 for (String suffix : SUBMIT_IMAGE_SUFFIXES) {//SUBMIT_IMAGE_SUFFIXES {.x,.y if (request.getparameter(name + suffix)!= null) {// name 是我们配置的 methodparamnames return true; (2 paramname: 根据请求参数名的值解析功能方法名, 默认的参数名是 action, 即请求的参数中含有 action=query, 则功能处理方法名为 query; (3 logicalmappings: 逻辑功能方法名到真实功能方法名映射, 如下所示 : <property name="logicalmappings"> <props> <prop key="dolist">list</prop> </props> </property> 即如果步骤 1 或 2 解析出逻辑功能方法名为 dolist( 逻辑的 ), 将会被重新映射为 list 功能方法名 ( 真正执行的 )

60 (4 defaultmethodname: 默认的方法名, 当以上策略失败时默认调用的方法名 3 PropertiesMethodNameResolver: 提供自定义的从请求 URL 解析功能方法的方法名, 使用一组用户自定义的模式到功能方法名的映射, 映射使用 Properties 对象存放, 具体配置示例如下 : <bean id="propertiesmethodnameresolver" class="org.springframework.web.servlet.mvc.multiaction.propertiesmethodnameresolver"> <property name="mappings"> <props> <prop key="/create">create</prop> <prop key="/update">update</prop> <prop key="/delete">delete</prop> <prop key="/list">list</prop> <!-- 默认的行为 --> <prop key="/**">list</prop> </props> </property> </bean> 对于 /create 请求将调用 create 方法,Spring 内部使用 PathMatcher 进行匹配 ( 默认实现是 AntPathMatcher) RequestToViewNameTranslator 用于直接将请求转换为逻辑视图名 默认实现为 DefaultRequestToViewNameTranslator 1 DefaultRequestToViewNameTranslator: 将请求 URL 转换为逻辑视图名, 默认规则如下 : 上下文 /list > 逻辑视图名为 list 上下文 /list.html > 逻辑视图名为 list( 默认删除扩展名 ) 上下文 /user/list.html > 逻辑视图名为 user/list 示例 (1 控制器 UserController

61 package cn.javass.chapter4.web.controller; // 省略 import public class UserController extends MultiActionController { // 用户服务类 private UserService userservice; // 逻辑视图名通过依赖注入方式注入, 可配置 private String createview; private String updateview; private String deleteview; private String listview; private String redirecttolistview; // 省略 setter/getter public String create(httpservletrequest request, HttpServletResponse response, UserModel user) { if("get".equals(request.getmethod())) { // 如果是 get 请求我们转向新增页面 return getcreateview(); userservice.create(user); // 直接重定向到列表页面 return getredirecttolistview(); public ModelAndView update(httpservletrequest request, HttpServletResponse response, UserModel user) { if("get".equals(request.getmethod())) { // 如果是 get 请求我们转向更新页面 ModelAndView mv = new ModelAndView(); // 查询要更新的数据 mv.addobject("command", userservice.get(user.getusername())); mv.setviewname(getupdateview()); return mv; userservice.update(user); // 直接重定向到列表页面 return new ModelAndView(getRedirectToListView());

62 public ModelAndView delete(httpservletrequest request, HttpServletResponse response, UserModel user) { if("get".equals(request.getmethod())) { // 如果是 get 请求我们转向删除页面 ModelAndView mv = new ModelAndView(); // 查询要删除的数据 mv.addobject("command", userservice.get(user.getusername())); mv.setviewname(getdeleteview()); return mv; userservice.delete(user); // 直接重定向到列表页面 return new ModelAndView(getRedirectToListView()); public ModelAndView list(httpservletrequest request, HttpServletResponse response) { ModelAndView mv = new ModelAndView(); mv.addobject("userlist", userservice.list()); mv.setviewname(getlistview()); return mv; // 如果使用委托方式, 命令对象名称只能是 command protected String getcommandname(object command) { // 命令对象的名字默认 command return "command"; 增删改 : 如果是 GET 请求方法, 则表示到展示页面,POST 请求方法表示真正的功能操作 ; getcommandname: 表示是命令对象名字, 默认 command, 对于委托对象实现方式无法改变, 因此我们就使用默认的吧 (2 spring 配置文件 chapter4-servlet.xml <bean id="userservice" class="cn.javass.chapter4.service.userservice"/> <bean name="/user/**" class="cn.javass.chapter4.web.controller.usercontroller"> <property name="userservice" ref="userservice"/> <property name="createview" value="user/create"/> <property name="updateview" value="user/update"/> <property name="deleteview" value="user/delete"/> <property name="listview" value="user/list"/> <property name="redirecttolistview" value="redirect:/user/list"/> <!-- 使用 PropertiesMethodNameResolver 来解析功能处理方法名 --> <!--property name="methodnameresolver" ref="propertiesmethodnameresolver"/--> </bean> userservice: 用户服务类, 实现业务逻辑 ; 依赖注入 : 对于逻辑视图页面通过依赖注入方式注入,redirectToListView 表示增删改成功后重定向的页面, 防止重复表单提交 ;

63 默认使用 InternalPathMethodNameResolver 解析请求 URL 到功能方法名 (3 视图页面 (3.1 list 页面 (WEB-INF/jsp/user/list.jsp) <a href="${pagecontext.request.contextpath/user/create"> 用户新增 </a><br/> <table border="1" width="50%"> <tr> <th> 用户名 </th> <th> 真实姓名 </th> <th> 操作 </th> </tr> <c:foreach items="${userlist" var="user"> <tr> <td>${user.username </td> <td>${user.realname </td> <td> <a href="${pagecontext.request.contextpath/user/update?username=${user.username"> 更新 </a> <a href="${pagecontext.request.contextpath/user/delete?username=${user.username"> 删除 </a> </td> </tr> </c:foreach> </table> (3.2 update 页面 (WEB-INF/jsp/user/update.jsp) <form action="${pagecontext.request.contextpath/user/update" method="post"> 用户名 : <input type="text" name="username" value="${command.username"/><br/> 真实姓名 :<input type="text" name="realname" value="${command.realname"/><br/> <input type="submit" value=" 更新 "/> </form> (4 测试 : 默认的 InternalPathMethodNameResolver 将进行如下解析 : >list 方法名 ; >create 方法名 ; >update 功能处理方法名 ; >delete 功能处理方法名 我们可以将默认的 InternalPathMethodNameResolver 改为 PropertiesMethodNameResolver:

64 <bean id="propertiesmethodnameresolver" class="org.springframework.web.servlet.mvc.multiaction.propertiesmethodnameresolver"> <property name="mappings"> <props> <prop key="/create">create</prop> <prop key="/update">update</prop> <prop key="/delete">delete</prop> <prop key="/list">list</prop> <prop key="/**">list</prop><!-- 默认的行为 --> </props> </property> </bean> <bean name="/user/**" class="cn.javass.chapter4.web.controller.usercontroller"> <! 省略其他配置, 详见配置文件 --> <!-- 使用 PropertiesMethodNameResolver 来解析功能处理方法名 --> <property name="methodnameresolver" ref="propertiesmethodnameresolver"/> </bean> /** 表示默认解析到 list 功能处理方法 如上配置方式可以很好的工作, 但必须继承 MultiActionController, Spring Web MVC 提供给我们无需继承 MultiActionController 实现方式, 即使有委托对象方式, 继续往下看吧 委托方式实现 (1 控制器 UserDelegate 将 UserController 复制一份, 改名为 UserDelegate, 并把继承 MultiActionController 去掉即可, 其他无需改变 (2 spring 配置文件 chapter4-servlet.xml <! 委托对象 --> <bean id="userdelegate" class="cn.javass.chapter4.web.controller.userdelegate"> <property name="userservice" ref="userservice"/> <property name="createview" value="user2/create"/> <property name="updateview" value="user2/update"/> <property name="deleteview" value="user2/delete"/> <property name="listview" value="user2/list"/> <property name="redirecttolistview" value="redirect:/user2/list"/> </bean> <! 控制器对象 --> <bean name="/user2/**" class="org.springframework.web.servlet.mvc.multiaction.multiactioncontroller"> <property name="delegate" ref="userdelegate"/> <property name="methodnameresolver" ref="parametermethodnameresolver"/> </bean> delegate: 控制器对象通过 delegate 属性指定委托对象, 即实际调用 delegate 委托对象的功能方法 methodnameresolver: 此处我们使用 ParameterMethodNameResolver 解析器 ;

65 <! ParameterMethodNameResolver --> <bean id="parametermethodnameresolver" class="org.springframework.web.servlet.mvc.multiaction.parametermethodnameresolver"> <!-- 1 根据请求参数名解析功能方法名 --> <property name="methodparamnames" value="create,update,delete"/> <!-- 2 根据请求参数名的值解析功能方法名 --> <property name="paramname" value="action"/> <!-- 3 逻辑方法名到真实方法名的映射 --> <property name="logicalmappings"> <props> <prop key="dolist">list</prop> </props> </property> <! 4 默认执行的功能处理方法 --> <property name="defaultmethodname" value="list"/> </bean> 1 methodparamnames:create,update,delete, 当请求中有参数名为这三个的将被映射为功能方法名, 如 <input type="submit" name="create" value=" 新增 "/> 提交后解析得到的功能方法名为 create; 2 paramname : 当请求中有参数名为 action, 则将值映射为功能方法名, 如 <input type="hidden" name="action" value="delete"/>, 提交后解析得到的功能方法名为 delete; 3 logicalmappings: 逻辑功能方法名到真实功能方法名的映射, 如 : 首先请求参数 action=dolist, 则第二步解析得到逻辑功能方法名为 dolist; 本步骤会把 dolist 再转换为真实的功能方法名 list 4 defaultmethodname: 以上步骤如果没有解析到功能处理方法名, 默认执行的方法名 (3 视图页面 (3.1 list 页面 (WEB-INF/jsp/user2/list.jsp) <a href="${pagecontext.request.contextpath/user2?action=create"> 用户新增 </a><br/> <table border="1" width="50%"> <tr> <th> 用户名 </th> <th> 真实姓名 </th> <th> 操作 </th> </tr> <c:foreach items="${userlist" var="user"> <tr> <td>${user.username </td> <td>${user.realname </td> <td> <a href="${pagecontext.request.contextpath/user2?action=update&username=${user.username"> 更新 </a> <a href="${pagecontext.request.contextpath/user2?action=delete&username=${user.username"> 删除 </a> </td> </tr> </c:foreach> </table>

66 (3.2 update 页面 (WEB-INF/jsp/user2/update.jsp) < form action="${pagecontext.request.contextpath/user2" method="post"> <input type="hidden" name="action" value="update"/> 用户名 : <input type="text" name="username" value="${command.username"/><br/> 真实姓名 :<input type="text" name="realname" value="${command.realname"/><br/> <input type="submit" value=" 更新 "/> </form> 通过参数 name="action" value="update" 来指定要执行的功能方法名 update (3.3 create 页面 (WEB-INF/jsp/user2/create.jsp) < form action="${pagecontext.request.contextpath/user2" method="post"> 用户名 : <input type="text" name="username" value="${command.username"/><br/> 真实姓名 :<input type="text" name="realname" value="${command.realname"/><br/> <input type="submit" name="create" value=" 新增 "/> </form> 通过参数 name="create" 来指定要执行的功能方法名 create (4 测试 : 使用 ParameterMethodNameResolver 将进行如下解析 : >create 功能处理方法名 ( 参数名映射 ); >create 功能处理方法名 ( 参数值映射 ); >update 功能处理方法名 ; >update 功能处理方法名 ; >delete 功能处理方法名 ; >delete 功能处理方法名 ; > 通过 logicalmappings 解析为 list 功能处理方法 > 通过 logicalmappings 解析为 list 功能处理方法 > 默认的功能处理方法名 list( 默认 )

67 4.16 数据类型转换和数据验证 流程 : 1 首先创建数据绑定器, 在此此会创建 ServletRequestDataBinder 类的对象, 并设置 messagecodesresolver( 错误码解 析器 ); 2 提供第一个扩展点, 初始化数据绑定器, 在此处我们可以覆盖该方法注册自定义的 PropertyEditor( 请求参数 > 命令对象属性的转换 ); 3 进行数据绑定, 即请求参数 > 命令对象的绑定 ; 4 提供第二个扩展点, 数据绑定完成后的扩展点, 此处可以实现一些自定义的绑定动作 ; 5 验证器对象的验证, 验证器通过 validators 注入, 如果验证失败, 需要把错误信息放入 Errors( 此处使用 BindException 实现 ); 6 提供第三个扩展点, 此处可以实现自定义的绑定 / 验证逻辑 ; 7 将 errors 传入功能处理方法进行处理, 功能方法应该判断该错误对象是否有错误进行相应的处理 数据类型转换 请求参数 (String) > 命令对象属性 ( 可能是任意类型 ) 的类型转换, 即数据绑定时的类型转换, 使用 PropertyEditor 实现绑定时的类型转换 一 Spring 内建的 PropertyEditor 如下所示 :

68 类名说明默认是否注册 ByteArrayPropertyEditor String< >byte[] ClassEditor CustomBooleanEditor CustomCollectionEditor String< >Class 当类没有发现抛出 IllegalArgumentException String< >Boolean true/yes/on/1 转换为 true,false/no/off/0 转换为 false 数组 /Collection >Collection 普通值 >Collection( 只包含一个对象 ) 如 String >Collection 不允许 Collection >String( 单方向转换 ) CustomNumberEditor String< >Number(Integer Long Double) FileEditor String< >File InputStreamEditor LocaleEditor String >InputStream 单向的, 不能 InputStream >String String< >Locale, (String 的形式为 [ 语言 ]_[ 国家 ]_[ 变量 ], 这与 Local 对象的 tostring() 方法得到的结果相同 ) PatternEditor String< >Pattern PropertiesEditor String< >java.lang.properties URLEditor String< >URL StringTrimmerEditor 一个用于 trim 的 String 类型的属性编辑器 如默认删除两边的空格,charsToDelete 属性 : 可以设置为其他字符 emptyasnull 属性 : 将一个空字符串转化为 null 值的选项 CustomDateEditor String< >java.util.date 二 Spring 内建的 PropertyEditor 支持的属性 ( 符合 JavaBean 规范 ) 操作 : 表达式 username schooinfo.schooltype hobbylist[0] map[key] 属性 username 设值 / 取值说明 设值方法 setusername()/ 取值方法 getusername() 或 isusername() 属性 schooinfo 的嵌套属性 schooltype 设值方法 getschooinfo().setschooltype()/ 取值方法 getschooinfo().getschooltype() 属性 hobbylist 的第一个元素 索引属性可能是一个数组 列表 其它天然有序的容器 属性 map(java.util.map 类型 ) map 中 key 对应的值 三 示例 : 接下来我们写自定义的属性编辑器进行数据绑定 : (1 模型对象 :

69 package cn.javass.chapter4.model; // 省略 import public class DataBinderTestModel { private String username; private boolean bool;//boolean 值测试 private SchoolInfoModel schooinfo; private List hobbylist;// 集合测试, 此处可以改为数组 /Set 进行测试 private Map map;//map 测试 private PhoneNumberModel phonenumber;//string-> 自定义对象的转换测试 private Date date;// 日期类型测试 private UserState state;//string >Enum 类型转换测试 // 省略 getter/setter package cn.javass.chapter4.model; // 如格式 public class PhoneNumberModel { private String areacode;// 区号 private String phonenumber;// 电话号码 // 省略 getter/setter (2 PhoneNumber 属性编辑器 前台输入如 自动转换为 PhoneNumberModel package cn.javass.chapter4.web.controller.support.editor; // 省略 import public class PhoneNumberEditor extends PropertyEditorSupport { Pattern pattern = public void setastext(string text) throws IllegalArgumentException { if(text == null!stringutils.haslength(text)) { setvalue(null); // 如果没值, 设值为 null Matcher matcher = pattern.matcher(text); if(matcher.matches()) { PhoneNumberModel phonenumber = new PhoneNumberModel(); phonenumber.setareacode(matcher.group(1)); phonenumber.setphonenumber(matcher.group(2)); setvalue(phonenumber); else { throw new IllegalArgumentException(String.format(" 类型转换失败, 需要格式 [ ], 但格式是 [%s]", public String getastext() { PhoneNumberModel phonenumber = ((PhoneNumberModel)getValue()); return phonenumber == null? "" : phonenumber.getareacode() + "-" + phonenumber.getphonenumber();

70 PropertyEditorSupport: 一个 PropertyEditor 的支持类 ; setastext: 表示将 String >PhoneNumberModel, 根据正则表达式进行转换, 如果转换失败抛出异常, 则接下来的验证器会进行验证处理 ; getastext: 表示将 PhoneNumberModel >String (3 控制器需要在控制器注册我们自定义的属性编辑器 此处我们使用 AbstractCommandController, 因为它继承了 BaseCommandController, 拥有绑定流程 package cn.javass.chapter4.web.controller; // 省略 import public class DataBinderTestController extends AbstractCommandController { public DataBinderTestController() { setcommandclass(databindertestmodel.class); // 设置命令对象 setcommandname("databindertest");// protected ModelAndView handle(httpservletrequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { command); // 输出 command 对象看看是否绑定正确 System.out.println(command); return new protected void initbinder(httpservletrequest request, ServletRequestDataBinder binder) throws Exception { super.initbinder(request, binder); // 注册自定义的属性编辑器 //1 日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateeditor = new CustomDateEditor(df, true); // 表示如果命令对象有 Date 类型的属性, 将使用该属性编辑器进行类型转换 binder.registercustomeditor(date.class, dateeditor); // 自定义的电话号码编辑器 binder.registercustomeditor(phonenumbermodel.class, new PhoneNumberEditor()); initbinder: 第一个扩展点, 初始化数据绑定器, 在此处我们注册了两个属性编辑器 ; CustomDateEditor: 自定义的日期编辑器, 用于在 String< > 日期之间转换 ; binder.registercustomeditor(date.class, dateeditor): 表示如果命令对象是 Date 类型, 则使用 dateeditor 进行类型转换 ; PhoneNumberEditor: 自定义的电话号码属性编辑器用于在 String< > PhoneNumberModel 之间转换 ; binder.registercustomeditor(phonenumbermodel.class, new PhoneNumberEditor()): 表示如果命 令对象是 PhoneNumberModel 类型, 则使用 PhoneNumberEditor 进行类型转换 ; (4 spring 配置文件 chapter4-servlet.xml <bean name="/databind" class="cn.javass.chapter4.web.controller.databindertestcontroller"/> (5 视图页面 (WEB-INF/jsp/bindAndValidate/success.jsp)

71 EL phonenumber:${databindertest.phonenumber<br/> EL state:${databindertest.state<br/> EL date:${databindertest.date<br/> 视图页面的数据没有预期被格式化, 如何进行格式化显示呢? 请参考 第七章注解式控制器的数据验证 类型转换及格式 化 (6 测试: 1 在浏览器地址栏输入请求的 URL, 如 =program&hobbylist[1]=music&map[key1]=value1&map[key2]=value2&phonenumber= &date= :48:48&state=blocked 2 控制器输出的内容 : DataBinderTestModel [username=zhang, bool=true, schooinfo=schoolinfomodel [schooltype=null, schoolname=null, specialty=computer], hobbylist=[program, music], map={key1=value1, key2=value2, phonenumber=phonenumbermodel [areacode=010, phonenumber= ], date=sun Mar 18 16:48:48 CST 2012, state= 锁定 ] 类型转换如图所示 : 四 注册 PropertyEditor 1 使用 WebDataBinder 进行控制器级别注册 PropertyEditor( 控制器独享 ) 如 三 示例 中所使用的方式, 使用 WebDataBinder 注册控制器级别的 PropertyEditor, 这种方式注册的 PropertyEditor 只对当前控制器独享, 即其他的控制器不会自动注册这个 PropertyEditor, 如果需要还需要再注册一下 2 使用 WebBindingInitializer 批量注册 PropertyEditor 如果想在多个控制器同时注册多个相同的 PropertyEditor 时, 可以考虑使用 WebBindingInitializer 示例 : (1 实现 WebBindingInitializer

72 package cn.javass.chapter4.web.controller.support.initializer; // 省略 import public class MyWebBindingInitializer implements WebBindingInitializer public void initbinder(webdatabinder binder, WebRequest request) { // 注册自定义的属性编辑器 //1 日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateeditor = new CustomDateEditor(df, true); // 表示如果命令对象有 Date 类型的属性, 将使用该属性编辑器进行类型转换 binder.registercustomeditor(date.class, dateeditor); // 自定义的电话号码编辑器 binder.registercustomeditor(phonenumbermodel.class, new PhoneNumberEditor()); 通过实现 WebBindingInitializer 并通过 binder 注册多个 PropertyEditor (2 修改 三 示例 中的 DataBinderTestController, 注释掉 initbinder 方法 ; (3 修改 chapter4-servlet.xml 配置文件 : <!-- 注册 WebBindingInitializer 实现 --> <bean id="mywebbindinginitializer" class="cn.javass.chapter4.web.controller.support.initializer.mywebbindinginitializer"/> <bean name="/databind" class="cn.javass.chapter4.web.controller.databindertestcontroller"> <!-- 注入 WebBindingInitializer 实现 --> <property name="webbindinginitializer" ref="mywebbindinginitializer"/> </bean> (4 尝试访问 三 示例 中的测试 URL 即可成功 使用 WebBindingInitializer 的好处是当你需要在多个控制器中需要同时使用多个相同的 PropertyEditor 可以在 WebBindingInitializer 实现中注册, 这样只需要在控制器中注入 WebBindingInitializer 即可注入多个 PropertyEditor 3 全局级别注册 PropertyEditor( 全局共享 ) 只需要将我们自定义的 PropertyEditor 放在和你的模型类同包下即可, 且你的 Editor 命名规则必须是 模型类名 Editor, 这样 Spring 会自动使用标准 JavaBean 架构进行自动识别, 如图所示 : 此时我们把 DataBinderTestController 的 binder.registercustomeditor(phonenumbermodel.class, PhoneNumberEditor()); 注释掉, 再尝试访问 三 示例 中的测试 URL 即可成功 new

73 这种方式不仅仅在使用 Spring 时可用, 在标准的 JavaBean 等环境都是可用的, 可以认为是全局共享的 ( 不仅仅是 Spring 环境 ) PropertyEditor 被限制为只能 String< >Object 之间转换, 不能 Object< >Object,Spring3 提供了更强大的类 型转换 (Type Conversion) 支持, 它可以在任意对象之间进行类型转换, 不仅仅是 String< >Object 如果我在地址栏输入错误的数据, 即数据绑定失败,Spring Web MVC 该如何处理呢? 如果我输入的数据不合法呢? 如 用户名输入 100 个字符 ( 超长了 ) 那又该怎么处理呢? 出错了需要错误消息, 那错误消息应该是硬编码? 还是可配置 呢? 接下来我们来学习一下数据验证器进行数据验证吧 数据验证 1 数据绑定失败: 比如需要数字却输入了字母 ; 2 数据不合法: 可以认为是业务错误, 通过自定义验证器验证, 如用户名长度必须在 5-20 之间, 我们却输入了 100 个字符等 ; 3 错误对象: 当我们数据绑定失败或验证失败后, 错误信息存放的对象, 我们叫错误对象, 在 Spring Web MVC 中 Errors 是具体的代表者 ; 线程不安全对象 ; 4 错误消息: 是硬编码, 还是可配置? 实际工作应该使用配置方式, 我们只是把错误码 (errorcode) 放入错误对象, 在展示时读取相应的错误消息配置文件来获取要显示的错误消息 (errormessage); 验证流程 1 首先进行数据绑定验证, 如果验证失败会通过 MessageCodesResolver 生成错误码放入 Errors 错误对象 ; 2 数据不合法验证, 通过自定义的验证器验证, 如果失败需要手动将错误码放入 Errors 错误对象 ; 错误对象和错误消息 错误对象的代表者是 Errors 接口, 并且提供了几个实现者, 在 Spring Web MVC 中我们使用的是如下实现 : 相关的错误方法如下 : Errors: 存储和暴露关于数据绑定错误和验证错误相关信息的接口, 提供了相关存储和获取错误消息的方法 :

74 package org.springframework.validation; public interface Errors { //========================= 全局错误消息 ( 验证 / 绑定对象全局的 )============================= // 注册一个全局的错误码 () void reject(string errorcode); // 注册一个全局的错误码, 当根据 errorcode 没有找到相应错误消息时, 使用 defaultmessage 作为错误消息 void reject(string errorcode, String defaultmessage); // 注册一个全局的错误码, 当根据 errorcode 没有找到相应错误消息时 ( 带错误参数的 ), 使用 defaultmessage 作为错误消息 void reject(string errorcode, Object[] errorargs, String defaultmessage); //========================= 全局错误消息 ( 验证 / 绑定整个对象的 )============================= //========================= 局部错误消息 ( 验证 / 绑定对象字段的 )============================= // 注册一个对象字段的错误码,field 指定验证失败的字段名 void rejectvalue(string field, String errorcode); void rejectvalue(string field, String errorcode, String defaultmessage); void rejectvalue(string field, String errorcode, Object[] errorargs, String defaultmessage); //========================= 局部错误消息 ( 验证 / 绑定对象字段的 )============================= boolean haserrors(); //// 是否有错误 boolean hasglobalerrors(); // 是否有全局错误 boolean hasfielderrors(); // 是否有字段错误 Object getfieldvalue(string field); // 返回当前验证通过的值, 或验证失败时失败的值 ; getfieldvalue: 可以得到验证失败的失败值, 这是其他 Web 层框架很少支持的, 这样就可以给用户展示出错时的值 ( 而不是空或其他的默认值等 ) BindingResult: 代表数据绑定的结果, 继承了 Errors 接口 BindException: 代表数据绑定的异常, 它继承 Exception, 并实现了 BindingResult, 这是内部使用的错误对象 示例 : (1 控制器 package cn.javass.chapter4.web.controller; // 省略 import public class ErrorController extends AbstractCommandController { public ErrorController() { setcommandclass(databindertestmodel.class); protected ModelAndView handle(httpservletrequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { // 表示用户名不为空 errors.reject("username.not.empty"); // 带有默认错误消息 errors.reject("username.not.empty1", " 用户名不能为空 1"); // 带有参数和默认错误消息 errors.reject("username.length.error", new Object[]{5, 10); // 得到错误相关的模型数据 Map model = errors.getmodel(); return new ModelAndView("bindAndValidate/error", model);

75 errors.reject("username.not.empty"): 注册全局错误码 username.not.empty, 我们必须提供 messagesource 来提供错 误码 username.not.empty 对应的错误信息 ( 如果没有会抛出 NoSuchMessageException 异常 ); errors.reject("username.not.empty1", " 用户名不能为空 1") : 注册全局错误码 username.not.empty1, 如果从 messagesource 没没有找到错误码 username.not.empty1 对应的错误信息, 则将显示默认消息 用户名不能为空 1 ; errors.reject("username.length.error", new Object[]{5, 10): 错误码为 username.length.error, 而且错误信息需要两个参数, 如我们在我们的配置文件中定义 用户名长度不合法, 长度必须在 {0 到 {1 之间, 则实际的错误消息为 用户名长度不合法, 长度必须在 5 到 10 之间 errors.getmodel(): 当有错误信息时, 一定将 errors.getmodel() 放入我们要返回的 ModelAndView 中, 以便使用里边的错误对象来显示错误信息 (2 spring 配置文件 chapter4-servlet.xml <bean id="messagesource" class="org.springframework.context.support.reloadableresourcebundlemessagesource"> <property name="basename" value="classpath:messages"/> <property name="fileencodings" value="utf-8"/> <property name="cacheseconds" value="120"/> </bean> <bean name="/error" class="cn.javass.chapter4.web.controller.errorcontroller"/> messagesource: 用于获取错误码对应的错误消息的, 而且 bean 名字默认必须是 messagesource messages.properties( 需要执行 NativeToAscii) username.not.empty= 用户名不能为空 username.length.error= 用户名长度不合法, 长度必须在 {0 到 {1 之间 (3 视图页面(WEB-INF/jsp/bindAndValidate/error.jsp) <%@taglib prefix="form" uri=" %> <!-- 表单的默认命令对象名为 command --> <form:form commandname="command"> <form:errors path="*"></form:errors> </form:form> form 标签库 : 此处我们使用了 spring 的 form 标签库 ; <form:form commandname="command">: 表示我们的表单标签,commandName 表示绑定的命令对象名字, 默认为 command; <form:errors path="*"></form:errors>: 表示显示错误信息的标签, 如果 path 为 * 表示显示所有错误信息 接下来我们来看一下数据绑定失败和数据不合法时, 如何处理 数据绑定失败 如我们的 DataBinderTestModel 类 : bool:boolean 类型, 此时如果我们前台传入非兼容的数据, 则会数据绑定失败 ; date:date 类型, 此时如果我们前台传入非兼容的数据, 同样会数据绑定失败 ; phonenumber: 自定义的 PhoneNumberModel 类型, 如果如果我们前台传入非兼容的数据, 同样会数据绑定失败

76 示例 : (1 控制器,DataBinderErrorTestController package cn.javass.chapter4.web.controller; // 省略 import public class DataBinderErrorTestController extends SimpleFormController { public DataBinderErrorTestController() { setcommandclass(databindertestmodel.class); protected ModelAndView showform(httpservletrequest request, HttpServletResponse response, BindException errors) throws Exception { // 如果表单提交有任何错误都会再回到表单展示页面 System.out.println(errors); return super.showform(request, response, protected void dosubmitaction(object command) throws Exception { System.out.println(command); // 表单提交成功 ( 数据绑定成功 ) protected void initbinder(httpservletrequest request, ServletRequestDataBinder binder) throws Exception { super.initbinder(request, binder); // 注册自定义的属性编辑器 //1 日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateeditor = new CustomDateEditor(df, true); // 表示如果命令对象有 Date 类型的属性, 将使用该属性编辑器进行类型转换 binder.registercustomeditor(date.class, dateeditor); // 自定义的电话号码编辑器 binder.registercustomeditor(phonenumbermodel.class, new PhoneNumberEditor()); 此处我们使用 SimpleFormController; showform: 展示表单, 当提交表单有任何数据绑定错误会再回到该方法进行表单输入 ( 在此处我们打印错误对象 ); dosubmitaction: 表单提交成功, 只要当表单的数据到命令对象绑定成功时, 才会执行 ; (2 spring 配置文件 chapter4-servlet.xml <bean name="/databinderror" class="cn.javass.chapter4.web.controller.databindererrortestcontroller"> <property name="formview" value="bindandvalidate/input"/> <property name="successview" value="bindandvalidate/success"/> </bean> (3 视图页面 (WEB-INF/jsp/bindAndValidate/ input.jsp)

77 page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> prefix="form" uri=" %> <!-- 表单的命令对象名为 databindertest --> <form:form commandname="databindertest"> <form:errors path="*" cssstyle="color:red"></form:errors><br/><br/> bool:<form:input path="bool"/><br/> phonenumber:<form:input path="phonenumber"/><br/> date:<form:input path="date"/><br/> <input type="submit" value=" 提交 "/> 此处一定要使用 form 标签库, 借此我们可以看到它的强大支持 ( 别的大部分 Web 框架所不具备的, 展示用户验证失败的数据 ) < form:form commandname="databindertest">: 指定命令对象为 databindertest, 默认 command; <form:errors path="*" cssstyle="color:red"></form:errors>: 显示错误消息, 当提交表单有错误时展示错误消息 ( 数据绑定错误 / 数据不合法 ); <form:input path="bool"/>: 等价于 (<input type= text >), 但会从命令对象中取出 bool 属性进行填充 value 属性, 或如果表单提交有错误会从错误对象取出之前的错误数据 ( 而非空或默认值 ); <input type="submit" value=" 提交 "/>:spring 没有提供相应的提交按钮, 因此需要使用 html 的 (4 测试 在地址栏输入如下地址 : 全部是错误数据, 即不能绑定到我们的命令对象 ; 当提交表单后, 我们又回到表单输入页面, 而且输出了一堆错误信息 1 错误消息不可读 ; 2 表单元素可以显示之前的错误的数据, 而不是默认值 / 空 ;

78 (5 问题 这里最大的问题是不可读的错误消息, 如何让这些错误消息可读呢? 首先我们看我们的 org.springframework.validation.bindexception: showform 方法里输出的 errors 错误对象信息 : org.springframework.validation.beanpropertybindingresult: 3 errors Field error in object 'databindertest' on field 'bool': rejected value [www]; codes [typemismatch.databindertest.bool,typemismatch.bool,typemismatch.boolean,typemismatch]; arguments [org.springframework.context.support.defaultmessagesourceresolvable: codes [databindertest.bool,bool]; arguments []; default message [bool]]; default message [Failed to convert property value of type 'java.lang.string' to required type 'boolean' for property 'bool';nestedexceptionisjava.lang.illegalargumentexception:invalidbooleanvalue [www]] Field error in object 'databindertest' on field 'date': rejected value [123]; codes [typemismatch.databindertest.date,typemismatch.date,typemismatch.java.util.date,typemis match]; arguments [org.springframework.context.support.defaultmessagesourceresolvable: codes [databindertest.date,date]; arguments []; default message [date]]; default message [Failed to convert property value of type 'java.lang.string' to required type 'java.util.date' for property 'date'; nested exception is java.lang.illegalargumentexception: Could not parse date: Unparseable date: "123"] Field error in object 'databindertest' on field 'phonenumber': rejected value [123]; codes [typemismatch.databindertest.phonenumber,typemismatch.phonenumber,typemismatch.cn.javas s.chapter4.model.phonenumbermodel,typemismatch]; arguments [org.springframework.context.support.defaultmessagesourceresolvable: codes [databindertest.phonenumber,phonenumber]; arguments []; default message [phonenumber]]; default message [Failed to convert property value of type 'java.lang.string' to required type 'cn.javass.chapter4.model.phonenumbermodel' for property 'phonenumber'; nested exception is java.lang.illegalargumentexception: 类型转换失败, 需要格式 [ ], 但格式是 [123]] 数据绑定失败 ( 类型不匹配 ) 会自动生成如下错误码 ( 错误码对应的错误消息按照如下顺序依次查找 ): 1 typemismatch. 命令对象名. 属性名 2 typemismatch. 属性名 3 typemismatch. 属性全限定类名 ( 包名. 类名 ) 4 typemismatch 内部使用 MessageCodesResolver 解析数据绑定错误到错误码, 默认 DefaultMessageCodesResolver, 因此想要详细了 解如何解析请看其 javadoc; 建议使用第 1 个进行错误码的配置 因此修改我们的 messages.properties 添加如下错误消息 ( 需要执行 NativeToAscii): typemismatch.databindertest.date= 您输入的数据格式错误, 请重新输入 ( 格式 : :17:17) #typemismatch.date=2 #typemismatch.java.util.date=3 #typemismatch=4 再次提交表单我们会看到我们设置的错误消息 :

79 到此, 数据绑定错误我们介绍完了, 接下来我们再看一下数据不合法错误 数据不合法 1 比如用户名长度必须在 5-20 之间, 而且必须以字母开头, 可包含字母 数字 下划线 ; 2 比如注册用户时用户名已经存在或邮箱已经存在等 ; 3 比如去一些论坛经常会发现, 您发的帖子中包含 屏蔽关键字等 还有很多数据不合法的场景, 在此就不罗列了, 对于数据不合法,Spring Web MVC 提供了两种验证方式 : 编程式验证器验证 声明式验证 先从编程式验证器开始吧 编程式验证器 一 验证器接口 package org.springframework.validation; public interface Validator { boolean supports(class<?> clazz); void validate(object target, Errors errors); Validator 接口 : 验证器, 编程实现数据验证的接口 ; supports 方法 : 当前验证器是否支持指定的 clazz 验证, 如果支持返回 true 即可 ; validate 方法 : 验证的具体方法,target 参数表示要验证的目标对象 ( 如命令对象 ), errors 表示验证出错后存放错误信息的错误对象 示例 : (1 验证器实现

80 package cn.javass.chapter4.web.controller.support.validator; // 省略 import public class UserModelValidator implements Validator { private static final Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z]\\w{4,19"); private static final Pattern PASSWORD_PATTERN = Pattern.compile("[a-zA-Z0-9]{5,20"); private static final Set<String> FORBINDDDEN_WORD_SET = new HashSet<String>(); static { FORBINDDDEN_WORD_SET.add("fuck"); public boolean supports(class<?> clazz) { return UserModel.class == clazz;// 表示只对 UserModel public void validate(object target, Errors errors) { // 这个表示如果目标对象的 username 属性为空, 则表示错误 ( 简化我们手工判断是否为空 ) ValidationUtils.rejectIfEmpty(errors, "username", "username.not.empty"); UserModel user = (UserModel) target; if(!username_pattern.matcher(user.getusername()).matches()) { errors.rejectvalue("username", "username.not.illegal");// 如果用户名不合法 for(string forbiddenword : FORBINDDDEN_WORD_SET) { if(user.getusername().contains(forbiddenword)) { errors.rejectvalue("username", "username.forbidden", new Object[]{forbiddenWord, " 您的用户名包含非法关键词 ");// 用户名包含屏蔽关键字 break; if(!password_pattern.matcher(user.getpassword()).matches()) { errors.rejectvalue("password","password.not.illegal", " 密码不合法 ");// 密码不合法 supports 方法 : 表示只对 UserModel 类型的对象验证 ; validate 方法 : 数据验证的具体方法, 有如下几个验证 : 1 用户名不合法( 长度 5-20, 以字母开头, 随后可以是字母 数字 下划线 ) USERNAME_PATTERN.matcher(user.getUsername()).matches() // 使用正则表达式验证 errors.rejectvalue("username", "username.not.illegal");// 验证失败为 username 字段添加错误码 2 屏蔽关键词: 即用户名中含有不合法的数据 ( 如 admin) user.getusername().contains(forbiddenword) // 用 contains 来判断我们的用户名中是否含有非法关键词 errors.rejectvalue("username", "username.forbidden", new Object[]{forbiddenWord, " 您的用户名包含非法关键词 ");// 验证失败为 username 字段添加错误码 ( 参数为当前屏蔽关键词 )( 默认消息为 " 您的用户名包含非法关键词 ")

81 3 密码不合法在此就不罗列代码了 ; 4 ValidationUtils ValidationUtils.rejectIfEmpty(errors, "username", "username.not.empty"); 表示如果目标对象的 username 属性数据为空, 则添加它的错误码 ; 内部通过 (value == null!stringutils.haslength(value.tostring())) 实现判断 value 是否为空, 从而简化代码 (2 spring 配置文件 chapter4-servlet.xml <bean id="usermodelvalidator" class="cn.javass.chapter4.web.controller.support.validator.usermodelvalidator"/> <bean name="/validator" class="cn.javass.chapter4.web.controller.registersimpleformcontroller"> <property name="formview" value="registerandvalidator"/> <property name="successview" value="redirect:/success"/> <property name="validator" ref="usermodelvalidator"/> </bean> 此处使用了我们第 4.9 节创建的 RegisterSimpleFormController (3 错误码配置 (messages.properties), 需要执行 NativeToAscii username.not.empty= 用户名不能为空 username.not.illegal= 用户名错误, 必须以字母开头, 只能出现字母 数字 下划线, 并且长度在 5-20 之间 username.forbidden= 用户名中包含非法关键词 {0 password.not.illegal= 密码长度必须在 5-20 之间 (4 视图页面 (/WEB-INF/jsp/registerAndValidator.jsp) <%@ page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> <%@taglib prefix="form" uri=" %> <form:form commandname="user"> <form:errors path="*" cssstyle="color:red"></form:errors><br/> username:<form:input path="username"/> <form:errors path="username" cssstyle="color:red"></form:errors> <br/> password:<form:password path="password"/> <form:errors path="password" cssstyle="color:red"></form:errors> <br/> <input type="submit" value=" 注册 "/> </form:form> form:errors path="username": 表示只显示 username 字段的错误信息 ; (5 测试 地址 :

82 当我们输入错误的数据后, 会报错 (form:errors path="*" 显示所有错误信息, 而 form:errors path="username" 只显示该字段相关的 ) 问题 : 如 MultiActionController 控制器相关方法没有提供给我们 errors 对象 (Errors), 我们应该怎么进行错误处理呢? 此处给大家一个思路,errors 本质就是一个 Errors 接口实现, 而且在页面要读取相关的错误对象, 该错误对象应该存放 在模型对象里边, 因此我们可以自己创建个 errors 对象并将其添加到模型对象中即可 此处我们复制 4.15 节的 UserController 类为 UserAndValidatorController, 并修改它的 create( 新增 ) 方法添加如下代码片段 : BindException errors = new BindException(user, getcommandname(user)); // 如果用户名为空 if(!stringutils.haslength(user.getusername())) { errors.rejectvalue("username", "username.not.empty"); if(errors.haserrors()) { return new ModelAndView(getCreateView()).addAllObjects(errors.getModel()); new BindException(user, getcommandname(user)): 使用当前的命令对象, 和命令对象的名字创建了一个 BindException 作为 errors; StringUtils.hasLength(user.getUsername()): 如果用户名为空就是用 errors.rejectvalue("username", "username.not.empty"); 注入错误码 ; errors.haserrors(): 表示如果有错误就返回到新增页面并显示错误消息 ; ModelAndView(getCreateView()).addAllObjects(errors.getModel()): 此处一定把 errors 对象的模型数据放在当前的 ModelAndView 中, 作为当前请求的模型数据返回 在浏览器地址栏输入 : 到新增页面 用户名什么都不输入, 提交后又返回到新增页面而且显示了错误消息说明我们的想法是正确的

83 声明式验证器 从 Spring3 开始支持 JSR-303 验证框架, 支持 XML 风格和注解风格的验证, 时才能使用, 也 就是说基于 Controller 接口的实现不能使用该方式 ( 但可以使用编程式验证, 有需要的可以参考 hibernate validator 实现 ), 我们将在第七章详细介绍 到此 Spring2 风格的控制器我们就介绍完了, 以上控制器从 spring3.0 开始已经不推荐使用了 ( 但考虑到还有部分公司 类, 在此也介绍了一下 ), 而是使用注解控制器实现 (@Controller

84 第五章处理器拦截器详解 5.1 处理器拦截器简介 Spring Web MVC 的处理器拦截器 ( 如无特殊说明, 下文所说的拦截器即处理器拦截器 ) 类似于 Servlet 开发中的过滤 器 Filter, 用于对处理器进行预处理和后处理 常见应用场景 1 日志记录: 记录请求信息的日志, 以便进行信息监控 信息统计 计算 PV(Page View) 等 2 权限检查: 如登录检测, 进入处理器检测检测是否登录, 如果没有直接返回到登录页面 ; 3 性能监控: 有时候系统在某段时间莫名其妙的慢, 可以通过拦截器在进入处理器之前记录开始时间, 在处理完后记录结束时间, 从而得到该请求的处理时间 ( 如果有反向代理, 如 apache 可以自动记录 ); 4 通用行为: 读取 cookie 得到用户信息并将用户对象放入请求, 从而方便后续流程使用, 还有如提取 Locale Theme 信息等, 只要是多个处理器都需要的即可使用拦截器实现 5 OpenSessionInView: 如 Hibernate, 在进入处理器打开 Session, 在完成后关闭 Session 本质也是 AOP( 面向切面编程 ), 也就是说符合横切关注点的所有功能都可以放入拦截器实现 拦截器接口 package org.springframework.web.servlet; public interface HandlerInterceptor { boolean prehandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; void posthandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelandview) throws Exception; void aftercompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; 我们可能注意到拦截器一个有 3 个回调方法, 而一般的过滤器 Filter 才两个, 这是怎么回事呢? 马上分析 prehandle: 预处理回调方法, 实现处理器的预处理 ( 如登录检查 ), 第三个参数为响应的处理器 ( 如我们上一章的 Controller 实现 );

85 返回值 :true 表示继续流程 ( 如调用下一个拦截器或处理器 ); false 表示流程中断 ( 如登录检查失败 ), 不会继续调用其他的拦截器或处理器, 此时我们需要通过 response 来产生响应 ; posthandle: 后处理回调方法, 实现处理器的后处理 ( 但在渲染视图之前 ), 此时我们可以通过 modelandview( 模型和视图对象 ) 对模型数据进行处理或对视图进行处理,modelAndView 也可能为 null aftercompletion: 整个请求处理完毕回调方法, 即在视图渲染完毕时回调, 如性能监控中我们可以在此记录结束时间并输出消耗时间, 还可以进行一些资源清理, 类似于 try-catch-finally 中的 finally, 但仅调用处理器执行链中 prehandle 返回 true 的拦截器的 aftercompletion 拦截器适配器 有时候我们可能只需要实现三个回调方法中的某一个, 如果实现 HandlerInterceptor 接口的话, 三个方法必须实现, 不管你需不需要, 此时 spring 提供了一个 HandlerInterceptorAdapter 适配器 ( 一种适配器设计模式的实现 ), 允许我们只实现需要的回调方法 public abstract class HandlerInterceptorAdapter implements HandlerInterceptor { // 省略代码此处所以三个回调方法都是空实现,preHandle 返回 true 运行流程图 图 5-1 正常流程

86 图 5-2 中断流程 中断流程中, 比如是 HandlerInterceptor2 中断的流程 (prehandle 返回 false), 此处仅调用它之前拦截器的 prehandle 返 回 true 的 aftercompletion 方法 接下来看一下 DispatcherServlet 内部到底是如何工作的吧 : //dodispatch 方法 //1 处理器拦截器的预处理( 正序执行 ) HandlerInterceptor[] interceptors = mappedhandler.getinterceptors(); if (interceptors!= null) { for (int i = 0; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; if (!interceptor.prehandle(processedrequest, response, mappedhandler.gethandler())) { //1.1 失败时触发 aftercompletion 的调用 triggeraftercompletion(mappedhandler, interceptorindex, processedrequest, response, null); return; interceptorindex = i;//1.2 记录当前预处理成功的索引 //2 处理器适配器调用我们的处理器 mv = ha.handle(processedrequest, response, mappedhandler.gethandler()); // 当我们返回 null 或没有返回逻辑视图名时的默认视图名翻译 ( 详解 RequestToViewNameTranslator) if (mv!= null &&!mv.hasview()) { mv.setviewname(getdefaultviewname(request)); //3 处理器拦截器的后处理( 逆序 ) if (interceptors!= null) { for (int i = interceptors.length - 1; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; interceptor.posthandle(processedrequest, response, mappedhandler.gethandler(), mv); //4 视图的渲染 if (mv!= null &&!mv.wascleared()) {

87 注 : 以上是流程的简化代码, 中间省略了部分代码, 不完整 // triggeraftercompletion 方法 private void triggeraftercompletion(handlerexecutionchain mappedhandler, int interceptorindex, HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception { // 5 触发整个请求处理完毕回调方法 aftercompletion ( 逆序从 1.2 中的预处理成功的索引处的拦截器执行 ) if (mappedhandler!= null) { HandlerInterceptor[] interceptors = mappedhandler.getinterceptors(); if (interceptors!= null) { for (int i = interceptorindex; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; try { interceptor.aftercompletion(request, response, mappedhandler.gethandler(), ex); catch (Throwable ex2) { logger.error("handlerinterceptor.aftercompletion threw exception", ex2); 5.2 入门 具体内容详见工程 springmvc-chapter 正常流程 (1 拦截器实现 package cn.javass.chapter5.web.interceptor; // 省略 import public class HandlerInterceptor1 extends HandlerInterceptorAdapter {// 此处一般继承 HandlerInterceptorAdapter public boolean prehandle(httpservletrequest request, HttpServletResponse response, Object handler) throws Exception {

88 以上是 HandlerInterceptor1 实现,HandlerInterceptor2 同理只是输出内容为 HandlerInterceptor2 (2 控制器 package cn.javass.chapter5.web.controller; // 省略 import public class TestController implements Controller public ModelAndView handlerequest(httpservletrequest req, HttpServletResponse resp) throws Exception { System.out.println("===========TestController"); return new ModelAndView("test"); (3 Spring 配置文件 chapter5-servlet.xml <bean name="/test" class="cn.javass.chapter5.web.controller.testcontroller"/> <bean id="handlerinterceptor1" class="cn.javass.chapter5.web.interceptor.handlerinterceptor1"/> <bean id="handlerinterceptor2" class="cn.javass.chapter5.web.interceptor.handlerinterceptor2"/> <bean class="org.springframework.web.servlet.handler.beannameurlhandlermapping"> <property name="interceptors"> <list> <ref bean="handlerinterceptor1"/> <ref bean="handlerinterceptor2"/> </list> </property> </bean> interceptors: 指定拦截器链, 拦截器的执行顺序就是此处添加拦截器的顺序 ; (4 视图页面 WEB-INF/jsp/test.jsp <%@ page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> <%System.out.println("==========test.jsp");%> test page

89 在控制台输出 test.jsp (5 启动服务器测试输入网址 : 控制台输出 : ===========HandlerInterceptor1 prehandle ===========HandlerInterceptor2 prehandle ===========TestController ===========HandlerInterceptor2 posthandle ===========HandlerInterceptor1 posthandle ==========test.jsp ===========HandlerInterceptor2 aftercompletion ===========HandlerInterceptor1 aftercompletion 到此一个正常流程的演示完毕 和图 5-1 一样, 接下来看一下中断的流程 中断流程 (1 拦截器 HandlerInterceptor3 和 HandlerInterceptor4 与之前的 HandlerInteceptor1 和 HandlerInterceptor2 一样, 只是在 HandlerInterceptor4 的 prehandle 方法返回 public boolean prehandle(httpservletrequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("===========HandlerInterceptor1 prehandle"); response.getwriter().print("break");// 流程中断的话需要我们进行响应的处理 return false;// 返回 false 表示流程中断 (2 控制器流程中断不会执行到控制器, 使用之前的 TestController 控制器 (3 Spring 配置文件 chapter5-servlet.xml <bean id="handlerinterceptor3" class="cn.javass.chapter5.web.interceptor.handlerinterceptor3"/> <bean id="handlerinterceptor4" class="cn.javass.chapter5.web.interceptor.handlerinterceptor4"/> <bean class="org.springframework.web.servlet.handler.beannameurlhandlermapping"> <property name="interceptors"> <list> <ref bean="handlerinterceptor3"/> <ref bean="handlerinterceptor4"/> </list> </property> </bean>

90 interceptors: 指定拦截器链, 拦截器的执行顺序就是此处添加拦截器的顺序 ; (4 视图页面流程中断, 不会执行到视图渲染 (5 启动服务器测试输入网址 : 控制台输出 : ===========HandlerInterceptor3 prehandle ===========HandlerInterceptor4 prehandle ===========HandlerInterceptor3 aftercompletion 此处我们可以看到只有 HandlerInterceptor3 的 aftercompletion 执行, 否和图 5-2 的中断流程 而且页面上会显示我们在 HandlerInterceptor4 prehandle 直接写出的响应 break 5.3 应用 性能监控 如记录一下请求的处理时间, 得到一些慢请求 ( 如处理时间超过 500 毫秒 ), 从而进行性能改进, 一般的反向代理服务 器如 apache 都具有这个功能, 但此处我们演示一下使用拦截器怎么实现 实现分析 : 1 在进入处理器之前记录开始时间, 即在拦截器的 prehandle 记录开始时间 ; 2 在结束请求处理之后记录结束时间, 即在拦截器的 aftercompletion 记录结束实现, 并用结束时间 - 开始时间得到这次请求的处理时间 问题 : 我们的拦截器是单例, 因此不管用户请求多少次都只有一个拦截器实现, 即线程不安全, 那我们应该怎么记录时间呢? 解决方案是使用 ThreadLocal, 它是线程绑定的变量, 提供线程局部变量 ( 一个线程一个 ThreadLocal,A 线程的 ThreadLocal 只能看到 A 线程的 ThreadLocal, 不能看到 B 线程的 ThreadLocal) 代码实现 : package cn.javass.chapter5.web.interceptor; public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter { private NamedThreadLocal<Long> starttimethreadlocal = new public boolean prehandle(httpservletrequest request, HttpServletResponse response, Object handler) throws Exception { long begintime = System.currentTimeMillis();//1 开始时间 starttimethreadlocal.set(begintime);// 线程绑定变量 ( 该数据只有当前请求的线程可见 ) return true;// public void aftercompletion(httpservletrequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { long endtime = System.currentTimeMillis();//2 结束时间

91 NamedThreadLocal:Spring 提供的一个命名的 ThreadLocal 实现 在测试时需要把 stopwatchhandlerinterceptor 放在拦截器链的第一个, 这样得到的时间才是比较准确的 登录检测 在访问某些资源时 ( 如订单页面 ), 需要用户登录后才能查看, 因此需要进行登录检测 流程 : 1 访问需要登录的资源时, 由拦截器重定向到登录页面 ; 2 如果访问的是登录页面, 拦截器不应该拦截 ; 3 用户登录成功后, 往 cookie/session 添加登录成功的标识 ( 如用户编号 ); 4 下次请求时, 拦截器通过判断 cookie/session 中是否有该标识来决定继续流程还是到登录页面 ; 5 在此拦截器还应该允许游客访问的资源 拦截器代码如下所示 public boolean prehandle(httpservletrequest request, HttpServletResponse response, Object handler) throws Exception { //1 请求到登录页面放行 if(request.getservletpath().startswith(loginurl)) { return true; //2 TODO 比如退出 首页等页面无需登录, 即此处要放行允许游客的请求 //3 如果用户已经登录放行 if(request.getsession().getattribute("username")!= null) { // 更好的实现方式的使用 cookie return true; //4 非法请求即这些请求需要登录后才能访问 // 重定向到登录页面 response.sendredirect(request.getcontextpath() + loginurl); return false;

92 提示 : 推荐能使用 servlet 规范中的过滤器 Filter 实现的功能就用 Filter 实现, 因为 HandlerInteceptor 只有在 Spring Web MVC 环境下才能使用, 因此 Filter 是最通用的 最先应该使用的 如登录这种拦截器最好使用 Filter 来实现

93 第六章注解式控制器详解 6.1 注解式控制器简介 一 Spring2.5 之前, 我们都是通过实现 Controller 接口或其实现来定义我们的处理器类 二 Spring2.5 引入注解式处理器支持, 注解定义我们的处理器类 并且提供了一组强大的注解 : 需要通过处理器映射 DefaultAnnotationHandlerMapping 和处理器适配器 AnnotationMethodHandlerAdapter 来开启支 用于标识是处理器类 请求到处理器功能方法的映射规则 请求参数到处理器功能处理方法的方法参数上的绑定 请求参数到命令对象的绑定 用于声明 session 级别存储的属性, 放置在处理器类上, 通常列出模型属性 ( 对应的名称, 则这些属性会透明的保存到 session 中 自定义数据绑定注册支持, 用于将请求参数转换到命令对象属性的对应类型 ; 三 Spring3.0 引入 RESTful 架构风格支持 ( 注解和一些其他特性支持 ), 且又引入了更多的注解支持 数据到处理器功能处理方法的方法参数上的绑定 请求头 (header) 数据到处理器功能处理方法的方法参数上的绑定 请求的 body 体的绑定 ( 通过 HttpMessageConverter 进行类型转换 处理器功能处理方法的返回值作为响应体 ( 通过 HttpMessageConverter 进行类型转换 定义处理器功能处理方法 / 异常处理器返回的状态码和原因 注解式声明异常处理器 请求 URI 中的模板变量部分到处理器功能处理方法的方法参数上的绑定, 从而支持 RESTful 架构风格的 URI; 四 Spring3.1 使用新的 HandlerMapping 和 HandlerAdapter 注解处理器 注解支持类 : 处理器映射 RequestMappingHandlerMapping 和处理器适配器 RequestMappingHandlerAdapter 组合来代替 Spring2.5 开始的处理器映射 DefaultAnnotationHandlerMapping 和处理 器适配器 AnnotationMethodHandlerAdapter, 提供更多的扩展点 接下来, 我们一起开始学习基于注解的控制器吧 6.2 入门 (1 控制器实现

94 package cn.javass.chapter6.web.controller; // 省略 // //1 将一个 POJO 类声明为处理器 public class HelloWorldController = "/hello") //2 请求 URL 到处理器功能处理方法的映射 public ModelAndView helloworld() { //1 收集参数 //2 绑定参数到命令对象 //3 调用业务对象 //4 选择下一个页面 ModelAndView mv = new ModelAndView(); // 添加模型数据可以是任意的 POJO 对象 mv.addobject("message", "Hello World!"); // 设置逻辑视图名, 视图解析器会根据该名字解析到具体的视图页面 mv.setviewname("hello"); return mv; // 3 模型数据和逻辑视图名 1 可以通过在一个 POJO 即可把一个 POJO 类变身为处理器 ; = "/hello") 请求 URL(/hello) 到处理器的功能处理方法的映射 ; 3 模型数据和逻辑视图名的返回 现在的处理器无需实现 / 继承任何接口 / 类, 只需要在相应的类 / 方法上放置相应的注解说明下即可, 非常方便 (2 Spring 配置文件 chapter6-servlet.xml (2.1 HandlerMapping 和 HandlerAdapter 的配置如果您使用的是 Spring3.1 之前版本, 开启注解式处理器支持的配置为 :DefaultAnnotationHandlerMapping 和 AnnotationMethodHandlerAdapter <! Spring3.1 之前的注解 HandlerMapping --> <bean class="org.springframework.web.servlet.mvc.annotation.defaultannotationhandlermapping"/ <! Spring3.1 之前的注解 HandlerAdapter --> <bean class="org.springframework.web.servlet.mvc.annotation.annotationmethodhandleradapter"/> 如果您使用的 Spring3.1 开始的版本, 建议使用 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter <!--Spring3.1 开始的注解 HandlerMapping --> <bean class="org.springframework.web.servlet.mvc.method.annotation.requestmappinghandlermapping" <!--Spring3.1 开始的注解 HandlerAdapter --> <bean class="org.springframework.web.servlet.mvc.method.annotation.requestmappinghandleradapter"

95 下一章我们介绍 DefaultAnnotationHandlerMapping 和 AnnotationMethodHandlerAdapter 与 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 的区别 (2.2 视图解析器的配置 还是使用之前的 org.springframework.web.servlet.view.internalresourceviewresolver (2.3 处理器的配置 <!-- 处理器 --> <bean class="cn.javass.chapter6.web.controller.helloworldcontroller"/> 只需要将处理器实现类注册到 spring 配置文件即可,spring 的 DefaultAnnotationHandlerMapping 或 RequestMappingHandlerMapping 自动发现 (2.4 视图页面 (/WEB-INF/jsp/hello.jsp) page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <title>hello World</title> </head> <body> ${message </body> </html> ${message: 表示显示由 HelloWorldController 处理器传过来的模型数据 (4 启动服务器测试 地址栏输入 我们将看到页面显示 HelloWorld!, 表示 成功了 整个过程和我们第二章中的 Hello World 类似, 只是处理器的实现不一样 接下来我们来看一下具体流程吧

96 6.3 运行流程 图 6-1 和第二章唯一不同的两处是 : 1 HandlerMapping 实现 : 使用 DefaultAnnotationHandlerMapping ( spring3.1 之前 ) 或 RequestMappingHandlerMapping(spring3.1) 替换之前的 BeanNameUrlHandlerMapping 注解式处理器映射会扫描 spring 容器中的 bean, 发现 bean 注解的 bean, 并将它们作为处理器 2 HandlerAdapter 实现 : 使用 AnnotationMethodHandlerAdapter(spring3.1 之前 ) 或 RequestMappingHandlerAdapter (spring3.1) 替换之前的 SimpleControllerHandlerAdapter 注解式处理器适配器会通过反射调用相应的功能处理方法 ( 注解 ) 好了到此我们知道 Spring 如何发现处理器 如何调用处理的功能处理方法了, 接下来我们详细学习下如何定义处理器 如何进行请求到功能处理方法的定义 6.4 public class HelloWorldController { 很好的对应了我们常见的三层开发架构的组件

97 6.4.2 public class HelloWorldController { 这种方式也是可以工作的, RequestMapping 注解一般是用于窄化功能处理方法的映射的, 详见 窄化请求映射 //1 处理器的通用映射前缀 public class HelloWorldController2 = "/hello2") //2 相对于 1 处的映射进行窄化 public ModelAndView helloworld() { // 省略实现 1 表示处理器的通用请求前缀 ; 2 处理器功能处理方法上的是对 1 处映射的窄化 因此 无法映射到 HelloWorldController2 的 helloworld 功能处理方法 ; 而 是可以的 窄化请求映射还有其他方式, 如在类级别指定 URL, 而方法级别指定请求方法类型或参数等等, 后续会详细介绍 到此, 我们知道如何定义处理器了, 接下来我们需要学习如何把请求映射到相应的功能处理方法进行请求处理 6.5 请求映射 处理器定义好了, 那接下来我们应该定义功能处理方法, 接收用户请求处理并选择视图进行渲染 首先我们看一下图 6-1:

98 图 1-1 http 请求信息包含六部分信息 : 1 请求方法, 如 GET 或 POST, 表示提交的方式 ; 2URL, 请求的地址信息 ; 3 协议及版本 ; 4 请求头信息 ( 包括 Cookie 信息 ); 5 回车换行 (CRLF); 6 请求内容区 ( 即请求的内容或数据 ), 如表单提交时的参数数据 URL 请求参数 (?abc=123? 后边的 ) 等 想要了解 HTTP/1.1 协议, 请访问 那此处我们可以看到有 一般是可变的, 因此我们可以这些信息进行请求到处理器的功能处理方法的映射, 因此请求的映射分为如下几种 : URL 路径映射 : 使用 URL 映射请求到处理器的功能处理方法 ; 请求方法映射限定 : 如限定功能处理方法只处理 GET 请求 ; 请求参数映射限定 : 如限定只处理包含 abc 请求参数的请求 ; 请求头映射限定 : 如限定只处理 Accept=application/json 的请求 接下来看看具体如何映射吧 URL 路径映射 普通 URL "/user/create"): 多个 URL 路径可以映射到同一个处理器的功能处理 方法

99 URI 占位符, 请求的 URL 可以是 /users/ 或 /users/abcd, 通过 可以提取 URI 模板模式中的 { 中的 : 这样也是可以的, 请求的 URL 可以是 这样也是可以的, 请求的 URL 可以是 /users/123/topics/ Ant 风格的 URL 可以匹配 /users/abc/abc, 但 /users/123 将会被 URI 模板模式映射 中的 /users/{userid 模式优先映射到 详见 4.14 可匹配 /product1 或 /producta, 但不匹配 /product 或 /productaa 可匹配 /productabc 或 /product, 但不匹配 /productabc/abc 可匹配 /product/abc, 但不匹配 /productabc 可匹配 /products/abc/abc/123 或 /products/123, 也就是 Ant 风格和 URI 模板变量风格可混用 ; 此处需要注意的是 4.14 中提到的最长匹配优先,Ant 风格的模式请参考 正则表达式风格的 URL 路径映射 从 Spring3.0 开始支持正则表达式风格的 URL 路径映射, 格式为 { 变量名 : 正则表达式, 这样我们就可以通过 讲的 提取模式中的 { : 正则表达式匹配的值 中的 : 可以匹配 /products/123-1, 但不能匹配 /products/abc-1, 这样可以设计更加严格的规则 正则表达式风格的 URL 路径映射是一种特殊的 URI 模板模式映射 : URI 模板模式映射是 {userid, 不能指定模板变量的数据类型, 如是数字还是字符串 ; 正则表达式风格的 URL 路径映射, 可以指定模板变量的数据类型, 可以将规则写的相当复杂 组合使用是 或 的关系 "/user/create") 组合使用是或的关系, 即 /test1 或 /user/create 请求 URL 指定的功能处理方法 以上 URL 映射的测试类为 :cn.javass.chapter6.web.controller.mapping.mappingcontroller.java 到此, 我们学习了 Spring Web MVC 提供的强大的 URL 路径映射, 而且可以实现非常复杂的 URL 规则 Spring Web MVC 不仅仅提供 URL 路径映射, 还提供了其他强大的映射规则 接下来我们看一下请求方法映射限定吧

100 6.5.2 请求方法映射限定 一般我们熟悉的表单一般分为两步 : 第一步展示, 第二步提交, 如 4.9 SimpleFormController 那样, 来实现呢? 请求方法映射限定 我们熟知的, 展示表单一般为 GET 请求方法 ; 提交表单一般为 POST 请求方法 但 节讲的 URL 路径映射方式对 任意请求方法是全盘接受的, 因此我们需要某种方式来告诉相应的功能处理方法只处理如 GET 请求方法的请求或 POST 请求方法的请求 来实现 SimpleFormController 的功能吧 package cn.javass.chapter6.web.controller.method; // //1 处理器的通用映射前缀 public class RequestMethodController method = RequestMethod.GET)//2 窄化 public String showform() { System.out.println("===============GET"); return method = RequestMethod.POST)//3 窄化 public String submit() { System.out.println("================POST"); return "redirect:/success"; 1 处理器的通用映射前缀 ( 父路径 ): 表示该处理器只处理匹配 /customers/** 的请求 ; 2 进行窄化, 表示 showform 可处理匹配 /customers/**/create 且请求方法为 GET 的请求 ; 3 进行窄化, 表示 submit 可处理匹配 /customers/**/create 且请求方法为 POST 的请求

101 组合使用是 或 method = {RequestMethod.POST, RequestMethod.GET): 即请 求方法可以是 GET 或 POST 提示 : 1 一般浏览器只支持 GET POST 请求方法, 如想浏览器支持 PUT DELETE 等请求方法只能模拟, 稍候章节介绍 2 除了 GET POST, 还有 HEAD OPTIONS PUT DELETE TRACE 3 DispatcherServlet 默认开启对 GET POST PUT DELETE HEAD 的支持 ; 4 如果需要支持 OPTIONS TRACE, 请添加 DispatcherServlet 在 web.xml 的初始化参数 :dispatchoptionsrequest 和 dispatchtracerequest 为 true 请求方法的详细使用请参考 RESTful 架构风格一章 以上请求方法映射限定测试类为 :cn.javass.chapter6.web.controller.method.requestmethodcontroller 请求参数数据映射限定 请求数据中有指定参数名 package cn.javass.chapter6.web.controller.parameter; // //1 处理器的通用映射前缀 public class RequestParameterController1 { //2 method=requestmethod.get) public String showform() { System.out.println("===============showForm"); return "parameter/create"; //3 method=requestmethod.post) public String submit() { System.out.println("================submit"); return "redirect:/success"; 2@RequestMapping(params="create", method=requestmethod.get) : 表示请求中有 create 的参数名且请求方法为 GET 即可匹配, 如可匹配的请求 URL /parameter1?create ; 3@RequestMapping(params="create", method=requestmethod.post): 表示请求中有 create 的参数名且请求方法为 POST 即可匹配 ;

102 此处的 create 请求参数名表示你请求的动作, 即你想要的功能的一个标识, 常见的 CRUD( 增删改查 ) 我们可以使用如下请求参数名来表达 : (create 请求参数名且 GET 请求方法 ) 新增页面展示 ( create 请求参数名且 POST 请求方法 ) 新增提交 ; (update 请求参数名且 GET 请求方法 ) 新增页面展示 ( update 请求参数名且 POST 请求方法 ) 新增提交 ; (delete 请求参数名且 GET 请求方法 ) 新增页面展示 ( delete 请求参数名且 POST 请求方法 ) 新增提交 ; (query 请求参数名且 GET 请求方法 ) 新增页面展示 ( query 请求参数名且 POST 请求方法 ) 新增提交 ; (list 请求参数名且 GET 请求方法 ) 列表页面展示 ; (view 请求参数名且 GET 请求方法 ) 查看单条记录页面展示 请求数据中没有指定参数名 // 请求参数不包含 create method=requestmethod.get)// 表示请求中没有 create 参数名且 请求方法为 GET 即可匹配, 如可匹配的请求 URL /parameter1?abc 请求数据中指定参数名 = 值 package cn.javass.chapter6.web.controller.parameter; // //1 处理器的通用映射前缀 public class RequestParameterController2 { //2 method=requestmethod.get) public String showform() { System.out.println("===============showForm"); return "parameter/create"; //3 method=requestmethod.post) public String submit() { System.out.println("===============submit"); return "redirect:/success"; method=requestmethod.get) : 表示请求中有 submitflag=create 请求参数且请求方法为 GET 即可匹配, 如请求 URL 为 /parameter2?submitflag=create; method=requestmethod.post) : 表示请求中有 submitflag=create 请求参数且请求方法为 POST 即可匹配 ;

103 此处的 submitflag=create 请求参数表示你请求的动作, 即你想要的功能的一个标识, 常见的 CRUD( 增删改查 ) 我们可以使用如下请求参数名来表达 : (submitflag=create 请求参数名且 GET 请求方法 ) 新增页面展示 ( submitflag=create 请求参数名且 POST 请求方法 ) 新增提交 ; (submitflag=update 请求参数名且 GET 请求方法 ) 新增页面展示 ( submitflag=update 请求参数名且 POST 请求方法 ) 新增提交 ; (submitflag=delete 请求参数名且 GET 请求方法 ) 新增页面展示 ( submitflag=delete 请求参数名且 POST 请求方法 ) 新增提交 ; (submitflag=query 请求参数名且 GET 请求方法 ) 新增页面展示 ( submitflag=query 请求参数名且 POST 请求方法 ) 新增提交 ; (submitflag=list 请求参数名且 GET 请求方法 ) 列表页面展示 ; (submitflag=view 请求参数名且 GET 请求方法 ) 查看单条记录页面展示 请求数据中指定参数名!= 值 // 请求参数 submitflag 不等于 method=requestmethod.get) : 表示请求中的参数 submitflag!=create 且请求方法为 GET 即可匹配, 如可匹配的请求 URL /parameter1?submitflag=abc 组合使用是 且 "test2=create") //2 "test2=create") : 表示请求中的有 test1 参数名且有 test2=create 参数即可匹配, 如可匹配的请求 URL /parameter3?test1&test2=create 以上请求参数数据映射限定测试类为 :cn.javass.chapter6.web.controller.method RequestParameterController1 RequestParameterController2 RequestParameterController3 包下的 请求头数据映射限定 准备环境 浏览器 : 建议 chrome 最新版本 ; 插件 :ModHeader 安装地址 : 插件安装步骤 : 1 打开 如图 6-2

104 图 点击 添加至 chrome 后弹出 确认安装 对话框, 点击 安装 按钮即可, 如图 6-3: 图 安装成功后, 在浏览器右上角出现如图 6-4 的图标表示安装成功 : 图 鼠标右击右上角的 Modify Header 图标, 选择选项, 打开如图 6-5: 图 修改完成后, 输入 URL 请求, 你可以在 chrome 的 开发人员工具的 网络选项卡下, 看到如图 6-7 的信息表示添 加请求头成功了 :

105 图 6-7 到此我们的工具安装完毕, 接下来看看如何使用请求头数据进行映射限定 = "Accept"): 表示请求的 URL 必须为 /header/test1 且请求头中必须有 Accept headers = "abc"): 表示请求的 URL 必须为 /header/test1 且请求头中必须有 abc 参数才能匹配, 如图 6-8 时可匹配 图 headers = "!abc"): 表示请求的 URL 必须为 /header/test2 且请求头中必须没有 abc 参数才能匹配 ( 将 Modify Header 的 abc 参数值删除即可 )

106 请求头数据中指定参数名 = headers = "Content-Type=application/json"): 表示请求 的 URL 必须为 /header/test3 且请求头中必须有 Content-Type=application/json 参数即可匹配 ( 将 Modify Header 的 Content-Type 参数值改为 application/json 即可 ); 当你请求的 URL 为 /header/test3 但如果请求头中没有或不是 Content-Type=application/json 参数 ( 如 text/html 其他参数 ), 将返回 HTTP Status 415 状态码 表示不支持的媒体类型 (Media Type), 也就是 MIME 类型, 即我们的功能处理方法只能处理 application/json headers = "Accept=application/json"): 表示请求的 URL 必 须为 /header/test4 且请求头中必须有 Accept =application/json 参数即可匹配 ( 将 Modify Header 的 Accept 参数值改为 application/json 即可 ); 当你请求的 URL 为 /header/test4 但如果请求头中没有 Accept=application/json 参数 ( 如 text/html 其他参数 ), 将返回 HTTP Status 406 状态码 不可接受, 服务器无法根据 Accept 头的媒体类型为客户端生成响应, 即客户只接受 application/json 媒体类型的数据, 即我们的功能处理方法的响应只能返回 application/json headers = "Accept=text/*") : 表示请求的 URL 必须为 /header/test5 且请求头中必须有如 Accept=text/plain 参数即可匹配 ( 将 Modify Header 的 Accept 参数值改 为 text/plain 即可 ); Accept=text/*: 表示主类型为 text, 子类型任意, 如 text/plain text/html headers = "Accept=*/*") : 表示请求的 URL 必须为 /header/test6 且请求头中必须有任意 Accept 参数即可匹配 ( 将 Modify Header 的 Accept 参数值改为 text/html 或 application/xml 等都可以 ) Accept=*/*: 表示主类型任意, 子类型任意, 如 text/plain application/xml 等都可以匹配 请求头数据中指定参数名!= headers = "Accept!=text/vnd.wap.wml"): 表示请求的 URL 必须为 /header/test7 且请求头中必须有 Accept 参数但值不等于 text/vnd.wap.wml 即可匹配

107 组合使用是 且 headers = {"Accept!=text/vnd.wap.wml", "abc=123"): 表示请求的 URL 必须为 /header/test8 且请求头中必须有 Accept 参数但值不等于 text/vnd.wap.wml 且 请求中必须有参数 abc=123 即可匹配 注 :Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 如果您的请求中含有 Accept: */*, 则可以匹配功能处理方法上的如 text/html text/*, application/xml 等 生产者 消费者限定 基本概念 首先让我们看一下通过 HTTP 协议传输的媒体类型及如何表示媒体类型 : 一 Media Type: 互联网媒体类型, 一般就是我们所说的 MIME 类型, 用来确定请求的内容类型或响应的内容类型 媒体类型格式 :type/subtype(;parameter)? type 主类型, 任意的字符串, 如 text, 如果是 * 号代表所有 ; subtype 子类型, 任意的字符串, 如 html, 如果是 * 号代表所有 ; parameter 可选, 一些参数, 如 Accept 请求头的 q 参数, Content-Type 的 charset 参数 详见 常见媒体类型 : text/html : HTML 格式 text/plain : 纯文本格式 text/xml :XML 格式 image/gif :gif 图片格式 image/jpeg :jpg 图片格式 image/png:png 图片格式 application/x-www-form-urlencoded : <form enctype= > 中默认的 enctype,form 表单数据被编码为 key/value 格式发 送到服务器 ( 表单默认的提交数据的格式 ) multipart/form-data : 当你需要在表单中进行文件上传时, 就需要使用该格式 ; application/xhtml+xml :XHTML 格式 application/xml : XML 数据格式 application/atom+xml :Atom XML 聚合格式 application/json : JSON 数据格式 application/pdf :pdf 格式 application/msword : Word 文档格式 application/octet-stream : 二进制流数据 ( 如常见的文件下载 ) 在如 tomcat 服务器的 conf/web.xml 中指定了扩展名到媒体类型的映射, 在此我们可以看到服务器支持的媒体类型 二 Content-Type: 内容类型, 即请求 / 响应的内容区数据的媒体类型 ; 2.1 请求头的内容类型, 表示发送到服务器的内容数据的媒体类型 ; request 中设置请求头 Content-Type: application/x-www-form-urlencoded 表示请求的数据为 key/value 数据 ; (1 控制器 = "/ContentType", method = RequestMethod.GET) public String showform() throws IOException { //form 表单, 使用 application/x-www-form-urlencoded 编码方式提交表单 return "consumesproduces/content-type";

108 showform 功能处理方式 : 展示表单, 且 form 的 enctype="application/x-www-form-urlencoded", 在提 交时请求的内容类型头为 Content-Type:application/x-www-form-urlencoded ; request1 功能处理方法 : 只对请求头为 Content-Type:application/x-www-form-urlencoded 的请求进行处理 ( 即消费请求内容区数据 ); request.getcontenttype(): 可以得到请求头的内容区数据类型 ( 即 Content-Type 头的值 ) request.getcharacterencoding(): 如 Content-Type:application/json;charset=GBK, 则得到的编码为 GBK, 否则如果你设置过滤器 ( CharacterEncodingFilter) 则得到它设置的编码, 否则返回 null request.getparameter(): 因为请求的内容区数据为 application/x-www-form-urlencoded 格式的数据, 因此我们可以通过 request.getparameter() 得到相应参数数据 request 中设置请求头 Content-Type:application/json;charset=GBK 表示请求的内容区数据为 json 类型数据, 且内容区 的数据以 GBK 进行编码 ; (1 控制器 = "/request/contenttype", method = RequestMethod.POST, headers = "Content-Type=application/json") public String request2(httpservletrequest request) throws IOException { //1 表示请求的内容区数据为 json 数据 InputStream is = request.getinputstream(); byte bytes[] = new byte[request.getcontentlength()]; is.read(bytes); //2 得到请求中的内容区数据 ( 以 CharacterEncoding 解码 ) // 此处得到数据后你可以通过如 json-lib 转换为其他对象 String jsonstr = new String(bytes, request.getcharacterencoding()); System.out.println("json data:" + jsonstr); return "success";

109 request2 功能处理方法 : 只对请求头为 Content-Type:application/json 的进行请求处理 ( 即消费请求内容区数据 ); request.getcontentlength(): 可以得到请求头的内容区数据的长度 ; request.getcharacterencoding(): 如 Content-Type:application/json;charset=GBK, 则得到的编码为 GBK, 否则如果你设置过滤器 (CharacterEncodingFilter) 则得到它设置的编码, 否则返回 null 我们得到 json 的字符串形式后就能很简单的转换为 JSON 相关的对象 (2 客户端发送 json 数据请求 // 请求的地址 String url = " //1 创建 Http Request( 内部使用 HttpURLConnection) ClientHttpRequest request = new SimpleClientHttpRequestFactory(). createrequest(new URI(url), HttpMethod.POST); //2 设置请求头的内容类型头和内容编码 (GBK) request.getheaders().set("content-type", "application/json;charset=gbk"); //3 以 GBK 编码写出请求内容体 String jsondata = "{\"username\":\"zhang\", \"password\":\"123\""; request.getbody().write(jsondata.getbytes("gbk")); //4 发送请求并得到响应 ClientHttpResponse response = request.execute(); System.out.println(response.getStatusCode()); 此处我们使用 Spring 提供的 Http 客户端 API SimpleClientHttpRequestFactory 创建了请求并设置了请求的 Content-Type 和编码并在响应体中写回了 json 数据 ( 即生产 json 类型的数据 ), 此处是硬编码, 实际工作可以使用 json-lib 等工具进行转换 具体代码在 cn.javass.chapter6.web.controller.consumesproduces.contenttype.requestcontenttypeclient 2.2 响应头的内容类型, 表示发送到客户端的内容数据类型, 和请求头的内容类型类似, public void response1(httpservletresponse response) throws IOException { //1 表示响应的内容区数据的媒体类型为 html 格式, 且编码为 utf-8( 客户端应该以 utf-8 解码 ) response.setcontenttype("text/html;charset=utf-8"); //2 写出响应体内容 response.getwriter().write("<font style='color:red'>hello</font>"); 如上所示, 通过 response.setcontenttype("text/html;charset=utf-8") 告诉客户端响应体媒体类型为 html, 编码为 utf-8, 大家可以通过 chrome 工具查看响应头为 Content-Type:text/html;charset=utf-8, 还一个 Content-Length:36 表示响应体大小 代码在 cn.javass.chapter6.web.controller.consumesproduces.contenttype.responsecontenttypecontroller 如上代码可以看出 Content-Type 可以指定请求 / 响应的内容体的媒体格式和可选的编码方式 如图 6-9

110 图 客户端 发送请求 服务器 : 客户端通过请求头 Content-Type 指定内容体的媒体类型 ( 即客户端此时是生产者 ), 服务器根据 Content-Type 消费内容体数据 ( 即服务器此时是消费者 ); 2 服务器 发送请求 客户端 : 服务器生产响应头 Content-Type 指定的响应体数据 ( 即服务器此时是生产者 ), 客户端根据 Content-Type 消费内容体数据 ( 即客户端此时是消费者 ) 问题 : 1 服务器端可以通过指定 headers = "Content-Type=application/json" 来声明可处理( 可消费 ) 的媒体类型, 即只消费 Content-Type 指定的请求内容体数据 ; 2 客户端如何告诉服务器端它只消费什么媒体类型的数据呢? 即客户端接受 ( 需要 ) 什么类型的数据呢? 服务器应该生产什么类型的数据? 此时我们可以请求的 Accept 请求头来实现这个功能 三 Accept: 用来指定什么媒体类型的响应是可接受的, 即告诉服务器我需要什么媒体类型的数据, 此时服务器应该 根据 Accept 请求头生产指定媒体类型的数据 2.1 json 数据 (1 = "/response/contenttype", headers = "Accept=application/json") public void response2(httpservletresponse response) throws IOException { //1 表示响应的内容区数据的媒体类型为 json 格式, 且编码为 utf-8( 客户端应该以 utf-8 解码 ) response.setcontenttype("application/json;charset=utf-8"); //2 写出响应体内容 String jsondata = "{\"username\":\"zhang\", \"password\":\"123\""; response.getwriter().write(jsondata); 服务器根据请求头 Accept=application/json 生产 json 数据 (2 客户端端接收服务器端 json 数据响应 使用浏览器测试 (Ajax 场景使用该方式 ) 请求地址为 : 且把修改请求头 Accept 改为 Accept=application/json :

111 大家可以下载 chrome 的 JSONView 插件来以更好看的方式查看 json 数据, 安装地址 : 使用普通客户端测试 ( 服务器之间通信可使用该方式 ) private static void jsonrequest() throws IOException, URISyntaxException { // 请求的地址 String url = " //1 创建 Http Request( 内部使用 HttpURLConnection) ClientHttpRequest request = new SimpleClientHttpRequestFactory(). createrequest(new URI(url), HttpMethod.POST); //2 设置客户端可接受的媒体类型 ( 即需要什么类型的响应体数据 ) request.getheaders().set("accept", "application/json"); //3 发送请求并得到响应 ClientHttpResponse response = request.execute(); //4 得到响应体的编码方式 Charset charset = response.getheaders().getcontenttype().getcharset(); //5 得到响应体的内容 InputStream is = response.getbody(); byte bytes[] = new byte[(int)response.getheaders().getcontentlength()]; is.read(bytes); String jsondata = new String(bytes, charset); System.out.println("charset : " + charset + ", json data : " + jsondata); request.getheaders().set("accept", "application/json"): 表示客户端只接受 ( 即只消费 )json 格式的响应数据 ; response.getheaders(): 可以得到响应头, 从而可以得到响应体的内容类型和编码 内容长度 2.2 xml 数据 (1 服务器端控制器

112 @RequestMapping(value = "/response/contenttype", headers = "Accept=application/xml") public void response3(httpservletresponse response) throws IOException { //1 表示响应的内容区数据的媒体类型为 xml 格式, 且编码为 utf-8( 客户端应该以 utf-8 解码 ) response.setcontenttype("application/xml;charset=utf-8"); //2 写出响应体内容 String xmldata = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; xmldata += "<user><username>zhang</username><password>123</password></user>"; response.getwriter().write(xmldata); 和生产 json 数据唯一不同的两点 : 请求头为 Accept=application/xml, 响应体数据为 xml (2 客户端端接收服务器端 xml 数据响应 使用浏览器测试 (Ajax 场景使用该方式 ) 请求地址为 : 且把修改请求头 Accept 改为 Accept=application/xml, 和 json 方式类似, 此处不再重复 使用普通客户端测试 ( 服务器之间通信可使用该方式 ) private static void xmlrequest() throws IOException, URISyntaxException { // 请求的地址 String url = " //1 创建 Http Request( 内部使用 HttpURLConnection) ClientHttpRequest request = new SimpleClientHttpRequestFactory(). createrequest(new URI(url), HttpMethod.POST); //2 设置客户端可接受的媒体类型 ( 即需要什么类型的响应体数据 ) request.getheaders().set("accept", "application/xml"); //3 发送请求并得到响应 ClientHttpResponse response = request.execute(); //4 得到响应体的编码方式 Charset charset = response.getheaders().getcontenttype().getcharset(); //5 得到响应体的内容 InputStream is = response.getbody(); byte bytes[] = new byte[(int)response.getheaders().getcontentlength()]; is.read(bytes); String xmldata = new String(bytes, charset); System.out.println("charset : " + charset + ", xml data : " + xmldata); request.getheaders().set("accept", "application/xml"): 表示客户端只接受 ( 即只消费 )xml 格式的响应 数据 ; response.getheaders(): 可以得到响应头, 从而可以得到响应体的内容类型和编码 内容长度 许多开放平台, 都提供了同一种数据的多种不同的表现形式, 此时我们可以根据 Accept 请求头告诉它们我们需要什么 类型的数据, 他们根据我们的 Accept 来判断需要返回什么类型的数据 实际项目使用 Accept 请求头是比较麻烦的, 现在大多数开放平台 ( 国内的新浪微博 淘宝 腾讯等开放平台 ) 使用如 下两种方式 : 扩展名 : 如 response/contenttype.json response/contenttype.xml 方式, 使用扩展名表示需要什么类型的数据 ;

113 参数 : 如 response/contenttype?format=json response/contenttype?format=xml, 使用参数表示需要什么类型的数据 ; 也就是说, 目前我们可以使用如上三种方式实现来告诉服务器我们需要什么类型的数据, 但麻烦的是现在有三种实现 方式, 难道我们为了支持三种类型的数据就要分别进行三种实现吗? 当然不要这么麻烦, 后续我们会学 ContentNegotiatingViewResolver, 它能帮助我们做到这一点 生产者消费者流程图 生产者消费者流程, 如图 6-10: 图 6-10 从图 6-10 可以看出 : 请求阶段 : 客户端是生产者 生产 Content-Type 媒体类型的请求内容区数据, 服务器是消费者 消费客户端生产的 Content-Type 媒体类型的请求内容区数据 ; 响应阶段 : 服务器是生产者 生产客户端请求头参数 Accept 指定的响应体数据, 客户端是消费者 消费服务器根据 Accept 请求头生产的响应体数据 如上生产者 / 消费者写法无法很好的体现我们分析的生产者 / 消费者模式,Spring3.1 为生产者 / 消费者模式提供了简化支 持, 接下来我们学习一下如何在 Spring3.1 中来实现生产者 / 消费者模式吧 生产者 消费者限定 Spring3.1 开始支持消费者 生产者限定, 而且必须使用如下 HandlerMapping 和 HandlerAdapter 才支持 : <!--Spring3.1 开始的注解 HandlerMapping --> <bean class="org.springframework.web.servlet.mvc.method.annotation.requestmappinghandlermapping"/> <!--Spring3.1 开始的注解 HandlerAdapter --> <bean class="org.springframework.web.servlet.mvc.method.annotation.requestmappinghandleradapter"/> 一 = "/consumes", consumes = {"application/json"): 此处使用 consumes 来指定

114 功能处理方法能消费的媒体类型, 其通过请求头的 Content-Type 来判断 的 headers = "Content-Type=application/json" 更能表明你的目的 服务器控制器代码详解 cn.javass.chapter6.web.controller.consumesproduces.consumescontroller; 客户端代码类似于之前的 Content-Type 中的客户端, 详见 ConsumesClient.java 代码 二 = "/produces",produces = "application/json"): 表示将功能处理方法将生产 json 格式的数据, 此时根据请求头中的 Accept 进行匹配, 如请求头 Accept:application/json 时即可匹配 = "/produces", produces = "application/xml"): 表示将功能处理方法将生产 xml 格式的数据, 此时根据请求头中的 Accept 进行匹配, 如请求头 Accept:application/xml 时即可匹配 的 headers = "Accept=application/json" 更能表明你的目的 服务器控制器代码详解 cn.javass.chapter6.web.controller.consumesproduces.producescontroller; 客户端代码类似于之前的 Content-Type 中的客户端, 详见 ProducesController.java 代码 当你有如下 Accept 头 : 1Accept:text/html,application/xml,application/json 将按照如下顺序进行 produces 的匹配 1text/html 2application/xml 3application/json 2Accept:application/xml;q=0.5,application/json;q=0.9,text/html 将按照如下顺序进行 produces 的匹配 1text/html 2application/json 3application/xml q 参数为媒体类型的质量因子, 越大则优先权越高 ( 从 0 到 1) 3Accept:*/*,text/*,text/html 将按照如下顺序进行 produces 的匹配 1text/html 2text/* 3*/* 即匹配规则为 : 最明确的优先匹配 代码详见 ProducesPrecedenceController1 ProducesPrecedenceController2 ProducesPrecedenceController3 Accept 详细信息, 请参考 三 produces="text/html"), 此时方法级别的映射将覆盖类级别的, 因此请求头 Accept:application/xml 是成功的, 而 text/html 将报 406 错误码, 表示不支持的请求媒体类型 详见 cn.javass.chapter6.web.controller.consumesproduces.narrowcontroller 只有生产者 / 消费者模式是覆盖, 其他的使用方法是继承, 如 headers params 等都是继承 四 组合使用是 或 "application/json") : 将匹配 Accept:text/html 或 Accept:application/json 五 问题 消费的数据, 如 JSON 数据 XML 数据都是由我们读取请求的 InputStream 并根据需要自己转换为相应的模型数据, 比 较麻烦 ;

115 生产的数据, 如 JSON 数据 XML 数据都是由我们自己先把模型数据转换为 json/xml 等数据, 然后输出响应流, 也是 比较麻烦的 Spring 和一组转换类 (HttpMessageConverter) 来完成我们遇到的 问题, 详见 节 6.6 数据绑定 到目前为止, 请求已经能交给我们的处理器进行处理了, 接下来的事情是要进行收集数据啦, 接下来我们看看我们能 从请求中收集到哪些数据, 如图 6-11: 图 6-11 绑定单个请求参数值 ; 绑定 URI 模板变量值 ; 绑定 Cookie 数据值 绑定请求头数据 ; 绑定参数到命令对象 ; 绑定命令对象到 session; 绑定请求的内容区数据并能进行自动类型转换等 绑定 multipart/data 数据, 能做到的请求参数外, 还能绑定上传的文件等 除了上边提到的注解, 我们还可以通过如 HttpServletRequest 等 API 得到请求数据, 但推荐使用注解方式, 因为使用起 来更简单 接下来先看一下功能处理方法支持的参数类型吧 功能处理方法支持的参数类型 在继续学习之前, 我们需要首先看看功能处理方法支持哪些类型的形式参数, 以及他们的具体含义 一 ServletRequest/HttpServletRequest 和 ServletResponse/HttpServletResponse public String requestorresponse ( ServletRequest servletrequest, HttpServletRequest httpservletrequest, ServletResponse servletresponse, HttpServletResponse httpservletresponse

116 Spring Web MVC 框架会自动帮助我们把相应的 Servlet 请求 / 响应 (Servlet API) 作为参数传递过来 二 InputStream/OutputStream 和 Reader/Writer public void inputoroutbody(inputstream requestbodyin, OutputStream responsebodyout) throws IOException { responsebodyout.write("success".getbytes()); requestbodyin: 获取请求的内容区字节流, 等价于 request.getinputstream(); responsebodyout: 获取相应的内容区字节流, 等价于 response.getoutputstream() public void readerorwritebody(reader reader, Writer writer) throws IOException { writer.write("hello"); reader: 获取请求的内容区字符流, 等价于 request.getreader(); writer: 获取相应的内容区字符流, 等价于 response.getwriter() InputStream/OutputStream 和 Reader/Writer 两组不能同时使用, 只能使用其中的一组 三 WebRequest/NativeWebRequest WebRequest 是 Spring Web MVC 提供的统一请求访问接口, 不仅仅可以访问请求相关数据 ( 如参数区数据 请求头数据, 但访问不到 Cookie 区数据 ), 还可以访问会话和上下文中的数据 ;NativeWebRequest 继承了 WebRequest, 并提供访问本地 Servlet API 的方法 public String webrequest(webrequest webrequest, NativeWebRequest nativewebrequest) { System.out.println(webRequest.getParameter("test"));//1 得到请求参数 test 的值 webrequest.setattribute("name", "value", WebRequest.SCOPE_REQUEST);//2 System.out.println(webRequest.getAttribute("name", WebRequest.SCOPE_REQUEST)); HttpServletRequest request = nativewebrequest.getnativerequest(httpservletrequest.class);//3 HttpServletResponse response = nativewebrequest.getnativeresponse(httpservletresponse.class); 1 webrequest.getparameter: 访问请求参数区的数据, 可以通过 getheader() 访问请求头数据 ; 2 webrequest.setattribute/getattribute: 到指定的作用范围内取 / 放属性数据,Servlet 定义的三个作用范围分别使用如下常量代表 : SCOPE_REQUEST : 代表请求作用范围 ; SCOPE_SESSION : 代表会话作用范围 ; SCOPE_GLOBAL_SESSION : 代表全局会话作用范围, 即 ServletContext 上下文作用范围 3 nativewebrequest.getnativerequest/nativewebrequest.getnativeresponse: 得到本地的 Servlet API 四 HttpSession public String session(httpsession session) { System.out.println(session);

117 此处的 session 永远不为 null 注意 : session 访问不是线程安全的, 如果需要线程安全, 需要设置 AnnotationMethodHandlerAdapter 或 RequestMappingHandlerAdapter 的 synchronizeonsession 属性为 true, 即可线程安全的访问 session 五 命令 / 表单对象 Spring Web MVC 能够自动将请求参数绑定到功能处理方法的命令 / = "/commandobject", method = RequestMethod.GET) public String tocreateuser(httpservletrequest request, UserModel user) { return = "/commandobject", method = RequestMethod.POST) public String createuser(httpservletrequest request, UserModel user) { System.out.println(user); return "success"; 如果提交的表单 ( 包含 username 和 password 文本域 ), 将自动将请求参数绑定到命令对象 user 中去 六 Model Map ModelMap Spring Web MVC 提供 Model Map 或 ModelMap = "/model") public String createuser(model model, Map model2, ModelMap model3) { model.addattribute("a", "a"); model2.put("b", "b"); model3.put("c", "c"); System.out.println(model == model2); System.out.println(model2 == model3); return "success"; 虽然此处注入的是三个不同的类型 (Model model, Map model2, ModelMap model3), 但三者是同一个对象, 如 图 6-12 所示 :

118 图 6-11 AnnotationMethodHandlerAdapter 和 RequestMappingHandlerAdapter 将使用 BindingAwareModelMap 作为模型对象的实现, 即此处我们的形参 (Model model, Map model2, ModelMap model3) 都是同一个 BindingAwareModelMap 实例 此处还有一点需要我们注意 = "/mergemodel") public ModelAndView mergemodel(model model) { model.addattribute("a", "a");//1 添加模型数据 ModelAndView mv = new ModelAndView("success"); mv.addobject("a", "update");//2 在视图渲染之前更新 3 处同名模型数据 model.addattribute("a", "new");//3 修改 1 处同名模型数据 // 视图页面的 a 将显示为 "update" 而不是 "new" return mv; 从代码中我们可以总结出功能处理方法的返回值中的模型数据 ( 如 ModelAndView) 会合并功能处理方法形式参数中的模型数据 ( 如 Model), 但如果两者之间有同名的, 返回值中的模型数据会覆盖形式参数中的模型数据 七 = "/error1") public String error1(usermodel user, BindingResult = "/error2") public String error2(usermodel user, BindingResult result, Model model) = "/error3") public String error3(usermodel user, Errors errors) 以上代码都能获取错误对象 Spring3.1 之前 ( 使用 AnnotationMethodHandlerAdapter) 错误对象必须紧跟在命令对象 / 表单对象之后, 如下定义是错误的 = "/error4") public String error4(usermodel user, Model model, Errors errors)

119 如上代码从 Spring3.1 开始 ( 使用 RequestMappingHandlerAdapter) 将能正常工作, 但还是推荐 错误对象紧跟在命令 对象 / 表单对象之后, 这样是万无一失的 Errors 及 BindingResult 的详细使用请参考 数据验证 八 其他杂项 public String other(locale locale, Principal principal) java.util.locale: 得到当前请求的本地化信息, 默认等价于 ServletRequest.getLocale(), 如果配置 LocaleResolver 解析器则由它决定 Locale, 后续介绍 ; java.security.principal : 该主体对象包含了验证通过的用户信息, 等价于 HttpServletRequest.getUserPrincipal() 以上测试在 cn.javass.chapter6.web.controller.paramtype.methodparamtypecontroller 中 其他功能处理方法的形式参数类型 ( 如 HttpEntity UriComponentsBuilder SessionStatus RedirectAttributes) 将在后续 章节详细讲解 用于将请求参数区数据映射到功能处理方法的参数上 public String requestparam1(@requestparam String username) 请求中包含 username 参数 ( 如 /requestparam1?username=zhang), 则自动传入 此处要特别注意 : 右击项目, 选择 属性, 打开 属性对话框, 选择 Java Compiler 然后再打开的选项卡将 Add variable attributes to generated class files 取消勾选, 意思是不将局部变量信息添加到类文件中, 如图 6-12 所示 : 图 6-12 当你在浏览器输入 URL, 如 requestparam1?username=123 时会报如下错误 Name for argument type [java.lang.string] not available, and parameter name information not found in class file either, 表示得不到功能处理方法的参数名, 此时我们需要如下方法进行入参 : public String requestparam2(@requestparam("username") String username) 明确告诉 Spring Web MVC 使用 username 进行入参 注解主要有哪些参数 :

120 value: 参数名字, 即入参的请求参数名字, 如 username 表示请求的参数区中的名字为 username 的参数的值将传入 ; required: 是否必须, 默认是 true, 表示请求中一定要有相应的参数, 否则将报 404 错误码 ; defaultvalue : 默认值, 表示如果请求中没有同名参数时的默认值, 默认值可以是 SpEL 表达式, 如 #{systemproperties['java.vm.version'] public String requestparam4(@requestparam(value="username",required=false) String username) 表示请求中可以没有名字为 username 的参数, 如果没有默认为 null, 此处需要注意如下几点 : 原子类型 : 必须有值, 否则抛出异常, 如果允许空值请使用包装类代替 Boolean 包装类型类型 : 默认 Boolean.FALSE, 其他引用类型默认为 null public String required=true, defaultvalue="zhang") String username) 表示如果请求中没有名字为 username 的参数, 默认值为 zhang 如果请求中有多个同名的应该如何接收呢? 如给用户授权时, 可能授予多个权限, 首先看下如下代码 : public String requestparam7(@requestparam(value="role") String rolelist) 如果请求参数类似于 url?role=admin&rule=user, 则实际 rolelist 参数入参的数据为 admin,user, 即多个数据之间使用, 分割 ; 我们应该使用如下方式来接收多个请求参数 : public String requestparam7(@requestparam(value="role") String[] rolelist) 或 public String requestparam8(@requestparam(value="list") List<String> list) 我们就介绍完了, 以上测试代码在 cn.javass.chapter6.web.controller. paramtype.requestparamtypecontroller 中 绑定 URI 用于将请求 URL public String int int topicid) 如请求的 URL 为 控制器 URL/users/123/topics/456, 则自动将 URL 中模板变量 {userid 和 {topicid 注解的同名参数上, 即入参后 userid=123 topicid=456 代码在 PathVariableTypeController 中 绑定 Cookie 用于将请求的 Cookie 数据映射到功能处理方法的参数上 public String test(@cookievalue(value="jsessionid", defaultvalue="") String sessionid) 如上配置将自动将 JSESSIONID 值入参到 sessionid 参数上,defaultValue 表示 Cookie 中没有 JSESSIONID 时默认为空

121 public String defaultvalue="") Cookie sessionid) 传入参数类型也可以是 javax.servlet.http.cookie 类型 测试代码在 CookieValueTypeController 相同的三个参数, 含义一样 public String String String[] accepts) 如上配置将自动将请求头 User-Agent 值入参到 useragent 参数上, 并将 Accept 请求头值入参到 accepts 参数上 测试代码在 HeaderValueTypeController 相同的三个参数, 含义一样 一个具有如下三个作用 : 1 绑定请求参数到命令对象 : 放在功能处理方法的入参上时, 用于将多个请求参数绑定到一个命令对象, 从而简化绑定流程, 而且自动暴露为模型数据用于视图页面展示时使用 ; 2 暴露表单引用对象为模型数据 : 放在处理器的一般方法 ( 非功能处理方法 ) 上时, 是为表单准备要展示的表单引用对象, 如注册时需要选择的所在城市等, 而且在执行功能处理方法 (@RequestMapping 注解的方法 ) 之前, 自动添加到模型对象中, 用于视图页面展示时使用 ; 3 方法返回值为模型数据 : 放在功能处理方法的返回值上时, 是暴露功能处理方法的返回值为模型数据, 用于视图页面展示时使用 一 绑定请求参数到命令对象如用户登录, 我们需要捕获用户登录的请求参数 ( 用户名 密码 ) 并封装为用户对象, 绑定多个请求参数到我们的命令对象 public String test1(@modelattribute("user") UserModel user) 和 一节中的五 命令 / 表单对象功能一样 它的作用是将该绑定的命令对象以 user 为名称添加到模型对象中供视图页面展示使用 我们此时可以在视图页面使用 ${user.username 来获取绑定的命令对象的属性 绑定请求参数到命令对象支持对象图导航式的绑定, 如请求参数包含?username=zhang&password=123&workInfo.city=bj 自动绑定到 user 中的 workinfo 属性的 city public String test2(@modelattribute("model") DataBinderTestModel model) { DataBinderTestModel 相关模型请从第三章拷贝过来, 请求参数到命令对象的绑定规则详见 数据绑定 一节, URI 模板变量也能自动绑定到命令对象中, 当你请求的 URL 中包含 bool=yes&schooinfo.specialty=computer&hobbylist[0]=program&hobbylist[1]=music&map[key1]=

122 value1&map[key2]=value2&state=blocked 会自动绑定到命令对象上 当 URI 模板变量和请求参数同名时,URI 模板变量具有高优先权 二 public List<String> citylist() { return Arrays.asList(" 北京 ", " 山东 "); 如上代码会在执行功能处理方法之前执行, 并将其自动添加到模型对象中, 在功能处理方法中调用 Model 入参的 containsattribute("citylist") 将会返回 //1 public UserModel getuser(@requestparam(value="username", defaultvalue="") String username) { //TODO 去数据库根据用户名查找用户对象 UserModel user = new UserModel(); user.setrealname("zhang"); return user; 如你要修改用户资料时一般需要根据用户的编号 / 用户名查找用户来进行编辑, 此时可以通过如上代码查找要编辑的用户 //2 public String test1(@modelattribute("user") UserModel user, Model model) 此处我们看到 1 和 2 有同名的命令对象, 那 Spring Web MVC 内部如何处理的呢 : (1 首先执行@ModelAttribute 注解的方法, 准备视图展示时所需要的模型数据 ;@ModelAttribute 规则一样, 等 ; (2 执行@RequestMapping 注解方法, 进行模型绑定时首先查找模型数据中是否含有同名对象, 如果有直接使用, 如果没有通过反射创建一个, 因此 2 处的 user 将使用 1 处返回的命令对象 即 2 处的 user 等于 1 处的 user 三 方法返回值为模型数据 UserModel test3(@modelattribute("user2") UserModel user) 大家可以看到返回值类型是命令对象类型, 注解, 此时会暴露返回值到模型数据 ( 名字为 user2 ) 中供视图展示使用 那哪个视图应该展示呢? 此时 Spring Web MVC 会根据 RequestToViewNameTranslator 进行逻辑视图名的翻译, 详见 RequestToViewNameTranslator 一节 此时又有问题了,@RequestMapping 注解方法的入参 user 暴露到模型数据中的名字也是 user2, 其实我们能猜到 : 注解的同名命令对 象 四 匿名绑定命令参数 public String test4(@modelattribute UserModel user, Model model) 或 public String test5(usermodel user, Model model) 此时我们没有为命令对象提供暴露到模型数据中的名字, 此时的名字是什么呢?Spring Web MVC 自动将简单类名 ( 首字母小写 ) 作为名字暴露, 如 cn.javass.chapter6.model.usermodel 暴露的名字为 usermodel

123 List<String> test6() 或 List<UserModel> test7() 对于集合类型 (Collection 接口的实现者们, 包括数组 ), 生成的模型对象属性名为 简单类名 ( 首字母小写 ) + List, 如 List<String> 生成的模型对象属性名为 stringlist,list<usermodel> 生成的模型对象属性名为 usermodellist 其他情况一律都是使用简单类名 ( 首字母小写 ) 作为模型对象属性名, 如 Map<String, UserModel> 类型的模型对象属性 名为 map 绑定命令对象到 session 有时候我们需要在多次请求之间保持数据, 一般情况需要我们明确的调用 HttpSession 的 API 来存取会话数据, 如多步骤提交的表单 Spring Web MVC 进行请求间透明的存取会话数据 //1 在控制器类头上添加@SessionAttributes = {"user") //1 public class SessionAttributeController //2 public UserModel inituser() //3 public String session1(@modelattribute("user") UserModel user) //4 通过 SessionStatus 的 setcomplete() //3 public String session(@modelattribute("user") UserModel user, SessionStatus status) { if(true) { //4 status.setcomplete(); return = {"user") 含义 = {"user") 标识将模型数据中的名字为 user 的对象存储到会话中 ( 默认 HttpSession), 此处 value 指定将模型数据中的哪些数据 ( 名字进行匹配 ) 存储到会话中, 此外还有一个 types 属性表示模型数据中的哪些类型的对象存储到会话范围内, 如果同时指定 value 和 types 属性则那些名字和类型都匹配的对象才能存储到会话范围内 的执行流程如下所示 : 1 注解信息查找会话内的对象放入到模型数据中 ; 2 注解的方法 : 如果模型数据中包含同名的数据, 注解方法进行准备表单引用数据, 而是使用 1 步骤中的会话数据 ; 如果模型数据中不包含同名的数据, 注解的方法并将返回值添加到模型数据中 ; 3 方法, 注解的参数 : 注解的同名对象, 如果有直接使用, 否则通过反射创建一个 ; 并将请求参数绑定到该命令对象 ; 此处需要注意 : 注解控制器类之后,3 步骤一定是从模型对象中取得同名的命令对象, 如

124 果模型数据中不存在将抛出 HttpSessionRequiredException Expected session attribute user (Spring3.1) 或 HttpSessionRequiredException Session attribute user required - not found in session(spring3.0) 异常 4 如果会话可以销毁了, 如多步骤提交表单的最后一步, 此时可以调用 SessionStatus 对象的 setcomplete() 指定的数据可以清理了, 功能处理方法执行完毕会进行清理会话数据 我们通过 Spring Web MVC 的源代码验证一下吧, 此处我们分析的是 Spring3.1 的 RequestMappingHandlerAdapter, 读者可以自行验证 Spring3.0 的 AnnotationMethodHandlerAdapter, 流程一样 : (1 RequestMappingHandlerAdapter.invokeHandlerMethod //1 RequestMappingHandlerAdapter 首先调用 ModelFactory 的 initmodel 方法准备模型数据 : modelfactory.initmodel(webrequest, mavcontainer, requestmappingmethod); //2 调用@RequestMapping 注解的功能处理方法 requestmappingmethod.invokeandhandle(webrequest, mavcontainer); //3 更新/ 合并模型数据 modelfactory.updatemodel(webrequest, mavcontainer); (2 ModelFactory.initModel Map<String,?> attributesinsession = this.sessionattributeshandler.retrieveattributes(request); //1.1 将与@SessionAttributes 注解相关的会话对象放入模型数据中 mavcontainer.mergeattributes(attributesinsession); //1.2 调用@ModelAttribute 方法添加表单引用对象 invokemodelattributemethods(request, mavcontainer); //1.3 验证模型数据中是否包含@SessionAttributes 注解相关的会话对象, 不包含抛出异常 for (String name : findsessionattributearguments(handlermethod)) { if (!mavcontainer.containsattribute(name)) { //1.4 此处防止在@ModelAttribute 注解方法又添加了会话对象 // 注解方法调用 session.setattribute("user", new UserModel()); Object value = this.sessionattributeshandler.retrieveattribute(request, name); if (value == null) { throw new HttpSessionRequiredException("Expected session attribute '" + name + "'"); mavcontainer.addattribute(name, value); (3 ModelFactory.invokeModelAttributeMethods for (InvocableHandlerMethod attrmethod : this.attributemethods) { String modelname = attrmethod.getmethodannotation(modelattribute.class).value(); //1.2.1 如果模型数据中包含同名数据则不再添加 if (mavcontainer.containsattribute(modelname)) { continue; //1.2.2 调用@ModelAttribute 注解方法并将返回值添加到模型数据中, 此处省略实现代码 (4 requestmappingmethod.invokeandhandle 调用功能处理方法, 此处省略 (5 ModelFactory.updateMode 更新模型数据 //3.1 如果会话被标识为完成, 注解相关的会话对象 if (mavcontainer.getsessionstatus().iscomplete()){ this.sessionattributeshandler.cleanupattributes(request); //3.2 如果会话没有完成, 注解相关的对象添加到会话中 else { this.sessionattributeshandler.storeattributes(request, mavcontainer.getmodel());

125 介绍完毕, 测试代码在 cn.javass.chapter6.web.controller.paramtype.sessionattributecontroller 中 另外 cn.javass.chapter6.web.controller.paramtype.wizardformcontroller 是一个类似于 4.11 AbstractWizardFormController 中介绍的多步骤表单实现, 此处不再贴代码, 多步骤提交表单需要考虑会话超时问题, 这种方式可能对用户不太友好, 我们可以采取隐藏表单 ( 即当前步骤将其他步骤的表单隐藏 ) 或表单数据存数据库 ( 每步骤更新下数据库数据 ) 等方案解决 绑定 SpEL 用于将一个 SpEL 表达式结果映射到到功能处理方法的参数上 public String test(@value("#{systemproperties['java.vm.version']") String jvmversion) 到此数据绑定我们就介绍完了, 对于没有介绍的方法参数和注解 ( 包括自定义注解 ) 在后续章节进行介绍 接下来我 们学习下数据类型转换吧

126 第七章注解式控制器的数据验证 类型转换及格式化 7.1 简介 在编写可视化界面项目时, 我们通常需要对数据进行类型转换 验证及格式化 一 在 Spring3 之前, 我们使用如下架构进行类型转换 验证及格式化 : 流程 : 1: 类型转换 : 首先调用 PropertyEditor 的 setastext(string), 内部根据需要调用 setvalue(object) 方法进行设置转换后的值 ; 2: 数据验证 : 需要显示调用 Spring 的 Validator 接口实现进行数据验证 ; 3: 格式化显示 : 需要调用 PropertyEditor 的 gettext 进行格式化显示 使用如上架构的缺点是 : (1 PropertyEditor 被设计为只能 String< >Object 之间转换, 不能任意对象类型 < > 任意类型, 如我们常见的 Long 时间戳到 Date 类型的转换是办不到的 ; (2 PropertyEditor 是线程不安全的, 也就是有状态的, 因此每次使用时都需要创建一个, 不可重用 ; (3 PropertyEditor 不是强类型的,setValue(Object) 可以接受任意类型, 因此需要我们自己判断类型是否兼容 ; (4 需要自己编程实现验证,Spring3 支持更棒的注解验证支持 ; (5 在使用 SpEL 表达式语言或 DataBinder 时, 只能进行 String<--->Object 之间的类型转换 ; (6 不支持细粒度的类型转换/ 格式化, 如 UserModel 的 registerdate 需要转换 / 格式化类似 的数据, 而 OrderModel 的 orderdate 需要转换 / 格式化类似 :11:13 的数据, 因为大家都为 java.util.date 类型, 因此不太容易进行细粒度转换 / 格式化 在 Spring Web MVC 环境中, 数据类型转换 验证及格式化通常是这样使用的 : 流程 : 1 类型转换: 首先表单数据 ( 全部是字符串 ) 通过 WebDataBinder 进行绑定到命令对象, 内部通过 PropertyEditor 实现 ; 2: 数据验证 : 在控制器中的功能处理方法中, 需要显示的调用 Spring 的 Validator 实现并将错误信息添加到 BindingResult 对象中 ; 3: 格式化显示 : 在表单页面可以通过如下方式展示通过 PropertyEditor 格式化的数据和错误信息 : <%@taglib prefix="spring" uri=" %> <%@taglib prefix="form" uri=" %>

127 首先需要通过如上 taglib 指令引入 spring 的两个标签库 //1 格式化单个命令 / 表单对象的值 ( 好像比较麻烦, 真心没有好办法 ) <spring:bind path="databindertest.phonenumber">${status.value</spring:bind> //2 通过 form 标签, 内部的表单标签会自动调用命令 / 表单对象属性对应的 PropertyEditor 进行格式化显示 <form:form commandname="databindertest"> <form:input path="phonenumber"/><!-- 如果出错会显示错误之前的数据而不是空 --> </form:form> //3 显示验证失败后的错误信息 <form:errors></form:errors> 如上 PropertyEditor 和验证 API 使用起来比较麻烦, 而且有许多缺点, 因此 Spring3 提供了更强大的类型转换 (Type Conversion) 支持, 它可以在任意对象之间进行类型转换, 不仅仅是 String< >Object; 也提供了强大的数据验证支 持 ; 同时提供了强大的数据格式化支持 二 从 Spring3 开始, 我们可以使用如下架构进行类型转换 验证及格式化 : 流程 : 1: 类型转换 : 内部的 ConversionService 会根据 S 源类型 /T 目标类型自动选择相应的 Converter SPI 进行类型转换, 而且是强类型的, 能在任意类型数据之间进行转换 ; 2: 数据验证 : 支持 JSR-303 验证框架, 放在需要验证的目标类型上即可 ; 3: 格式化显示 : 其实就是任意目标类型 ---->String 的转换, 完全可以使用 Converter SPI 完成 Spring 为了更好的诠释格式化 / 解析功能提供了 Formatter SPI, 支持根据 Locale 信息进行格式化 / 解析, 而且该套 SPI 可以支持字段 / 参数级别的细粒度格式化 / 解析, 流程如下 : 1: 类型解析 ( 转换 ): String---->T 类型目标对象的解析, 和 PropertyEditor 类似 ; 3: 格式化显示 : 任意目标类型 ---->String 的转换, 和 PropertyEditor 类似 Formatter SPI 最大特点是能进行字段 / 参数级别的细粒度解析 / 格式化控制, 即使是 Converter SPI 也是粗粒度的 ( 到某个 具体类型, 而不是其中的某个字段单独控制 ), 目前 Formatter SPI 还不是很完善, 如果您有好的想法可以到 Spring 官 网提建议 Formatter SPI 内部实现实际委托给 Converter SPI 进行转换, 即约束为解析 / 格式化 String<----> 任意目标类型 在 Spring Web MVC 环境中, 数据类型转换 验证及格式化通常是这样使用的 :

128 1 类型转换: 首先表单数据 ( 全部是字符串 ) 通过 WebDataBinder 进行绑定到命令对象, 内部通过 Converter SPI 实现 ; 2: 数据验证 : 使用 JSR-303 验证框架进行验证 ; 3: 格式化显示 : 在表单页面可以通过如下方式展示通过内部通过 Converter SPI 格式化的数据和错误信息 : <%@taglib prefix="spring" uri=" %> <%@taglib prefix="form" uri=" %> 首先需要通过如上 taglib 指令引入 spring 的两个标签库 //1 格式化单个命令 / 表单对象的值 ( 好像比较麻烦, 真心没有好办法 ) <spring:bind path="databindertest.phonenumber">${status.value</spring:bind> //2 <spring:eval> 标签, 自动调用 ConversionService 并选择相应的 Converter SPI 进行格式化展示 <spring:eval expression="databindertest.phonenumber"></spring:eval> 如上代码能工作的前提是在 RequestMappingHandlerMapping 配置了 ConversionServiceExposingInterceptor, 它的作用是暴露 conversionservice 到请求中以便如 <spring:eval> 标签使用 //3 通过 form 标签, 内部的表单标签会自动调用命令 / 表单对象属性对应的 PropertyEditor 进行格式化显示 <form:form commandname="databindertest"> <form:input path="phonenumber"/><!-- 如果出错会显示错误之前的数据而不是空 --> </form:form> //4 显示验证失败后的错误信息 <form:errors></form:errors> 接下来我们就详细学习一下这些知识吧 7.2 数据类型转换 Spring3 之前的 PropertyEditor PropertyEditor 介绍请参考 数据类型转换 一 测试之前我们需要准备好测试环境 : (1 模型对象, 和 数据类型转换 使用的一样, 需要将 DataBinderTestModel 模型类及相关类拷贝过来放入 cn.javass.chapter7.model 包中

129 (2 控制器定义: package cn.javass.chapter7.web.controller; // 省略 public class DataBinderTestController = "/databind") public String test(databindertestmodel command) { // 输出 command 对象看看是否绑定正确 System.out.println(command); model.addattribute("databindertest", command); return "bind/success"; (3 Spring 配置文件定义, 请参考 chapter7-servlet.xml, 并注册 DataBinderTestController: <bean class="cn.javass.chapter7.web.controller.databindertestcontroller"/> (4 测试的 URL: =program&hobbylist[1]=music&map[key1]=value1&map[key2]=value2&phonenumber= &date= :48:48&state=blocked 二 注解式控制器注册 PropertyEditor: 1 使用 WebDataBinder 进行控制器级别注册 PropertyEditor( 控制器独享 // 此处的参数也可以是 ServletRequestDataBinder 类型 public void initbinder(webdatabinder binder) throws Exception { // 注册自定义的属性编辑器 //1 日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateeditor = new CustomDateEditor(df, true); // 表示如果命令对象有 Date 类型的属性, 将使用该属性编辑器进行类型转换 binder.registercustomeditor(date.class, dateeditor); // 自定义的电话号码编辑器 ( 和 数据类型转换 一样) binder.registercustomeditor(phonenumbermodel.class, new PhoneNumberEditor()); 和 数据类型转换 一节类似, 来注册自定义的 PropertyEditor 2 使用 WebBindingInitializer 批量注册 PropertyEditor 和 数据类型转换 不太一样, 因为我们的注解式控制器是 POJO, 没有实现任何东西, 因此无法注入 WebBindingInitializer, 此时我们需要把 WebBindingInitializer 注入到我们的 RequestMappingHandlerAdapter 或 AnnotationMethodHandlerAdapter, 这样对于所有的注解式控制器都是共享的 <bean class="org.springframework.web.servlet.mvc.method.annotation.requestmappinghandleradapte <property name="webbindinginitializer"> <bean class="cn.javass.chapter7.web.controller.support.initializer.mywebbindinginitializer </property> </bean>

130 注册 PropertyEditor 的方法 3 全局级别注册 PropertyEditor( 全局共享 ) 和 数据类型转换 一节一样, 此处不再重复 请参考 数据类型转换 的 全局级别注册 PropertyEditor ( 全局共享 ) 接下来我们看一下 Spring3 提供的更强大的类型转换支持 Spring3 开始的类型转换系统 Spring3 引入了更加通用的类型转换系统, 其定义了 SPI 接口 (Converter 等 ) 和相应的运行时执行类型转换的 API (ConversionService 等 ), 在 Spring 中它和 PropertyEditor 功能类似, 可以替代 PropertyEditor 来转换外部 Bean 属性的 值到 Bean 属性需要的类型 该类型转换系统是 Spring 通用的, 其定义在 org.springframework.core.convert 包中, 不仅仅在 Spring Web MVC 场景下 目标是完全替换 PropertyEditor, 提供无状态 强类型且可以在任意类型之间转换的类型转换系统, 可以用于任何需要 的地方, 如 SpEL 数据绑定 Converter SPI 完成通用的类型转换逻辑, 如 java.util.date<---->java.lang.long 或 java.lang.string---->phonenumbermodel 等 架构 1 类型转换器 : 提供类型转换的实现支持 一个有如下三种接口 : (1 Converter: 类型转换器, 用于转换 S 类型到 T 类型, 此接口的实现必须是线程安全的且可以被共享 package org.springframework.core.convert.converter; public interface Converter<S, T> { //1 S 是源类型 T 是目标类型 T convert(s source); //2 转换 S 类型的 source 到 T 目标类型的转换方法 示例 : 请参考 cn.javass.chapter7.converter.support.stringtophonenumberconverter 转换器, 用于将 String--->PhoneNumberModel 此处我们可以看到 Converter 接口实现只能转换一种类型到另一种类型, 不能进行多类型转换, 如将一个数组转换 成集合, 如 (String[] ----> List<String> String[]----->List<PhoneNumberModel> 等 )

131 (2 GenericConverter 和 ConditionalGenericConverter:GenericConverter 接口实现能在多种类型之间进行转换, ConditionalGenericConverter 是有条件的在多种类型之间进行转换 package org.springframework.core.convert.converter; public interface GenericConverter { Set<ConvertiblePair> getconvertibletypes(); Object convert(object source, TypeDescriptor sourcetype, TypeDescriptor targettype); getconvertibletypes: 指定了可以转换的目标类型对 ; convert: 在 sourcetype 和 targettype 类型之间进行转换 package org.springframework.core.convert.converter; public interface ConditionalGenericConverter extends GenericConverter { boolean matches(typedescriptor sourcetype, TypeDescriptor targettype); matches: 用于判断 sourcetype 和 targettype 类型之间能否进行类型转换 示例 : 如 org.springframework.core.convert.support.arraytocollectionconverter 和 CollectionToArrayConverter 用于在数组 和集合间进行转换的 ConditionalGenericConverter 实现, 如在 String[]<---->List<String> String[]<---->List<PhoneNumberModel> 等之间进行类型转换 对于我们大部分用户来说一般不需要自定义 GenericConverter, 如果需要可以参考内置的 GenericConverter 来实现自己 的 (3 ConverterFactory: 工厂模式的实现, 用于选择将一种 S 源类型转换为 R 类型的子类型 T 的转换器的工厂接口 package org.springframework.core.convert.converter; public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getconverter(class<t> targettype); S: 源类型 ;R 目标类型的父类型 ;T: 目标类型, 且是 R 类型的子类型 ; getconverter: 得到目标类型的对应的转换器 示例 : 如 org.springframework.core.convert.support.numbertonumberconverterfactory 用于在 Number 类型子类型之间进 行转换, 如 Integer--->Double, Byte---->Integer, Float--->Double 等 对于我们大部分用户来说一般不需要自定义 ConverterFactory, 如果需要可以参考内置的 ConverterFactory 来实现自己 的 2 类型转换器注册器 类型转换服务 : 提供类型转换器注册支持, 运行时类型转换 API 支持

132 一共有如下两种接口 : (1 ConverterRegistry: 类型转换器注册支持, 可以注册 / 删除相应的类型转换器 package org.springframework.core.convert.converter; public interface ConverterRegistry { void addconverter(converter<?,?> converter); void addconverter(class<?> sourcetype, Class<?> targettype, Converter<?,?> converter); void addconverter(genericconverter converter); void addconverterfactory(converterfactory<?,?> converterfactory); void removeconvertible(class<?> sourcetype, Class<?> targettype); 可以注册 :Converter 实现,GenericConverter 实现,ConverterFactory 实现 (2 ConversionService: 运行时类型转换服务接口, 提供运行期类型转换的支持 package org.springframework.core.convert; public interface ConversionService { boolean canconvert(class<?> sourcetype, Class<?> targettype); boolean canconvert(typedescriptor sourcetype, TypeDescriptor targettype); <T> T convert(object source, Class<T> targettype); Object convert(object source, TypeDescriptor sourcetype, TypeDescriptor targettype); canconvert: 用于判断源类型和目标类型之间是否可以转换 ; convert: 将源对象转换为目标类型的目标对象 Spring 提供了两个默认实现 ( 其都实现了 ConverterRegistry ConversionService 接口 ): DefaultConversionService: 默认的类型转换服务实现 ; DefaultFormattingConversionService: 带数据格式化支持的类型转换服务实现, 一般使用该服务实现即可 Spring 内建的类型转换器如下所示 : 类名 说明 第一组 : 标量转换器

133 类名 说明 StringToBooleanConverter ObjectToStringConverter String----->Boolean true:true/on/yes/1; false:false/off/no/0 Object----->String 调用 tostring 方法转换 StringToNumberConverterFactory String----->Number( 如 Integer Long 等 ) NumberToNumberConverterFactory Number 子类型 (Integer Long Double 等 )< > Number 子类型 (Integer Long Double 等 ) StringToCharacterConverter NumberToCharacterConverter String----->java.lang.Character 取字符串第一个字符 Number 子类型 (Integer Long Double 等 ) > java.lang.character CharacterToNumberFactory java.lang.character >Number 子类型 (Integer Long Double 等 ) StringToEnumConverterFactory EnumToStringConverter StringToLocaleConverter PropertiesToStringConverter StringToPropertiesConverter String----->enum 类型通过 Enum.valueOf 将字符串转换为需要的 enum 类型 enum 类型 ----->String 返回 enum 对象的 name() 值 String----->java.util.Local java.util.properties----->string 默认通过 ISO 解码 String----->java.util.Properties 默认使用 ISO 编码 第二组 : 集合 数组相关转换器 ArrayToCollectionConverter CollectionToArrayConverter ArrayToArrayConverter CollectionToCollectionConverter MapToMapConverter ArrayToStringConverter StringToArrayConverter ArrayToObjectConverter ObjectToArrayConverter CollectionToStringConverter StringToCollectionConverter CollectionToObjectConverter ObjectToCollectionConverter 任意 S 数组 ----> 任意 T 集合 (List Set) 任意 T 集合 (List Set)----> 任意 S 数组任意 S 数组 <----> 任意 T 数组任意 T 集合 (List Set)<----> 任意 T 集合 (List Set) 即集合之间的类型转换 Map<---->Map 之间的转换任意 S 数组 ---->String 类型 String-----> 数组默认通过, 分割, 且去除字符串的两边空格 (trim) 任意 S 数组 ----> 任意 Object 的转换 ( 如果目标类型和源类型兼容, 直接返回源对象 ; 否则返回 S 数组的第一个元素并进行类型转换 ) Object-----> 单元素数组任意 T 集合 (List Set)---->String 类型 String-----> 集合 (List Set) 默认通过, 分割, 且去除字符串的两边空格 (trim) 任意 T 集合 ----> 任意 Object 的转换 ( 如果目标类型和源类型兼容, 直接返回源对象 ; 否则返回 S 数组的第一个元素并进行类型转换 ) Object-----> 单元素集合

134 类名 说明 第三组 : 默认 (fallback) 转换器 : 之前的转换器不能转换时调用 ObjectToObjectConverter IdToEntityConverter FallbackObjectToStringConverter Object(S)----->Object(T) 首先尝试 valueof 进行转换 没有则尝试 new 构造器 (S) Id(S)----->Entity(T) 查找并调用 public static T find[entityname](s) 获取目标对象,EntityName 是 T 类型的简单类型 Object----->String ConversionService 作为恢复使用, 即其他转换器不能转换时调用 ( 执行对象的 tostring() 方法 ) S: 代表源类型,T: 代表目标类型 如上的转换器在使用转换服务实现 DefaultConversionService 和 DefaultFormattingConversionService 时会自动注册 示例 (1 自定义 String----->PhoneNumberModel 的转换器 package cn.javass.chapter7.web.controller.support.converter; // 省略 import public class StringToPhoneNumberConverter implements Converter<String, PhoneNumberModel> { Pattern pattern = public PhoneNumberModel convert(string source) { if(!stringutils.haslength(source)) { //1 如果 source 为空返回 null return null; Matcher matcher = pattern.matcher(source); if(matcher.matches()) { //2 如果匹配进行转换 PhoneNumberModel phonenumber = new PhoneNumberModel(); phonenumber.setareacode(matcher.group(1)); phonenumber.setphonenumber(matcher.group(2)); return phonenumber; else { //3 如果不匹配转换失败 throw new IllegalArgumentException(String.format(" 类型转换失败, 需要格式 [ ], 但格式是 [%s]", source)); String 转换为 Date 的类型转换器, 请参考 cn.javass.chapter7.web.controller.support.converter.stringtodateconverter (2 测试用例 public void teststringtophonenumberconvert() { DefaultConversionService conversionservice = new DefaultConversionService(); conversionservice.addconverter(new StringToPhoneNumberConverter());

135 类似于 PhoneNumberEditor 将字符串 转换为 public void testotherconvert() { DefaultConversionService conversionservice = new DefaultConversionService(); //"1"--->true( 字符串 1 可以转换为布尔值 true) Assert.assertEquals(Boolean.valueOf(true), Boolean.class)); conversionservice.convert("1", //"1,2,3,4"--->List( 转换完毕的集合大小为 4) Assert.assertEquals(4, conversionservice.convert("1,2,3,4", List.class).size()); 其他类型转换器使用也是类似的, 此处不再重复 集成到 Spring Web MVC 环境 (1 注册 ConversionService 实现和自定义的类型转换器 <!-- 1 注册 ConversionService --> <bean id="conversionservice" class="org.springframework.format.support. FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="cn.javass.chapter7.web.controller.support. converter.stringtophonenumberconverter"/> <bean class="cn.javass.chapter7.web.controller.support. converter.stringtodateconverter"> <constructor-arg value="yyyy-mm-dd"/> </bean> </list> </property> </bean> FormattingConversionServiceFactoryBean: 是 FactoryBean 实现, 默认使用 DefaultFormattingConversionService 转换器服务实现 ; converters: 注册我们自定义的类型转换器, 此处注册了 String--->PhoneNumberModel 和 String--->Date 的类型转换器

136 (2 通过 ConfigurableWebBindingInitializer 注册 ConversionService <!-- 2 使用 ConfigurableWebBindingInitializer 注册 conversionservice --> <bean id="webbindinginitializer" class="org.springframework.web.bind.support. ConfigurableWebBindingInitializer"> <property name="conversionservice" ref="conversionservice"/> </bean> 此处我们通过 ConfigurableWebBindingInitializer 绑定初始化器进行 ConversionService 的注册 ; 3 注册 ConfigurableWebBindingInitializer 到 RequestMappingHandlerAdapter <bean class="org.springframework.web.servlet.mvc.method.annotation. RequestMappingHandlerAdapter"> <property name="webbindinginitializer" ref="webbindinginitializer"/> </bean> 通过如上配置, 我们就完成了 Spring3.0 的类型转换系统与 Spring Web MVC 的集成 此时可以启动服务器输入之前的 URL 测试了 此时可能有人会问, 如果我同时使用 PropertyEditor 和 ConversionService, 执行顺序是什么呢? 内部首先查找 PropertyEditor 进行类型转换, 如果没有找到相应的 PropertyEditor 再通过 ConversionService 进行转换 如上集成过程看起来比较麻烦, 后边我们会介绍 <mvc:annotation-driven> 会自动 注册, 后续章节再详细介绍 7.3 数据格式化 在如 Web / 客户端项目中, 通常需要将数据转换为具有某种格式的字符串进行展示, 因此上节我们学习的数据类型转换系统核心作用不是完成这个需求, 因此 Spring3 引入了格式化转换器 (Formatter SPI) 和格式化服务 API (FormattingConversionService) 从而支持这种需求 在 Spring 中它和 PropertyEditor 功能类似, 可以替代 PropertyEditor 来进行对象的解析和格式化, 而且支持细粒度的字段级别的格式化 / 解析 Formatter SPI 核心是完成解析和格式化转换逻辑, 在如 Web 应用 / 客户端项目中, 需要解析 打印 / 展示本地化的对象值 时使用, 如根据 Locale 信息将 java.util.date---->java.lang.string 打印 / 展示 java.lang.string---->java.util.date 等 该格式化转换系统是 Spring 通用的, 其定义在 org.springframework.format 包中, 不仅仅在 Spring Web MVC 场景下 架构 1 格式化转换器 : 提供格式化转换的实现支持

137 一共有如下两组四个接口 : (1 Printer 接口 : 格式化显示接口, 将 T 类型的对象根据 Locale 信息以某种格式进行打印显示 ( 即返回字符串形式 ); package org.springframework.format; public interface Printer<T> { String print(t object, Locale locale); (2 Parser 接口 : 解析接口, 根据 Locale 信息解析字符串到 T 类型的对象 ; package org.springframework.format; public interface Parser<T> { T parse(string text, Locale locale) throws ParseException; 解析失败可以抛出 java.text.parseexception 或 IllegalArgumentException 异常即可 (3 Formatter 接口 : 格式化 SPI 接口, 继承 Printer 和 Parser 接口, 完成 T 类型对象的格式化和解析功能 ; package org.springframework.format; public interface Formatter<T> extends Printer<T>, Parser<T> { (4 AnnotationFormatterFactory 接口 : 注解驱动的字段格式化工厂, 用于创建带注解的对象字段的 Printer 和 Parser, 即用于格式化和解析带注解的对象字段 package org.springframework.format; public interface AnnotationFormatterFactory<A extends Annotation> {//1 可以识别的注解类型 Set<Class<?>> getfieldtypes();//2 可以被 A 注解类型注解的字段类型集合 Printer<?> getprinter(a annotation, Class<?> fieldtype);//3 根据 A 注解类型和 fieldtype 类型获取 Printer Parser<?> getparser(a annotation, Class<?> fieldtype);//4 根据 A 注解类型和 fieldtype 类型获取 Parser 返回用于格式化和解析被 A 注解类型注解的字段值的 Printer 和 Parser 如 JodaDateTimeFormatAnnotationFormatterFactory 注解的 java.util.date 字段类型创建相应的 Printer 和 Parser 进行格式化和解析 2 格式化转换器注册器 格式化服务 : 提供类型转换器注册支持, 运行时类型转换 API 支持

138 一个有如下两种接口 : ( 1 FormatterRegistry : 格式化转换器注册器, 用于注册格式化转换器 (Formatter Printer 和 Parser AnnotationFormatterFactory); package org.springframework.format; public interface FormatterRegistry extends ConverterRegistry { //1 添加格式化转换器 (Spring3.1 新增 API) void addformatter(formatter<?> formatter); //2 为指定的字段类型添加格式化转换器 void addformatterforfieldtype(class<?> fieldtype, Formatter<?> formatter); //3 为指定的字段类型添加 Printer 和 Parser void addformatterforfieldtype(class<?> fieldtype, Printer<?> printer, Parser<?> parser); //4 添加注解驱动的字段格式化工厂 AnnotationFormatterFactory void addformatterforfieldannotation( AnnotationFormatterFactory<? extends Annotation> annotationformatterfactory); (2 FormattingConversionService: 继承自 ConversionService, 运行时类型转换和格式化服务接口, 提供运行期类型 转换和格式化的支持 FormattingConversionService 内部实现如下图所示 : 我们可以看到 FormattingConversionService 内部实现如上所示, 当你调用 convert 方法时 : ⑴ 若是 S 类型 ----->String: 调用私有的静态内部类 PrinterConverter, 其又调用相应的 Printer 的实现进行格式化 ; ⑵ 若是 String----->T 类型 : 调用私有的静态内部类 ParserConverter, 其又调用相应的 Parser 的实现进行解析 ; ⑶ 若是 A 注解类型注解的 S 类型 ----->String: 调用私有的静态内部类 AnnotationPrinterConverter, 其又调用相应的 AnnotationFormatterFactory 的 getprinter 获取 Printer 的实现进行格式化 ; ⑷ 若是 String----->A 注解类型注解的 T 类型 : 调用私有的静态内部类 AnnotationParserConverter, 其又调用相应的 AnnotationFormatterFactory 的 getparser 获取 Parser 的实现进行解析 注 :S 类型表示源类型,T 类型表示目标类型,A 表示注解类型

139 此处可以可以看出之前的 Converter SPI 完成任意 Object 与 Object 之间的类型转换, 而 Formatter SPI 完成任意 Object 与 String 之间的类型转换 ( 即格式化和解析, 与 PropertyEditor 类似 ) Spring 内建的格式化转换器如下所示 : 类名 说明 DateFormatter NumberFormatter CurrencyFormatter PercentFormatter NumberFormatAnnotationFormatterFactory JodaDateTimeFormatAnnotationFormatterFactory java.util.date<---->string 实现日期的格式化 / 解析 java.lang.number<---->string 实现通用样式的格式化 / 解析 java.lang.bigdecimal<---->string 实现货币样式的格式化 / 解析 java.lang.number<---->string 实现百分数样式的格式化 / 注解类型的数字字段类型 <---->String 1 指定格式化 / 解析格式 2 可以格式化 / 解析的数字类型 :Short Integer Long Float Double BigDecimal 注解类型的日期字段类型 <---->String 1 指定格式化 / 解析格式 2 可以格式化 / 解析的日期类型 : joda 中的日期类型 ( org.joda.time 包中的 ): LocalDate LocalDateTime LocalTime ReadableInstant java 内置的日期类型 :Date Calendar Long classpath 中必须有 Joda-Time 类库, 否则无法格式化日期类型 NumberFormatAnnotationFormatterFactory 和 JodaDateTimeFormatAnnotationFormatterFactory( 如果 classpath 提供了 Joda-Time 类库 ) 在使用格式化服务实现 DefaultFormattingConversionService 时会自动注册 示例 在示例之前, 我们需要到 下载 Joda-Time 类库, 本书使用的是 joda-time-2.1 版本, 将如 下 jar 包添加到 classpath: joda-time-2.1.jar 类型级别的解析 / 格式化 一 直接使用 Formatter SPI 进行解析 / 格式化

140 // 二 CurrencyFormatter: 实现货币样式的格式化 / 解析 CurrencyFormatter currencyformatter = new CurrencyFormatter(); currencyformatter.setfractiondigits(2);// 保留小数点后几位 currencyformatter.setroundingmode(roundingmode.ceiling);// 舍入模式 (ceilling 表示四舍五入 ) //1 将带货币符号的字符串 $ 转换为 BigDecimal("123.00") Assert.assertEquals(new BigDecimal("123.13"), currencyformatter.parse("$ ", Locale.US //2 将 BigDecimal("123") 格式化为字符串 $ 展示 Assert.assertEquals("$123.00", currencyformatter.print(new BigDecimal("123"), Locale.US)); Assert.assertEquals(" ", currencyformatter.print(new BigDecimal("123"), Locale.CHINA Assert.assertEquals(" ", currencyformatter.print(new BigDecimal("123"), Locale.JAPAN parse 方法 : 将带格式的字符串根据 Locale 信息解析为相应的 BigDecimal 类型数据 ; print 方法 : 将 BigDecimal 类型数据根据 Locale 信息格式化为字符串数据进行展示 不同于 Convert SPI,Formatter SPI 可以根据本地化 (Locale) 信息进行解析 / 格式化 其他测试用例请参考 cn.javass.chapter7.web.controller.support.formatter.innerformattertest 的 testnumber 测试方法和 testdate 测试方法 二 使用 DefaultFormattingConversionService 进行解析 / public void testwithdefaultformattingconversionservice() { DefaultFormattingConversionService conversionservice = new DefaultFormattingConversionServ // 默认不自动注册任何 Formatter CurrencyFormatter currencyformatter = new CurrencyFormatter(); currencyformatter.setfractiondigits(2);// 保留小数点后几位 currencyformatter.setroundingmode(roundingmode.ceiling);// 舍入模式 (ceilling 表示四舍五入 ) // 注册 Formatter SPI 实现 conversionservice.addformatter(currencyformatter); // 绑定 Locale 信息到 ThreadLocal //FormattingConversionService 内部自动获取作为 Locale 信息, 如果不设值默认是 Locale.getDefault() LocaleContextHolder.setLocale(Locale.US); Assert.assertEquals("$1,234.13", conversionservice.convert(new BigDecimal("1234. String.class)); LocaleContextHolder.setLocale(null); LocaleContextHolder.setLocale(Locale.CHINA); Assert.assertEquals(" 1,234.13", conversionservice.convert(new BigDecimal("1234. String.class)); Assert.assertEquals(new BigDecimal(" "), conversionservice.convert(" 1,23 BigDecimal.class)); LocaleContextHolder.setLocale(null); DefaultFormattingConversionService: 带数据格式化功能的类型转换服务实现 ; conversionservice.addformatter(): 注册 Formatter SPI 实现 ; conversionservice.convert(new BigDecimal(" "), String.class): 用于将 BigDecimal 类型数据

141 格式化为字符串类型, 此处根据 LocaleContextHolder.setLocale(locale) 设置的本地化信息进行格式化 ; conversionservice.convert(" 1,234.13", BigDecimal.class): 用于将字符串类型数据解析为 BigDecimal 类型数据, 此处也是根据 LocaleContextHolder.setLocale(locale) 设置的本地化信息进行解 ; LocaleContextHolder.setLocale(locale): 设置本地化信息到 ThreadLocal, 以便 FormatterSPI 根据本地化信息进行解析 / 格式化 ; 具体测试代码请参考 cn.javass.chapter7.web.controller.support.formatter.innerformattertest 的 testwithdefaultformattingconversionservice 测试方法 三 自定义 Formatter 进行解析 / 格式化此处以解析 / 格式化 PhoneNumberModel 为例 (1 定义 Formatter SPI 实现 package cn.javass.chapter7.web.controller.support.formatter; // 省略 import public class PhoneNumberFormatter implements Formatter<PhoneNumberModel> { Pattern pattern = public String print(phonenumbermodel phonenumber, Locale locale) {//1 格式化 if(phonenumber == null) { return ""; return new public PhoneNumberModel parse(string text, Locale locale) throws ParseException {//2 解析 if(!stringutils.haslength(text)) { //1 如果 source 为空返回 null return null; Matcher matcher = pattern.matcher(text); if(matcher.matches()) { //2 如果匹配进行转换 PhoneNumberModel phonenumber = new PhoneNumberModel(); phonenumber.setareacode(matcher.group(1)); phonenumber.setphonenumber(matcher.group(2)); return phonenumber; else { //3 如果不匹配转换失败 throw new IllegalArgumentException(String.format(" 类型转换失败, 需要格式 [ ], 但格式是 [%s]" text)); 类似于 Convert SPI 实现, 只是此处的相应方法会传入 Locale 本地化信息, 这样可以为不同地区进行解析 / 格式化数据 (2 测试用例 : package cn.javass.chapter7.web.controller.support.formatter; // import

142 通过 PhoneNumberFormatter 可以解析 String--->PhoneNumberModel 和格式化 PhoneNumberModel--->String 到此, 类型级别的解析 / 格式化我们就介绍完了, 从测试用例可以看出类型级别的是对项目中的整个类型实施相同的解 析 / 格式化逻辑 有的同学可能需要在不同的类的字段实施不同的解析 / 格式化逻辑, 如用户模型类的注册日期字段只需要如 格式进行解析 / 格式化即可, 而订单模型类的下订单日期字段可能需要如 :13:13 格 式进行展示 接下来我们学习一下如何进行字段级别的解析 / 格式化吧 字段级别的解析 / 格式化 一 使用内置的注解进行字段级别的解析 / 格式化 : (1 测试模型类准备: package cn.javass.chapter7.model; public class FormatterModel pattern="#,###") private int private double private double private Date HH:mm:ss") private Date orderdate; // 省略 getter/setter

143 此处我们使用了 Spring 字段级别解析 / 格式化的两个内置注解 定义数字相关的解析 / 格式化元数据 ( 通用样式 货币样式 百分数样式 ), 参数如下 : style: 用于指定样式类型, 包括三种 :Style.NUMBER( 通用样式 ) Style.CURRENCY( 货币样式 ) Style.PERCENT ( 百分数样式 ), 默认 Style.NUMBER; pattern: 自定义样式, 如 定义日期相关的解析 / 格式化元数据, 参数如下 : pattern: 指定解析 / 格式化字段数据的模式, 如 yyyy-mm-dd HH:mm:ss iso: 指定解析 / 格式化字段数据的 ISO 模式, 包括四种 :ISO.NONE( 不使用 ) ISO.DATE(yyyy-MM-dd) ISO.TIME(hh:mm:ss.SSSZ) ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.sssz), 默认 ISO.NONE; style : 指定用于格式化的样式模式, 默认 SS, 具体使用请参考 Joda-Time 类库的 org.joda.time.format.datetimeformat 的 forstyle 的 javadoc; 优先级 : pattern 大于 iso 大于 style (2 测试用例 public void test() throws SecurityException, NoSuchFieldException { // 的支持 DefaultFormattingConversionService conversionservice = new DefaultFormattingConversionService(); // 准备测试模型对象 FormatterModel model = new FormatterModel(); model.settotalcount(10000); model.setdiscount(0.51); model.setsummoney( ); model.setregisterdate(new Date( , 4, 1)); model.setorderdate(new Date( , 4, 1, 20, 18, 18)); // 获取类型信息 TypeDescriptor descriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("totalCount")); TypeDescriptor stringdescriptor = TypeDescriptor.valueOf(String.class); Assert.assertEquals("10,000", conversionservice.convert(model.gettotalcount(), descriptor, stringdescriptor)); Assert.assertEquals(model.getTotalCount(), conversionservice.convert("10,000", stringdescriptor, descriptor)); TypeDescriptor: 拥有类型信息的上下文, 用于 Spring3 类型转换系统获取类型信息的 ( 可以包含类 字段 方法参数 属性信息 ); 通过 TypeDescriptor, 我们就可以获取 ( 类 字段 方法参数 属性 ) 的各种信息, 如注解类型信息 ; conversionservice.convert(model.gettotalcount(), descriptor, stringdescriptor) : 将 totalcount 格式化为字符串类型, 此处会根据 totalcount 字段的注解信息 ( 通过 descriptor 对象获取 ) 来进行格式化 ; conversionservice.convert("10,000", stringdescriptor, descriptor): 将字符串 10,000 解析

144 为 totalcount 字段类型, 此处会根据 totalcount 字段的注解信息 ( 通过 descriptor 对象获取 ) 来进行解析 (3 通过为不同的字段指定不同的注解信息进行字段级别的细粒度数据解析/ 格式化 descriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("registerDate")); Assert.assertEquals(" ", conversionservice.convert(model.getregisterdate(), descriptor, stringdescriptor)); Assert.assertEquals(model.getRegisterDate(), conversionservice.convert(" ", stringdescriptor, descriptor)); descriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("orderDate")); Assert.assertEquals(" :18:18", conversionservice.convert(model.getorderdate(), descriptor, stringdescriptor)); Assert.assertEquals(model.getOrderDate(), conversionservice.convert(" :18:18", stringdescriptor, descriptor)); 通过如上测试可以看出, 我们可以通过字段注解方式实现细粒度的数据解析 / 格式化控制, 但是必须使用 TypeDescriptor 来指定类型的上下文信息, 即编程实现字段的数据解析 / 格式化比较麻烦 其他测试用例请参考 cn.javass.chapter7.web.controller.support.formatter.innerfieldformattertest 的 test 测试方法 二 自定义注解进行字段级别的解析 / 格式化 : 此处以解析 / 格式化 PhoneNumberModel 字段为例 (1 定义解析/ 格式化字段的注解类型 : package cn.javass.chapter7.web.controller.support.formatter; // 省略 ElementType.FIELD, PhoneNumber { (2 实现 AnnotationFormatterFactory 注解格式化工厂 : package cn.javass.chapter7.web.controller.support.formatter; // 省略 import

145 AnnotationFormatterFactory 实现会根据注解信息和字段类型获取相应的解析器 / 格式化器 (3 修改 FormatterModel 添加如下代码 private PhoneNumberModel phonenumber; (4 public void throws

146 此处使用 DefaultFormattingConversionService 的 addformatterforfieldannotation 注册自定义的注 解格式化工厂 PhoneNumberFormatAnnotationFormatterFactory 到此, 编程进行数据的格式化 / 解析我们就完成了, 使用起来还是比较麻烦, 接下来我们将其集成到 Spring Web MVC 环境中 集成到 Spring Web MVC 环境 一 注册 FormattingConversionService 实现和自定义格式化转换器 : <bean id="conversionservice" class="org.springframework.format.support.formattingconversionservicefactorybean"> <! 此处省略之前注册的自定义类型转换器 --> <property name="formatters"> <list> <bean class="cn.javass.chapter7.web.controller.support.formatter. PhoneNumberFormatAnnotationFormatterFactory"/> </list> </property> </bean> 其他配置和之前学习 一节一样 二 示例 : (1 模型对象字段的数据解析 / 格式化 = "/format1") public String test1(@modelattribute("model") FormatterModel formatmodel) { return "format/success";

147 totalcount:<spring:bind path="model.totalcount">${status.value</spring:bind><br/> discount:<spring:bind path="model.discount">${status.value</spring:bind><br/> summoney:<spring:bind path="model.summoney">${status.value</spring:bind><br/> phonenumber:<spring:bind path="model.phonenumber">${status.value</spring:bind><br/> <!-- 如果没有配置 org.springframework.web.servlet.handler.conversionserviceexposinginterceptor 将会报错 --> phonenumber:<spring:eval expression="model.phonenumber"></spring:eval><br/> <br/><br/> <form:form commandname="model"> <form:input path="phonenumber"/><br/> <form:input path="summoney"/> </form:form> 在浏览器输入测试 URL: er= 数据会正确绑定到我们的 formatmodel, 即请求参数能被正确的解析并绑定到我们的命令对象上, 而且在 JSP 页面也能 正确的显示格式化后的数据 ( 即正确的被格式化显示 ) ( 2 功能处理方法参数级别的数据解析 = "/format2") public Date date) { System.out.println(phoneNumber); System.out.println(date); return "format/success2"; 此处我们可以直接在功能处理方法的参数上使用格式化注解类型进行注解,Spring Web MVC 能根据此注解信息对请求 参数进行解析并正确的绑定 在浏览器输入测试 URL: 数据会正确的绑定到我们的 phonenumber 和 date 上, 即请求的参数能被正确的解析并绑定到我们的参数上 控制器代码位于 cn.javass.chapter7.web.controller.dataformattestcontroller 中 如果我们请求参数数据不能被正确解析并绑定或输入的数据不合法等该怎么处理呢? 接下来的一节我们来学习下绑定 失败处理和数据验证相关知识

148 7.4 数据验证 编程式数据验证 Spring 2.x 提供了编程式验证支持, 详见 数据验证 章节, 在此我们重写 编程式验证器 一节示 例 (1 验证器实现 复制 cn.javass.chapter4.web.controller.support.validator.usermodelvalidator 到 cn.javass.chapter7.web.controller.support.validator.usermodelvalidator (2 public class RegisterSimpleFormController { private UserModelValidator validator = new //1 暴露表单引用对象为模型数据 public UserModel getuser() { return new = "/validator", method = RequestMethod.GET) public String showregisterform() { //2 表单展示 return = "/validator", method = RequestMethod.POST) public String UserModel user, Errors errors) { //3 表单提交 validator.validate(user, errors); //1 调用 UserModelValidator 的 validate 方法进行验证 if(errors.haserrors()) { //2 如果有错误再回到表单展示页面 return showregisterform(); return "redirect:/success"; 在 submitform 方法中, 我们首先调用之前写的 UserModelValidator 的 validate 方法进行验证, 当然此处可以直接验证 并通过 Errors 接口来保留错误 ; 此处还通过 Errors 接口的 haserrors 方法来决定当验证失败时显示的错误页面 (3 spring 配置文件 chapter7-servlet.xml <bean class="cn.javass.chapter7.web.controller.registersimpleformcontroller"/> (4 错误码配置 (messages.properties), 需要执行 NativeToAscii 直接将 springmvc-chapter4 项目中 src 下的 messages.properties 复制到 src 目录下 在 spring 配置文件 chapter7-servlet.xml 中添加 messagesource: <bean id="messagesource" class="org.springframework.context.support.reloadableresourcebundlemessagesource"> <property name="basename" value="classpath:messages"/>

149 (5 视图页面 (/WEB-INF/jsp/registerAndValidator.jsp) 直接将 springmvc-chapter4 项目中的 /WEB-INF/jsp/registerAndValidator.jsp /WEB-INF/jsp/validate/registerAndValidator.jsp 复制到当前项目下的 (6 启动服务器测试 : 在浏览器地址栏输入 进行测试, 测试步骤和 编程式验证 器 一样 其他编程式验证的使用, 请参考 数据验证 章节 声明式数据验证 Spring3 开始支持 JSR-303 验证框架,JSR-303 支持 XML 风格的和注解风格的验证, 接下来我们首先看一下如何和 Spring 集成 集成 (1 添加 jar 包 : 此处使用 Hibernate-validator 实现 ( 版本 :hibernate-validator final-dist.zip), 将如下 jar 包添加到 classpath (WEB-INF/lib 下即可 ): dist/lib/required/validation-api ga.jar dist/hibernate-validator final.jar JSR-303 规范 API 包 Hibernate 参考实现 ( 2 在 Spring 配置总添加对 JSR-303 验证框架的支持 <!-- 以下 validator ConversionService 在使用 mvc:annotation-driven 会自动注册 --> <bean id="validator" class="org.springframework.validation.beanvalidation.localvalidatorfactorybean"> <property name="providerclass" value="org.hibernate.validator.hibernatevalidator"/> <!-- 如果不加默认到使用 classpath 下的 ValidationMessages.properties --> <property name="validationmessagesource" ref="messagesource"/> </bean> 此处使用 Hibernate validator 实现 : validationmessagesource 属性 : 指定国际化错误消息从哪里取, 此处使用之前定义的 messagesource 来获取国际化消息 ; 如果此处不指定该属性, 则默认到 classpath 下的 ValidationMessages.properties 取国际化错误消息 通过 ConfigurableWebBindingInitializer 注册 validator: <bean id="webbindinginitializer" class="org.springframework.web.bind.support.configurablewebbindinginitializer"> <property name="conversionservice" ref="conversionservice"/> <property name="validator" ref="validator"/>

150 其他配置和之前学习 一节一样 如上集成过程看起来比较麻烦, 后边我们会介绍 <mvc:annotation-driven> 会自动 注册, 后续章节再详细介绍 (3 使用 JSR-303 验证框架注解为模型对象指定验证信息 package cn.javass.chapter7.model; import javax.validation.constraints.notnull; public class UserModel private String username; 指定此 username 字段不允许为空, 当验证失败时将从之前指定的 messagesource 中获取 username.not.empty 对于的错误信息, 此处只有通过 { 错误消息键值 格式指定的才能从 messagesource 获取 (4 控制器 package cn.javass.chapter7.web.controller.validate; // 省略 public class HelloWorldController public String UserModel user, Errors errors) { if(errors.haserrors()) { return "validate/error"; return "redirect:/success"; 来告诉 Spring MVC 此命令对象在绑定完毕后需要进行 JSR-303 验证, 如果验证失败会 将错误信息添加到 errors 错误对象中 (5 验证失败后需要展示的页面 (/WEB-INF/jsp/validate/error.jsp) <%@ page language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8"%> <%@taglib prefix="form" uri=" %> <form:form commandname="user"> <form:errors path="*" cssstyle="color:red"></form:errors><br/> </form:form> (6 测试 在浏览器地址栏中输入 即没有 username 数据, 请求后将直接到 验证失败界面并显示错误消息 用户名不能为空, 如果请求时带上?username=zhang 将重定向到成功页面 到此集成就完成, 接下来我们详细学习下有哪些验证约束注解吧

151 内置的验证约束注解 内置的验证约束注解如下表所示 ( 摘自 hibernate validator reference): 验证注解 验证的数据类型 Boolean,boolean 验证注解的元素值是 Boolean,boolean 验证注解的元素值是 任意类型 验证注解的元素值不是 任意类型 验证注解的元素值是 值 ) BigDecimal,BigInteger, byte, short, int, long, 等任何 Number 或 CharSequence( 存储的是数字 ) 子类型 指定的 value 值 ) 指定的 value 值 值 整数位数, fraction= 小数位数 下限, max= 上限 要求一样 要求一样 要求一样 字符串 Collection Map 数组等 java.util.date, java.util.calendar; Joda Time 类库的日期类型 DecimalMin 指定的 value DecimalMax 指定的 value 值验证注解的元素值的整数位数和小数位数上限验证注解的元素值的在 min 和 max( 包含 ) 指定区间之内, 如字符长度 集合大小验证注解的元素值 ( 日期类型 ) 要求一样验证注解的元素值 ( 日期类型 下限, max= 上限 ) CharSequence 子类型 CharSequence 子类型 验证注解的元素值不为空 ( 不为 null 去除首位空格后长度为 0), 只应用于字符串且在比较时会去除字符串的首位空格验证注解的元素值长度在 min 和 max CharSequence 子类型 Collection Map 验证注解的元素值不为 null 且不为空 ( 字符串长度不数组为 0 集合大小不为 最小值, max= 最大值 正则表达式, flag= 标志的模式 正则表达式, flag= 标志的模式 ) BigDecimal,BigInteger,CharSequence, byte, short, int, long 等原子类型和包装类 型 CharSequence 子类型 ( 如 String) String, 任何 CharSequence 的子类型 验证注解的元素值在最小值和最大值之间验证注解的元素值是 , 也可以通过 regexp 和 flag 指定自定义的 任何非原子类型指定递归验证关联的对象 ; 如用户对象中有个地址对象属性, 如果想在验证用户

152 验证注解验证的数据类型说明 对象时一起验证地址对象的话, 注解即可级联验证 此处只列出 Hibernate Validator 提供的大部分验证约束注解, 请参考 hibernate validator 官方文档了解其他验证约束注解 和进行自定义的验证约束注解定义 具体演示实例请参考 cn.javass.chapter7.web.controller.validate.validatorannotationtestcontroller 错误消息 当验证出错时, 我们需要给用户展示错误消息告诉用户出错的原因, 因此我们要为验证约束注解指定错误消息 错误消息是通过在验证约束注解的 message 属性指定 验证约束注解指定错误消息有如下两种方式 : 1 硬编码错误消息; 2 从资源消息文件中根据消息键读取错误消息 一 硬编码错误消息直接在验证约束注解上指定错误消息, 如下所示 = " 用户名不能为空 max=20, message=" 用户名长度必须在 5-20 之间 = "^[a-za-z_]\\w{4,19$", message = " 用户名必须以字母下划线开头, 可由字母数字下划线组成 ") private String username; 如上所示, 错误消息使用硬编码指定, 这种方式是不推荐使用的, 因为在如下场景是不适用的 : 1 在国际化场景下, 需要对不同的国家显示不同的错误消息 ; 2 需要更换错误消息时是比较麻烦的, 需要找到相应的类进行更换, 并重新编译发布 二 从资源消息文件中根据消息键读取错误消息 2.1 默认的错误消息文件及默认错误消息键值 默认的错误消息文件是 /org/hibernate/validator/validationmessages.properties, 如下图所示 : 默认的错误消息键值如下图所示 :

本章学习目标 小风 Java 实战系列教程 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc

本章学习目标 小风 Java 实战系列教程 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc 本章学习目标 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 配置视图解析器 @RequestMapping 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc 和 Struts2 都属于表现层的框架, 它是 Spring 框架的一部分, 我们可 以从 Spring 的整体结构中看得出来 :

More information

(TestFailure) JUnit Framework AssertionFailedError JUnit Composite TestSuite Test TestSuite run() run() JUnit

(TestFailure) JUnit Framework AssertionFailedError JUnit Composite TestSuite Test TestSuite run() run() JUnit Tomcat Web JUnit Cactus JUnit Java Cactus JUnit 26.1 JUnit Java JUnit JUnit Java JSP Servlet JUnit Java Erich Gamma Kent Beck xunit JUnit boolean JUnit Java JUnit Java JUnit Java 26.1.1 JUnit JUnit How

More information

基于CDIO一体化理念的课程教学大纲设计

基于CDIO一体化理念的课程教学大纲设计 Java 语 言 程 序 设 计 课 程 教 学 大 纲 Java 语 言 程 序 设 计 课 程 教 学 大 纲 一 课 程 基 本 信 息 1. 课 程 代 码 :52001CC022 2. 课 程 名 称 :Java 语 言 程 序 设 计 3. 课 程 英 文 名 称 :Java Programming 4. 课 程 类 别 : 理 论 课 ( 含 实 验 上 机 或 实 践 ) 5. 授

More information

untitled

untitled JavaEE+Android - 6 1.5-2 JavaEE web MIS OA ERP BOSS Android Android Google Map office HTML CSS,java Android + SQL Sever JavaWeb JavaScript/AJAX jquery Java Oracle SSH SSH EJB+JBOSS Android + 1. 2. IDE

More information

Microsoft Word - Hibernate与Struts2和Spring组合指导.doc

Microsoft Word - Hibernate与Struts2和Spring组合指导.doc 1.1 组合 Hibernate 与 Spring 1. 在 Eclipse 中, 新建一个 Web project 2. 给该项目增加 Hibernate 开发能力, 增加 Hibernate 相关类库到当前项目的 Build Path, 同时也提供了 hibernate.cfg.xml 这个配置文件 3. 给该项目增加 Spring 开发能力, 增加 spring 相关类库到当前项目的 Build

More information

PrintWriter s = new PrintWriter(writer); ex.printstacktrace(s); mv.addobject("exception", writer.tostring()); mv.setviewname("error"); return

PrintWriter s = new PrintWriter(writer); ex.printstacktrace(s); mv.addobject(exception, writer.tostring()); mv.setviewname(error); return 本章学习目标 小风 Java 实战系列教程 SpringMVC 异常处理 SpringMVC 文件上传 SpringMVC 处理 JSON 格式数据 SpringMVC 拦截器 SpringMVC 对 restful 风格的支持 1. SpringMVC 异常处理 1.1. @ExceptionHandler 注解处理异常 @ExceptionHandler 该注解使用在异常处理方法上面 1.1.1.

More information

EJB-Programming-4-cn.doc

EJB-Programming-4-cn.doc EJB (4) : (Entity Bean Value Object ) JBuilder EJB 2.x CMP EJB Relationships JBuilder EJB Test Client EJB EJB Seminar CMP Entity Beans Session Bean J2EE Session Façade Design Pattern Session Bean Session

More information

1.JasperReport ireport JasperReport ireport JDK JDK JDK JDK ant ant...6

1.JasperReport ireport JasperReport ireport JDK JDK JDK JDK ant ant...6 www.brainysoft.net 1.JasperReport ireport...4 1.1 JasperReport...4 1.2 ireport...4 2....4 2.1 JDK...4 2.1.1 JDK...4 2.1.2 JDK...5 2.1.3 JDK...5 2.2 ant...6 2.2.1 ant...6 2.2.2 ant...6 2.3 JasperReport...7

More information

Microsoft Word - json入门.doc

Microsoft Word - json入门.doc Json 入门 送给亲爱的女朋友, 祝她天天快乐 作者 :hlz QQ:81452743 MSN/Email:hulizhong2008@163.com json 入门 (1) json 是 JavaScript Object Notation 的简称 ; 在 web 系统开发中与 AJAX 相结合用的比较多 在 ajax 中数据传输有 2 中方式 : 文本类型, 常用 responsetext 属性类获取

More information

第03章 控制反转(Spring IoC)

第03章  控制反转(Spring IoC) 3 Spring IoC GoF Design Patterns: Elements of Reusable Object-Oriented Software Programming to an Interface not an Implementation Java Java Java GoF Service Locator IoC IoC Spring IoC 3.1 IoC IoC IoC Dependency

More information

EJB-Programming-3.PDF

EJB-Programming-3.PDF :, JBuilder EJB 2.x CMP EJB Relationships JBuilder EJB Test Client EJB EJB Seminar CMP Entity Beans Value Object Design Pattern J2EE Design Patterns Value Object Value Object Factory J2EE EJB Test Client

More information

untitled

untitled PowerBuilder Tips 利 PB11 Web Service 年度 2 PB Tips PB9 EAServer 5 web service PB9 EAServer 5 了 便 web service 來說 PB9 web service 力 9 PB11 release PB11 web service 力更 令.NET web service PB NVO 論 不 PB 來說 說

More information

Microsoft Word - 01.DOC

Microsoft Word - 01.DOC 第 1 章 JavaScript 简 介 JavaScript 是 NetScape 公 司 为 Navigator 浏 览 器 开 发 的, 是 写 在 HTML 文 件 中 的 一 种 脚 本 语 言, 能 实 现 网 页 内 容 的 交 互 显 示 当 用 户 在 客 户 端 显 示 该 网 页 时, 浏 览 器 就 会 执 行 JavaScript 程 序, 用 户 通 过 交 互 式 的

More information

Servlet

Servlet Servlet Allen Long Email: allen@huihoo.com http://www.huihoo.com 2004-04 Huihoo - Enterprise Open Source http://www.huihoo.com 1 Huihoo - Enterprise Open Source http://www.huihoo.com 2 GET POST Huihoo

More information

Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7.

Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7. Kubernetes 包管理理 工具 Helm 蔺礼强 Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7. Kubernetes

More information

D getinitparameternames() 9 下 列 选 项 中, 属 于 Servlet API 中 提 供 的 request 对 象 的 包 装 类 的 是 ( ) A HttpServletRequestWrapper B HttpServletRequest C HttpServ

D getinitparameternames() 9 下 列 选 项 中, 属 于 Servlet API 中 提 供 的 request 对 象 的 包 装 类 的 是 ( ) A HttpServletRequestWrapper B HttpServletRequest C HttpServ 第 四 章 Filter( 过 滤 器 ) 样 题 A 卷 一 选 择 题 ( 每 小 题 2 分, 共 20 分 ) 1 下 面 选 项 中, 用 于 实 现 初 始 化 过 滤 器 的 方 法 是 ( ) A init(filterconfig filterconfig) B dofilter(servletrequest req,servletresponse resp,filterchain

More information

untitled

untitled -JAVA 1. Java IDC 20 20% 5 2005 42.5 JAVA IDC JAVA 60% 70% JAVA 3 5 10 JAVA JAVA JAVA J2EE J2SE J2ME 70% JAVA JAVA 20 1 51 2. JAVA SUN JAVA J2EE J2EE 3. 1. CSTP CSTP 2 51 2. 3. CSTP IT CSTP IT IT CSTP

More information

OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数

OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数 复习 类的复用 组合 (composition): has-a 关系 class MyType { public int i; public double d; public char c; public void set(double

More information

优迈科技教学大纲2009版本

优迈科技教学大纲2009版本 java 软 件 工 程 师 培 训 教 学 大 纲 1 JAVA 软 件 工 程 师 培 训 教 学 大 纲 深 圳 软 件 园 人 才 实 训 基 地 2009 年 3 月 目 录 java 软 件 工 程 师 培 训 教 学 大 纲 2 教 学 阶 段...3 第 一 章 JAVA 起 步...3 第 二 章 面 向 对 象 的 编 程...4 第 三 章 数 据 结 构 IO 线 程 网 络...5

More information

resp.getwriter().print(j + "*" + i + "=" + j * i+" "); resp.getwriter().print("<br/>"); protected void dopost(httpservletrequest req, HttpServletRespo

resp.getwriter().print(j + * + i + = + j * i+ ); resp.getwriter().print(<br/>); protected void dopost(httpservletrequest req, HttpServletRespo 第三章补充案例 案例 3-1 HttpServlet 一 案例描述 1 考核知识点名称 :HttpServlet 编号 : 2 练习目标 掌握 HttpServlet 的 doget() 方法和 dopost() 方法 3 需求分析由于大多数 Web 应用都是通过 HTTP 协议和客户端进行交互, 因此, 在 Servlet 接口中, 提供了 一个抽象类 javax.servlet.http.httpservlet,

More information

Microsoft PowerPoint - 05-Status-Codes-Chinese.ppt

Microsoft PowerPoint - 05-Status-Codes-Chinese.ppt 2004 Marty Hall 服务器响应的生成 : HTTP 状态代码 JSP, Servlet, & Struts Training Courses: http://courses.coreservlets.com Available in US, China, Taiwan, HK, and Worldwide 2 JSP and Servlet Books from Sun Press: http://www.coreservlets.com

More information

拦截器(Interceptor)的学习

拦截器(Interceptor)的学习 二 拦截器 (Interceptor) 的学习 拦截器可以监听程序的一个或所有方法 拦截器对方法调用流提供了细粒度控制 可以在无状态会话 bean 有状态会话 bean 和消息驱动 bean 上使用它们 拦截器可以是同一 bean 类中的方法或是一个外部类 下面介绍如何在 Session Bean 类中使用外部拦截器类 @Interceptors 注释指定一个或多个在外部类中定义的拦截器 下面拦截器

More information

TopTest_Adminstrator.doc

TopTest_Adminstrator.doc 壹 前 言... 3 貳 系 統 簡 介... 4 一 TKB multimedia Top-Test 系 統 架 構...4 1. 使 用 者 介 面 層 (Presentation tier)...5 2. 商 業 邏 輯 層 (business logic tier)...5 3. 資 料 服 務 層 (data services tier)...5 二 TKB Multimedia Top-Test

More information

设计模式 Design Patterns

设计模式 Design Patterns 丁勇 Email:18442056@QQ.com 学习目标 掌握 Model I 体系结构 掌握 Model II 体系结构 掌握 MVC 应用程序 Model I 体系结构 6 1 Model I 体系结构结合使用 JSP 页面和 Bean 来开发 Web 应用程序 应用服务器 请求 JSP 页面 响应 Bean 数据库服务器 Model I 体系结构 6 2 Model I 体系结构用于开发简单的应用程序

More information

Microsoft PowerPoint - 02-Servlet-Basics-Chinese.ppt

Microsoft PowerPoint - 02-Servlet-Basics-Chinese.ppt 2004 Marty Hall servlet 基础 JSP, Servlet, & Struts Training Courses: http://courses.coreservlets.com Available in US, China, Taiwan, HK, and Worldwide 2 JSP and Servlet Books from Sun Press: http://www.coreservlets.com

More information

没 有 多 余 的 Contruol 或 Action 了 原 来 Domain 层 被 服 务 层 Service layer 遮 挡, 在 右 边 图 中, 则 Domain 层 直 接 暴 露 给 前 台 了, 没 有 被 遮 挡, 裸 露 了 这 样 一 步 到 位 实 现 领 域 模 型

没 有 多 余 的 Contruol 或 Action 了 原 来 Domain 层 被 服 务 层 Service layer 遮 挡, 在 右 边 图 中, 则 Domain 层 直 接 暴 露 给 前 台 了, 没 有 被 遮 挡, 裸 露 了 这 样 一 步 到 位 实 现 领 域 模 型 文 章 编 号 :1007-757X(2012)1-0036-04 领 域 驱 动 模 型 的 WEB 软 件 系 统 设 计 研 究 摘 要 : J2EE 3 JDK1.7 Tomcat WEB 关 键 词 : 中 图 分 类 号 :TP311 文 献 标 志 码 :A 0 引 言 Web 软 件 系 统 的 分 层 结 构 典 型 的 J2EE 软 件 系 统 开 发 方 法 分 为 三 层 结

More information

在所有的项目开发中, 一定是多人协作的团队开发, 但是使用框架就会出现一个问题, 我们所 有的 Action 以及相关的路径都要求在我们的 struts.xml 文件中配置, 如果所有的人去修改一个 文件, 那么就会变得混乱, 而且有可能出现冲突, 那么在 struts.xml 文件中为了解决这个问

在所有的项目开发中, 一定是多人协作的团队开发, 但是使用框架就会出现一个问题, 我们所 有的 Action 以及相关的路径都要求在我们的 struts.xml 文件中配置, 如果所有的人去修改一个 文件, 那么就会变得混乱, 而且有可能出现冲突, 那么在 struts.xml 文件中为了解决这个问 内置对象的取得和多人开发 一 内置对象的取得 在使用的 servlet 的时候可以通过 HttpServletResquest 获取到一些内置对象, 但是在 struts2 中为了方便取得内置对象, 专门提供了一个 ServletActionContext 这个类取得取得内置对象, 观察如下方法 public static javax.servlet.jsp.pagecontext() 取得 pagecontext

More information

目 录 1. 业 务 流 程 系 统 开 发 面 临 的 挑 战 与 机 遇... 3 1.1 业 务 流 程 管 理... 4 2. 新 一 代 开 源 业 务 流 程 开 发 平 台 BPMX3... 5 2.1 BPMX3 是 什 么... 5 2.2 为 什 么 要 优 先 采 用 BPMX

目 录 1. 业 务 流 程 系 统 开 发 面 临 的 挑 战 与 机 遇... 3 1.1 业 务 流 程 管 理... 4 2. 新 一 代 开 源 业 务 流 程 开 发 平 台 BPMX3... 5 2.1 BPMX3 是 什 么... 5 2.2 为 什 么 要 优 先 采 用 BPMX BPMX3 技 术 白 皮 书 业 务 流 程 开 发 平 台 介 绍 目 录 1. 业 务 流 程 系 统 开 发 面 临 的 挑 战 与 机 遇... 3 1.1 业 务 流 程 管 理... 4 2. 新 一 代 开 源 业 务 流 程 开 发 平 台 BPMX3... 5 2.1 BPMX3 是 什 么... 5 2.2 为 什 么 要 优 先 采 用 BPMX3... 5 2.2.1 BPMX3

More information

Microsoft Word - 扉页.doc

Microsoft Word - 扉页.doc 第 5 章 chapter 5 数据验证 学习目的与要求本章重点讲解 Spring MVC 框架的输入验证体系 通过本章的学习, 理解输入验证的流程, 能够利用 Spring 的自带验证框架和 JSR 303(Java 验证规范 ) 对数据进行验证 本章主要内容 数据验证概述 Spring 验证 JSR 303 验证所有用户的输入一般都是随意的, 为了保证数据的合法性, 数据验证是所有 Web 应用必须处理的问题

More information

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 odps-sdk 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基 开放数据处理服务 ODPS SDK SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基础功能的主体接口, 搜索关键词 "odpssdk-core" 一些

More information

在Spring中使用Kafka:Producer篇

在Spring中使用Kafka:Producer篇 在某些情况下, 我们可能会在 Spring 中将一些 WEB 上的信息发送到 Kafka 中, 这时候我们就需要在 Spring 中编写 Producer 相关的代码了 ; 不过高兴的是,Spring 本身提供了操作 Kafka 的相关类库, 我们可以直接通过 xml 文件配置然后直接在后端的代码中使用 Kafka, 非常地方便 本文将介绍如果在 Spring 中将消息发送到 Kafka 在这之前,

More information

IoC容器和Dependency Injection模式.doc

IoC容器和Dependency Injection模式.doc IoC Dependency Injection /Martin Fowler / Java Inversion of Control IoC Dependency Injection Service Locator Java J2EE open source J2EE J2EE web PicoContainer Spring Java Java OO.NET service component

More information

untitled

untitled 653 JAVA 2008 11 Institution of Software Engineer... 2... 4... 4... 5... 5... 8... 8... 8... 8... 8... 9... 9... 9... 11... 13... 13... 13... 13... 15... 15... 15... 15... 16... 16... 17... 17... 17...

More information

1 1 大概思路 创建 WebAPI 创建 CrossMainController 并编写 Nuget 安装 microsoft.aspnet.webapi.cors 跨域设置路由 编写 Jquery EasyUI 界面 运行效果 2 创建 WebAPI 创建 WebAPI, 新建 -> 项目 ->

1 1 大概思路 创建 WebAPI 创建 CrossMainController 并编写 Nuget 安装 microsoft.aspnet.webapi.cors 跨域设置路由 编写 Jquery EasyUI 界面 运行效果 2 创建 WebAPI 创建 WebAPI, 新建 -> 项目 -> 目录 1 大概思路... 1 2 创建 WebAPI... 1 3 创建 CrossMainController 并编写... 1 4 Nuget 安装 microsoft.aspnet.webapi.cors... 4 5 跨域设置路由... 4 6 编写 Jquery EasyUI 界面... 5 7 运行效果... 7 8 总结... 7 1 1 大概思路 创建 WebAPI 创建 CrossMainController

More information

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 4: 4 月 19 日晚 9 点 复习 类的复用 组合 (composition): has-a 关系 class MyType { public int i; public double d; public char c; public void set(double x) { d

More information

目 录 目 录... 2 1 平 台 概 述... 3 2 技 术 架 构... 4 3 技 术 特 点... 7 3.1 基 于 统 一 平 台 的 多 产 品 线 支 撑... 7 3.2 先 进 性... 7 3.3 安 全 性... 7 3.4 开 放 性... 8 3.5 高 性 能 和

目 录 目 录... 2 1 平 台 概 述... 3 2 技 术 架 构... 4 3 技 术 特 点... 7 3.1 基 于 统 一 平 台 的 多 产 品 线 支 撑... 7 3.2 先 进 性... 7 3.3 安 全 性... 7 3.4 开 放 性... 8 3.5 高 性 能 和 致 远 协 同 管 理 软 件 V5 平 台 白 皮 书 北 京 致 远 协 创 软 件 有 限 公 司 2014 年 6 月 1 / 20 目 录 目 录... 2 1 平 台 概 述... 3 2 技 术 架 构... 4 3 技 术 特 点... 7 3.1 基 于 统 一 平 台 的 多 产 品 线 支 撑... 7 3.2 先 进 性... 7 3.3 安 全 性... 7 3.4 开 放

More information

WebSphere Studio Application Developer IBM Portal Toolkit... 2/21 1. WebSphere Portal Portal WebSphere Application Server stopserver.bat -configfile..

WebSphere Studio Application Developer IBM Portal Toolkit... 2/21 1. WebSphere Portal Portal WebSphere Application Server stopserver.bat -configfile.. WebSphere Studio Application Developer IBM Portal Toolkit... 1/21 WebSphere Studio Application Developer IBM Portal Toolkit Portlet Doug Phillips (dougep@us.ibm.com),, IBM Developer Technical Support Center

More information

Guava学习之Resources

Guava学习之Resources Resources 提供提供操作 classpath 路径下所有资源的方法 除非另有说明, 否则类中所有方法的参数都不能为 null 虽然有些方法的参数是 URL 类型的, 但是这些方法实现通常不是以 HTTP 完成的 ; 同时这些资源也非 classpath 路径下的 下面两个函数都是根据资源的名称得到其绝对路径, 从函数里面可以看出,Resources 类中的 getresource 函数都是基于

More information

计算机软件技术专业教学计划

计算机软件技术专业教学计划 计 算 机 软 件 技 术 专 业 人 才 培 养 方 案 ( 服 务 外 包 方 向 ) 专 业 大 类 名 称 ( 代 码 ):++(++) 专 业 类 名 称 ( 代 码 ):++++++(++++) 专 业 名 称 ( 代 码 ):+++++++(++++++) 修 业 年 限 : 三 年, 全 日 制 招 生 对 象 : 三 年 制 普 通 高 中 及 对 口 中 职 专 业 毕 业 生

More information

Chapter 9: Objects and Classes

Chapter 9: Objects and Classes Java application Java main applet Web applet Runnable Thread CPU Thread 1 Thread 2 Thread 3 CUP Thread 1 Thread 2 Thread 3 ,,. (new) Thread (runnable) start( ) CPU (running) run ( ) blocked CPU sleep(

More information

Stateless Session Beans(无状态bean)的学习

Stateless Session Beans(无状态bean)的学习 一 Stateless Session Beans( 无状态 bean) 的学习 第一步 : 要定义一个会话 Bean, 首先需要定义一个包含他所有业务方法的接口 这个接口不需要任何注释, 就像普通的 java 接口那样定义 调用 EJB 的客户端通过使用这个接口引用从 EJB 容器得到的会话 Bean 对象 stub 接口的定义如下: HelloWorld.java package com.foshanshop.ejb3;

More information

雲端 Cloud Computing 技術指南 運算 應用 平台與架構 10/04/15 11:55:46 INFO 10/04/15 11:55:53 INFO 10/04/15 11:55:56 INFO 10/04/15 11:56:05 INFO 10/04/15 11:56:07 INFO

雲端 Cloud Computing 技術指南 運算 應用 平台與架構 10/04/15 11:55:46 INFO 10/04/15 11:55:53 INFO 10/04/15 11:55:56 INFO 10/04/15 11:56:05 INFO 10/04/15 11:56:07 INFO CHAPTER 使用 Hadoop 打造自己的雲 8 8.3 測試 Hadoop 雲端系統 4 Nodes Hadoop Map Reduce Hadoop WordCount 4 Nodes Hadoop Map/Reduce $HADOOP_HOME /home/ hadoop/hadoop-0.20.2 wordcount echo $ mkdir wordcount $ cd wordcount

More information

设计模式 Design Patterns

设计模式 Design Patterns 丁勇 Email:18442056@QQ.com 学习目标 理解 Struts 框架的工作原理 掌握使用 Struts 框架开发 Web 应用的基本步骤 熟悉 MyEclipse 对 Struts 开发的支持 Web 框架事实标准 : Web 框架的事实标准 http://struts.apache.org Java EE 主流技术趋势图 主流 Web 框架趋势图 使用 Struts 实现加法器 使用开发的

More information

无类继承.key

无类继承.key 无类继承 JavaScript 面向对象的根基 周爱 民 / aimingoo aiming@gmail.com https://aimingoo.github.io https://github.com/aimingoo rand = new Person("Rand McKinnon",... https://docs.oracle.com/cd/e19957-01/816-6408-10/object.htm#1193255

More information

使用Cassandra和Spark 2.0实现Rest API服务

使用Cassandra和Spark 2.0实现Rest API服务 使用 Cassandra 和 Spark 2.0 实现 Rest API 服务 在这篇文章中, 我将介绍如何在 Spark 中使用 Akkahttp 并结合 Cassandra 实现 REST 服务, 在这个系统中 Cassandra 用于数据的存储 我们已经见识到 Spark 的威力, 如果和 Cassandra 正确地结合可以实现更强大的系统 我们先创建一个 build.sbt 文件, 内容如下

More information

云数据库 RDS SDK

云数据库 RDS SDK 云数据库 RDS SDK SDK SDK 下载 SDK 下载 最新版本 java_sdk.zip python_sdk.zip php_sdk.zip c#_sdk.zip 历史版本 2015-11-3 java_sdk.zip python_sdk.zip php_sdk.zip c#_sdk.zip JAVA 教程 JAVA 创建 Access Key 登陆阿里云账号 打开 我的 Access

More information

mvc

mvc Build an application Tutor : Michael Pan Application Source codes - - Frameworks Xib files - - Resources - ( ) info.plist - UIKit Framework UIApplication Event status bar, icon... delegation [UIApplication

More information

JSP基础编程

JSP基础编程 JSP 基础编程 报告人 : 包亮 邮箱 :rslab@lzb.ac.cn HTTP 基础 HTTP is a simple, stateless protocol. A client, such as a web browser, makes a request, the web server responds, and the transaction is done. Client 发出一个请求

More information

Struts2自定义类型转换.doc

Struts2自定义类型转换.doc Struts2 自定义类型转换 原理 struts2 的自定义类型转换机制为复杂类型的输入输出处理提供了便捷.struts2 已经为我们提供了几乎所有的 primitive 类型以及常用类型 ( 如 Date) 的类型转换器, 我们也可以为我们自定义类添加自定义类型转化器. struts2 为我们提供了一个类型转化器的入口 : ognl.defaulttypeconverter, 或继承 org.apache.struts2.util.strutstypeconverter,

More information

使用MapReduce读取XML文件

使用MapReduce读取XML文件 使用 MapReduce 读取 XML 文件 XML( 可扩展标记语言, 英语 :extensible Markup Language, 简称 : XML) 是一种标记语言, 也是行业标准数据交换交换格式, 它很适合在系统之间进行数据存储和交换 ( 话说 Hadoop H ive 等的配置文件就是 XML 格式的 ) 本文将介绍如何使用 MapReduce 来读取 XML 文件 但是 Had oop

More information

Microsoft Word - JavaWeb程序开发入门—教学大纲.doc

Microsoft Word - JavaWeb程序开发入门—教学大纲.doc JavaWeb 程序开发入门 课程教学大纲 ( 课程英文名称 ) 课程编号 : 201409210011 学分 : 5 学分学时 : 54 学时 ( 其中 : 讲课学时 :38 上机学时 :16) 先修课程 :Java 基础入门 MySQL 数据库入门后续课程 :JavaWeb 程序开发进阶教程适用专业 : 信息及其计算机相关专业开课部门 : 计算机系 一 课程的性质与目标 JavaWeb 程序开发入门

More information

基于ECO的UML模型驱动的数据库应用开发1.doc

基于ECO的UML模型驱动的数据库应用开发1.doc ECO UML () Object RDBMS Mapping.Net Framework Java C# RAD DataSetOleDbConnection DataGrod RAD Client/Server RAD RAD DataReader["Spell"].ToString() AObj.XXX bug sql UML OR Mapping RAD Lazy load round trip

More information

詞 彙 表 編 號 詞 彙 描 述 1 預 約 人 資 料 中 文 姓 名 英 文 姓 名 身 份 證 字 號 預 約 人 電 話 性 別 2 付 款 資 料 信 用 卡 別 信 用 卡 號 信 用 卡 有 效 日 期 3 住 房 條 件 入 住 日 期 退 房 日 期 人 數 房 間 數 量 入

詞 彙 表 編 號 詞 彙 描 述 1 預 約 人 資 料 中 文 姓 名 英 文 姓 名 身 份 證 字 號 預 約 人 電 話 性 別 2 付 款 資 料 信 用 卡 別 信 用 卡 號 信 用 卡 有 效 日 期 3 住 房 條 件 入 住 日 期 退 房 日 期 人 數 房 間 數 量 入 100 年 特 種 考 試 地 方 政 府 公 務 人 員 考 試 試 題 等 別 : 三 等 考 試 類 科 : 資 訊 處 理 科 目 : 系 統 分 析 與 設 計 一 請 參 考 下 列 旅 館 管 理 系 統 的 使 用 案 例 圖 (Use Case Diagram) 撰 寫 預 約 房 間 的 使 用 案 例 規 格 書 (Use Case Specification), 繪 出 入

More information

untitled

untitled 1 Outline 數 料 數 數 列 亂數 練 數 數 數 來 數 數 來 數 料 利 料 來 數 A-Z a-z _ () 不 數 0-9 數 不 數 SCHOOL School school 數 讀 school_name schoolname 易 不 C# my name 7_eleven B&Q new C# (1) public protected private params override

More information

2. AOP 底层技术实现 小风 Java 实战系列教程 关键词 : 代理模式 代理模型分为两种 : 1) 接口代理 (JDK 动态代理 ) 2) 子类代理 (Cglib 子类代理 ) 需求 :CustomerService 业务类, 有 save,update 方法, 希望在 save,updat

2. AOP 底层技术实现 小风 Java 实战系列教程 关键词 : 代理模式 代理模型分为两种 : 1) 接口代理 (JDK 动态代理 ) 2) 子类代理 (Cglib 子类代理 ) 需求 :CustomerService 业务类, 有 save,update 方法, 希望在 save,updat 本章学习目标 小风 Java 实战系列教程 AOP 思想概述 AOP 底层技术实现 AOP 术语介绍 SpringAOP 的 XML 方式 HelloWorld SpringAOP 的 XML 方式配置细节 SpringAOP 的注解方式 SpringAOP 的零配置方式 1. AOP 思想概述 1.1. AOP 思想简介 1.2. AOP 的作用 2. AOP 底层技术实现 小风 Java 实战系列教程

More information

jsp

jsp JSP Allen Long Email: allen@huihoo.com http://www.huihoo.com 2004-04 Huihoo - Enterprise Open Source http://www.huihoo.com 1 JSP JSP JSP JSP MVC Huihoo - Enterprise Open Source http://www.huihoo.com 2

More information

res/layout 目录下的 main.xml 源码 : <?xml version="1.0" encoding="utf 8"?> <TabHost android:layout_height="fill_parent" xml

res/layout 目录下的 main.xml 源码 : <?xml version=1.0 encoding=utf 8?> <TabHost android:layout_height=fill_parent xml 拓展训练 1- 界面布局 1. 界面布局的重要性做应用程序, 界面是最基本的 Andorid 的界面, 需要写在 res/layout 的 xml 里面, 一般情况下一个 xml 对应一个界面 Android 界面布局有点像写 html( 连注释代码的方式都一样 ), 要先给 Android 定框架, 然后再在框架里面放控件,Android 提供了几种框架,AbsoluteLayout,LinearLayout,

More information

RxJava

RxJava RxJava By 侦跃 & @hi 头 hi RxJava 扩展的观察者模式 处 观察者模式 Observable 发出事件 Subscriber 订阅事件 bus.post(new AnswerEvent(42)); @Subscribe public void onanswer(answerevent event) {! }! Observable observable = Observable.create(new

More information

1 请求跳转及转发 1.1 举例 : 页面流转 可以在首页选择自己喜欢的颜色, 进入对应的页面 选择绿色, 会进入绿色界面 : 选择红色, 会进入红色界面 :

1 请求跳转及转发 1.1 举例 : 页面流转 可以在首页选择自己喜欢的颜色, 进入对应的页面 选择绿色, 会进入绿色界面 : 选择红色, 会进入红色界面 : 1 请求跳转及转发 1.1 举例 : 页面流转 可以在首页选择自己喜欢的颜色, 进入对应的页面 选择绿色, 会进入绿色界面 : 选择红色, 会进入红色界面 : 这里我们会看到四个页面 : 1. index.jsp 中选择颜色, 点击按钮后提交到 test.jsp 2. test.jsp 取得用户选择的颜色, 根据颜色值显示对应的页面 3. 如果选择了红色, 就显示 red.jsp 4. 如果选择了绿色,

More information

untitled

untitled 1 .NET sln csproj dll cs aspx 說 料 料 利 來 料 ( 來 ) 利 [] [] 來 說 切 切 理 [] [ ] 來 說 拉 類 類 [] [ ] 列 連 Web 行流 來 了 不 不 不 流 立 行 Page 類 Load 理 Click 滑 料 Response 列 料 Response HttpResponse 類 Write 料 Redirect URL Response.Write("!!

More information

1. 2. Flex Adobe 3.

1. 2. Flex Adobe 3. 1. 2. Flex Adobe 3. Flex Adobe Flex Flex Web Flex Flex Flex Adobe Flash Player 9 /rich Internet applications/ria Flex 1. 2. 3. 4. 5. 6. SWF Flash Player Flex 1. Flex framework Adobe Flex 2 framework RIA

More information

chapter 2 HTML5 目錄iii HTML HTML HTML HTML HTML canvas

chapter 2 HTML5 目錄iii HTML HTML HTML HTML HTML canvas Contents 目錄 chapter 1 1-1... 1-2 1-2... 1-3 HTML5... 1-3... 1-5 1-3... 1-9 Web Storage... 1-9... 1-10 1-4 HTML5... 1-14... 1-14... 1-15 HTML5... 1-15... 1-15... 1-16 1-5... 1-18 Apps... 1-18 HTML5 Cache

More information

2 WF 1 T I P WF WF WF WF WF WF WF WF 2.1 WF WF WF WF WF WF

2 WF 1 T I P WF WF WF WF WF WF WF WF 2.1 WF WF WF WF WF WF Chapter 2 WF 2.1 WF 2.2 2. XAML 2. 2 WF 1 T I P WF WF WF WF WF WF WF WF 2.1 WF WF WF WF WF WF WF WF WF WF EDI API WF Visual Studio Designer 1 2.1 WF Windows Workflow Foundation 2 WF 1 WF Domain-Specific

More information

Microsoft Word - 第4章 Servlet开发—教学设计.doc

Microsoft Word - 第4章 Servlet开发—教学设计.doc 传智播客 JavaWeb 程序开发入门 教学设计 课程名称 : JavaWeb 程序开发入门 授课年级 : 2014 年级 授课学期 : 2014 学年第一学期 教师姓名 : 某某老师 2014 年 09 月 09 日 课题名称内容分析教学目标及基本要求重点及措施 计划第 4 章 Servlet 技术 6 课时学时随着 Web 应用业务需求的增多, 动态 Web 资源的开发变得越来越重要, 为此 Sun

More information

untitled

untitled 4.1AOP AOP Aspect-oriented programming AOP 來說 AOP 令 理 Cross-cutting concerns Aspect Weave 理 Spring AOP 來 AOP 念 4.1.1 理 AOP AOP 見 例 來 例 錄 Logging 錄 便 來 例 行 留 錄 import java.util.logging.*; public class HelloSpeaker

More information

1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10

1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10 Java V1.0.1 2007 4 10 1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10 6.2.10 6.3..10 6.4 11 7.12 7.1

More information

http://panweizeng.com http://meituan.com http://meituan.com hosts http://meituan.com hosts localhost 127.0.0.1 /etc/nsswitch.conf /etc/hosts /etc/resolv.conf Mail Client Web Browser cache 1-30mins Clients

More information

输入 project name 选择完成

输入 project name 选择完成 JAVA 程序访问 HighGo DB 的环境准备 山东瀚高科技有限公司版权所有仅允许不作任何修改的转载和转发 Hibernate 的配置 MyEclipse 中创建新项目 : 选择菜单栏 file---new---project 选择 web project 进行下一步 输入 project name 选择完成 4. 单击 " 添加 JAR/ 文件夹 ", 会如下图出现 JDBC 下载 Hibernate

More information

KillTest 质量更高 服务更好 学习资料 半年免费更新服务

KillTest 质量更高 服务更好 学习资料   半年免费更新服务 KillTest 质量更高 服务更好 学习资料 http://www.killtest.cn 半年免费更新服务 Exam : 310-055Big5 Title : Sun Certified Programmer for the Java 2 Platform.SE 5.0 Version : Demo 1 / 22 1. 11. public static void parse(string str)

More information

Java java.lang.math Java Java.util.Random : ArithmeticException int zero = 0; try { int i= 72 / zero ; }catch (ArithmeticException e ) { // } 0,

Java java.lang.math Java Java.util.Random : ArithmeticException int zero = 0; try { int i= 72 / zero ; }catch (ArithmeticException e ) { // } 0, http://debut.cis.nctu.edu.tw/~chi Java java.lang.math Java Java.util.Random : ArithmeticException int zero = 0; try { int i= 72 / zero ; }catch (ArithmeticException e ) { // } 0, : POSITIVE_INFINITY NEGATIVE_INFINITY

More information

通过Hive将数据写入到ElasticSearch

通过Hive将数据写入到ElasticSearch 我在 使用 Hive 读取 ElasticSearch 中的数据 文章中介绍了如何使用 Hive 读取 ElasticSearch 中的数据, 本文将接着上文继续介绍如何使用 Hive 将数据写入到 ElasticSearch 中 在使用前同样需要加入 elasticsearch-hadoop-2.3.4.jar 依赖, 具体请参见前文介绍 我们先在 Hive 里面建个名为 iteblog 的表,

More information

XXXXXXXX http://cdls.nstl.gov.cn 2 26

XXXXXXXX http://cdls.nstl.gov.cn 2 26 [ ] [ ] 2003-7-18 1 26 XXXXXXXX http://cdls.nstl.gov.cn 2 26 (2003-7-18) 1...5 1.1...5 1.2...5 1.3...5 2...6 2.1...6 2.2...6 2.3...6 3...7 3.1...7 3.1.1...7 3.1.2...7 3.1.2.1...7 3.1.2.1.1...8 3.1.2.1.2...10

More information

Microsoft Word - Broker.doc

Microsoft Word - Broker.doc Broker 模式 采用 broker 模式对分布式计算进行简单模拟 系统在一个进程内模拟分布式环境, 因此不涉及网络编程和进程间通信,Broker 通过本地函数调用的方式实现 request 和 response 的转发 采用 broker 模式对分布式计算进行简单的模拟, 要求如下 : 设计四个 server, 一个 server 接收两个整数, 求和并返回结果, 一个 server 接收两个整数,

More information

untitled

untitled ArcGIS Server Web services Web services Application Web services Web Catalog ArcGIS Server Web services 6-2 Web services? Internet (SOAP) :, : Credit card authentication, shopping carts GIS:, locator services,

More information

RunPC2_.doc

RunPC2_.doc PowerBuilder 8 (5) PowerBuilder Client/Server Jaguar Server Jaguar Server Connection Cache Thin Client Internet Connection Pooling EAServer Connection Cache Connection Cache Connection Cache Connection

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

<ADB6ADB1C25EA8FAA6DB2D4D56432E706466>

<ADB6ADB1C25EA8FAA6DB2D4D56432E706466> packages 3-31 PART 3-31 03-03 ASP.NET ASP.N MVC ASP.NET ASP.N MVC 4 ASP.NET ASP.NE MVC Entity Entity Framework Code First 2 TIPS Visual Studio 20NuGetEntity NuGetEntity Framework5.0 CHAPTER 03 59 3-3-1

More information

序号:001

序号:001 第 一 组 选 题 简 介 序 号 :001 题 目 : 基 于 BPEL 的 网 上 订 餐 系 统 的 设 计 与 实 现 网 上 订 餐 系 统 是 在 互 联 网 上 进 行 菜 单 信 息 发 布 网 上 订 餐 以 及 维 护 客 户 关 系 的 电 子 商 务 系 统, 餐 饮 企 业 可 以 通 过 这 个 电 子 商 务 系 统 发 布 自 己 的 菜 单 信 息 以 供 客 户

More information

1

1 PRIMETON TECHNOLOGIES, LTD. EOS EOS Manager No part of this document may be reproduced, stored in any electronic retrieval system, or transmitted in any form or by any means, mechanical, photocopying,

More information

epub 61-2

epub 61-2 2 Web Dreamweaver UltraDev Dreamweaver 3 We b We b We Dreamweaver UltraDev We b Dreamweaver UltraDev We b We b 2.1 Web We b We b D r e a m w e a v e r J a v a S c r i p t We b We b 2.1.1 Web We b C C +

More information

59 1 CSpace 2 CSpace CSpace URL CSpace 1 CSpace URL 2 Lucene 3 ID 4 ID Web 1. 2 CSpace LireSolr 3 LireSolr 3 Web LireSolr ID

59 1 CSpace 2 CSpace CSpace URL CSpace 1 CSpace URL 2 Lucene 3 ID 4 ID Web 1. 2 CSpace LireSolr 3 LireSolr 3 Web LireSolr ID 58 2016. 14 * LireSolr LireSolr CEDD Ajax CSpace LireSolr CEDD Abstract In order to offer better image support services it is necessary to extend the image retrieval function of our institutional repository.

More information

OOP with Java 通知 Project 3: 3 月 29 日晚 9 点 4 月 1 日上课

OOP with Java 通知 Project 3: 3 月 29 日晚 9 点 4 月 1 日上课 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 3: 3 月 29 日晚 9 点 4 月 1 日上课 复习 Java 包 创建包 : package 语句, 包结构与目录结构一致 使用包 : import restaurant/ - people/ - Cook.class - Waiter.class - tools/ - Fork.class

More information

1.1 Filter 的特性 请求映射 filter-mapping 和 servlet-mapping 都是将对应的 filter 或 servlet 映射到某 个 url-pattern 上, 当客户发起某一请求时, 服务器先将此请求与 web.xml 中定 义的所有 url-pat

1.1 Filter 的特性 请求映射 filter-mapping 和 servlet-mapping 都是将对应的 filter 或 servlet 映射到某 个 url-pattern 上, 当客户发起某一请求时, 服务器先将此请求与 web.xml 中定 义的所有 url-pat 1.1 Filter 的特性 1.1.1 请求映射 filter-mapping 和 servlet-mapping 都是将对应的 filter 或 servlet 映射到某 个 url-pattern 上, 当客户发起某一请求时, 服务器先将此请求与 web.xml 中定 义的所有 url-pattern 进行匹配, 然后执行匹配通过的 filter 和 servlet 你可以使用三种方式定义 url-pattern

More information

<4D6963726F736F667420576F7264202D20BBF9D3DA416E64726F6964C6BDCCA8B5C4B5E7D7D3C5C4C2F4CFB5CDB32E646F63>

<4D6963726F736F667420576F7264202D20BBF9D3DA416E64726F6964C6BDCCA8B5C4B5E7D7D3C5C4C2F4CFB5CDB32E646F63> 基 于 Android 平 台 的 电 子 拍 卖 系 统 摘 要 本 电 子 拍 卖 系 统 其 实 就 是 一 个 电 子 商 务 平 台, 只 要 将 该 系 统 部 署 到 互 联 网 上, 客 户 都 可 以 在 该 系 统 上 发 布 想 出 售 的 商 品, 也 可 以 对 拍 卖 中 的 商 品 参 与 竞 价 整 个 过 程 无 须 人 工 干 预, 由 系 统 自 动 完 成 本

More information

北 风 网 讲 师 原 创 作 品 ---- 仅 供 学 员 内 部 交 流 使 用 前 言 吾 尝 终 日 而 思 矣, 不 如 须 臾 之 所 学 也 ; 吾 尝 跂 而 望 矣, 不 如 登 高 之 博 见 也 登 高 而 招, 臂 非 加 长 也, 而 见

北 风 网 讲 师 原 创 作 品 ---- 仅 供  学 员 内 部 交 流 使 用 前 言 吾 尝 终 日 而 思 矣, 不 如 须 臾 之 所 学 也 ; 吾 尝 跂 而 望 矣, 不 如 登 高 之 博 见 也 登 高 而 招, 臂 非 加 长 也, 而 见 北 风 网 讲 师 原 创 作 品 ---- 仅 供 www.ibeifeng.com 学 员 内 部 交 流 使 用 前 言 吾 尝 终 日 而 思 矣, 不 如 须 臾 之 所 学 也 ; 吾 尝 跂 而 望 矣, 不 如 登 高 之 博 见 也 登 高 而 招, 臂 非 加 长 也, 而 见 者 远 ; 顺 风 而 呼, 声 非 加 疾 也, 而 闻 者 彰 假 舆 马 者, 非 利 足 也,

More information

epub83-1

epub83-1 C++Builder 1 C + + B u i l d e r C + + B u i l d e r C + + B u i l d e r C + + B u i l d e r 1.1 1.1.1 1-1 1. 1-1 1 2. 1-1 2 A c c e s s P a r a d o x Visual FoxPro 3. / C / S 2 C + + B u i l d e r / C

More information

第一章 章标题-F2 上空24,下空24

第一章 章标题-F2 上空24,下空24 Web 9 XML.NET Web Web Service Web Service Web Service Web Service Web Service ASP.NET Session Application SOAP Web Service 9.1 Web Web.NET Web Service Web SOAP Simple Object Access Protocol 9.1.1 Web Web

More information

untitled

untitled 1 行 行 行 行.NET 行 行 類 來 行 行 Thread 類 行 System.Threading 來 類 Thread 類 (1) public Thread(ThreadStart start ); Name 行 IsAlive 行 行狀 Start 行 行 Suspend 行 Resume 行 行 Thread 類 (2) Sleep 行 CurrentThread 行 ThreadStart

More information

1: public class MyOutputStream implements AutoCloseable { 3: public void close() throws IOException { 4: throw new IOException(); 5: } 6:

1: public class MyOutputStream implements AutoCloseable { 3: public void close() throws IOException { 4: throw new IOException(); 5: } 6: Chapter 15. Suppressed Exception CH14 Finally Block Java SE 7 try-with-resources JVM cleanup try-with-resources JVM cleanup cleanup Java SE 7 Throwable getsuppressed Throwable[] getsuppressed() Suppressed

More information

OSWorkflow Documentation

OSWorkflow Documentation OSWorkflow Documentation Update Time: 05/09/15 OSWorkflow Java workflow engine API 理 flow 行 XML 來 流 Database UI 不 流 GUI Designer end user 行 JSP+Servlet 行 OSWorkflow 2.8 說 2.7 2.7 了 OSWorkflow library library

More information

Office Office Office Microsoft Word Office Office Azure Office One Drive 2 app 3 : [5] 3, :, [6]; [5], ; [8], [1], ICTCLAS(Institute of Computing Tech

Office Office Office Microsoft Word Office Office Azure Office One Drive 2 app 3 : [5] 3, :, [6]; [5], ; [8], [1], ICTCLAS(Institute of Computing Tech - OfficeCoder 1 2 3 4 1,2,3,4 xingjiarong@mail.sdu.edu.cn 1 xuchongyang@mail.sdu.edu.cn 2 sun.mc@outlook.com 3 luoyuanhang@mail.sdu.edu.cn 4 Abstract. Microsoft Word 2013 Word 2013 Office Keywords:,, HTML5,

More information

J2EE MVC with Webwork2 Xwork, to J2EE MVC with Webwork2 Xwork

J2EE MVC with Webwork2 Xwork,  to J2EE MVC with Webwork2 Xwork MVC with Webwork2 Xwork Action...1 ActionContext...3 ActionProxyFactory Factory...4 ActionProxyFactory Proxy AOP...7 XworkInterceptor...8 Interceptor...9 LoginAction...10 LoginInterceptor...12 Action Result

More information

LiveBOS产品白皮书

LiveBOS产品白皮书 面 向 对 象 的 业 务 支 撑 平 台 与 建 模 工 具 * 实 现 应 您 所 需, 随 时 而 变 的 应 用 * 业 务 管 理 应 用 软 件 最 佳 选 择 * LiveBOS 产 品 白 皮 书 LiveBOS 产 品 白 皮 书 福 州 顶 点 信 息 管 理 有 限 公 司 http://www.apexinfo.com.cn Copyright c 2008-2015 版 权

More information

Ioncube Php Encoder 8 3 Crack 4. llamaba octobre traslado General Search colony

Ioncube Php Encoder 8 3 Crack 4. llamaba octobre traslado General Search colony Ioncube Php Encoder 8 3 Crack 4 ->>->>->> DOWNLOAD 1 / 5 2 / 5 Press..the..General..Tools..category4Encrypt..and..protect..files..with..PHP..encoding,..encryption,..ob fuscation..and..licensing... 2016

More information

chp6.ppt

chp6.ppt Java 软 件 设 计 基 础 6. 异 常 处 理 编 程 时 会 遇 到 如 下 三 种 错 误 : 语 法 错 误 (syntax error) 没 有 遵 循 语 言 的 规 则, 出 现 语 法 格 式 上 的 错 误, 可 被 编 译 器 发 现 并 易 于 纠 正 ; 逻 辑 错 误 (logic error) 即 我 们 常 说 的 bug, 意 指 编 写 的 代 码 在 执 行

More information

声 明 本 公 司 及 全 体 董 事 监 事 高 级 管 理 人 员 承 诺 不 存 在 任 何 虚 假 记 载 误 导 性 陈 述 或 重 大 遗 漏, 并 对 其 真 实 性 准 确 性 完 整 性 承 担 个 别 和 连 带 的 法 律 责 任 本 公 司 负 责 人 和 主 管 会 计 工

声 明 本 公 司 及 全 体 董 事 监 事 高 级 管 理 人 员 承 诺 不 存 在 任 何 虚 假 记 载 误 导 性 陈 述 或 重 大 遗 漏, 并 对 其 真 实 性 准 确 性 完 整 性 承 担 个 别 和 连 带 的 法 律 责 任 本 公 司 负 责 人 和 主 管 会 计 工 ( 申 报 稿 ) 主 办 券 商 二 〇 一 五 年 十 月 声 明 本 公 司 及 全 体 董 事 监 事 高 级 管 理 人 员 承 诺 不 存 在 任 何 虚 假 记 载 误 导 性 陈 述 或 重 大 遗 漏, 并 对 其 真 实 性 准 确 性 完 整 性 承 担 个 别 和 连 带 的 法 律 责 任 本 公 司 负 责 人 和 主 管 会 计 工 作 的 负 责 人 会 计 机 构

More information

Spring 的入门程序 依赖注入的概念 依赖注入的实现方式 Spring 的核心容器 Spring 的入门程序 依赖注入的概念 依赖注入的实现方式 依赖注入的概念 了解 Spring 的概念和优点 理解 Spring 中的 IoC 和 DI 思想 掌握 ApplicationContext 容器的

Spring 的入门程序 依赖注入的概念 依赖注入的实现方式 Spring 的核心容器 Spring 的入门程序 依赖注入的概念 依赖注入的实现方式 依赖注入的概念 了解 Spring 的概念和优点 理解 Spring 中的 IoC 和 DI 思想 掌握 ApplicationContext 容器的 Java EE 企业级应用开发教程 (Spring+Spring MVC+MyBatis) 课程教学大纲 ( 课程英文名称 ) 课程编号 : XXXX 学分 : 5 学分学时 : 90 学时 ( 其中 : 讲课学时 :55 上机学时 :35) 先修课程 :Java 基础案例教程 Java Web 程序设计任务教程 MySQL 数据库入门适用专业 : 信息及其计算机相关专业开课部门 : 计算机系 一

More information

untitled

untitled 1 .NET 利 [] [] 來 說 切 切 理 [] [ ] 來 說 拉 類 類 [] [ ] 列 連 Web 行流 來 了 不 不 不 流 立 行 Page 類 Load 理 Response 類 Write 料 Redirect URL Response.Write("!! ives!!"); Response.Redirect("WebForm2.aspx"); (1) (2) Web Form

More information