目录
[九、SpringMVC 中的 AJAX 请求](#九、SpringMVC 中的 AJAX 请求)
[4、为什么不用手动接收 JSON 字符串、转换 JSON 字符串](#4、为什么不用手动接收 JSON 字符串、转换 JSON 字符串)
[5、preHandle 返回 false 的情况](#5、preHandle 返回 false 的情况)
[2、使用 xml 配置](#2、使用 xml 配置)
九、SpringMVC 中的 AJAX 请求
Ajax 请求的前端发送有很多种方式,可以使用 Vue,也可以使用 JQuery。
1、简单示例
下面是一个服务器获取请求头中的信息,并响应一个字符串的简单 Ajax 示例。
(1)index.html
使用 Ajax 请求传递两个请求参数,并接收服务器端的响应数据,格式为 text。
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" th:src="@{/static/js/JQuery-3.7.0.js}"></script>
<script type="text/javascript">
$(function() {
$("#ajaxButton").click(function() {
$.ajax({
url:"/Demo_Rest/testAjax",
data: { // 在请求头
"username": "admin",
"password": "123456",
},
type: "get",
dataType: "text",
success: function(data) {
alert(data);
}
});
});
});
</script>
</head>
<body>
<h1>index 页面</h1>
<input id="ajaxButton" type="button" value="发起 ajax 请求"/>
</body>
</html>
(2)java 代码
注意,因为 Ajax 请求是做局部更新,因此不需要进行转发,也不需要重定向,返回类型是用 void 即可。
java
@RequestMapping(value = "/testAjax")
public void testAjax(String username, String password, HttpServletResponse resp) throws IOException {
System.out.println("username: " + username + ", password: " + password);
resp.getWriter().write("testAjax");
return;
}
2、@RequestBody(重点关注"赋值形式")
@RequestBody 可以获取请求体,在控制器方法设置一个形参,使用 @RequestBody 进行标识,当前请求的请求体就会为当前注解所标识的形参赋值。
需要注意的是,使用了 @RequestBody 后,Ajax 就要发送 POST 请求,而不能发送 GET 请求 。可见:https://juejin.cn/post/7222833868503236667
(1)导入依赖 jackson
Spring 默认的 json 解析器就是 jackson,其实没必要导入,但是可以写上。
XML
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
(2)@RequestBody 的赋值形式
当 @RequestBody 修饰一个方法参数时,会将所有的请求参数都赋值给方法参数。什么意思呢?
假设请求体中的请求参数 为 username : "admin", password : "123456", age : 12:
- 若方法参数为 @RequestBody String user:
- 若方法参数为 @RequestBody User user:
- 若方法参数为 @RequestBody Map<String, Object> map:
关于 @RequestBody 与 @RequestParam 的区别 以及 是否添加 @RequestBody 注解,这两个问题也很重要。
可以参考:https://blog.csdn.net/weixin_43606226/article/details/106545024
(3)使用注意
仅对使用 JQuery 的情况而言,其他方法(比如:vue)不需要注意。
当我们传递 JSON 格式的数据时:
- 需要添加 contentType: "application/json";
- 需要使用 JSON.stringify(data);
html
<script type="text/javascript">
$(function() {
$("#ajaxButton").click(function() {
var user = {
username: "admin",
password: "123456",
age: 12,
};
$.ajax({
url:"/Demo_Rest/testAjax",
data: JSON.stringify(user),
contentType: "application/json",
type: "post",
dataType: "text", /* 返回数据类型 */
success: function(data) {
alert(data);
}
});
});
});
</script>
3、@ResponseBody(经常用)
@ResponseBody 用于标识一个控制器方法,可以将该方法的返回值(String、实体类)直接作为响应体响应到浏览器。
一般情况下,就是将一个 JSON 格式的 json 字符串,响应到客户端,再由客户端浏览器将其解析成 json 对象。而使用了 @ResponseBody 和 @RequestBody 之后,就不是这样的了,具体后面解释。
(1)返回普通字符串的情况
java
@RequestMapping(value = "/testResponseBody")
@ResponseBody
public String responseBody() {
return "success";
}
访问 /testResponseBody,那么就会出现如下结果:
而这显然不是我们之前所编写的 success.html 页面,而仅仅是一个字符串数据。
(2)返回实体类的情况
将 user 的信息发送给服务器,服务器修改 username 后,响应新的 user 信息。
(2-1)index.html
html
<script type="text/javascript">
$(function() {
$("#ajaxButton").click(function() {
var user = {
username: "admin",
password: "123456",
age: 12,
};
$.ajax({
url:"/Demo_Rest/testResponseBody",
data: JSON.stringify(user),
contentType: "application/json",
type: "post",
dataType: "json", /* 返回数据类型 */
success: function(data) {
alert(data.username);
}
});
});
});
</script>
(2-2)Java 代码
java
@RequestMapping(value = "/testResponseBody")
@ResponseBody
public User responseBody(@RequestBody User user) {
System.out.println(user);
user.setUsername("wyt");
return user;
}
(3)常见的 Java 对象转换为 Json 之后的类型
- 实体类:Json 对象;
- Map:Json 对象;
- List:Json 数组;
4、为什么不用手动接收 JSON 字符串、转换 JSON 字符串
在 SpringMVC 的核心配置文件中开启 mvc 的注解驱动:
XML
<mvc:annotation-driven/>
此时在 HandlerAdaptor 中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,它是由 JackSon 这个 JSON 解析器提供的,其作用有:
- 既可以将浏览器传递的请求参数构成的 JSON 字符串转换为 JSON 对象
- 也可以将响应到浏览器的 Java 对象转换为 JSON 格式的字符串
5、@RestController
实际开发中,@ResponseBody 使用的情况非常多,而我们只需要给控制类写上一个 @RestController,就可以同时起到 @Controller 和 @ResponseBody 的作用。
十、文件上传与下载
文件上传与下载的方式有很多,不一定要用 ResponseEntity 这种方法。
1、ResponseEntity
ResponseEntity 是一个类型,用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文。
有关文件上传与下载的步骤,可以查看:https://blog.csdn.net/joyride_run/article/details/132814877
2、文件下载
在此最奉上喜欢的图片(R-C.jpg):
(1)Java 代码
java
@RequestMapping(value = "/fileDownload", method = RequestMethod.GET)
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
// 1.获取要下载的文件名
String downloadFilename = "R-C.jpg";
// 2.获取ServletContext对象
ServletContext servletContext = session.getServletContext();
// 3.获取服务器中文件的真实路径
String realPath = servletContext.getRealPath("/static/img/" + downloadFilename);
// 4.创建输入流
InputStream inputStream = new FileInputStream(realPath);
// 5.创建字节数组
int count = 0;
while (count == 0) count = inputStream.available();// 防止数据未送达,导致count=0,但是本地读文件一般不会有这种错误
byte[] bytes = new byte[count];
// 6.将流读到字节数组中
inputStream.read(bytes);
// 7.创建HttpHeaders对象设置响应头信息
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment;filename=" + downloadFilename);
// 8.设置响应状态码
HttpStatus statusCode = HttpStatus.OK;
// 9.创建ResponseEntity对象
ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
// 10.关闭输入流
inputStream.close();
return responseEntity;
}
(2)index.html
html
<a th:href="@{/fileDownload}">下载R-C.jpg文件</a>
(3)效果
3、文件上传
(1)如何获取上传的文件信息
- SpringMVC 将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息
- 使用 MultipartFile 对象需要配置文件上传解析器
- 使用 MultipartFile 对象需要引入依赖 commons-fileupload
由于 MultipartResolver 是一个接口,我们需要配置它的实现类 CommonsMulitipartResolver。
XML
<!-- 配置文件上传解析器
id 必须为 multipartResolver
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
XML
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
(2)输出文件名
- 使用 MultipartFile 的类方法 getOriginalFilename(),可以获取上传文件的文件名。
(2-1)Java 代码
java
@RequestMapping(value = "/fileUpload", method = RequestMethod.POST)
public String upload(MultipartFile multipartFile) {
String filename = multipartFile.getOriginalFilename();
System.out.println(filename);
return "success";
}
(2-2)index.html
- 这里有一个很重要的点,就是表单上传文件的 name 的属性值必须要与 MultipartFile 的参数名相同。
html
<form th:action="@{/fileUpload}" method="post" enctype="multipart/form-data">
头像:<input type="file" name="multipartFile"/> <br/>
<input type="submit" value="上传"/>
</form>
(3)保存文件
- 保存文件使用 SpringMVC 封装的 transferTo() 方法,则不需要再使用 IO 流进行持久化存储;
- 因为我们不知道目录之间的分隔符是 / 还是 \,所以可以使用 File.separator;
java
@RequestMapping(value = "/fileUpload", method = RequestMethod.POST)
public String upload(MultipartFile multipartFile, HttpSession session) throws IOException {
// 1.获取上传的文件名
String filename = multipartFile.getOriginalFilename();
// 2.获取文件保存的路径
ServletContext servletContext = session.getServletContext();
String dir = servletContext.getRealPath("/static/img/upload");
// 3.创建文件对象
File file = new File(dir);
if (!file.exists()) {
file.mkdirs(); // mkdir 只能创建下一级目录
}
// 4.保存文件
String savePath = dir + File.separator + filename;
System.out.println(savePath);
multipartFile.transferTo(new File(savePath));
return "success";
}
4、解决文件重名导致内容覆盖的问题
解决方法:
- 使用时间戳;
- 使用 UUID;
(1)使用 UUID
java
String filename = multipartFile.getOriginalFilename();
String extensionName = filename.substring(filename.lastIndexOf("."));
filename = UUID.randomUUID().toString() + extensionName;
十一、拦截器
SpringMVC 中的拦截器用于拦截控制器方法的执行。作用类似于 Filter,但不是完全相同。
默认情况下,未指定对哪些资源进行拦截,则会拦截所有交给 DispatcherServlet 的请求,比如:对 index 的访问,对 controller 的访问。
1、拦截器的三个方法
SpringMVC 中的拦截器有三个抽象方法:
- preHandle:控制器方法执行之前执行,其返回值表示对控制器方法的放行(true)或拦截(false);
- postHandle:控制器方法执行之后执行;
- afterComplation:处理完视图和模型数据,渲染视图完毕之后执行;
2、拦截器的简单示例
SpringMVC 中的拦截器需要实现 HandlerInterceptor,并且在 SpringMVC 的配置文件中进行配置:
(1)SpringMVC 配置文件
- 由 SpringMVC 加载,不需要写 id
XML
<!-- 配置拦截器 -->
<mvc:interceptors>
<bean class="com.demo.interceptor.FirstInterceptor"></bean>
</mvc:interceptors>
(2)FirstInterceptor.java
- 重写三个方法,其中 preHandle 的返回 false
java
package com.demo.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FirstInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("FirstInterceptor -- preHandle");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("FirstInterceptor -- postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("FirstInterceptor -- afterCompletion");
}
}
(3)运行结果
访问 index.html,观察页面响应结果和控制台输出:
- 首页无法访问,以及如下输出
3、拦截器的配置方法
(1)直接嵌套 <bean> 标签
- 在 SpringMVC 的配置文件中直接写上:
XML
<mvc:interceptors>
<bean class="com.demo.interceptor.FirstInterceptor"></bean>
</mvc:interceptors>
(2)链接外部 <bean>
- <ref> 链接 <mvc:interceptors> 外部的 <bean>
XML
<bean id="firstInterceptor" class="com.demo.interceptor.FirstInterceptor"/>
<!-- 配置拦截器 -->
<mvc:interceptors>
<ref bean="firstInterceptor"/>
</mvc:interceptors>
(3)扫描注解方式
- 在 FirstInterceptor 上添加 @Component,然后直接 <ref>
java
@Component(value = "firstInterceptor")
public class FirstInterceptor implements HandlerInterceptor {
}
XML
<mvc:interceptors>
<ref bean="firstInterceptor"/>
</mvc:interceptors>
(4)<mvc:interceptor>
它有两个重要的属性:
- mvc:mapping path ="":表示需要拦截的请求;
- mvc:exclude-mapping path="":表示不需要拦截的请求;
- <ref> 和 <bean> 要写在这两个属性之后;
设置了这两个请求之后,就会按照设置的内容进行拦截,而不是去拦截 DispatcherServlet。
XML
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*"/>
<mvc:exclude-mapping path="/static"/>
<ref bean="firstInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
还需要注意,path = " /* ",表示的是,拦截 / 后的一层目录的请求,也就是说 /abc/* 这类请求是无法拦截的。
想要拦截对所有资源的请求,又要求使用 mvc:mapping,那么可以写 path = " /** "。
4、多个拦截器的执行顺序
我们设置两个拦截器:FirstInterceptor、SecondInterceptor。将他们的 preHandler 都设置为放行(true)。
(1)在 SpringMVC 中作如下顺序
- 先配置 FirstInterceptor,后配置 SecondInterceptor。
XML
<mvc:interceptors>
<ref bean="firstInterceptor"/>
<ref bean="secondInterceptor"/>
</mvc:interceptors>
(2)访问 index.html,观察输出
显然可以看出,执行 preHandle() 方法,是按照配置顺序来执行;执行另外 2 个方法,是按照配置的逆序来执行。
5、preHandle 返回 false 的情况
当拦截器 C 的 preHandle() 返回了 false:
- C 以及 C 之前的拦截器的 preHandle() 都会执行;
- 所有的 postHandle() 都不执行;
- C 之前的拦截器的 afterComplation() 都会执行;
十二、控制器方法异常处理
1、异常处理解析器
对于异常处理解析器,我们可以配置,也可以不配置,SpringMVC 已经默认使用了解析器。
- SpringMVC 提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver。
- HandlerExceptionResolver 接口的实现类有:DefaultHandlerExceptionResolver(SpringMVC 默认使用)和 SimpleMappingExceptionResolver(用户自定义使用)。
在他们的底层实现中,会返回一个 ModelAndView,这代表着我们可以利用 Model 将错误信息保存到 Request 域,还可以利用 View 进行页面跳转。比如:跳转至 error404.html 页面,并显示错误信息。
2、使用 xml 配置
(1)Java 代码
- 手动添加一个 1 / 0 的异常。
java
@RequestMapping(value = "/testException")
public String testException() {
int i = 1 / 0;
return "success";
}
(2)index.html 以及 error666.html
- 访问控制器方法
html
<body>
<a th:href="@{/testException}">测试异常处理</a>
</body>
- 展示异常页面
html
<body>
<h1>error666 页面</h1>
</body>
(3)配置异常解析器的 View
- prop 的 key 表示处理器方法执行过程中出现的异常;
- prop的 value 表示若出现指定异常时,设置一个逻辑视图,跳转到指定页面;
XML
<!-- 配置异常解析器 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArithmeticException">error666</prop>
<!--
.... 其他异常
-->
</props>
</property>
</bean>
(4)运行结果
- 由于 1 / 0 出现了异常,因此没有转发到 success.html 页面,而是来到了 error666.html 页面。
3、显示域对象的数据
刚才的示例中,只有 View 的功能,现在我们要添加 Model 的功能。
<property> 除了前面的 exceptionMappings,还有一个 exceptionAttribute,他们就是分别表示 View 和 Model:
- 通过 exceptionAttribute 的 value 设置一个域数据的属性名,、即可通过这个属性名访问异常信息。
(1)SpringMVC 配置文件
- 添加了 <property name="exceptionAttribute" value="exceptionInfo"/>
XML
<!-- 配置异常解析器 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArithmeticException">error666</prop>
<!--
.... 其他异常
-->
</props>
</property>
<property name="exceptionAttribute" value="exceptionInfo"/>
</bean>
(2)error666.html
- 获取域对象,显示异常信息
html
<body>
<h1>error666 页面</h1>
<p th:text="${exceptionInfo}"></p>
</body>
(3)输出结果
4、使用注解配置
(1)@ControllerAdvice
- @ControllerAdvice 将当前类标识为异常处理的 Controller。
(2)@ExceptionHandler
- @ExceptionHandler 有一个 value 属性,需要继承自 Throwable 的异常类的 Class 数组作为属性值,其中传入我们需要处理的异常类的 class 属性。
(3)返回值
- 如果使用 String,那么返回值可以设置成错误页面的逻辑视图;
- 如果使用 ModelAndView,就使用 setView 方法设置逻辑视图;
(4)Controller
- 产生一个 math 异常
java
@RequestMapping(value = "/testException")
public String testException() {
int i = 1 / 0;
return "success";
}
(5)ExceptionHandler
- 由于 Controller 有 math 方面的异常,因此用 ArithmeticException
java
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(value = {ArithmeticException.class})
public String handleException(Model model, Throwable exception) {
model.addAttribute("exceptionInfo", exception);
return "error666";
}
}
(6)运行结果