Skip to content

响应处理与视图解析

一、返回值类型

返回视图名

java
@Controller
public class ViewController {
    
    // 返回 String → 视图名
    @GetMapping("/user/list")
    public String list(Model model) {
        model.addAttribute("users", userService.findAll());
        return "user-list";  // 视图名
    }
    
    // 返回 ModelAndView
    @GetMapping("/user/{id}")
    public ModelAndView detail(@PathVariable Long id) {
        ModelAndView mav = new ModelAndView("user-detail");
        mav.addObject("user", userService.findById(id));
        return mav;
    }
}

返回 JSON

java
@RestController
public class JsonController {
    
    // 返回对象 → JSON
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // 返回 Map → JSON
    @GetMapping("/user/{id}/info")
    public Map<String, Object> getUserInfo(@PathVariable Long id) {
        return Map.of("name", "张三", "age", 18);
    }
    
    // 返回 List → JSON 数组
    @GetMapping("/users")
    public List<User> list() {
        return userService.findAll();
    }
}

返回 ResponseEntity

java
@GetMapping("/user/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    
    if (user == null) {
        return ResponseEntity.notFound().build();
    }
    
    return ResponseEntity.ok(user);
}

// 带自定义 Header
@GetMapping("/user/{id}")
public ResponseEntity<User> getUserWithHeader(@PathVariable Long id) {
    User user = userService.findById(id);
    
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Custom-Header", "value");
    headers.add("ETag", "\"12345\"");
    
    return new ResponseEntity<>(user, headers, HttpStatus.OK);
}

二、视图解析器

InternalResourceViewResolver(JSP)

java
@Configuration
public class ViewConfig implements WebMvcConfigurer {
    
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setOrder(1);  // 解析器优先级
        return resolver;
    }
}

ThymeleafViewResolver

java
@Configuration
public class ThymeleafConfig {
    
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
    
    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        return engine;
    }
    
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode("HTML");
        return resolver;
    }
}

多视图解析器

java
@Configuration
public class MultiViewConfig implements WebMvcConfigurer {
    
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // 方式1:默认配置
        // registry.jsp("/WEB-INF/views/", ".jsp");
        
        // 方式2:多个视图解析器
        registry.enableContentNegotiation(
            new MappingJackson2JsonView(),  // JSON 视图
            new BeanNameViewResolver()       // 按 Bean 名称解析
        );
    }
}

三、ModelAndView

创建方式

java
@GetMapping("/user/{id}")
public ModelAndView getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    
    // 方式1:构造函数
    ModelAndView mav = new ModelAndView("user-detail");
    mav.addObject("user", user);
    
    // 方式2:构造+链式
    return new ModelAndView("user-detail", "user", user);
    
    // 方式3:Model 构造
    ModelAndView mav = new ModelAndView("user-detail", 
        new HashMap<String, Object>() {{ put("user", user); }});
    
    // 方式4:视图对象
    View view = new RedirectView("/users");
    return new ModelAndView(view);
}

ModelAndView 常用方法

java
ModelAndView mav = new ModelAndView("user-detail");

// 设置模型数据
mav.addObject("user", user);          // 单个
mav.addObject("title", "用户详情");     // 单个
mav.addAllObjects(map);               // 批量

// 设置视图
mav.setViewName("other-view");        // 修改视图名
mav.setView(view);                    // 设置视图对象

// 获取数据
mav.getModel();                       // 返回 Map
mav.getModelObject();                 // 返回 Object
mav.getViewName();                    // 返回视图名

四、转发与重定向

转发(Forward)

java
@GetMapping("/forward")
public String forward() {
    // 转发到其他 Controller
    // URL 不变,一次请求
    return "forward:/other/endpoint";
}

@GetMapping("/old-url")
public String redirectOld() {
    // 转发到视图
    return "forward:/WEB-INF/views/old.jsp";
}

重定向(Redirect)

java
@GetMapping("/redirect")
public String redirect() {
    // 重定向,浏览器发起新请求
    // URL 改变
    return "redirect:/new-url";
}

@PostMapping("/user/save")
public String save(User user) {
    userService.save(user);
    // PRG 模式:Post-Redirect-Get
    return "redirect:/users/" + user.getId();
}

flash 属性

java
// 重定向时传递数据(存储在 session 中,重定向后自动清除)
@PostMapping("/user/save")
public String save(User user, RedirectAttributes attributes) {
    userService.save(user);
    attributes.addFlashAttribute("message", "保存成功!");
    return "redirect:/users";
}

@GetMapping("/users")
public String list(Model model) {
    // model 中有 flash 属性
    // ${message} 可在 Thymeleaf 中使用
    return "user-list";
}

// 添加多个 flash 属性
attributes.addFlashAttribute("success", true);
attributes.addFlashAttribute("userId", user.getId());

转发 vs 重定向

对比转发 (Forward)重定向 (Redirect)
请求次数1次2次
URL 变化不变改变
共享 request
共享 session
跳转位置服务端浏览器
适用内部跳转跨控制器跳转

五、视图渲染

JSP EL 表达式

jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>${title}</title>
</head>
<body>
    <h1>用户详情</h1>
    <p>姓名:${user.name}</p>
    <p>年龄:${user.age}</p>
    
    <%-- 循环 --%>
    <c:forEach var="item" items="${userList}">
        <li>${item.name}</li>
    </c:forEach>
    
    <%-- 条件 --%>
    <c:if test="${not empty user}">
        <p>用户存在</p>
    </c:if>
</body>
</html>

Thymeleaf 模板

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="${title}">默认标题</title>
</head>
<body>
    <h1>用户详情</h1>
    <p th:text="${user.name}">姓名</p>
    <p th:text="${user.age}">年龄</p>
    
    <!-- 循环 -->
    <ul>
        <li th:each="item : ${userList}" th:text="${item.name}">姓名</li>
    </ul>
    
    <!-- 条件 -->
    <p th:if="${not empty user}">用户存在</p>
    
    <!-- 内联 JS -->
    <script th:inline="javascript">
        var user = /*[[${user}]]*/ {};
    </script>
</body>
</html>

Thymeleaf 常用语法

html
<!-- 文本 -->
<span th:text="${name}">文本</span>
<span th:utext="${htmlContent}">HTML内容</span>

<!-- 循环 -->
<tr th:each="user, stat : ${users}">
    <td th:text="${stat.count}">序号</td>
    <td th:text="${user.name}">姓名</td>
</tr>

<!-- 条件 -->
<div th:if="${user.role == 'admin'}">管理员</div>
<div th:unless="${user.role == 'admin'}">普通用户</div>

<!-- Switch -->
<div th:switch="${user.role}">
    <span th:case="'admin'">管理员</span>
    <span th:case="'user'">用户</span>
    <span th:case="*">其他</span>
</div>

<!-- URL -->
<a th:href="@{/user/{id}(id=${user.id})}">详情</a>
<a th:href="@{/users(role=${role})}">筛选</a>

<!-- 表单 -->
<form th:action="@{/user/save}" th:object="${user}" method="post">
    <input th:field="*{name}" />
</form>

<!-- 局部替换 -->
<div th:insert="~{fragment :: content}">替换内容</div>

<!-- 三元运算符 -->
<span th:text="${user.status ? '启用' : '禁用'}">状态</span>

<!-- 默认值 -->
<span th:text="${user.name} ?: '匿名'">默认值</span>

六、JSON 视图

@RestController 返回 JSON

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

配置 Jackson

yaml
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
    serialization:
      write-dates-as-timestamps: false
      pretty-print: true
    deserialization:
      fail-on-unknown-properties: false

自定义 JSON 视图

java
// 方式1:@JsonView
public class User {
    public interface BasicView {}
    public interface DetailView extends BasicView {}
    
    @JsonView(BasicView.class)
    private Long id;
    
    @JsonView(BasicView.class)
    private String name;
    
    @JsonView(DetailView.class)
    private String email;
}

@RestController
public class UserController {
    
    @GetMapping("/{id}")
    @JsonView(User.BasicView.class)
    public User getBasic(@PathVariable Long id) { ... }
    
    @GetMapping("/{id}/detail")
    @JsonView(User.DetailView.class)
    public User getDetail(@PathVariable Long id) { ... }
}

返回 Map 而非对象

java
@GetMapping("/user/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    return Map.of(
        "id", user.getId(),
        "name", user.getName(),
        "timestamp", System.currentTimeMillis()
    );
}

七、Content Negotiation

客户端 Accept 头

Accept: application/json     → 返回 JSON
Accept: text/html            → 返回 HTML
Accept: application/xml      → 返回 XML

路径后缀

yaml
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true  # ?format=json
GET /users?format=json   → JSON
GET /users?format=xml    → XML

配置多种视图

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true)
            .parameterName("format")
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("html", MediaType.TEXT_HTML);
    }
}

八、错误处理

@ExceptionHandler

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> handleNotFound(UserNotFoundException e) {
        return Map.of(
            "code", 404,
            "message", e.getMessage()
        );
    }
    
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleGeneral(Exception e) {
        return Map.of(
            "code", 500,
            "message", "服务器内部错误"
        );
    }
}

@ResponseStatus

java
// 异常类标注
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

// Controller 方法标注
@GetMapping("/user/{id}")
@ResponseStatus(HttpStatus.OK)
public User getUser(@PathVariable Long id) {
    // ...
}

九、面试高频问题

Q1: Controller 返回值类型有哪些?

  • String:视图名
  • ModelAndView:视图+模型
  • Object:JSON(@ResponseBody)
  • ResponseEntity:完整响应(状态码+Header+Body)
  • void:通过 HttpServletResponse 直接响应

Q2: forward 和 redirect 区别?

  • forward:服务端转发,URL 不变,一次请求
  • redirect:浏览器重定向,URL 改变,两次请求

Q3: @RestController 和 @Controller 区别?

  • @Controller:返回视图
  • @RestController = @Controller + @ResponseBody,所有方法返回 JSON

Q4: 如何返回 XML?

  1. 添加 JAXB 依赖
  2. 对象添加 @XmlRootElement 注解
  3. 请求时 Accept: application/xml

Q5: Thymeleaf 如何获取 Session 属性?

html
<span th:text="${session.user.name}">用户</span>

十、下一章预告

下一章我们将学习 Spring MVC 拦截器与过滤器

  • HandlerInterceptor 拦截器
  • Filter 过滤器
  • 登录拦截
  • CORS 跨域处理

基于 MIT 许可发布