Spring MVC

发布于 2026-07-30 19:35 更新于 2026-07-30 19:35 1732 字 9 min read ... 访问量

本文详细介绍了 Spring MVC 的核心执行流程及关键组件职责,包括前端控制器 DispatcherServlet、请求映射、参数绑定、视图跳转、静态资源处理、前后端分离开发中的 JSON 处理、跨域配置、文件上传等核心功能。同时强调了参数绑定的规范、安全实践以及在实际项目中使用注解和配置的最佳实践。

Spring MVC

image-001
image-001

核心执行流程

Spring MVC 处理请求时,主要组件及职责如下:

  • DispatcherServlet:前端控制器,统一接收请求并协调后续处理流程。
  • HandlerMapping:根据请求地址查找对应的处理器方法。
  • HandlerAdapter:以统一方式调用处理器,并完成参数解析等工作。
  • Handler:实际处理请求的控制器方法。
  • ModelAndView:同时封装模型数据和逻辑视图名。
  • ViewResolver:把逻辑视图名解析为具体视图。
  • View:负责渲染响应内容。

搭建传统 Spring MVC 项目

创建 Maven Web 工程

引入 Spring MVC 依赖。以下版本仅对应原课程示例环境:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.4.RELEASE</version>
</dependency>

配置 DispatcherServlet

web.xml 中注册前端控制器:

<servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

/ 表示让 DispatcherServlet 处理除容器内部特殊请求之外的大部分请求,同时仍可由默认 Servlet 处理静态资源。通常不建议配置为 /*,因为它可能影响 JSP 转发等容器行为。

创建 Spring MVC 配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/mvc
           https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.hyxy.controller"/>
    <mvc:annotation-driven/>
</beans>

创建控制器

@Controller
public class TestController {
    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        return "test";
    }
}

启动服务器后访问 /test,响应体中会返回 test

请求映射

@RequestMapping

@RequestMapping 可以修饰类和方法。修饰类时通常表示统一的地址前缀;修饰方法时表示具体请求映射。

常用属性包括:

  • valuepath:请求地址。
  • method:允许的请求方法。
  • params:要求请求中包含或不包含指定参数。
  • headers:根据请求头进一步限制映射。
  • consumes:限制请求体的媒体类型。
  • produces:声明响应媒体类型。
@Controller
@RequestMapping("/test")
public class TestController {
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @ResponseBody
    public String add() {
        return "add";
    }
}

在实际项目中,也可使用语义更明确的组合注解,例如 @GetMapping@PostMapping@PutMapping@DeleteMapping

请求参数绑定

绑定简单参数

请求参数名与方法参数名一致时,Spring MVC 可以自动绑定。若编译时未保留参数名,或希望显式指定参数名称,应使用 @RequestParam

@GetMapping("/user")
@ResponseBody
public String getUser(
        @RequestParam(name = "name") String username,
        @RequestParam(name = "age", required = false, defaultValue = "0") Integer age) {
    return username + ":" + age;
}

使用基本类型接收可选参数时,参数缺失会导致类型转换失败,因此可选数字参数一般使用包装类型。

绑定路径变量

@GetMapping("/add/{name}/{pwd}")
@ResponseBody
public String add(
        @PathVariable("name") String username,
        @PathVariable("pwd") String password) {
    return username + ":" + password;
}

例如访问 http://localhost:8088/mvc/test/add/tom/123username 的值为 tompassword 的值为 123

@GetMapping("/header")
@ResponseBody
public String header(
        @RequestHeader("Accept") String accept,
        @CookieValue(value = "username", defaultValue = "rose") String username) {
    return accept + ":" + username;
}

绑定 Java 对象

请求参数名与对象属性名一致时,可以把参数绑定到对象中。

@PostMapping("/emp")
@ResponseBody
public Emp add(Emp emp) {
    return emp;
}

对于关联对象,可使用属性路径传参。例如 Emp 中包含 Dept dept 时,请求参数可写成 dept.deptName=开发部

public class Emp {
    private String empNo;
    private String empName;
    private Dept dept;

    // getter 和 setter
}

public class Dept {
    private String deptName;

    // getter 和 setter
}

获取 Servlet API 对象

需要访问原始请求或会话时,可直接在控制器方法参数中声明。

@GetMapping("/request-info")
@ResponseBody
public String requestInfo(HttpServletRequest request, HttpSession session) {
    session.setAttribute("method", request.getMethod());
    return request.getMethod();
}

业务代码不应过度依赖 Servlet API。能通过注解和参数绑定完成的场景,应优先使用 Spring MVC 的参数解析机制。

视图跳转与模型数据

返回逻辑视图名

@GetMapping("/books")
public String books(Model model) {
    model.addAttribute("bookList", List.of("Java", "Spring"));
    return "book/list";
}

配合视图解析器时,book/list 会被解析为具体页面路径。

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"/>
    <property name="suffix" value=".jsp"/>
</bean>

返回 ModelAndView

@GetMapping("/detail")
public ModelAndView detail() {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("name", "Spring MVC");
    modelAndView.setViewName("book/detail");
    return modelAndView;
}

静态资源处理

可以显式配置静态资源映射:

<mvc:annotation-driven/>
<mvc:resources mapping="/html/**" location="/html/"/>

也可使用默认 Servlet 处理未匹配的静态资源:

<mvc:default-servlet-handler/>

前后端分离开发

@RequestBody

@RequestBody 会读取请求体,并通过消息转换器把 JSON 转换为 Java 对象。请求头通常应包含 Content-Type: application/json

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.14.2</version>
</dependency>
@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/emp")
    public Emp save(@RequestBody Emp emp) {
        return emp;
    }
}
image-002
image-002

JSON 字段名应与 Java 属性名匹配。字段命名不一致时,可以使用 Jackson 注解进行映射。

@ResponseBody

@ResponseBody 表示把方法返回值写入响应体。返回对象时,Spring MVC 会通过消息转换器将其序列化为 JSON;返回字符串时,通常直接写入文本内容。

@GetMapping("/get")
@ResponseBody
public User get() {
    User user = new User();
    user.setUsername("jack");
    user.setPassword("123456");
    return user;
}

@RestController

@RestController 相当于类级别的 @Controller@ResponseBody 组合,适合以 JSON 为主要响应内容的控制器。

统一响应对象

public class Result<T> {
    private boolean success;
    private T data;
    private String message;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.success = true;
        result.data = data;
        return result;
    }

    public static <T> Result<T> failure(String message) {
        Result<T> result = new Result<>();
        result.success = false;
        result.message = message;
        return result;
    }

    // getter 和 setter
}

跨域请求

浏览器的同源策略会限制脚本访问不同源的资源。源由协议、主机和端口共同确定,任意一项不同都属于跨源请求。

使用 @CrossOrigin

@RestController
@RequestMapping("/book")
@CrossOrigin(origins = "http://localhost:8081")
public class BookController {
}

全局配置 CORS

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:8081")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

开启凭据时,不能把允许来源简单配置为通配符。生产环境应只开放实际需要的来源、方法和请求头。

前端代理与 Nginx 反向代理

开发环境可以通过前端开发服务器代理接口请求;生产环境通常使用 Nginx 将前端静态资源和后端接口统一到同一站点下。

image-003
image-003
image-004
image-004

反向代理只改变请求转发方式,不会替代后端自身的鉴权、权限校验和输入校验。

文件上传

配置文件解析器

在传统 Spring MVC 项目中,可配置 CommonsMultipartResolver

<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="5242880"/>
    <property name="defaultEncoding" value="UTF-8"/>
</bean>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

表单应使用 POST 请求,并设置 enctype="multipart/form-data"

编写上传接口

@RestController
public class FileUploadController {
    private static final Path UPLOAD_DIR = Paths.get("D:/Lesson/uploadFile");

    @PostMapping("/upload")
    public Result<String> upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return Result.failure("上传文件不能为空");
        }

        String originalName = file.getOriginalFilename();
        String safeName = originalName == null
                ? UUID.randomUUID().toString()
                : Paths.get(originalName).getFileName().toString();

        try {
            Files.createDirectories(UPLOAD_DIR);
            Path target = UPLOAD_DIR.resolve(safeName).normalize();
            if (!target.startsWith(UPLOAD_DIR)) {
                return Result.failure("文件名不合法");
            }

            file.transferTo(target);
            return Result.success(safeName);
        } catch (IOException e) {
            return Result.failure(e.getMessage());
        }
    }
}

不要直接信任客户端提交的原始文件名。实际项目还应检查文件大小、扩展名、MIME 类型和存储权限,并避免把上传目录暴露为可执行目录。

image-005
image-005

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka