Skip to content

Spring MVC 基础

1. Spring MVC 概述

1.1 什么是 Spring MVC

Spring MVC 是 Spring Framework 的 Web 模块,基于 Model-View-Controller 设计模式实现的 Web 框架。

1.2 MVC 设计模式

组件职责
Model(模型)业务逻辑处理,数据封装
View(视图)页面展示,用户界面
Controller(控制器)请求分发,协调 Model 和 View

1.3 Spring MVC 架构

客户端请求 → DispatcherServlet → HandlerMapping → Controller

客户端响应 ← ViewResolver ← View ← ModelAndView

核心组件:

  • DispatcherServlet:前端控制器,统一请求分发
  • HandlerMapping:处理器映射器,找到对应的 Controller
  • HandlerAdapter:处理器适配器,执行 Controller 方法
  • ViewResolver:视图解析器,解析视图页面
  • ModelAndView:模型和视图的封装对象

2. 快速入门

2.1 添加依赖

xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>7.0.0</version>
</dependency>
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>

2.2 配置 DispatcherServlet

java
public class WebAppInitializer implements WebApplicationInitializer {
    
    @Override
    public void onStartup(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context = 
            new AnnotationConfigWebApplicationContext();
        context.register(WebConfig.class);
        
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = 
            servletContext.addServlet("dispatcher", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }
}

2.3 配置类

java
@Configuration
@EnableWebMvc
@ComponentScan("com.example.controller")
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/");
    }
}

3. 控制器开发

3.1 @Controller 注解

java
@Controller
@RequestMapping("/user")
public class UserController {
    
    @RequestMapping("/list")
    public String list() {
        return "user/list";
    }
}

3.2 @RequestMapping 注解

java
@Controller
@RequestMapping("/api")
public class ApiController {
    
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello() {
        return "hello";
    }
    
    @GetMapping("/users")
    public String getUsers() {
        return "users";
    }
    
    @PostMapping("/users")
    public String createUser() {
        return "redirect:/api/users";
    }
    
    @PutMapping("/users/{id}")
    public String updateUser(@PathVariable Long id) {
        return "redirect:/api/users";
    }
    
    @DeleteMapping("/users/{id}")
    public String deleteUser(@PathVariable Long id) {
        return "redirect:/api/users";
    }
}

3.3 RESTful 控制器

java
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    
    private final ProductService productService;
    
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping
    public List<Product> getAll() {
        return productService.findAll();
    }
    
    @GetMapping("/{id}")
    public Product getById(@PathVariable Long id) {
        return productService.findById(id);
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Product create(@RequestBody Product product) {
        return productService.save(product);
    }
    
    @PutMapping("/{id}")
    public Product update(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        return productService.save(product);
    }
    
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        productService.deleteById(id);
    }
}

4. 请求参数处理

4.1 基本参数

java
@GetMapping("/search")
public String search(@RequestParam String keyword,
                     @RequestParam(defaultValue = "1") int page,
                     @RequestParam(required = false) String category) {
    return "search";
}

4.2 路径变量

java
@GetMapping("/products/{category}/{id}")
public String getProduct(@PathVariable String category,
                         @PathVariable Long id) {
    return "product";
}

@GetMapping("/users/{userId}/posts/{postId}")
public String getPost(@PathVariable Long userId,
                      @PathVariable Long postId) {
    return "post";
}

4.3 请求体

java
@PostMapping("/users")
public User createUser(@RequestBody User user) {
    return userService.save(user);
}

4.4 请求头

java
@GetMapping("/info")
public String getInfo(@RequestHeader("User-Agent") String userAgent,
                      @RequestHeader(value = "X-Token", required = false) String token) {
    return "info";
}
java
@GetMapping("/profile")
public String getProfile(@CookieValue(value = "sessionId", required = false) 
                         String sessionId) {
    return "profile";
}

4.6 表单对象

java
@PostMapping("/register")
public String register(@ModelAttribute UserForm form) {
    userService.register(form);
    return "redirect:/login";
}

public record UserForm(
    String username,
    String password,
    String email
) {}

5. 响应处理

5.1 返回视图

java
@GetMapping("/home")
public String home() {
    return "home";
}

@GetMapping("/profile")
public String profile(Model model) {
    model.addAttribute("user", userService.getCurrentUser());
    return "profile";
}

5.2 返回 JSON

java
@GetMapping("/api/user")
@ResponseBody
public User getUser() {
    return userService.getCurrentUser();
}

@RestController
public class ApiController {
    @GetMapping("/api/data")
    public Map<String, Object> getData() {
        return Map.of("code", 200, "data", "success");
    }
}

5.3 ResponseEntity

java
@GetMapping("/download")
public ResponseEntity<Resource> download() {
    Resource resource = new ClassPathResource("files/document.pdf");
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename=document.pdf")
            .body(resource);
}

@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
    User saved = userService.save(user);
    URI location = URI.create("/api/users/" + saved.getId());
    return ResponseEntity.created(location).body(saved);
}

6. 数据验证

6.1 添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

6.2 验证注解

java
public record UserRequest(
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20位")
    String username,
    
    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
             message = "密码至少8位,包含字母和数字")
    String password,
    
    @Email(message = "邮箱格式不正确")
    String email,
    
    @Min(value = 18, message = "年龄必须大于等于18")
    @Max(value = 100, message = "年龄必须小于等于100")
    Integer age
) {}

6.3 控制器验证

java
@PostMapping("/users")
public ResponseEntity<?> createUser(
        @Valid @RequestBody UserRequest request,
        BindingResult result) {
    
    if (result.hasErrors()) {
        Map<String, String> errors = new HashMap<>();
        result.getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }
    
    return ResponseEntity.ok(userService.create(request));
}

@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(
        @PathVariable Long id,
        @Valid @RequestBody UserRequest request) {
    return ResponseEntity.ok(userService.update(id, request));
}

7. 异常处理

7.1 @ExceptionHandler

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            e.getMessage(),
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage()));
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "参数验证失败",
            errors
        );
        return ResponseEntity.badRequest().body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "服务器内部错误",
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

public record ErrorResponse(
    int status,
    String message,
    Object data
) {}

7.2 @ResponseStatus

java
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "资源未找到")
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

8. 拦截器

8.1 定义拦截器

java
public class LoginInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response,
                            Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response,
                          Object handler,
                          ModelAndView modelAndView) throws Exception {
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request,
                               HttpServletResponse response,
                               Object handler,
                               Exception ex) throws Exception {
    }
}

8.2 注册拦截器

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/auth/**", "/api/public/**");
    }
}

9. 文件上传下载

9.1 文件上传

java
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return "文件为空";
    }
    
    String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
    Path path = Paths.get("uploads/" + fileName);
    
    try {
        Files.createDirectories(path.getParent());
        Files.copy(file.getInputStream(), path);
        return "上传成功: " + fileName;
    } catch (IOException e) {
        return "上传失败: " + e.getMessage();
    }
}

9.2 多文件上传

java
@PostMapping("/uploads")
public String uploadMultiple(@RequestParam("files") MultipartFile[] files) {
    List<String> fileNames = new ArrayList<>();
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
            Path path = Paths.get("uploads/" + fileName);
            try {
                Files.copy(file.getInputStream(), path);
                fileNames.add(fileName);
            } catch (IOException e) {
                return "上传失败";
            }
        }
    }
    
    return "上传成功: " + String.join(", ", fileNames);
}

9.3 文件下载

java
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) {
    Path path = Paths.get("uploads/" + fileName);
    Resource resource = new FileSystemResource(path);
    
    if (!resource.exists()) {
        return ResponseEntity.notFound().build();
    }
    
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + fileName + "\"")
            .body(resource);
}

10. Spring 7 新特性

10.1 HTTP Service Clients

Spring 7 引入了声明式 HTTP 客户端:

java
@HttpServiceClient
public interface UserClient {
    
    @GetExchange("/users/{id}")
    User getUser(@PathVariable Long id);
    
    @PostExchange("/users")
    User createUser(@RequestBody User user);
    
    @GetExchange("/users")
    List<User> getAllUsers();
}

@Configuration
public class HttpConfig {
    
    @Bean
    public UserClient userClient() {
        return HttpServiceClientProxyFactory.builder()
                .baseUrl("https://api.example.com")
                .build(UserClient.class);
    }
}

10.2 API 版本控制

java
@RestController
@RequestMapping("/api")
public class VersionedController {
    
    @GetMapping(value = "/users", headers = "X-API-Version=1")
    public List<UserV1> getUsersV1() {
        return userService.findAllV1();
    }
    
    @GetMapping(value = "/users", headers = "X-API-Version=2")
    public List<UserV2> getUsersV2() {
        return userService.findAllV2();
    }
}

11. 最佳实践

11.1 统一响应格式

java
public record ApiResponse<T>(
    int code,
    String message,
    T data,
    long timestamp
) {
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
    }
    
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null, System.currentTimeMillis());
    }
}

@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public ApiResponse<User> getUser(@PathVariable Long id) {
        return ApiResponse.success(userService.findById(id));
    }
}

11.2 分页查询

java
@GetMapping("/users")
public Page<User> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id,asc") String sort) {
    
    Pageable pageable = PageRequest.of(page, size, Sort.by(sort).ascending());
    return userService.findAll(pageable);
}

12. 小结

本章学习了 Spring MVC 的核心概念和开发技能:

内容要点
架构DispatcherServlet、HandlerMapping、ViewResolver
控制器@Controller、@RestController、@RequestMapping
参数处理@RequestParam、@PathVariable、@RequestBody、@RequestHeader
响应处理视图返回、JSON返回、ResponseEntity
数据验证@Valid、@NotBlank、@Size、@Pattern
异常处理@ExceptionHandler、@RestControllerAdvice
拦截器HandlerInterceptor、preHandle、postHandle
文件操作MultipartFile、上传、下载

下一章将学习 Spring MVC 的高级特性。