SpringMVC笔记
SpringMVC的工作流程
SpringMVC的请求处理流程
流程说明:
第一步:用户发送请求至前端控制器DispactherServlet
第二步:DispactherServlet收到请求调用处理器映射器HandlerMapping
第三步:HandlerMapping根据请求的URL,找到具体的Handler(后端控制器,可以是个方法,也可以是个类),生成处理器对象和拦截器(如果有),并返回给DispatcherServlet
第四步:DispatcherServlet调用HandlerAdapter处理器适配器去调用Handler
第五步:执行Handler方法
第六步:Handler方法执行完毕,返回ModelAndView给HandlerAdapter
第七步:HandlerAdapter返回ModelAndView到DispatcherServlet
第八步:DispatcherServlet调用视图解析器,解析视图,根据视图的逻辑名来解析真正的视图
第九步:视图解析器返回解析完成的视图View给DispatcherServlet
第十步:前端控制器DispatcherServlet进行视图渲染,就是将ModelAndView对象中的数据模型,填充到request域中
第十一步:前端控制器向用户响应结果
SpringMVC的九大组件
- HandleMapping(处理器映射器):处理器映射器通过收到的用户请求,找到处理对应请求的Handler(后端控制器和对应的拦截器),Handler可以是方法也可以是类。比如,在@Controller标注了的类中,被@RequestMapping标注了的方法都可以看出是一个Handler,handler负责具体请求的处理,在请求到达后,HandlerMapping 就根据具体的Url找到相应的处理器Handler和拦截器Interceptor
- HandlerAdapter(处理器适配器):HandlerAdapter是一个适配器,因为SpringMVC中的Handler是可以任意形式的,只要能处理请求即可,但是把请求交给Servlet处理时,用于Servlet都是doServer(HttpServletRequest request,HttpServletResponse respose)形式的,要让固定的处理方法调用Handler来进行处理,便是HandlerAdapter的职责
- ViewResovler(视图解析器):视图解析器用于将String类型的视图名和Locale(国际化,zh-CN)解析为View类型的视图,只有一个resovleViewName()方法。Controller层返回的String类型的视图名ViewName,会在这里解析成View。View是⽤来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件。ViewResolver 在这个过程主要完成两件事情:ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个InternalResourceViewResolver,是针对 JSP 类型视图的。
- MultipartResovler:主要用于上传请求的处理,通过将普通的请求包装成MultpartHttpServletRequest来实现。
- HandlerExceptionResovler:用于处理Handler产生的异常情况。它的作用是根据异常设置ModelAndView,之后交给渲染方法进行渲染。
- RequestToViewNameTranslator:RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName.因为 ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,便要通过这个组件从请求中查找 ViewName。(如果没有设置viewName,默认把请求地址当作逻辑视图名)
- LocaleResovler:ViewResovler组件的resovleViewName方法要传两个参数,一个是视图名,一个是Locale。LocaleResovler用于从请求中解析出Locale,比如中国是zh-CN,用来表示一个区域,这个组件也是国际化的基础
- ThemeResovler:ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。Spring MVC 中⼀套主题对应⼀个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等。创建主题⾮常简单,只需准备好资源,然后新建⼀个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使⽤了。SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名,ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。
- FlashMapManager:FlashMap ⽤于重定向时的参数传递,⽐如在处理⽤户订单时候,为了避免重复提交,可以处理完post请求之后重定向到⼀个get请求,这个get请求可以⽤来显示订单详情之类的信息。这样做虽然可以规避⽤户重新提交订单的问题,但是在这个⻚⾯上要显示订单的信息,这些数据从哪⾥来获得呢?因为重定向时么有传递参数这⼀功能的,如果不想把参数写进URL(不推荐),那么就可以通过FlashMap来传递。(具体查看后面“基于Flash属性的跨重定向请求数据传递”)
SpringMVC请求参数的绑定
-
简单数据类型参数的绑定,使用其包装类
整型:Integer、int
字符串:String
单精度:Float、flfloat
双精度:Double、double
布尔型:Boolean、boolean
…
说明:对于布尔类型的参数,请求的参数值为true或false。或者1或0
注意:绑定简单数据类型参数,只需要直接声明形参即可(形参参数名和传递的参数名要保持⼀
致,建议 使⽤包装类型,当形参参数名和传递参数名不⼀致时可以使⽤@RequestParam注解进⾏
⼿动映射)
-
绑定Pojo类型参数
- Get请求的处理方式
/* * SpringMVC接收pojo类型参数 url:/demo/handle04?id=1&username=zhangsan * * 接收pojo类型参数,直接形参声明即可,类型就是Pojo的类型,形参名⽆所谓 * 但是要求传递的参数名必须和Pojo的属性名保持⼀致 */ @RequestMapping("/handle04") public ModelAndView handle04(User user) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; }
- 如果是Post请求,参数名称和Pojo的属性名保持一致即可
-
绑定Pojo包装对象参数
-
包装类型 QueryVo
public class QueryVo { private String mail; private String phone; // 嵌套了另外的Pojo对象 private User user; public String getMail() { return mail; } public void setMail(String mail) { this.mail = mail; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } }
Handler⽅法
/* * SpringMVC接收pojo包装类型参数 url:/demo/handle05? user.id=1&user.username=zhangsan * 不管包装Pojo与否,它⾸先是⼀个pojo,那么就可以按照上述pojo的要求来 * 1、绑定时候直接形参声明即可 * 2、传参参数名和pojo属性保持⼀致,如果不能够定位数据项,那么通过属性名 + "." 的 ⽅式进⼀步锁定数据 * */ @RequestMapping("/handle05") public ModelAndView handle05(QueryVo queryVo) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; }
-
-
绑定⽇期类型参数(需要配置⾃定义类型转换器)
-
自定义类型转化器
import org.springframework.core.convert.converter.Converter; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * ⾃定义类型转换器 * S:source,源类型 * T:target:⽬标类型 */ public class DateConverter implements Converter<String, Date> { @Override public Date convert(String source) { // 完成字符串向⽇期的转换 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); try { Date parse = simpleDateFormat.parse(source); return parse; } catch (ParseException e) { e.printStackTrace(); } return null; } }
-
注册⾃定义类型转换器
<!-- ⾃动注册最合适的处理器映射器,处理器适配器(调⽤handler⽅法) --> <mvc:annotation-driven conversionservice="conversionServiceBean"/> <!--注册⾃定义类型转换器--> <bean id="conversionServiceBean" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.lagou.edu.converter.DateConverter"></bean> </set> </property> </bean>
-
前端jsp请求
<p>测试⽤例:SpringMVC接收⽇期类型参数</p> <a href="/demo/handle06?birthday=2019-10-08">点击测试</a>
-
后端Handler方法
/** * 绑定⽇期类型参数 * 定义⼀个SpringMVC的类型转换器 接⼝,扩展实现接⼝接⼝,注册你的实现 * @param birthday * @return */ @RequestMapping("/handle06") public ModelAndView handle06(Date birthday) { Date date = new Date();ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; }
-
SpringMVC对Restful风格请求的支持
-
什么是Restful
- Restful 是⼀种 web 软件架构⻛格,它不是标准也不是协议,它倡导的是⼀个资源定位及资源操作的⻛格。
-
Restful 的优点
- 它结构清晰、符合标准、易于理解、扩展⽅便,所以正得到越来越多⽹站的采⽤。
-
Restful 的特性
- 资源(Resources):⽹络上的⼀个实体,或者说是⽹络上的⼀个具体信息。
-
RESTful 的示例
- 获取id为1的用户信息
- GET http://localhost:8080/user/1
- 更新
- PUT http://localhost:8080/user/1
- 删除
- DELETE http://localhost:8080/user/1
- 获取id为1的用户信息
rest⻛格带来的直观体现:就是传递参数⽅式的变化,参数可以在uri中了
-
示例代码
- 前端JSP
<div> <h2>SpringMVC对Restful⻛格url的⽀持</h2> <fieldset> <p>测试⽤例:SpringMVC对Restful⻛格url的⽀持</p> <a href="/demo/handle/15">rest_get测试</a> <form method="post" action="/demo/handle"> <input type="text" name="username"/> <input type="submit" value="提交rest_post请求"/> </form> <form method="post" action="/demo/handle/15/lisi"> <input type="hidden" name="_method" value="put"/> <input type="submit" value="提交rest_put请求"/> </form> <form method="post" action="/demo/handle/15"> <input type="hidden" name="_method" value="delete"/> <input type="submit" value="提交rest_delete请求"/> </form> </fieldset> </div>
-
后台Handler方法
/* * restful get /demo/handle/15 */ @RequestMapping(value = "/handle/{id}",method = {RequestMethod.GET}) public ModelAndView handleGet(@PathVariable("id") Integer id) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; } /* * restful post /demo/handle */ @RequestMapping(value = "/handle",method = {RequestMethod.POST}) public ModelAndView handlePost(String username) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; } /* * restful put /demo/handle/15/lisi */ @RequestMapping(value = "/handle/{id}/{name}",method = {RequestMethod.PUT}) public ModelAndView handlePut(@PathVariable("id") Integer id,@PathVariable("name") String username) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; } /* * restful delete /demo/handle/15 */ @RequestMapping(value = "/handle/{id}",method = {RequestMethod.DELETE}) public ModelAndView handleDelete(@PathVariable("id") Integer id) { Date date = new Date(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("date",date); modelAndView.setViewName("success"); return modelAndView; }
-
web.xml中配置请求⽅式过滤器(将特定的post请求转换为put和delete请求)
<!--配置springmvc请求⽅式转换过滤器,会检查请求参数中是否有_method参数,如果有就 按照指定的请求⽅式进⾏转换--> <filter> <filter-name>hiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <filter-mapping> <filter-name>hiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
拦截器(Interceptor)使用
过滤器、监听器、拦截器的对比
-
过滤器(Filter):对Request请求起到过滤作用,作⽤在Servlet之前,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进⾏过滤处理
-
监听器(Listener):实现了javax.servlet.ServletContextListener 接⼝的服务器端组件,它随Web应⽤的启动⽽启动,只初始化⼀次,然后会⼀直运⾏监视,随Web应⽤的停⽌⽽销毁
作⽤⼀:做⼀些初始化⼯作,web应⽤中spring容器启动ContextLoaderListener
作⽤⼆:监听web中的特定事件,⽐如HttpSession,ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控,⽐如统计在线⼈数,利⽤HttpSessionLisener等
-
拦截器(Interceptor):是SpringMVC、Struts等表现层框架⾃⼰的,不会拦截jsp/html/css/image的访问等,只会拦截访问的控制器⽅法(Handler)。
从配置的⻆度也能够总结发现:filter、listener是配置在web.xml中的,⽽interceptor是配置在表现层框架⾃⼰的配置⽂件中的
-
在Handler业务逻辑执⾏之前拦截⼀次
-
在Handler逻辑执⾏完毕但未跳转⻚⾯之前拦截⼀次
-
在跳转⻚⾯之后拦截⼀次
-
拦截器的执行流程
在运⾏程序时,拦截器的执⾏是有⼀定顺序的,该顺序与配置⽂件中所定义的拦截器的顺序相关。 单个
拦截器,在程序中的执⾏流程如下图所示:
- 程序先执行preHandle方法,如果该方法返回值为true,则继续向下执行处理器中的方法,否则将不再向下执行
- 在业务处理器执行完毕后,未进行页面跳转之前,执行postHandler方法,然后通过DispatcherServlet向客户端返回相应
- 在DispatcherServlet处理完毕,跳转完页面之后,执行afterComletion方法
多个拦截器的执行流程
多个拦截器(假设有两个拦截器Interceptor1和Interceptor2,并且在配置⽂件中, Interceptor1拦截
器配置在前),在程序中的执⾏流程如下图所示:
从图可以看出,当有多个拦截器同时⼯作时,它们的preHandle()⽅法会按照配置⽂件中拦截器的配置
顺序执⾏,⽽它们的postHandle()⽅法和afterCompletion()⽅法则会按照配置顺序的反序执⾏。
-
示例代码
-
自定义拦截器
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * ⾃定义springmvc拦截器 */ public class MyIntercepter01 implements HandlerInterceptor { /** * 会在handler⽅法业务逻辑执⾏之前执⾏ * 往往在这⾥完成权限校验⼯作 * @param request * @param response * @param handler * @return 返回值boolean代表是否放⾏,true代表放⾏,false代表中⽌ * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyIntercepter01 preHandle......"); return true; } /** * 会在handler⽅法业务逻辑执⾏之后尚未跳转⻚⾯时执⾏ * @param request * @param response * @param handler * @param modelAndView 封装了视图和数据,此时尚未跳转⻚⾯呢,你可以在这⾥针对返回的 数据和视图信息进⾏修改 * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyIntercepter01 postHandle......"); } /** * ⻚⾯已经跳转渲染完毕之后执⾏ * @param request * @param response * @param handler * @param ex 可以在这⾥捕获异常 * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyIntercepter01 afterCompletion......"); } }
-
注册SpringMVC拦截器
<mvc:interceptors> <!--拦截所有handler--> <!--<bean class="com.lagou.edu.interceptor.MyIntercepter01"/>--> <mvc:interceptor> <!--配置当前拦截器的url拦截规则,**代表当前⽬录下及其⼦⽬录下的所有url--> <mvc:mapping path="/**"/> <!--exclude-mapping可以在mapping的基础上排除⼀些url拦截--> <!--<mvc:exclude-mapping path="/demo/**"/>--> <bean class="com.xxx.xxx.interceptor.MyIntercepter01"/> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.xxx.xxx.interceptor.MyIntercepter02"/> </mvc:interceptor> </mvc:interceptors>
-
处理Multipart形式的数据
-
⽂件上传所需jar包
<!--⽂件上传所需jar坐标--> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>
-
配置文件上传解析器
<!--配置⽂件上传解析器,id是固定的multipartResolver--> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!--设置上传⼤⼩,单位字节--> <property name="maxUploadSize" value="1000000000"/> </bean>
-
前端form
<%-- 1 method="post" 2 enctype="multipart/form-data" 3 type="file" --%> <form method="post" enctype="multipart/form-data" action="/demo/upload"> <input type="file" name="uploadFile"/> <input type="submit" value="上传"/> </form
-
后台接收Handler
@RequestMapping("/demo/upload") public String upload(MultipartFile uploadFile, HttpServletRequest request) throws IOException { // ⽂件原名,如xxx.jpg String originalFilename = uploadFile.getOriginalFilename(); // 获取⽂件的扩展名,如jpg String extendName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1, originalFilename.length()); String uuid = UUID.randomUUID().toString(); // 新的⽂件名字 String newName = uuid + "." + extendName; String realPath = request.getSession().getServletContext().getRealPath("/uploads"); // 解决⽂件夹存放⽂件数量限制,按⽇期存放 String datePath = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); File floder = new File(realPath + "/" + datePath); if(!floder.exists()) { floder.mkdirs(); } uploadFile.transferTo(new File(floder,newName)); //跳转到成功页面 return "success"; }
在控制器中处理异常
-
自定义全局异常处理器
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @ControllerAdvice public class GlobalExceptionResolver { @ExceptionHandler(ArithmeticException.class) public ModelAndView handleException(ArithmeticException exception,HttpServletResponse response) { ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg",exception.getMessage()); //跳转到请求的错误处理页面 modelAndView.setViewName("error"); return modelAndView; } }
基于Flash属性的跨重定向请求数据传递
重定向时请求参数会丢失,我们往往需要重新携带请求参数,我们可以进⾏⼿动参数拼接如下:
return "redirect:handle01?name=" + name;
但是上述拼接参数的⽅法属于get请求,携带参数⻓度有限制,参数安全性也不⾼,此时,我们可以使⽤SpringMVC提供的flash属性机制,向上下⽂中添加flflash属性,框架会在session中记录该属性值,当跳转到⻚⾯之后框架会⾃动删除flash属性,不需要我们⼿动删除,通过这种⽅式进⾏重定向参数传递,参数⻓度和安全性都得到了保障,如下:
* SpringMVC 重定向时参数传递的问题
* 转发:A 找 B 借钱400,B没有钱但是悄悄的找到C借了400块钱给A
* url不会变,参数也不会丢失,⼀个请求
* 重定向:A 找 B 借钱400,B 说我没有钱,你找别⼈借去,那么A ⼜带着400块的借钱需求找到C
* url会变,参数会丢失需要重新携带参数,两个请求
*/
@RequestMapping("/handleRedirect")
public String handleRedirect(String name,RedirectAttributes redirectAttributes) {
//return "redirect:handle01?name=" + name; // 拼接参数安全性、参数⻓度都有局限
// addFlashAttribute⽅法设置了⼀个flash类型属性,该属性会被暂存到session中,在跳转到⻚⾯之后该属性销毁
redirectAttributes.addFlashAttribute("name",name);
return "redirect:handle01";
}