SpringMVC系列文章
文章目录
1. 响应数据和结果视图
对于控制器(Controller)中方法的返回值,可以为以下其中一种:
String
字符串类型void
无返回值ModelAndView
SpringMVC 为我们提供的一种可以包含数据和视图的对象- JavaBean 对象 (通过 json 数据响应)
1.1 返回值为 String
- 当控制器返回值为
String
类型的时候,默认情况是返回逻辑视图名,然后被视图解析器解析为物理视图地址,最终底层通过请求转发将请求转发到对应页面。见代码 - jsp代码
<a href="${pageContext.request.contextPath}/user/testString">testString</a><hr/>
- 控制器代码
/**
* 返回值为 String ,结合 model 对象保存数据
* @param model
* @return
*/
@RequestMapping("/testString")
public String testString(Model model) {
System.out.println("testString方法执行了...");
// 模拟从数据库查数据
User user = new User();
user.setUsername("鱼开饭");
user.setPassword("123456");
user.setAge(22);
// 添加到request域
model.addAttribute("user", user);
// 逻辑视图名 success 会被视图解析器解析为 /WEB-INF/pages/success.jsp
// 可以在 springmvc 配置文件中修改视图解析器的解析
return "success";
}
1.2 返回值为 void
- 当使用
void
作为控制器方法的返回值时,方法执行结束后默认转发的路径为当前方法绑定的路径,也就是@RequestMapping
中的值。但是实际上这个返回值也会被视图解析器解析,所以一般都会报 404 错误。譬如,此时有一个控制器方法为:
@RequestMapping("/testVoid")
public void testVoid() {
// ...
}
- 当方法执行结束后,默认会返回
/testVoid
,经过视图解析器解析后则为/WEB-INF/pages/testVoid.jsp
,如图
- 如果既想使用
void
返回值,同时又想对请求做出响应,譬如请求转发或者重定向,那么我们就可以利用 Servlet 提供的原生 API 作为控制器方法的参数 - 请求转发
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
System.out.println("testVoid方法执行了...");
// 转发是一次请求,无需加项目虚拟目录,但是因为直接通过原生api进行转发,不会经过视图解析器,所以应该写具体路径
request.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(request, response);
// 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
return;
}
- 重定向
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
System.out.println("testVoid方法执行了...");
// 重定向,需要项目虚拟目录
response.sendRedirect(request.getContextPath() + "/index.jsp");
// 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
return;
}
- 直接像客户端生成响应,譬如响应 json 数据
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
System.out.println("testVoid方法执行了...");
// 直接响应,先设置编码
response.setContentType("text/html;charset=utf-8");
response.getWriter().println("假设这是 Json 数据");
// 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
return;
}
当方法的参数中含有
HttpServletResponse
时,即使方法体中不做任何处理,方法结束后也不会跳转到别的页面。关于这点,查阅官方文档,是因为此时 SpringMVC 会认为对请求已经作出了响应,所以不会再帮我们跳转到别的页面。
1.3 返回值为 ModelAndView
- 当控制器方法返回值为
ModelAndView
的时候,其实就相当于第一种方式的返回值为String
加上参数为Model
,因为ModelAndView
也可以传入数据(在页面上直接使用 EL 表达式获取:${attributeName}
),同时还可以设置将要跳转的逻辑视图名称。见代码 - jsp 代码
<!-- response.jsp -->
<a href="${pageContext.request.contextPath}/user/testModelAndView">testModelAndView</a>
<!-- success.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>成功</title>
</head>
<body>
<h3>执行成功</h3>
${requestScope.user}
</body>
</html>
- 控制器方法
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
System.out.println("testModelAndView方法执行了...");
// 创建 ModelAndView 对象
ModelAndView mv = new ModelAndView();
// 模拟查询数据
User user = new User();
user.setUsername("用户1");
user.setPassword("654321");
user.setAge(10);
// 将 user 对象存入到 ModelAndView 对象中,最终存入 request 域
mv.addObject("user", user);
// 指定要跳转的逻辑视图名,会使用视图解析器
mv.setViewName("success");
return mv;
}
1.4 请求转发和重定向
- 当控制器方法的返回值为
String
的时候,默认就是请求转发,我们也可以像上面的例子一样,使用 Servlet 原生的 API 来完成请求转发或者重定向。不过,SpringMVC 还为我们提供了另一种方式,那就是使用forward:
和redirect:
关键字。见代码
@RequestMapping("/testForward")
public String testForward() {
System.out.println("testForward方法执行了...");
// forward: 关键字转发,不会使用视图解析器
return "forward:/WEB-INF/pages/success.jsp";
}
@RequestMapping("/testRedirect")
public String testRedirect() {
System.out.println("testRedirect方法执行了...");
// redirect: 关键字重定向,不用加项目路径
return "redirect:/index.jsp";
}
- 对于
forward:
关键字,根据官方文档,底层也是使用 Servlet 原生 API 进行转发,也就是RequestDispatcher.forward()
,所以不会经过视图解析器,因此转发路径需要具体路径。
- 对于
response:
关键字,根据官方文档,SpringMVC 会根据当前 Servlet 的上下文进行重定向,因此不需要写项目路径。
1.5 使用 @RequestBody 和 @ResponseBody 进行 json 交互
- 在开发中,很多时候都是客户端提交 json 数据,服务器接收 json 数据并解析,然后生成 json 响应给客户端。这个时候我们就可以使用
@RequestBody
和@ResponseBody
来完成 json 数据的交互。见代码 - jsp代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Json 数据交互</title>
<script src="js/jquery-1.8.2.min.js"></script>
<script type="text/javascript">
// 页面加载
$(function () {
// 绑定单击事件
$("#btn").click(function () {
// 发送ajax请求
$.ajax({
url: "${pageContext.request.contextPath}/user/testAjax",
// 发送数据的 MIME 类型
contentType: "application/json;charset=utf-8",
// 发送数据为一个 json 串
data: '{"username":"鱼开饭","password":"123456","age":22}',
// 返回的数据类型
dataType: "json",
// 请求方式
type: "post",
// 成功回调函数
success: function (data) {
console.log(data);
console.log(data.username);
console.log(data.password);
console.log(data.age);
}
});
});
});
</script>
</head>
<body>
<button id="btn">测试 ajax 请求 json 和响应 json</button>
</body>
</html>
因为我们配置了前端控制器
DispatcherServlet
,因此对于所有的请求都会进行拦截,包括静态资源。为了使引入的jquery.js
不被拦截,我们需要在配置文件springmvc.xml
中配置不拦截静态资源<!-- 设置静态资源不过滤 --> <mvc:resources location="/css/" mapping="/css/**"/> <!-- 样式 --> <mvc:resources location="/images/" mapping="/images/**"/> <!-- 图片 --> <mvc:resources location="/js/" mapping="/js/**"/> <!-- javascript -->
location
属性 : 指定webapp
目录下的包mapping
属性 : 表示以/static
开头的所有请求路径
- 控制器代码
/**
* responsebody 加在方法上或者加在返回值前面都可以
*
* @param user
*/
@RequestMapping("/testAjax")
public @ResponseBody User testAjax(@RequestBody User user) {
System.out.println("testAjax方法执行了...");
System.out.println(user);
// 模拟查询出新的用户
user.setPassword("654321");
user.setAge(10);
return user;
}
- 客户端发送了一个 json 串,服务器使用
@RequestBody
获取请求体内容,并使用 jackson 将 json 串封装到 User 对象中;服务器使用@ResponseBody
生成响应,通过 jackson 将 User 对象转成 json 串。- SpringMVC 默认使用
MappingJacksonHttpMessageConverter
进行转换,因此需要导入jackson
的依赖<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency>
- 使用 jackson 进行 json 和 JavaBean 对象的自动转换时,需要保证 json 串的键与 JavaBean 的属性名称一一对应
2. SpringMVC 文件上传
2.1 传统方式文件上传
- 在讲文件上传之前,我们需要了解以下几点:
- 用于提交文件的表单的
enctype
属性必须为multipart/form-data
(默认值为application/x-www-form-urlencoded
),该属性是指定表单请求正文的类型 - 表单的
method
属性必须为post
- 表单中需要提供一个文件选择域
<input type="file" />
- 用于提交文件的表单的
- 使用传统方式进行文件上传时,我们需要依赖
Commons-fileupload
jar 包,所以需要先导入依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
- 编写 jsp 代码
<h3>传统方式文件上传</h3>
<form action="${pageContext.request.contextPath}/fileupload/test1" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload"/><br/>
<input type="submit" value="上传文件">
</form>
- 编写控制器代码
@Controller
@RequestMapping("/fileupload")
public class FileUploadController {
/**
* 使用 fileupload jar包实现传统文件上传,上传的文件和访问的应用存在于同一台服务器上
*
* @param request
* @return
*/
@RequestMapping("/test1")
public String test1(HttpServletRequest request) throws Exception {
System.out.println("传统方式文件上传...");
// 获取要保存的真实目录路径
String path = request.getSession().getServletContext().getRealPath("/uploads");
// 判断文件夹是否存在,不存在则创建
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 创建磁盘文件工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
// 解析 request 对象,获取上传文件项
List<FileItem> fileItems = upload.parseRequest(request);
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
// 普通表单项,不做处理
} else {
// 上传文件项,获取文件名
String filename = fileItem.getName();
// 加时间戳防止重名
filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;
// 上传文件
fileItem.write(new File(path, filename));
// 删除临时文件
fileItem.delete();
}
}
return "success";
}
}
2.2 SpringMVC 方式文件上传
- 从上面的例子可以看出,如果使用传统方式进行文件上传,我们需要自己解析
request
,分辨每一项究竟是普通表单项还是文件项,特别繁琐。而如果使用 SpringMVC,我们只需要配置一个文件解析器,由文件解析器来替我们解析,然后我们在控制器使用MultipartFile
对象接收即可(该对象表示要上传的文件)。 - 控制器代码
/**
* SpringMVC 方式上传,同服务器
*
* @param request
* @param upload 参数名称必须与表单中 file 组件的名称保持一致
* @return
*/
@RequestMapping("/test2")
public String test2(HttpServletRequest request, MultipartFile upload) throws Exception {
System.out.println("SpringMVC方式文件上传...");
// 获取要保存的真实目录路径
String path = request.getSession().getServletContext().getRealPath("/uploads");
// 判断文件夹是否存在,不存在则创建
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 获取文件原始名
String filename = upload.getOriginalFilename();
// 加时间戳
filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;
// 上传文件
upload.transferTo(new File(path, filename));
return "success";
}
- 使用该方式上传文件时,要确保
MultipartFile
对象名称与表单中file
组件的名称保持一致。如果不一致,则需要使用@RequestParam
进行参数的绑定。譬如表单为:<input type="file" name="file"/>
,则控制器方法应该为test2(HttpServletRequest request, @RequestParam("file") MultipartFile upload)
- 获取上传文件的文件名时,应该使用
getOriginalFilename()
,不能使用getName()
,getName()
是获取表单中file
组件的名称。- 如果要想实现多文件上传,那么需要在
file
组件加上属性multiple="multiple"
(HTML5 的新属性),然后控制器用List<MultipartFile>
接收即可。
- 在 SpringMVC 配置文件中配置文件解析器
<!-- 配置文件解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 配置上传文件最大大小,字节(Byte)为单位。这里设置 10MB=10*1024KB=10*1024*1024B -->
<property name="maxUploadSize" value="10485760"/>
<!-- 设置编码,防止中文乱码 -->
<property name="defaultEncoding" value="utf-8"/>
</bean>
- 文件解析器的
id
必须为multipartResolver
,maxUploadSize
属性以Byte
为单位。
2.3 SpringMVC 跨服务器方式文件上传
- 在实际开发中,我们可能会有好几台服务器,有的服务器用于部署应用,而有的服务器用于存储用户上传的文件。那么,此时我们如果需要跨服务器上传文件,就需要借助
jersey-client
的 jar 包。
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.18.1</version>
</dependency>
- 控制器代码
/**
* SpringMVC 跨服务器方式的文件上传
*
* @param upload
* @return
*/
@RequestMapping("/test3")
public String test3(MultipartFile upload) throws IOException {
System.out.println("SpringMVC 跨服务器文件上传...");
// 定义文件服务器存储路径,注意这里末尾的 /
String path = "http://localhost:8888/uploadserver/uploads/";
// 获取上传文件名
String filename = upload.getOriginalFilename();
filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;
// 创建客户端对象
Client client = Client.create();
// 和文件服务器进行连接,指定上传文件的地址,该地址是 web 路径
WebResource resource = client.resource(path + filename);
// 上传文件,使用字节数组传输
resource.put(upload.getBytes());
return "success";
}
- 想要测试跨服务器文件上传,需要部署两个 Tomcat 服务器,一个服务器部署应用,一个服务器用于接收文件。这里将存放文件的路径指定为接收文件项目的根目录下的
uploads
文件夹。- 如果程序报 405 错误,那么应该在文件服务器的 Tomcat 的配置文件
conf\web.xml
中加入以下配置,表示允许服务器进行读写操作(默认只读)
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>readonly</param-name> <param-value>false</param-value> </init-param> </servlet>
- 如果程序报 409 错误,那么应该在文件服务器的
target
目录下创建uploads
文件夹
3. SpringMVC 中的异常处理
- 在开发中,一般我们都是
Controller
调用Service
,Service
调用Dao
,在这个过程中,如果有某个地方出现了异常,那么异常都会被往上一层一层throws Exception
(抛出)。Dao
抛给Service
,Service
抛给DispatcherServlet
,最后由DispatcherServlet
将异常交给HandlerExceptionResolver
(异常处理器)进行处理。
3.1 操作步骤
- 编写自定义异常类,记得继承
Exception
类
public class SysException extends Exception {
private String message;
public SysException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
- 编写错误页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>错误页面</title>
</head>
<body>
${errorMsg}
</body>
</html>
- 编写自定义异常处理器,需要实现
HandlerExceptionResolver
接口
public class SysExceptionResolver implements HandlerExceptionResolver {
/**
* 异常处理
*
* @param httpServletRequest
* @param httpServletResponse
* @param handler 当前异常处理器对象
* @param e 捕获的异常对象
* @return
*/
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, Exception e) {
// 打印异常信息
e.printStackTrace();
SysException sysException;
// 如果抛出的是自定义异常,那么就直接转换
if (e instanceof SysException) {
sysException = (SysException) e;
} else {
// 如果抛出的不是自定义异常,那么就创建一个新的自定义异常
sysException = new SysException("系统正在维护...");
}
// 创建 ModelAndView 对象
ModelAndView mv = new ModelAndView();
mv.addObject("errorMsg",sysException.getMessage());
mv.setViewName("error");
return mv;
}
}
- 在 SpringMVC 配置文件中配置异常处理器
<!-- 配置异常处理器 -->
<bean id="sysExceptionResolver" class="cn.ykf.exception.SysExceptionResolver"></bean>
- 测试代码
@Controller
@RequestMapping("/exception")
public class ExceptionController {
@RequestMapping("/testException")
public String testException() throws SysException {
System.out.println("testException方法执行了...");
try {
int i = 1 / 0;
} catch (Exception e) {
// 抛出自定义异常,这里必须在方法声明上 throws
throw new SysException("抛出了一个自定义异常...");
}
return "success";
}
4. SpringMVC 中的拦截器
4.1 拦截器的说明
- SpringMVC 中的
Interceptor
(拦截器) 类似于 Servlet 中的Filter
(过滤器),拦截器是用于对Controller
(处理器)进行预处理和后处理的。和过滤器类似,过滤器可以定义过滤链,拦截器也可以定义拦截器链(Interceptor Chain),也就是将拦截器按一定的顺序联结成一条链,在访问被拦截的方法之前,拦截器链中的拦截器就会按之前定义的顺序进行拦截。 - 过滤器和拦截器虽然比较类似,但是还是有区别的:
- 过滤器是 Servlet 规范中的,也就是说,只要是 JavaWeb 工程就可以使用;而拦截器是 SpringMVC 框架的,只有使用了 SpringMVC 框架的工程才可以使用
- 过滤器可以拦截所有的资源,也就是不仅可以拦截
Controller
,其它的所有资源(像 jsp、js 这些静态资源)它都可以拦截;而拦截器是针对Controller
的,它只能拦截控制器。 - 拦截器是 AOP 思想的一种实现方式
- 如果想自定义拦截器,那么就需要实现
HandlerInterceptor
接口,该接口中有 3 个default
方法:boolean preHandle(..)
: 该方法在Controller
方法执行前调用。如果返回true
就代表放行,执行下一个拦截器,如果没有拦截器,那就执行Controller
的方法;如果返回false
就代表不放行,此时可以使用转发或者重定向跳转到指定的页面。该方法是按拦截器定义顺序进行调用的。void postHandle(..)
: 该方法在Controller
方法执行后(但是在视图渲染之前)调用。此时我们可以通过ModelAndView
(模型和视图对象)来对模型数据或视图进行处理,也可以通过HttpServletRequest
和HttpServletResponse
进行转发或者重定向。该方法是按拦截器定义逆序进行调用的。void afterCompletion(..)
: 该方法在 整个请求处理完毕后调用(也就是在视图渲染完毕后)。 可以在该方法中进行一些资源清理的操作(不能在该方法中进行页面的跳转了),相当于try...catch...finally
中的finally
。该方法是按拦截器定义逆序进行调用的,而且只有preHandle()
返回true
才调用。
4.2 操作步骤
- 创建自定义拦截器,实现
Interceptor
接口并重写其中的方法
public class MyInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("第 1 个拦截器中的preHandle方法执行了...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("第 1 个拦截器中的postHandle方法执行了...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("第 1 个拦截器中的afterCompletion方法执行了...");
}
}
- 在 SpringMVC 配置文件中配置拦截器
<!-- 配置拦截器链 -->
<mvc:interceptors>
<!-- 配置第一个拦截器 -->
<mvc:interceptor>
<!-- 要拦截的路径 -->
<mvc:mapping path="/user/*"/>
<!-- 不拦截的路径 -->
<!--<mvc:exclude-mapping path=""/>-->
<!-- 注册拦截器对象 -->
<bean class="cn.ykf.interceptor.MyInterceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
<mvc:mapping>
标签用于指定要拦截的路径,而<mvc:exclude-mapping>
标签用于指定不拦截的路径,两者选其一即可。
- 编写控制器代码
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 测试拦截器
* @return
*/
@RequestMapping("/testInterceptor")
public String testInterceptor() {
System.out.println("testInterceptor方法执行了...");
return "success";
}
}
- 编写jsp代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>成功</title>
</head>
<body>
<h3>执行成功</h3>
<%
System.out.println("success.jsp 执行了");
%>
</body>
</html>
- 运行结果
4.3 特殊情况
- 如果有多个拦截器,此时拦截器 1 的
preHandle
方法返回true
,但是拦截器 2 的preHandle
方法返回false
,那么拦截器 1 的afterCompletion
的方法是否执行?- 因为我们上面说过,
afterCompletion
方法只有当preHandle
返回true
的时候执行。所以,拦截器 1 的afterCompletion
方法会执行,而拦截器 2 的afterCompletion
方法不会执行。
- 因为我们上面说过,
- 如果某个拦截器在
postHandle
方法中使用request
和response
进行转发,那么最终的结果视图(这里是success.jsp
)还会不会执行?- 我们上面说过,
postHandle
方法是在控制器执行后,视图渲染前被调用。所以,当我们转发到某个页面时,浏览器会把这个页面展示出来,但是postHandle
方法还是会继续执行,也就是说success.jsp
还是会被执行到,只不过不会再被渲染展示了。
- 我们上面说过,
- 如果某个拦截器在
postHandle
方法中不通过request
进行转发,而是使用ModelAndView.setViewName("error")
,那么最终的结果视图(这里是success.jsp
)还会不会执行?- 因为
ModelAndView
中存储的是结果视图,也就是控制器方法的返回值success
,此时我们直接调用setViewName()
方法,就相当于修改了控制器方法的返回值,所以最终只会跳转到error.jsp
而不是success.jsp
- 因为