Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架(SpringMVC是Spring的一个子模块),本质上底层是封装了 Servlet,并且和 Spring 框架无缝结合。
MVC模式:分别是 Model、View 和 Controller 。
在 Spring MVC 框架中,Controller 替换 Servlet 来担负控制器的职责,用于接收请求,调用相应的 Model 进行处理,处理器完成业务处理后返回处理结果。Controller 调用相应的 View 并对处理结果进行视图渲染,最终客户端得到响应信息。
新建Maven的Web工程,并导入依赖。
org.springframework
spring-webmvc
5.2.22.RELEASE
注意:spring-webmvc包不是仅仅导入一个包,他还会关联导入spring的依赖
cn.springmvc.controller
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "hello mvc";
}
}
//解释:
//1: @Controller 配合包扫描创建当前controller的bean
//2: @RequestMapping 用于接收客户端请求,将请求映射到具体的方法上
//3: @ResponseBody 将方法返回值转为json返回给客户端,如果是字符串原样返回
在resources目录
中添加springmvc的配置文件:applicationContext-mvc.xml
在web.xml中配置springmvc的核心控制器DispatcherServlet
,用于读取springmvc的配置文件。
Archetype Created Web Application
springmvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:applicationContext-mvc.xml
1
springmvc
/*
① 将项目部署到tomcat并启动
② 浏览器输入 http://localhost:8080/hello
③ 页面显示: hello mvc
,表示成功
导入servlet的依赖
javax.servlet
javax.servlet-api
3.1.0
provided
URL:http://localhost:8080/test1?name=tom&age=30
在controller中编写方法,参数中传入HttpServletRequest 用于获取参数。
@RequestMapping("/test1")
@ResponseBody
public String test1(HttpServletRequest request){
String name = request.getParameter("name");
String age = request.getParameter("age");
System.out.println("name:"+name+",age:"+age);
return "ok";
}
URL:http://localhost:8080/test2?name=tom&age=30
ontroller中编写方法:
@RequestMapping("/test2")
@ResponseBody
public String test2(String name,Integer age){
System.out.println("name:"+name+",age:"+age);
return "ok";
}
特殊情况,如果请求中的参数名和控制器方法中的参数名不一致,需要使用@RequestParam
注解来解决。
案例如下:
// http://localhost:8080/test2?myname=tom&age=30
@RequestMapping("/test2")
@ResponseBody
public String test2(@RequestParam("myname") String name, Integer age){
System.out.println("name:"+name+",age:"+age);
return "ok";
}
URL:http://localhost:8080/test3?name=tom&age=30
Controller中编写方法:
@RequestMapping("/test3")
@ResponseBody
public String test3(User user){
System.out.println(user);
return "ok";
}
编写一个满足Javabean规范的实体类
public class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
URL:http://localhost:8080/test4/1
使用 @PathVariable
来接收,代码如下:
@RequestMapping("/test4/{id}")
@ResponseBody
public String test4(@PathVariable("id") Integer id){
System.out.println("id:"+id);
return "ok";
}
如果客户端提交的是json数据,需要接收时需要使用 @RequestBody
修饰。
@RequestMapping("/test5")
@ResponseBody
public String test5(@RequestBody User user){
System.out.println("接收到:"+user);
return "ok";
}
在第三节中我们尝试接收了参数,但是测试中我们都传递的是英文和数字,如果传递中文,是有可能乱码的。
tomcat8及以上版本无需处理,由tomcat已经自行处理了。低版本tomcat需要在 server.xml中的Connector标签中添加属性:URIEncoding="UTF-8"
。
在springmvc中处理post乱码需要使用到servlet阶段所学习的过滤器Filter
的知识,因为Filter是在Servlet之前执行,所以咱们在Filter中进行编码的处理即可。
而springmvc中已经为我们提供了CharacterEncodingFilter
来处理编码,无需开发者进行编写代码,配置即可生效。
在web.xml中进入如下配置:
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
encodingFilter
/*
SpringMVC返回JSON数据有两种方式,但是springmvc返回json需要jackson
的支持。需要导入相关jar包:
com.fasterxml.jackson.core
jackson-core
2.9.8
com.fasterxml.jackson.core
jackson-databind
2.9.8
方式1:使用@ResponseBody
注解修饰方法
@Controller
public class HelloController {
@RequestMapping("/json1")
@ResponseBody
public User testJson1(){
User user = new User();
user.setName("张三");
user.setAge(30);
return user;
}
}
//方法返回对象时会直接将对象转为json并返回
//如果是字符串原样返回
方式2:使用@RestController
注解,该注解用于修饰类
@RestController
public class HelloController {
@RequestMapping("/json2")
public User testJson1(){
User user = new User();
user.setName("张三");
user.setAge(30);
return user;
}
}
//类被@RestController修饰后,该类中的所有方法默认都返回json
基于异步请求时,就可以使用该方式返回JSON数据,也是目前的主流方式。
跳转页面有两种方式转发和重定向,在servlet阶段我们就使用过,在springmvc中实现更加简单。
目前我们配置DispatcherServlet的拦截路径是/*,意为拦截所有请求交由DispatcherServlet来处理,但是假设我现在就仅仅是想访问web目录下的一个静态资源,比如:html,css,图片等,会发现也被拦截了,所以,我们如果想直接访问静态资源,还需要额外进行配置。
① 放行jsp资源,修改DispatcherServlet
的url-pattern
<servlet>
<servlet-name>springmvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:applicationContext-mvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>springmvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
② 放行html,css,js,图片等资源,需在springmvc的配置文件applicationContext-mvc.xml
中增加如下配置:
<mvc:default-servlet-handler/>
语法:forward:地址
@Controller
public class HelloController {
//转发:
//1.方法返回值为 String
//2.在地址前加:forward
@RequestMapping("/forward")
public String test1(){
return "forward:/hello.html";
}
}
//注意:return 中不写forward默认也是转发。
语法:redirect:地址
@Controller
public class HelloController {
//重定向:
//1.方法返回值为 String
//2.在地址前加:redirect
@RequestMapping("/redirect")
public String test2(){
return "redirect:/hello.html";
}
}
来看这样一段代码:
@Controller
public class HelloController {
@RequestMapping("/p1")
public String p1(){
return "/WEB-INF/views/page1.jsp";
}
@RequestMapping("/p2")
public String p2(){
return "/WEB-INF/views/page2.jsp";
}
}
会发现都是转发到views中的某个页面,但是,两个方法中的路径有大量重复的部分,前面开头都是/WEB-INF/views/
,后结尾都是.jsp
,那么重复部分能不能省略不写呢?答案是可以的,配置视图解析器 InternalResourceViewResolver
。
在springmvc的配置文件applicationContext-mvc.xml加入配置:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/">property>
<property name="suffix" value=".jsp">property>
bean>
这样代码就可以简化为:
@Controller
public class HelloController {
@RequestMapping("/p1")
public String p1(){
return "page1"; // /WEB-INF/views/page1.jsp
}
@RequestMapping("/p2")
public String p2(){
return "page2"; // /WEB-INF/views/page2.jsp
}
}
顺便说下ModelAndView
对象用于返回页面。
该对象是模型视图对象,可以用于封装模型数据 和 视图信息。
@Controller
public class HelloController {
@RequestMapping("/p3")
public ModelAndView p3(){
ModelAndView mv = new ModelAndView();
mv.setViewName("page3"); //设置视图
//mv.addObject(xx); //设置模型数据
return mv; // /WEB-INF/views/page3.jsp
}
}
@Controller
public class HelloController {
@RequestMapping("/t1")
public String t1(HttpServletRequest request){
//1.在request中绑定参数
request.setAttribute("name", "tom");
//2.转发到 /WEB-INF/views/page.jsp
return "page";
}
}
//页面中el取值:
${name}
//为什么转发到页面可以取值呢?
//因为转发使用的是同一个request对象。
org.springframework.ui.Model
该对象是由springmvc提供,其底层封装是request对象。
@Controller
public class HelloController {
@RequestMapping("/t2")
public String t2(Model model){
//1.在model中绑定参数
model.addAttribute("name", "tom");
//2.转发到 /WEB-INF/views/page.jsp
return "page";
}
}
//页面中el取值:
${name}
模型和视图对象:org.springframework.web.servlet.ModelAndView
@Controller
public class HelloController {
@RequestMapping("/t3")
public ModelAndView t3(){
//1.创建ModelAndView对象
ModelAndView mv = new ModelAndView();
mv.addObject("name","tom"); //绑定数据
mv.setViewName("page"); //设置视图信息
//2.返回ModelAndView对象
return mv;
}
}
//页面中el取值:
${name}
@Controller
public class HelloController {
@RequestMapping("/t4")
public String t4(HttpSession session){
//1.在session中绑定参数
session.setAttribute("name", "tom");
//2.转发到 /WEB-INF/views/page.jsp
return "page"; //这里转发或重定向都可以在页面取到值,因为参数是绑定到session作用域中
}
}
//页面中el取值:
${name}
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Documenttitle>
head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
文件:<input type="file" name="myFile"> <br/>
<input type="submit" value="提交">
form>
body>
html>
接收文件需要fileupload
依赖的支持
<dependency>
<groupId>commons-fileuploadgroupId>
<artifactId>commons-fileuploadartifactId>
<version>1.4version>
dependency>
@Controller
public class FileController {
// 方法参数类型是: MultipartFile,参数名和表单中的name属性值一致
@RequestMapping("/upload")
@ResponseBody
public String upload(MultipartFile myFile) throws IOException {
System.out.println("文件原始名称:"+myFile.getOriginalFilename());
System.out.println("文件类型:"+myFile.getContentType());
System.out.println("文件大小:"+myFile.getSize()+"(单位:字节)");
System.out.println("文件输入流:"+myFile.getInputStream());
System.out.println("文件是否为空:"+myFile.isEmpty());
System.out.println("表单文件控件的name值:"+myFile.getName());
return "ok";
}
}
在springmvc的配置文件applicationContext-mvc.xml
中添如下配置:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760" />
bean>
如果不配置上传解析器,会报如下错误:
org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: 由于没有提供multi-part配置,无法处理parts
方式1:使用MultipartFile提供的 transferTo()实现
@Controller
public class FileController {
// 方法参数类型是: MultipartFile,参数名和表单中的name属性值一致
@RequestMapping("/upload")
@ResponseBody
public String upload(MultipartFile myFile) throws IOException {
//使用MultipartFile提供的 transferTo()
//该代码意为:将文件保存到d盘下的file目录中
//如果file目录不存在,会被自动创建
myFile.transferTo(new File("d:/file/"+myFile.getOriginalFilename()));
}
}
方式2:使用流完成文件接收
@Controller
public class FileController {
// 方法参数类型是: MultipartFile,参数名和表单中的name属性值一致
@RequestMapping("/upload")
@ResponseBody
public String upload(MultipartFile myFile) throws IOException {
//使用流完成文件接收
InputStream in = myFile.getInputStream();
OutputStream out = new FileOutputStream("d:/"+myFile.getOriginalFilename());
//使用工具类进行进行复制
IOUtils.copy(in,out);
out.close();
in.close();
}
}
//说明:IOUtils需要commons-io的支持,依赖如下:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
运行原理如下图:
1、客户端请求到达DispatcherServlet,会被其统一拦截。
2、DispatcherServlet会根据请求的URL地址到HandlerMapping中寻找具体的处理器是哪个,找到处理器信息后返回给DispatcherServlet。
3、DispatcherServlet会根据拿到的处理器信息去执行具体的代码,就是调用controller等代码,执行完毕后返回ModelAndView信息。
4、DispatcherServlet根据拿到的模型数据和视图信息,使用ViewResolver视图解析器,解析出view视图信息。
5、DispatcherServlet再根据视图信息找到具体的视图,并将数据渲染到视图中,并返回给客户端。
之前我们在servlet阶段学习过过滤器Filter,在springmvc中也提供了类似的技术叫做:拦截器(interceptor),用于对处理器进行预处理和后处理。
① 编写自定义拦截器
package cn.springmvc.interceptor;
...
/**
* 自定义拦截,需要实现HandlerInterceptor接口,并重写相关方法
*/
public class MyInterceptor implements HandlerInterceptor {
/**
* 在controller方法执行之前被调用(需要掌握)
* return false:表示不放行,true 表示放行
* 在该方法中可以设置条件,满足就可以放行,不满足就拦截,权限相关的业务就可以在这里处理。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return false;
}
//在controller方法执行之后,视图渲染之前,执行该方法
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//在DispatcherServlet进行视图的渲染之后,返回页面之前执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
//执行流程:preHandle() -> postHandle() -> afterCompletion()
② 配置拦截器,在springmvc的配置文件applicationContext-mvc.xml
中增加如下配置:
<!-- 配置拦截器组 -->
<mvc:interceptors>
<!-- 拦截器 -->
<mvc:interceptor>
<!-- 要拦截的配置,该配置必须写在不拦截的上面,/*拦截一级请求,/**拦截多级请求 -->
解释:该配置表示放行/login
请求,其他请求全部拦截。
1.执行时机不一样,看上图
2.过滤器是servlet体系的,而拦截器是springmvc体系的。这就导致拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
3.过滤器和拦截器底层实现方式大不相同,过滤器是基于函数回调的,拦截器则是基于Java的反射机制(动态代理)实现的。
4.拦截器是springmvc中的组件,这就使得他可以注入容器中的bean,从而进行service的调用,而过滤器不可以。
这里介绍@ControllerAdvice
的处理方式。
实现方式为:
@ControllerAdvice
public class ControllerException {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public String ex1(Exception e){
//捕获到异常后的具体处理代码
return "error1:"+e.getMessage();
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String ex2(Exception e){
//捕获到异常后的具体处理代码
return "error2:"+e.getMessage();
}
//其他异常捕获 ... 可以很方便的进行异常捕获和捕获后的处理,以及返回值的定义
/*
@ExceptionHandler(异常名.class)
@ResponseBody
public 返回值 方法名(Exception e){
//捕获到异常后的具体处理代码
}
*/
}