抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文学习了使用SpringMVC如何返回响应到客户端。

环境

Windows 10 企业版 LTSC 21H2
Java 1.8
Tomcat 8.5.50
Maven 3.6.3
Spring 5.2.25.RELEASE

1 响应方式

1.1 String

由视图解析器将视图名称转为视图,这是最常用的响应方式:

java
1
2
3
4
5
6
7
8
9
10
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name) {
System.out.println("id: " + id);
System.out.println("name: " + name);
return "success";
}
}

视图页面:

success.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div>
<a href="${pageContext.request.contextPath}/">返回首页</a>
</div>
</body>
</html>

1.2 @ResponseBody

使用@ResponseBody注解直接返回JSON数据:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
@ResponseBody
public User test(Integer id, String name) {
System.out.println("id: " + id);
System.out.println("name: " + name);
User user = new User();
user.setId(id);
user.setName(name);
return user;
}
}

可以使用@RestController注解代替@Controller注解,所有方法都会自动添加@ResponseBody注解。

1.3 ResponseEntity

使用ResponseEntity可以更精确地控制响应:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
@ResponseBody
public ResponseEntity test(User user) {
System.out.println("id: " + user.getId());
System.out.println("name: " + user.getName());
if (user.getId() != null) {
return ResponseEntity.ok().body(user);
} else {
return ResponseEntity.badRequest().body(null);
}
}
}

支持设置响应头:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
@ResponseBody
public ResponseEntity test(User user) {
System.out.println("id: " + user.getId());
System.out.println("name: " + user.getName());
if (user.getId() != null) {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Response-Time", String.valueOf(System.currentTimeMillis()));
headers.add("X-User-Role", "admin");
return ResponseEntity.ok().headers(headers).body(user);
} else {
return ResponseEntity.badRequest().body(null);
}
}
}

2 域对象

在给域对象设置数据以后,可以在JSP页面通过EL表达式使用模型数据:

success.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div>
<p>ID:${id}</p>
<p>姓名:${name}</p>
<a href="${pageContext.request.contextPath}/">返回首页</a>
</div>
</body>
</html>

2.1 page

当前页面范围的域对象,只在JSP页面有效。

2.2 request

请求范围的域对象。

通过ModelAndView设置域对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public ModelAndView test(Integer id, String name) {
System.out.println("id: " + id);
System.out.println("name: " + name);
ModelAndView mav = new ModelAndView();
mav.addObject("id", id);
mav.addObject("name", name);
mav.setViewName("success");
return mav;
}
}

通过ModelMap设置域对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name, ModelMap model) {
System.out.println("id: " + id);
System.out.println("name: " + name);
model.put("id", id);
model.put("name", name);
return "success";
}
}

通过Map设置域对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name, Map<String, Object> model) {
System.out.println("id: " + id);
System.out.println("name: " + name);
model.put("id", id);
model.put("name", name);
return "success";
}
}

通过HttpServletRequest设置域对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name, HttpServletRequest request) {
System.out.println("id: " + id);
System.out.println("name: " + name);
request.setAttribute("id", id);
request.setAttribute("name", name);
return "success";
}
}

2.3 session

会话范围的域对象。

通过HttpSession设置域对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name, HttpSession session) {
System.out.println("id: " + id);
System.out.println("name: " + name);
session.setAttribute("id", id);
session.setAttribute("name", name);
return "success";
}
}

2.4 application

应用程序范围的域对象。

通过ServletContext设置域对象,需要先获取ServletContext对象:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class UserController {
// id=1&name=张三
@GetMapping("/test")
public String test(Integer id, String name, HttpServletRequest request) {
System.out.println("id: " + id);
System.out.println("name: " + name);
ServletContext servletContext = request.getServletContext();
servletContext.setAttribute("id", id);
servletContext.setAttribute("name", name);
return "success";
}
}

3 转发和重定向

转发(Forward)和重定向(Redirect)是Web开发中两种重要的页面跳转方式,它们在实现机制和应用场景上有所不同。

3.1 转发

转发是在服务器内部完成的页面跳转,客户端浏览器地址栏不会发生变化。整个过程对客户端来说是透明的,客户端只发送了一次请求,但服务器内部可能处理多个资源。

特点:

  • 浏览器地址栏不改变
  • 只发送一次请求
  • 可以共享request域中的数据
  • 不能跳转到其他服务器的资源
  • 效率相对较高

可以通过返回带有forward:前缀的URL实现转发:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class DemoController {
@GetMapping("/forward")
public String forward(HttpServletRequest request) {
// 设置request域中的共享数据
request.setAttribute("message", "来自请求转发的消息");
// 使用forward前缀显式指定转发
return "forward:/target";
}
@GetMapping("/target")
public String target(HttpServletRequest request) {
// 获取request域中的共享数据
System.out.println(request.getAttribute("message"));
// 返回视图名称
return "demo";
}
}

3.2 重定向

重定向是由服务器告诉浏览器重新向另一个URL发起请求的过程。客户端浏览器地址栏会更新为新的URL地址,整个过程涉及两次请求。

特点:

  • 浏览器地址栏会发生变化
  • 发送两次请求(第一次请求,第二次重定向请求)
  • 不能共享request域中的数据(因为是两次独立请求)
  • 可以跳转到其他服务器的资源
  • 效率相对较低,但更安全

可以通过返回带有redirect:前缀的URL实现转发:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class DemoController {
@GetMapping("/redirect")
public String redirect(HttpServletRequest request) {
// 设置request域中的共享数据
request.setAttribute("message", "来自响应重定向的消息");
// 使用redirect前缀显式指定重定向
return "redirect:/target";
}
@GetMapping("/target")
public String target(HttpServletRequest request) {
// 无法获取request域中的共享数据
System.out.println(request.getAttribute("message"));
// 返回视图名称
return "demo";
}
}

如果需要在重定向时传递参数,可以在URL中直接拼接参数:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
@GetMapping("/redirect")
public String redirect() throws UnsupportedEncodingException {
// 对参数进行编码,防止中文乱码
String message = URLEncoder.encode("来自响应重定向的消息", StandardCharsets.UTF_8.toString());
// 使用redirect前缀显式指定重定向
return "redirect:/target?message=" + message;
}
@GetMapping("/target")
public String target(String message) {
// 使用@RequestParam注解获取URL参数,可以省略
System.out.println(message);
// 返回视图名称
return "demo";
}
}

或者使用session域传递数据:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
@GetMapping("/redirect")
public String redirect(HttpSession session) {
// 设置session域中的共享数据
session.setAttribute("message", "来自响应重定向的消息");
// 使用redirect前缀显式指定重定向
return "redirect:/target";
}
@GetMapping("/target")
public String target(HttpSession session) {
// 获取session域中的共享数据
System.out.println(session.getAttribute("message"));
// 返回视图名称
return "demo";
}
}

或者使用RedirectAttributes传递参数,支持在URL中直接拼接参数,也支持使用Session闪存传递参数:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
public class UserController {
@GetMapping("/redirect")
public String redirect(RedirectAttributes redirectAttributes) {
// 将数据添加到URL参数,在URL后面拼接参数
redirectAttributes.addAttribute("messageFromParam", "来自响应重定向的消息-参数");
// 将数据添加到Session闪存,闪存只能获取一次
redirectAttributes.addFlashAttribute("messageFromFlash", "来自响应重定向的消息-闪存");
// 使用redirect前缀显式指定重定向
return "redirect:/target";
}
@GetMapping("/target")
public String target(String messageFromParam, String messageFromFlash) {
// 使用@RequestParam注解获取参数,可以省略
System.out.println(messageFromParam);
// 使用@ModelAttribute注解获取闪存,可以省略,只能获取一次
System.out.println(messageFromFlash);
// 返回视图名称
return "demo";
}
}

4 文件下载

实现文件下载功能:

java
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
29
30
31
32
33
34
35
36
@GetMapping("/download")
public ResponseEntity<Resource> download(String filename, HttpServletRequest request) {
try {
// 获取项目根目录绝对路径
String rootPath = request.getServletContext().getRealPath("/");
Path uploadDir = Paths.get(rootPath, "uploads");
Path filePath = uploadDir.resolve(filename).normalize();
// 安全检查
if (!filePath.startsWith(uploadDir)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 加载文件
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists()) {
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
// 获取原始文件名
String originalFileName = resource.getFilename();
// 对文件名进行编码
String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
// 兼容新旧浏览器
String contentDisposition = "attachment; filename=\"" + originalFileName + "\"; filename*=UTF-8''" + encodedFileName;
// 返回文件
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

5 异步响应

在传统的Servlet模型中,每个请求会被一个容器线程从头到尾处理。如果请求中包含耗时操作,该线程会长时间阻塞,导致容器线程池耗尽,降低系统吞吐量。

为了解决这个问题,从Servlet的3.0版本开始,引入了异步处理支持,允许将请求处理转移到其他线程,释放容器线程。

SpringMVC在此基础上提供了三种主要的异步返回值方式。

5.1 Callable

Callable是Java标准库中的一个接口,代表可返回结果的任务。

在SpringMVC中,如果Controller方法返回的是Callable类型的数据,容器会自动将其提交到一个配置好的TaskExecutor中执行,并且立即释放当前容器线程。当Callable执行完成后,再会将结果返回给客户端。

默认使用的TaskExecutor是SimpleAsyncTaskExecutor类,它是简单的线程池,会在每次任务提交时创建新的线程,并且不限制线程数量。在高并发情况下会导致系统创建大量线程,耗尽内存和CPU资源,最终导致应用崩溃或响应缓慢。所以一般需要显示配置线程池管理异步任务线程,避免资源耗尽。

配置自定义异步任务线程池:

java
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Configuration
@EnableWebMvc
@ComponentScan("com.example.controller")
public class WebConfig implements WebMvcConfigurer {
// 配置异步任务线程池
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 设置自定义线程池作为异步任务执行器
configurer.setTaskExecutor(mvcTaskExecutor());
// 设置默认超时时间为5秒,异步任务执行时间超过此限制将触发超时处理机制
configurer.setDefaultTimeout(5000);
}
// 创建自定义线程池
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数,线程池中始终保留的最小线程数量,即使它们处于空闲状态
executor.setCorePoolSize(10);
// 最大线程数,线程池允许创建的最大线程数量
executor.setMaxPoolSize(20);
// 队列容量,用于存放等待执行的任务的阻塞队列大小,线程数达到核心线程数后,新任务会被放入此队列
executor.setQueueCapacity(100);
// 线程名称前缀,为线程池中创建的线程命名,便于在日志和监控中识别
executor.setThreadNamePrefix("mvc-async-");
// 优雅关闭,是否等待所有已提交任务完成后再销毁线程池,避免因容器关闭而导致正在执行的任务被中断
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待终止时间,设置线程池关闭后等待任务完成的最大时间为60秒,超过此时间,即使还有未完成任务,也会强制销毁线程池
executor.setAwaitTerminationSeconds(60);
// 任务装饰器,确保线程池中的线程能继承原请求的上下文,避免异步任务中请求属性丢失
executor.setTaskDecorator(runnable -> {
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
});
// 初始化线程池,必须调用此方法,否则线程池不会真正创建核心线程。
executor.initialize();
// 返回线程池
return executor;
}
// ...
}

使用Callable进行异步处理:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class DemoController {
// 执行异步请求
@GetMapping("/callable")
@ResponseBody
public Callable<String> callable() {
System.out.println(Thread.currentThread().getName() + " 正常执行");
return () -> {
// 模拟耗时操作(毫秒)
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 异步执行");
return "异步执行结束";
};
}
}

3.2 DeferredResult

DeferredResult是Spring提供的更灵活的异步处理类,支持在任意线程(甚至是非Spring管理的线程)中设置返回结果。

当请求到达时,立即返回DeferredResult对象,容器线程释放,其他线程调用setResult()方法,将结果写回响应,返回客户端。

使用DeferredResult进行异步处理:

java
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
29
30
31
32
33
34
35
@Controller
public class DemoController {
// 创建执行队列
private final Queue<DeferredResult<String>> queue = new ConcurrentLinkedQueue<>();
// 接收异步请求
@GetMapping("/deferred")
@ResponseBody
public DeferredResult<String> deferred() {
System.out.println(Thread.currentThread().getName() + " 正常执行");
// 设置超时时间(毫秒)和超时返回结果
DeferredResult<String> deferredResult = new DeferredResult<>(5000L, "异步执行超时");
// 请求完成回调
deferredResult.onCompletion(() -> System.out.println(Thread.currentThread().getName() + " 请求完成"));
// 请求超时回调
deferredResult.onTimeout(() -> System.out.println(Thread.currentThread().getName() + " 请求超时"));
// 放入执行队列,由其他线程处理
queue.add(deferredResult);
// 返回结果
return deferredResult;
}
// 其他线程执行异步请求设置结果
@GetMapping("/produce")
@ResponseBody
public String produce() {
DeferredResult<String> result = queue.poll();
// 执行异步请求
if (result != null) {
System.out.println(Thread.currentThread().getName() + " 异步执行");
result.setResult("异步执行结束");
return "其他线程执行";
}
// 队列为空表示没有异步请求
return "没有异步请求";
}
}

3.3 WebAsyncTask

使用WebAsyncTask实现更灵活的异步响应。

使用WebAsyncTask进行异步处理:

java
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
@Controller
public class DemoController {
// 执行异步请求
@GetMapping("/task")
@ResponseBody
public WebAsyncTask<String> task() {
System.out.println(Thread.currentThread().getName() + " 正常执行");
// 执行异步任务
Callable<String> callable = () -> {
System.out.println(Thread.currentThread().getName() + " 异步执行");
// 模拟耗时操作(毫秒)
Thread.sleep(2000);
return "异步执行结束";
};
// 创建异步执行对象,设置超时时间(毫秒)
WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(5000, callable);
// 设置超时回调
webAsyncTask.onTimeout(() -> {
System.out.println(Thread.currentThread().getName() + " 请求超时");
return "异步执行超时";
});
// 设置完成回调,无论什么情况都会执行
webAsyncTask.onCompletion(() -> {
System.out.println(Thread.currentThread().getName() + " 请求完成");
});
return webAsyncTask;
}
}

6 统一响应

创建统一的响应类:

java
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
29
public class CommonResult<T> {
private int code;
private String message;
private T data;
private long timestamp;
public CommonResult() {
this.timestamp = System.currentTimeMillis();
}
public static <T> CommonResult<T> success(T data) {
CommonResult<T> response = new CommonResult<>();
response.setCode(200);
response.setData(data);
return response;
}
public static <T> CommonResult<T> error(int code, String message) {
CommonResult<T> response = new CommonResult<>();
response.setCode(code);
response.setMessage(message);
return response;
}
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}

评论