Spring MVC(基础)

Spring MVC 起步

执行流程

Spring MVC执行流程大概可以分为7步:

  1. 请求离开浏览器,发送到服务器
  2. DispatcherServlet(前端控制器)收到请求,查询一个或多个处理器映射(handler mapping)来确定请求的下一站,处理器映射通过URL信息来决策.
  3. DispatcherServlet卸下用户提交的信息并等待控制器(handler)处理这些信息.
  4. 控制器处理好信息,将数据打包,并且指定渲染输出的视图名,最后把这些信息发送给DispatcherServlet.
  5. DispatcherServlet通过得到的视图名使用视图解析器(view resolver)来将逻辑视图名匹配为一个特定的视图实现.
  6. DispatcherServlet已经知道使用哪个视图解析了,然后他会交付模型数据(4中打包的数据).视图将这些数据渲染输出.
  7. 视图输入会通过响应对象传递给客户端.

搭建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());//假装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个视图解析器,下面举几个我认为有用可以去看看的:

  1. FreeMarkerViewResolver 解析为FreeMarker模板
  2. InternalResourceViewResolver 解析为Web应用内部资源(一般为JSP)
  3. TilesViewResolver 书上讲了感觉不好用
  4. VelocityViewResolver 将视图解析为Velocity模板(强哥项目用过,感觉好屌)
  5. ThymeleafViewResolver(这个不是Spring自带的,但是Spring推荐使用)

举个例子

InternalResourceViewResolver的例子

1
2
3
4
5
6
7
8
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--解析jstl-->
<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());//假装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给了我们两个解析器:

  1. CommonsMultipartResolver Servlet容器在3.0版本一下使用
  2. 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">
<!-- 设置上传文件的最大尺寸为5MB -->
<property name="maxUploadSize">
<value>5242880</value>
</property>
<!-- 还有很多配置,可以配缓存文件夹 -->
</bean>

处理multipart请求

上面讲了,首先表单的提交类型需要为enctype="multipart/form-data".

然后,有三种方式接受文件.

  1. 数组接收
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();
}
  1. 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";
}
  1. 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. 然后给它加注解

    1
    2
    3
    4
    @ResponseStatus(value = HttpStatus.NOT_FOUND,reason = "自定义异常")
    public class MyException extends RuntimeException {

    }
  3. 代码中抛出

    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注解的,这个类有如下类型的方法:

  1. @ExceptionHandler标注的方法;
  2. @InitBinder标注的方法;
  3. 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属性就可以避免了,它底层分如下步骤:

  1. 原始请求
  2. 添加对象到flash属性中
  3. 对象转移到会话(session)中存储
  4. 重定向到另外个处理器
  5. 会话中的对象转移到模型中

所以,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中没有user对象
//查询并加入到model中
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) { //User中birthday为Date类型
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
<!--开启注解模式-->
<!--1.自动注册DefaultAnnotationHandlerMapping和AnnotationHandlerAdapter-->
<!--2.提供一系列:数据绑定,数字和日期类format,
@NumberFormat, ,xml,json,默认读写支持-->
<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)可以决定是否拦截;

对于其他方法可以总结如下:

  1. 顺序
    • preHandle按拦截器定义顺序调用
    • postHandler按拦截器定义逆序调用
    • afterCompletion按拦截器定义逆序调用
  2. 是否拦截
    • postHandler在拦截器链内所有拦截器返成功调用
    • afterCompletion只有preHandle返回true才调用

拦截器中的方法很是全面,可以自定义拦截规则,并且重要一点拦截器作用在前端控制器到(DispatcherServlet)到控制器(Handler)之间