前端控制器
在 CS 架构标准的 MVC 模型可以主动推送数据更新视图(观察者设计模式,在模型上注册视图,当模型改变时直接通知视图刷新数据),但是在 Web 开发中模型是无法主动推送给视图(无法主动更新用户界面),因为在 WEB 开发是请求-响应模型
因为要处理的功能太多:设置请求密码、接受请求参数、输入校验、参数类型转换、把参数封装成对象、文件上传、处理响应、国际化处理、自定义标签等等。所以需要一个统一的头部,过滤相同的事情。
Front Controller 模式,在WEB应用系统的前端设置一个入口控制器,提供一个集中处理请求的地方,先把所有的请求过滤然后分发给各自相应的处理程序。
一般前端控制器职责是,权限检查、日志记录等等。
一般把处理请求的对象成为处理器,Apache 成为 Action,EmployeeAction,Spring 叫 Controller,EmployeeController。
一般,前端控制器要么用 Filter, 要么是 Servilet, Struts2 基于 Filter, SpringMVC 基于 Servlet。
MVC框架,它能解决 Web 开发中的常见问题(参数接收、文件上传、表单验证、国际化),使用简单,与Spring无缝集成。
Spring3.0后,全面超越 Struts2 成为最优秀的 MVC 框架,支持 RESTful 风格的 URL 请求,非常容易与其他视图技术集成,如 Velocity、FreeMarker。采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性。
Spring WebFlux 是Spring 另一个 Web 框架,基于异步非阻塞的。(需要去学习)
SpringMVC 是同步阻塞的 IO 模型,资源浪费严重,处理比较耗时的任务,比如上传文件,服务器线程会一直等待接收文件, Spring WebFlux 可以做到异步非阻塞。
SpringMVC 和 Struts2 对比:
- Spring MVC 的前端控制器是 Servlet,而 Stuts2 的前端控制器是 Filter。
- Spring MVC 会稍微比 Sturts2 快,Spring MVC 是基于方法设计,处理器是单例,而 Sturts2 是基于类,每次发送一次请求都会实例一个新的 Action 对象。
- Spring MVC 更加简洁,开发效率高,处理 AJAX 请求更方便。
第一个程序
需求:实现一个 SpringMVC 的 hello 程序:
步骤:
- 搭建 web 项目,拷贝 jar
- 在 web.xml 中配置前端控制器:DispatcherServlet
- 在 mvc.xml 中配置处理映射器:BeanNameUrlHandlerMapping,处理器的映射器是哪个,作用:将 XxxController 和 请求url 通过 id 或者 name 关联
- 在 mvc.xml 中配置处理器适配器:SimpleControllerHandlerAdapter(handleRequest方法),处理器的适配器是哪个,作用:告诉调度器调用 XxxController 的哪个方法
- 在 mvc.xml 中配置视图解析器:InternalResourceViewResolver,视图解析器是谁,作用:告诉调度器使用哪个解析器解析视图。
- 在 mvc.xml 中开发和配置处理器:创建处理器对象,并根据 id、name 设置 url 路径。
- 第一步需要注意的点:
appache 基础类库:commons-logging
需要导入 Spring 核心包:spring-core
、spring-beans
、spring-expression
、spring-context
SpringMVC 核心包:spring-web
、spring-webmvc
- 第二步:在 web.xml 中配置前端控制器
1 | <servlet> |
配置前端控制器的拦截路径,url-pattern,配置如 *.do
、*htm
,不会导致静态文件(jpg,js,css)被拦截,但不支持 RESTful风格,配置成/,可以支持RESTfull风格,但会导致静态文件(jpg,js,css)被拦截后不能正常显示。
DispatcherServlet 默认会去 WEB-INF 目录下,按照 servletName-servlet.xml 方式去加载 SpringMVC 配置文件。如果 servlet-name 元素文本内容是Springmvc,则去WEB-INF 目录下寻找 Springmvc-servlet.xml 文件,一般从 classpath 路径,加载 SpringMVC 的配置文件。
- 配置处理映射器:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
中配置处理映射器:处理器的映射器是哪个,BeanNameUrlHandlerMapping 作用:将 XxxController 和 请求url 通过 id 或者 name 关联
- 配置处理器适配器:
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
所有适配器都要实现 HandlerAdapter 接口
所有处理器需要实现 Controller 接口
调用 Controller 处理请求的方法:handleRequest
SimpleControllerHandlerAdapter(handleRequest方法),处理器的适配器是哪个,作用:告诉调度器调用 XxxController 的哪个方法
- 配置视图解析器
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
表示配置当前我们使用的是什么视图技术,当前是jsp, default 为 JSTL。
- 开发和配置 HelloController(用id、name关联url)
<bean id="/hello" class="cn.lizhaoloveit.hello.HelloController"/>
实现,Controller 接口
1 | public class HelloController implements Controller { |
Dispatcher 工作原理
详情请见:SpringMVC之Dispatcher
SpringMVC 注解
MVC 注解解析器:<mvc:annotation-driver>
会自动注册 RequestMappingHandlerMapping、RequestMappingHandlerAdapter、ExceptionHandlerExceptionResolver 三个 bean。
此外,还支持:
- 支持 ConversionService 实例对表单参数进行类型转换
- @NumberFormat、@DateTimeFormat 注解完成数据格式化操作
- @Valid 注解对 JavaBean 实例进行 JSR303 验证
- @RequestBody 和 @ResponseBody 注解读写 JSON
RequestMapping 注解可
可用于类或者方法上,如果贴在雷伤,表示类中所有响应请求的方法都是以该地址作为父路径。
参数:
参数名 | 含义 |
---|---|
value | 请求的URL |
method | 请求的 method 类型,GET、POST、PUT、DELETE |
consumes | 处理请求的提交内容类型(Content-Type),application/json |
produces | 返回的内容类型,如 application/json;charset=UTF-8,仅当 request 请求头中的 Accept 类型中包含该指定类型才返回 |
params | request中必须包含某些参数值,才让该方法处理 |
headers | 指定 request 中必须包含某些指定的 header 值,才让该方法处理 |
SpringMVC 访问静态资源
当设置 Dispatcher 的 url-pattern 为 / ,当请求一个资源时,比如 js.html,Dispatcher 会忽略 .html,只拿到 js 去寻找动态文件(jsp),如果没有找到,就会把该资源抛给 Tomcat,让 Tomcat 处理。
如果此时,刚好有一个 Controller 的 @RequestMapping(“/js”) 为js,而 js 的方法返回的字符串恰好为 js.html,则就会形成死循环。会无限找动态资源,找到后返回 js.html,又会去找 js。报错如下:
javax.servlet.ServletException: Circular view path [/js.html]: would dispatch back to the current handler URL [/js.html] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
解决访问静态资源的三种方式:
url-pattern 拦截方式:
*.do
、*.htm
是最传统方式,不会导致静态文件(jpg、js、css)被拦截,但不支持 RESTful风格。- 配置成 / ,可以支持 RESTful 分各个,但会导致静态文件被拦截后不能正常显示,原因,Tomcat 自己的 web.xml 配置在处理静态文件会用到 DefaultServlet,而且url-pattern 为 / ,所以我们的 DispatcherServlet 的映射路径覆盖了 Tomcat 默认对静态资源的处理路径,此时 SpringMVC 会把静态资源当做 Controller,寻找并访问,结果肯定是找不到
- 为什么不用
/*
? 因为如果用/*
jsp 中的语法不会起作用,会原样输出。而/
不会。
解决方案一:
<mvc:default-servlet-handler/>
在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,它会对进入 DispatcherServlet 的请求进行筛选,如果发现是没有经过映射的请求,就将该请求交给 Tomcat 默认的 Servlet 处理,如果不是静态资源的请求,才由 DispatcherServlet 继续处理。
解决方案二:
资源映射
<mvc:resources location="/" mapping="/**"/>
<mvc:resources>
更进一步,由 SpringMVC 框架自己处理静态资源,并添加一些有用的附加功能。允许静态资源放在任何地方,如 WEB-INF 目录下,类路径下,甚至可以把 JS 等静态文件打包到 jar 中,通过 location 属性指定静态资源的位置,由于 location 属性时 Resource 类型,所以可以使用前缀 classpath。
示例:
<mvc:resources location="classpath:/static/" mapping="/**"></mvc:resources>
Mapped to ResourceHttpRequestHandler [“classpath:/static/“]
映射路径。
Response
@Controller 中的方法可以有多个不同类型的参数,一个多种类型的返回结果。我们只能用到其中不多的类型
所有类型如下图:
配置项目 spring-web 项目
1 |
|
ModelAndView
从 ModelAndView 的属性:
1 | /** View instance or view name String. */ |
view
可以传入一个 View 视图,也可以传入 viewName 字符串。setViewName:(String viewName)
本身其实是传入一个视图的路径。
View 视图发现是一个接口,看看有哪些实现类:cmd + T
还记得当时配置视图解析器时创建的对象
org.springframework.web.servlet.view.InternalResourceViewResolver ,就是用来解析 指定路径的JSP视图文件的。
model
model 的类型是 ModelMap,ModelMap extends LinkedHashMap<String, Object>
,可以看到就是一个 HashMap。
逻辑路径
物理路径 = 前缀路径 + 逻辑路径 + 后缀路径
项目中为了管理业务代码,会把视图分模块、存放,比如
每次访问 JSP 时,都要带上路径 “WEB-INF/View/“ 和 后缀 “.JSP”,非常的麻烦。
所以在创建视图解析器时,InternalResourceViewResolver 的父类 UrlBasedViewResolver ,有两个方法:
1 | public void setPrefix(@Nullable String prefix) { |
可以设置前缀和后缀路径,mvc.xml
1 | <!-- IoC 注解解释器 --> |
1 | "/test2") ( |
返回 String 响应视图
前面介绍了返回 ModelAndView 对象响应,也可以通过返回字符串路径去访问前端页面。有三种形式:
- 第一种
1 | /** |
可以在方法中出现的参数如上图
- 第二种
1 | /** |
如果返回没有带/,forward:hello.html,表示从当前路径转发,而不是跟路径转发。
1 | /** |
地址栏显示 localhost/hello.html?username=springmvc,使用 redirect: 相当于将冒号后面的路径直接拼到域名下并访问。
如果要重定向到某个 jsp,需要通过 Controller 中的 RequestMapping 映射。比如:重定向到列表页
redirect:/dept/list
相当于在浏览器中输入localhost/dept/list
Request
SpringMVC 获取请求参数:请求行、请求头、请求体
Servlet API
为了操作 Servlet API 对象,可以直接以参数形式传递、也可以通过 DI 注入。
1 |
|
简单类型
1 | // localhost/request/test1 GET OR POST:username=李朝&age=17 |
注意:之所以传入中文不会乱码,是因为 Tomcat7 在 GET 请求中做了支持中文的转码,但 POST 请求中没有,因此 POST 请求传入中文会出现乱码。
框架支持直接传入接收的参数名。如果请求参数和 Controller 方法的形参不同名,使用 @RequestParam 注解贴在形参上,设置对应的请求参数名称。
1 | // localhost/request/test2 GET OR POST:name=李朝&age=17 |
POST 请求时中文乱码问题
web.xml
1 | <!-- 解决 POST 请求中文乱码问题 --> |
需要说明的是:如果在应用中已经有了其他编码了,需要设置 org.springframework.web.filter.CharacterEncodingFilter 的两个参数。
forceRequestEncoding
、forceResponseEncoding
JavaBean 类型参数
能够自动把请求参数封装到 Controller 方法的形参对象中,此时请求参数必须和对象的属性同名,如果页面中有多个同名参数,需要使用对应的 JavaBean 类型名.参数名的形式传递。比如
1 | <div class="form-group"> |
上面的 name 属性设置为 customer.name、customer.id 当表单提交时,会被封装成 Customer 对象的 name 属性和 id 属性。
只要方法中有参数,参数必然不会为空,即使所有参数的属性都为空,SpringMVC 都会为其生成一个所有属性为 null 的参数。
数组、List
复选框、批量删除
1 | "/test4") ( |
浏览器请求URL:/request/test4?ids=10&ids=20&ids=30,参数名与数组名相同
使用 List 类型封装参数,必须绑定在对象上,不能直接作为 Controller 方法参数
1 | public class FormBean { |
接受 List 类型参数:
1 | "/test5") ( |
浏览器请求URL:/request/test4?ids[0]=10&ids[1]=20&ids[2]=30
修改共享属性 Key 值
1 | "/test6") ( |
此时,可以在 JSP 页面上,通过${user} 获取共享数据
如果使用 ModelAttribute 注解,可以自定义共享数据的 key。
1 | "/test7") ( |
此时 JSP 页面上,可以通过 ${myUser} 获取共享数据
平时做条件过滤查询,常用 ModelAttribute 共享数据
1 | "Xxx") ( |
Json 数据处理
见文章 Ajax
SpringMVC 返回 Json 数据时,如果数据时纯字符串,会出现中文乱码问题。
1 | "content") ( |
解决方案:
- 使用(produces = “application/json; charset=utf-8”)
1 | "/getUsersByPage",produces = "application/json; charset=utf-8") (value= |
- 在springmvc.xml文件中添加
1 | <!-- 处理请求返回json字符串的中文乱码问题 --> |
日期处理
JSP 中显示 Date 类型 (Date -> String)
taglibs-standard-spec
、taglibs-standard-impl
@DateTimeFormat(pattern=”…”)、@ControllerAdvice
方式一:在对象字段或形参上添加 @DateTimeFormat 注解
1 | "/test8") ( |
Date 作为对象的字段存在。此时在该字段上贴
1 | public class User { |
方式二:在 Controller 类中加入以下代码
1 | public void initBinderDateType(WebDataBinder binder) { |
不推荐,因为此时只能在 Controller 类中使用,其他 Controller 的 Date 类型不能转换。
方式三:使用 @ControllerAdvice 注解
1 |
|
此时,需要保证 DateFormatControllerAdvice 类所在包能被组件扫描器扫描即可,也就是 <context:component-scan base-package=”cn.lizhaoloveit.hello”>
但是这个是旧版本的 SpringMVC 这样处理,新版本的 SpringMVC 应该是改用了 converter 接口。
响应 Json 格式 Date 类型数据处理
问题:后台往前台响应数据,若返回数据存在 Date 类型字段,SpringMVC 自动把 Date 类型数据解析成毫秒值
方式一 在需要解析的字段上添加注解
1 | public class User { |
方式二 在 mvc.xml 中配置全局的解析器
1 | <mvc:annotation-driven> |
其他请求信息(Session、Cookie)
获取请求头信息
@RequestHeader
获取 Cookie 信息
@CookieValue
1 | "/test7") ( |
操作 HttpSession
默认情况下,模型数据保存到 request 作用域,如果需要保存到 session,使用 SessionAttributes 注解。@SessionAttributes 贴在处理器上,生命 HttpSession 级别存储的属性,通常列出模型属性 @ModelAttribute 对应的名称,这些属性会自动保存到 session 中。
1 |
|
${requestScope.USER_IN_SESSION.username }
${sessionScope.USER_IN_SESSION.username }
都可以拿到数据
${cookie.username.name}
获得 cookie 名称 – “username”,${cookie.username.value}
获得 cookie 值– “Username in cookie”。
多对象封装传参
假设有两个类 Cat 和 Dog,这两个类中有相同的属性 name 和 age。
期望在提交表单时,同时保存 Cat 和 Dog 信息
1 | <form action="/request/save" method="POST"> |
Controller 方法:
1 | "/save") ( |
SpringMVC 不支持 Struts2 中类似 对象名.属性名 的传参方式。
此时需要对数据设置绑定规则:
InitBinder 注解:自定义数据绑定注册支持,用于将请求参数转换到命令对象属性的对应类型。
由 @InitBinder 注解标注的方法,可以对 WebDataBinder 进行初始化,WebDataBinder 是 DataBinder 的子类,用于完成由请求参数到 JavaBean 的属性绑定。
@InitBinder 标注的方法不能有返回值,必须声明为 void
@InitBinder 标注的方法的参数通常是 WebDataBinder。
1 | "Cat") ( |
拦截器
作用:拦截经过处理器的请求。
基于处理器,在 mvc.xml 中配置,由 Spring 容器创建并放在 Spring 容器中,可以注入其他 bean ,假设配置 /**,对所有的经过处理器的请求进行拦截。
为什么不用过滤器实现登录
登录想拦截的东西是对数据库的 crud 操作,现在对数据库的增删改查都是写在 Controller 中,所以不需要使用向过滤器这样大的拦截范围,只需要拦截 Controller 就行,所以使用拦截器即可。
https://www.lizhaoloveit.cn/blogimages/java/frames/spring
拦截原理
- preHandle 方法
- 请求到达处理器之前,预先执行这个前置处理方法,返回 false,请求直接返回,不会传递到链中的下一个拦截器,更不会传递到处理链末端的 Handler 中。
- postHandle 方法
- 控制器方法执行后,视图渲染之前会执行(可以加入以下统一的响应信息)
- afterCompletion 方法
- 视图渲染之后执行(处理 Controller 异常信息,记录操作日志,清理资源)
使用
mvc.xml 中配置
1 | <!-- 配置拦截器 --> |
TestInterceptor.java
1 | public class TestInterceptor implements HandlerInterceptor { |
案例:未登录拦截的实现
CheckLoginInterceptor.java
1 | public class CheckLoginInterceptor implements HandlerInterceptor { |
mvc.xml
1 | <!-- 配置拦截器 --> |