略
Spring MVC 起步
执行流程
Spring MVC执行流程大概可以分为7步:
- 请求离开浏览器,发送到服务器
- DispatcherServlet(前端控制器)收到请求,查询一个或多个处理器映射(handler mapping)来确定请求的下一站,处理器映射通过URL信息来决策.
- DispatcherServlet卸下用户提交的信息并等待控制器(handler)处理这些信息.
- 控制器处理好信息,将数据打包,并且指定渲染输出的视图名,最后把这些信息发送给DispatcherServlet.
- DispatcherServlet通过得到的视图名使用视图解析器(view resolver)来将逻辑视图名匹配为一个特定的视图实现.
- DispatcherServlet已经知道使用哪个视图解析了,然后他会交付模型数据(4中打包的数据).视图将这些数据渲染输出.
- 视图输入会通过响应对象传递给客户端.
搭建Spring MVC
一个简单的Controller
1 2 3 4 5 6 7
| @Controller public class UserController { @RequestMapping(value = "/",method = RequestMethod.GET) public String home() { return "home"; } }
|
访问http://localhost:8080/HelloWorld/home/
即可进入该方法,然后跳转到逻辑名为home的视图中.
以上基础步骤写的很简略,很多都没写上去,配置Spring MVC本文不做详解,主要写Spring MVC的用法.
控制器的使用
测试控制器
一般的测试方法对于存在用户请求的Spring MVC并不管用,需要模拟客户端发出请求才可以.
1 2 3 4 5 6 7 8 9 10 11 12
| public class UserControllerTest { @Test public void demo1() throws Exception { UserController userController = new UserController(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); mockMvc.perform(MockMvcRequestBuilders .get("/")) .andExpect(MockMvcResultMatchers .view() .name("home")); } }
|
定义类级别的请求处理
类的前面加上@RequestMapping(value = "/home")
,此时类下面的方法都是以/home
作为根路径,其中的value是数组类型,所以可以设置多个路径映射到本控制器上.
传递模型数据到视图
1 2 3 4 5
| @RequestMapping(value = "/",method = RequestMethod.GET) public String home(Model model) { model.addAttribute(new User()); return "home"; }
|
方法返回值为String,此时视图的逻辑名就是该方法的返回值,同时它的参数列表里面有个Model类型的参数,向这个model里面添加的数据就会被打包发送给视图home
;
model就类似一个map,所以你可以用Map来代替Model,然后通过put方法把数据放里面去.Spring MVC同样可以识别出来,并把数据发送到视图里渲染.
接受请求的输入
这是本文的一个重点,参数太多了,各种类型,各种注解,各种作用,贼烦.
Spring MVC允许多种方式,将客户端的数据送到控制器的处理器方法中.
- 查询参数(Query Parameter)
- 表单参数(Form Parameter)
- 路径参数(Path Variable)
处理查询参数
1 2 3 4 5 6
| @RequestMapping(value="home",method = RequestMethod.GET) public List<User> getUsers( @RequestParam("max") long max, @RequestParam("count") int count){ return userService.listAll(max,count); }
|
传递最大页数和数量,@RequestParam("max")
和@RequestParam("count")
表示:路径为/home?max=100&count=100
的请求的参数可以用个这个方式获取到并作为传递到方法中.
返回类型为一个集合,后面讲.
同样可以通过@RequestParam(value="max",defaultValue="1")
来设置默认值,它还有很多其他参数,这里不一一讲解.
通过路径参数接收数据
1 2 3 4 5 6 7 8
| @RequestMapping(value = "/mvc3/{name}", method = RequestMethod.GET) public String home3(@PathVariable(value = "name") String name, Model model) { if (name != null) { User user = userService.getUser(name); model.addAttribute(user); } return "home3"; }
|
@PathVariable(value = "name")
和@RequestMapping(value = "/mvc3/{name}", method = RequestMethod.GET)
配合使用,当路径为/mvc3/canyuda时,name可以获得参数canyuda.该方式是实现Restful风格的基础.
表单处理
重点之二,主要理解转发和重定向的使用.
简单表单处理控制类
1 2 3 4 5
| @RequestMapping(value = "/mvc6", method = RequestMethod.POST) public String mvc6(User user) { System.out.println(user); return "redirect:/hello/mvc"; }
|
通过POST传递的数据会自动的装载到User类型的参数中传给方法,要注意:前端表单中的name属性,需要与User实体类字段名一致;
看返回值会发现多了个redirect:
,这个就表示使用重定向到/hello/mvc
,如果需要参数传递可以用"redirect:/hello/mvc/"+user.getId();
校验表单
使用Validation API处理,判断传入的表单中各个不同数据的类型的最大值,最小值,长度,日期和当前对比,是否为空,是否符合正则等等等.
具体略
渲染Web视图
现在数据已经打包到Model中了,然后通过视图逻辑名传递给视图,这时就需要使用视图解析器来装配传来的数据,生成用户想要的视图,再发送给响应体中.
Spring自带了13个视图解析器,下面举几个我认为有用可以去看看的:
- FreeMarkerViewResolver 解析为FreeMarker模板
- InternalResourceViewResolver 解析为Web应用内部资源(一般为JSP)
- TilesViewResolver 书上讲了感觉不好用
- VelocityViewResolver 将视图解析为Velocity模板(强哥项目用过,感觉好屌)
- ThymeleafViewResolver(这个不是Spring自带的,但是Spring推荐使用)
举个例子
InternalResourceViewResolver的例子
1 2 3 4 5 6 7 8
| <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>
|
1 2 3 4 5
| @RequestMapping(value = "/",method = RequestMethod.GET) public String home(Model model) { model.addAttribute(new User()); return "home"; }
|
返回的视图逻辑名为home,根据视图解析器渲染,就会根据/WEB-INF/jsp/home.jsp
作为模板进行渲染视图.
标签库
1 2 3
| <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="sf" uri="http://www.springframework.org/tags/form" %> <%@taglib prefix="s" uri="http://www.springframework.org/tags" %>
|
不重要,略了略了.会有专门的文章介绍Thymeleaf模板的使用,毕竟Spring推荐使用.
文件上传
表单上传文本数值内容很容易,但是平常会有些图片,压缩包,等非文本数据需要上传到服务器中.这里需要对表单进行设置enctype
属性,参数为multipart/form-data
.
这时,表单上传的数据格式就会发生改变,multipart格式的数据会被一个表单拆分为多个部分(part),每个对应一个输入域,一般表单输入域中,它对应的部分会被放入文本类型数据,如果上传文件的话,它就会把文件转换为一个二进制数据,上传就可以进行了.
配置multipart解析器
Spring给了我们两个解析器:
- CommonsMultipartResolver Servlet容器在3.0版本一下使用
- StandardServletMultipartResolver Servlet3.0以后使用
Tomcat7.X是Servlet3.0 , Tomcat6.X是Servlet2.5; 所以使用StandardServletMultipartResolver更加符合主流的发展.
使用Servlet3.0解析multpart请求
StandardServletMultipartResolver需要在Spring.xml中进行配置,加入到Spring容器中管理.
1 2
| <bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver"/>
|
注意:
- 上传文件的最大容量(字节)默认无限制;
- 每个multipart请求的最大容量(字节),不会关心有几个part以及每个part的大小,默认没有限制;
- 上传过程中,如果文件达到最大容量,就会写入到临时文件路径中.默认值为0,也就是说,所有上传的文件都会写入磁盘;
最大容量是可以配置的,需要配置到DispatcherServlet的配置(web.xml)中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <multipart-config> <location>/tmp/uploads</location> <max-file-size>2097152</max-file-size> <max-request-size>4194304</max-request-size> <file-size-threshold>1024</file-size-threshold> </multipart-config> </servlet>
|
Jakarta Commons FileUpload multipart解析器
CommonsMultipartResolver 老牌解析器
需要其他包支持:
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>
|
Spirng管理
1 2 3 4 5 6 7 8
| <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize"> <value>5242880</value> </property> </bean>
|
处理multipart请求
上面讲了,首先表单的提交类型需要为enctype="multipart/form-data"
.
然后,有三种方式接受文件.
- 数组接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @RequestMapping(value = "/mvc4", method = RequestMethod.POST) public String processRegistration(HttpServletRequest request, @RequestPart("myFile") byte[] myFile, @Valid User user, Errors errors) throws IOException {
String realPath = request.getSession().getServletContext().getRealPath("/uploads");
System.out.println("目的路径为:" + realPath);
File file = new File(realPath); if (!file.exists()||!file.isDirectory()){ file.mkdirs(); } FileOutputStream fileOutputStream = new FileOutputStream(new File(file,"canyuda.png"));
fileOutputStream.write(myFile, 0, myFile.length);
System.out.println("保存了文件" + myFile.length);
if (errors.hasErrors()) { return "error"; } userService.save(user); return "redirect:/hello/mvc3/" + user.getUsername(); }
|
- MultipartFile接口接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @RequestMapping(value = "/mvc4", method = RequestMethod.POST) public String processRegistration(HttpServletRequest request, @RequestPart("myFile") MultipartFile myFile, @Valid User user, Errors errors) throws IOException {
String filename = myFile.getOriginalFilename(); System.out.println(filename);
String realPath = request.getSession().getServletContext().getRealPath("/uploads");
System.out.println("目的路径为:" + realPath);
File file = new File(realPath); if (!file.exists() || !file.isDirectory()) { file.mkdirs(); } FileOutputStream fileOutputStream = new FileOutputStream(new File(file, filename)); System.out.println("文件类型:" + myFile.getContentType()); System.out.println(myFile.getName()); FileCopyUtils.copy(myFile.getInputStream(), fileOutputStream);
if (errors.hasErrors()) { return "error"; } userService.save(user); return "redirect:/hello/mvc"; }
|
- Part形式接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @RequestMapping(value = "/mvc4", method = RequestMethod.POST) public String processRegistration(HttpServletRequest request, @RequestPart("myFile") Part myFile, @Valid User user, Errors errors) throws IOException {
String filename = myFile.getName(); System.out.println("Part的方式:" + filename);
String realPath = request.getSession().getServletContext().getRealPath("/uploads");
System.out.println("目的路径为:" + realPath);
File file = new File(realPath); if (!file.exists() || !file.isDirectory()) { file.mkdirs(); } FileOutputStream fileOutputStream = new FileOutputStream(new File(file, filename)); System.out.println("文件类型:" + myFile.getContentType()); System.out.println(myFile.getName()); FileCopyUtils.copy(myFile.getInputStream(), fileOutputStream);
if (errors.hasErrors()) { return "error"; } userService.save(user); return "redirect:/hello/mvc"; }
|
Part形式和MultipartFile接口形式差不多,一些方法是对应的,例如getSUbmittedFileName()对应于getOriginalFilename()方法,write()方法对应于transferTo()方法,重要的区别是:Part形式不需要配置MultipartResolver.
既然Part形式不需要配置MultipartResolver,那就用这个吧.
异常处理
Spring提供了多种方式吧异常转换为响应:
- 特定的Spring异常会自动转;
- 异常上加
@ResponseStatus
注解,可以将其映射为某一个HTTP状态码;
- 在方法上加
@ExceptionHandler
注解,使其用来处理异常.
将异常映射为HTTP状态码
Spirng的一些异常会默认映射HTTP状态码看到400,500,406,415,404就要注意了,查看是否是Spirng自动为你映射的.
@ResponseStatus
注解修饰异常类,当抛出该异常的时候,Spring就会让跳转到该异常映射的状态码页面.
首先写一个异常类
然后给它加注解
1 2 3 4
| @ResponseStatus(value = HttpStatus.NOT_FOUND,reason = "自定义异常") public class MyException extends RuntimeException {
}
|
代码中抛出
1 2 3 4 5 6 7
| @RequestMapping("/mvc") public String helloMVC() { if (1 == 1) { throw new MyException(); } return "home"; }
|
访问~/mvc
时就会抛出404异常
处理异常
异常被抛出了,就应该有方法可以处理,让其跳转到用户友好的页面,这时就需要异常处理的方法,但是使用catch来捕获异常会让代码变的非常不整洁,Spring利用AOP(我猜的)提供了异常统一处理的解决方案.
先来看看不用这个方案的代码:
1 2 3 4 5 6 7 8 9 10
| @RequestMapping("/mvc") public String helloMVC() { try{ method(); return "home"; } catch (MyException e){ return "error" } }
|
再看看使用后:
1 2 3 4 5 6
| @RequestMapping("/mvc") public String helloMVC() { method(); return "home"; }
|
需要加入@ExceptionHandler
注解标注的方法
1 2 3 4 5
| @ExceptionHandler(value = MyException.class) public String exception(){ System.out.println("处理异常中...."); return "error"; }
|
该注解的value属性表示,它所在的Controller如果抛出MyException异常的话,就会执行该修饰的方法.
为控制器添加通知
控制器通知就是带有@ControllerAdivce
注解的类,这个类有如下类型的方法:
@ExceptionHandler
标注的方法;
@InitBinder
标注的方法;
ModelAttribute
标注的方法;
@ControllerAdivce
注解本身已经使用了@Controller
,它是可以被Spring扫描到的.
它的最实用的场景就是,它把所有的@ExceptionHandler
方法收集到一个类中去管理,也就是说,该项目的站点所有异常都可以被这里面的方法捕获,然后可以指定它跳转的友好页面或者在方法中对异常进行处理.
1 2 3 4 5 6 7 8
| @ControllerAdvice public class AllExceptionHandler { @ExceptionHandler(value = MyException.class) public String exception1(){ System.out.println("处理异常中...."); return "error"; } }
|
跨重定向请求传递数据
重定向:两次请求:如何传递数据?
使用URL模板以路径变量或查询参数来传递
1 2 3 4 5 6 7 8 9
| //重定向到~/hello/mvc5/11 return "redirect:/hello/mvc5/"+user.getID;
//重定向到~/hello/mvc6/canyuda?password=123 model.addAtrtribute("password","123"); model.addAtrtribute("username","canyuda"); return "redirect:/hello/mvc6/{username}"
|
使用flash属性
使用URL模板以路径变量或查询参数来传递有个弊端,就是无法直接传递对象过去
使用flash属性就可以避免了,它底层分如下步骤:
- 原始请求
- 添加对象到flash属性中
- 对象转移到会话(session)中存储
- 重定向到另外个处理器
- 会话中的对象转移到模型中
所以,flash属性可以传递对象
1 2 3 4 5 6 7
| @RequestMapping(value = "/mvc5", method = RequestMethod.GET) public String mvc5(User user, RedirectAttributes model) { service.save(user); model.addAttribute("username",user.getName()); model.addFlashAttribute("user",user); return "redirect:/hello/{username}"; }
|
1 2 3 4 5 6 7 8 9
| @RequestMapping(value = "/{username}", method = RequestMethod.GET) public String mvc5(@PathVariable String username, Model model) { if (!model.containsAttribute("user")){ model.addAttribute(service.find(username)); } return "home2"; }
|
额外
转换器
解决前端传来都是String类型数据,然而实体类中存在Date类型参数,无法自动装配属性到User中
1 2 3 4 5 6 7 8 9 10 11 12
| import org.springframework.core.convert.converter.Converter;
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date;
@RequestMapping(value = "/insert", method = RequestMethod.POST) public String insert(User user, Model model) { System.out.println(user); model.addAttribute("user", user); return "user"; }
|
User中birthday为Date类型,无法做到从表单中直接获取数据并装配,这里用到了转换器.
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class StringToDateConverter implements Converter<String, Date>{ @Override public Date convert(String source) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date parse = null; try { parse = sdf.parse(source); } catch (ParseException e) { e.printStackTrace(); } return parse; } }
|
注意看Converter接口属于哪个包,写错过
该转换器表示,如果前端传来了String类型数据,然而实体类中属性是Date类型的时候,它就会生效.自动为您转换为Date类型,但是前提是必须要配置好FormattingConversionServiceFactoryBean
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
<mvc:annotation-driven conversion-service="conversionService"/> <bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService"> <property name="converters"> <set> <bean class="com.yuda.converter.StringToDateConverter"/> </set> </property> </bean>
|
<mvc:annotation-driven />其实已经配置了字符串转日期的功能,只需要添加注解而已,这里只是举了一个例子,转换器它还可处理Spring无法转换的其他类型,比如传入一个命令,然后自动转换成一个想要的对象放到实体类属性中,转换器给了我一些遐想(瞎想).
拦截器
啥也不说先上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.yuda.intercepter;
import org.springframework.http.HttpMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
public class TestInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(request.getMethod()); return true; }
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("TestInterceptor.postHandle"); }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("TestInterceptor.afterCompletion"); } }
|
通过preHandle()方法的返回值(true/false)可以决定是否拦截;
对于其他方法可以总结如下:
- 顺序
- preHandle按拦截器定义顺序调用
- postHandler按拦截器定义逆序调用
- afterCompletion按拦截器定义逆序调用
- 是否拦截
- postHandler在拦截器链内所有拦截器返成功调用
- afterCompletion只有preHandle返回true才调用
拦截器中的方法很是全面,可以自定义拦截规则,并且重要一点拦截器作用在前端控制器到(DispatcherServlet)到控制器(Handler)之间