摘要:本文学习了使用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
由视图解析器将视图名称转为视图,这是最常用的响应方式:
java1 2 3 4 5 6 7 8 9 10
| @Controller public class UserController { @GetMapping("/test") public String test(Integer id, String name) { System.out.println("id: " + id); System.out.println("name: " + name); return "success"; } }
|
视图页面:
success.jsp1 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数据:
java1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller public class UserController { @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可以更精确地控制响应:
java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Controller public class UserController { @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); } } }
|
支持设置响应头:
java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Controller public class UserController { @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.jsp1 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设置域对象:
java1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller public class UserController { @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设置域对象:
java1 2 3 4 5 6 7 8 9 10 11 12
| @Controller public class UserController { @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设置域对象:
java1 2 3 4 5 6 7 8 9 10 11 12
| @Controller public class UserController { @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设置域对象:
java1 2 3 4 5 6 7 8 9 10 11 12
| @Controller public class UserController { @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设置域对象:
java1 2 3 4 5 6 7 8 9 10 11 12
| @Controller public class UserController { @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对象:
java1 2 3 4 5 6 7 8 9 10 11 12 13
| @Controller public class UserController { @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实现转发:
java1 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.setAttribute("message", "来自请求转发的消息"); return "forward:/target"; } @GetMapping("/target") public String target(HttpServletRequest request) { System.out.println(request.getAttribute("message")); return "demo"; } }
|
3.2 重定向
重定向是由服务器告诉浏览器重新向另一个URL发起请求的过程。客户端浏览器地址栏会更新为新的URL地址,整个过程涉及两次请求。
特点:
- 浏览器地址栏会发生变化
- 发送两次请求(第一次请求,第二次重定向请求)
- 不能共享request域中的数据(因为是两次独立请求)
- 可以跳转到其他服务器的资源
- 效率相对较低,但更安全
可以通过返回带有redirect:前缀的URL实现转发:
java1 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.setAttribute("message", "来自响应重定向的消息"); return "redirect:/target"; } @GetMapping("/target") public String target(HttpServletRequest request) { System.out.println(request.getAttribute("message")); return "demo"; } }
|
如果需要在重定向时传递参数,可以在URL中直接拼接参数:
java1 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()); return "redirect:/target?message=" + message; } @GetMapping("/target") public String target(String message) { System.out.println(message); return "demo"; } }
|
或者使用session域传递数据:
java1 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.setAttribute("message", "来自响应重定向的消息"); return "redirect:/target"; } @GetMapping("/target") public String target(HttpSession session) { System.out.println(session.getAttribute("message")); return "demo"; } }
|
或者使用RedirectAttributes传递参数,支持在URL中直接拼接参数,也支持使用Session闪存传递参数:
java1 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) { redirectAttributes.addAttribute("messageFromParam", "来自响应重定向的消息-参数"); redirectAttributes.addFlashAttribute("messageFromFlash", "来自响应重定向的消息-闪存"); return "redirect:/target"; } @GetMapping("/target") public String target(String messageFromParam, String messageFromFlash) { System.out.println(messageFromParam); System.out.println(messageFromFlash); return "demo"; } }
|
4 文件下载
实现文件下载功能:
java1 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资源,最终导致应用崩溃或响应缓慢。所以一般需要显示配置线程池管理异步任务线程,避免资源耗尽。
配置自定义异步任务线程池:
java1 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()); 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); 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进行异步处理:
java1 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进行异步处理:
java1 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进行异步处理:
java1 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 统一响应
创建统一的响应类:
java1 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; } }
|
条