Spring Boot Web MVC配置详解

0 阅读8分钟

一、Spring MVC概述

1.1 什么是Spring MVC?

Spring MVC是Spring框架提供的Web层解决方案,基于经典的MVC(Model-View-Controller)设计模式。它通过将业务逻辑、数据和界面显示分离,使Web应用的开发更加清晰和模块化。

1.2 Spring MVC核心组件

┌─────────────────────────────────────────────────────────────────┐
│                    Spring MVC 核心架构                           │
└─────────────────────────────────────────────────────────────────┘
 
                    HTTP请求
                       │
                       ▼
           ┌─────────────────────┐
           │  DispatcherServlet  │  ← 前端控制器(核心)
           │    (核心调度器)      │
           └──────────┬──────────┘
                      │
        ┌─────────────┼─────────────┐
        │             │             │
        ▼             ▼             ▼
┌───────────┐  ┌───────────┐  ┌───────────┐
│HandlerMap │  │ Handler   │  │ ViewResolver│
│  ping     │  │ Adapter   │  │ (视图解析器)│
│(处理器映射)│  │(处理器适配器)│  └───────────┘
└─────┬─────┘  └─────┬─────┘
      │              │
      ▼              ▼
┌───────────┐  ┌───────────┐
│Controller │  │  Model    │
│(控制器)   │  │ (模型数据) │
└───────────┘  └───────────┘
                      │
                      ▼
              ┌───────────┐
              │   View    │
              │  (视图)   │
              └───────────┘
                      │
                      ▼
                   HTTP响应

1.3 核心组件职责

组件职责
DispatcherServlet前端控制器,接收请求并分发到相应的处理器
HandlerMapping处理器映射器,根据请求URL找到对应的Controller
HandlerAdapter处理器适配器,执行Controller方法
Controller控制器,处理业务逻辑
ModelAndView封装模型数据和视图信息
ViewResolver视图解析器,解析视图名称到具体视图
View视图,渲染响应结果

1.4 请求处理流程

二、自动配置原理

2.1 WebMvcAutoConfiguration

Spring Boot通过WebMvcAutoConfiguration自动配置Spring MVC:

@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebMvcAutoConfiguration {
    
    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({ WebMvcProperties.class, 
        WebProperties.class, HttpProperties.class })
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter 
            implements WebMvcConfigurer, ResourceLoaderAware {
        
        // 自动配置内容...
    }
}

2.2 自动配置内容

配置项默认值
静态资源路径/static/public/resources/META-INF/resources
视图解析器ContentNegotiatingViewResolver
消息转换器Jackson(JSON)、StringHttpMessageConverter等
格式化器NumberFormat、DateFormat
静态首页index.html
faviconfavicon.ico

2.3 配置属性类

@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    
    private String staticPathPattern = "/**";
    private boolean dispatchOptionsRequest = false;
    private boolean dispatchTraceRequest = false;
    private boolean logResolvedException = false;
    private String viewPrefix;
    private String viewSuffix;
    private String servletPath = "/";
    // ...
}
 
@ConfigurationProperties(prefix = "spring.web")
public class WebProperties {
    
    private Resources resources = new Resources();
    
    public static class Resources {
        private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
        private boolean addMappings = true;
        private boolean cache = true;
        private Cache cacheProperties;
        // ...
    }
}

三、基本配置

3.1 服务器配置

server:
  port: 8080                          # 端口号
  servlet:
    context-path: /api                # 上下文路径
    session:
      timeout: 30m                    # Session超时时间
  tomcat:
    uri-encoding: UTF-8               # URI编码
    max-threads: 200                  # 最大线程数
    min-spare-threads: 10             # 最小空闲线程数
    accept-count: 100                 # 等待队列长度
    max-connections: 10000            # 最大连接数
  ssl:
    enabled: true                     # 启用SSL
    key-store: classpath:keystore.p12
    key-store-password: your-password
    key-store-type: PKCS12
    key-alias: tomcat

3.2 Spring MVC配置

spring:
  mvc:
    servlet:
      path: /                         # DispatcherServlet路径
    static-path-pattern: /static/**   # 静态资源访问路径
    view:
      prefix: /WEB-INF/views/         # 视图前缀
      suffix: .jsp                    # 视图后缀
    contentnegotiation:
      favor-parameter: true           # 支持参数决定返回格式
      parameter-name: format          # 参数名
    format:
      date: yyyy-MM-dd                # 日期格式
      date-time: yyyy-MM-dd HH:mm:ss  # 日期时间格式
      time: HH:mm:ss                  # 时间格式
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/
      cache:
        period: 3600                  # 缓存时间(秒)

3.3 编码配置

server:
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true

四、视图解析器配置

4.1 Thymeleaf配置

添加依赖

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

配置文件

spring:
  thymeleaf:
    prefix: classpath:/templates/     # 模板路径
    suffix: .html                     # 模板后缀
    mode: HTML                        # 模板模式
    encoding: UTF-8                   # 编码
    cache: false                      # 开发环境关闭缓存
    servlet:
      content-type: text/html;charset=UTF-8

使用示例

@Controller
public class UserController {
    
    @GetMapping("/users")
    public String userList(Model model) {
        model.addAttribute("users", userService.findAll());
        return "user/list";  // 对应 templates/user/list.html
    }
}

4.2 FreeMarker配置

添加依赖

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

配置文件

spring:
  freemarker:
    template-loader-path: classpath:/templates/
    suffix: .ftl
    content-type: text/html;charset=UTF-8
    cache: false
    settings:
      number_format: 0.##########
      datetime_format: yyyy-MM-dd HH:mm:ss

4.3 视图解析器对比

特性ThymeleafFreeMarkerJSP
模板文件.html.ftl.jsp
自然模板✅ 支持❌ 不支持❌ 不支持
Spring集成✅ 官方推荐✅ 支持✅ 支持
性能中等
学习曲线平缓中等平缓
热部署✅ 支持✅ 支持❌ 需重启

五、拦截器配置

5.1 自定义拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {
    
    private static final Logger log = LoggerFactory.getLogger(LoginInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
            Object handler) throws Exception {
        log.info("请求路径: {}", request.getRequestURI());
        
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
            return false;
        }
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
            Object handler, ModelAndView modelAndView) throws Exception {
        log.info("请求处理完成");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
            Object handler, Exception ex) throws Exception {
        log.info("请求结束");
    }
}

5.2 注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private LoginInterceptor loginInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                    "/login",
                    "/register",
                    "/static/**",
                    "/error",
                    "/swagger-ui/**",
                    "/v3/api-docs/**"
                )
                .order(1);
    }
}

5.3 多拦截器执行顺序

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 按order值从小到大执行preHandle,从大到小执行postHandle和afterCompletion
        registry.addInterceptor(new FirstInterceptor())
                .addPathPatterns("/**")
                .order(1);
        
        registry.addInterceptor(new SecondInterceptor())
                .addPathPatterns("/**")
                .order(2);
    }
}

执行流程

请求 → FirstInterceptor.preHandle() 
     → SecondInterceptor.preHandle() 
     → Controller 
     → SecondInterceptor.postHandle() 
     → FirstInterceptor.postHandle() 
     → 视图渲染 
     → SecondInterceptor.afterCompletion() 
     → FirstInterceptor.afterCompletion() 
     → 响应

六、消息转换器配置

6.1 自定义Jackson配置

@Configuration
public class JacksonConfig {
    
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
            builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
            builder.featuresToDisable(
                SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                SerializationFeature.FAIL_ON_EMPTY_BEANS,
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
            );
            builder.featuresToEnable(
                JsonInclude.Include.NON_NULL,
                JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS
            );
            builder.serializerByType(LocalDateTime.class, 
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            builder.deserializerByType(LocalDateTime.class, 
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        };
    }
}

6.2 自定义消息转换器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        converter.setObjectMapper(objectMapper);
        converters.add(0, converter);
    }
    
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new CustomMessageConverter());
    }
}

6.3 常用消息转换器

转换器用途
StringHttpMessageConverter处理String类型
MappingJackson2HttpMessageConverter处理JSON
ByteArrayHttpMessageConverter处理字节数组
FormHttpMessageConverter处理表单数据
ResourceHttpMessageConverter处理资源文件
AllEncompassingFormHttpMessageConverter处理多部分表单

七、静态资源处理

7.1 默认静态资源路径

Spring Boot默认从以下路径加载静态资源:

classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/

7.2 自定义静态资源路径

spring:
  web:
    resources:
      static-locations: 
        - classpath:/static/
        - classpath:/public/
        - file:/data/files/
      cache:
        period: 31536000           # 缓存1年
        cachecontrol:
          max-age: 31536000
          public: true
  mvc:
    static-path-pattern: /static/**

7.3 代码配置

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
        
        registry.addResourceHandler("/upload/**")
                .addResourceLocations("file:/data/upload/");
        
        registry.addResourceHandler("/swagger-ui/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
    }
}

7.4 静态资源版本控制

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        VersionResourceResolver versionResolver = new VersionResourceResolver()
                .addContentVersionStrategy("/**");
        
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
                .resourceChain(true)
                .addResolver(versionResolver);
    }
}

使用方式:

<link rel="stylesheet" href="/static/css/style-abc123.css">

八、文件上传下载

8.1 文件上传配置

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB              # 单个文件最大大小
      max-request-size: 100MB          # 总请求最大大小
      file-size-threshold: 2KB         # 超过此大小写入临时文件
      location: /tmp/upload            # 临时文件存储位置

8.2 单文件上传

@RestController
@RequestMapping("/api/files")
public class FileUploadController {
    
    private static final String UPLOAD_DIR = "/data/upload/";
    
    @PostMapping("/upload")
    public Result<String> upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return Result.error("文件不能为空");
        }
        
        String originalFilename = file.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        String newFilename = UUID.randomUUID().toString() + extension;
        
        File dest = new File(UPLOAD_DIR + newFilename);
        try {
            file.transferTo(dest);
            return Result.success("/api/files/download/" + newFilename);
        } catch (IOException e) {
            return Result.error("文件上传失败: " + e.getMessage());
        }
    }
}

8.3 多文件上传

@PostMapping("/upload/multiple")
public Result<List<String>> uploadMultiple(@RequestParam("files") MultipartFile[] files) {
    List<String> urls = new ArrayList<>();
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            String url = saveFile(file);
            urls.add(url);
        }
    }
    
    return Result.success(urls);
}

8.4 文件下载

@GetMapping("/download/{filename}")
public void download(@PathVariable String filename, HttpServletResponse response) {
    File file = new File(UPLOAD_DIR + filename);
    
    if (!file.exists()) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }
    
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition", 
        "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
    response.setContentLengthLong(file.length());
    
    try (InputStream is = new FileInputStream(file);
         OutputStream os = response.getOutputStream()) {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        os.flush();
    } catch (IOException e) {
        log.error("文件下载失败", e);
    }
}

8.5 文件上传进度监听

@Component
public class FileUploadProgressListener {
    
    private final Map<String, Double> progressMap = new ConcurrentHashMap<>();
    
    public void updateProgress(String fileId, double progress) {
        progressMap.put(fileId, progress);
    }
    
    public double getProgress(String fileId) {
        return progressMap.getOrDefault(fileId, 0.0);
    }
}
 
@RestController
public class UploadProgressController {
    
    @PostMapping("/upload/progress")
    public Result<String> uploadWithProgress(
            @RequestParam("file") MultipartFile file,
            @RequestParam("fileId") String fileId) {
        
        ProgressTrackingInputStream progressStream = 
            new ProgressTrackingInputStream(file.getInputStream(), 
                progress -> progressListener.updateProgress(fileId, progress));
        
        // 保存文件...
        
        return Result.success("上传成功");
    }
    
    @GetMapping("/upload/progress/{fileId}")
    public Result<Double> getProgress(@PathVariable String fileId) {
        return Result.success(progressListener.getProgress(fileId));
    }
}

九、全局CORS配置

9.1 @CrossOrigin注解

@RestController
@RequestMapping("/api/users")
@CrossOrigin(
    origins = "http://localhost:3000",
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT},
    allowedHeaders = "*",
    allowCredentials = "true",
    maxAge = 3600
)
public class UserController {
    
    @GetMapping
    public List<User> list() {
        return userService.findAll();
    }
}

9.2 全局CORS配置

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

9.3 CorsFilter方式

@Configuration
public class CorsFilterConfig {
    
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

9.4 CORS配置对比

方式适用场景优先级
@CrossOrigin单个Controller或方法最高
WebMvcConfigurer.addCorsMappings全局配置,推荐使用
CorsFilter需要更细粒度控制时可配置

十、最佳实践与总结

10.1 配置最佳实践

场景建议
开发环境关闭模板缓存、开启详细日志
生产环境开启静态资源缓存、压缩响应
文件上传限制文件大小、校验文件类型
拦截器合理设置排除路径、注意执行顺序
CORS使用白名单、不要使用*

10.2 性能优化建议

spring:
  mvc:
    async:
      request-timeout: 30000          # 异步请求超时
  web:
    resources:
      cache:
        period: 31536000              # 静态资源缓存
      chain:
        enabled: true                 # 启用资源链
        compressed: true              # 启用压缩
server:
  compression:
    enabled: true                     # 启用响应压缩
    mime-types: text/html,text/xml,text/plain,application/json
    min-response-size: 1024           # 最小压缩大小

10.3 常见问题

问题一:静态资源无法访问

// 检查是否被拦截器拦截
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns("/static/**");  // 排除静态资源
}

问题二:中文乱码

@Bean
public FilterRegistrationBean<CharacterEncodingFilter> encodingFilter() {
    FilterRegistrationBean<CharacterEncodingFilter> registration = 
        new FilterRegistrationBean<>();
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding("UTF-8");
    filter.setForceEncoding(true);
    registration.setFilter(filter);
    registration.addUrlPatterns("/*");
    return registration;
}

问题三:跨域请求失败

// 确保allowCredentials为true时,allowedOrigins不能为"*"
@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowedOriginPatterns("*")  // 使用allowedOriginPatterns
            .allowCredentials(true);
}

10.4 总结

Spring Boot Web MVC配置要点:

配置项关键点
自动配置理解WebMvcAutoConfiguration的工作原理
基本配置通过application.yml配置服务器和MVC属性
视图解析Thymeleaf是Spring Boot推荐的模板引擎
拦截器实现HandlerInterceptor并注册
消息转换自定义Jackson配置处理日期和JSON
静态资源配置资源路径和缓存策略
文件上传配置大小限制,使用MultipartFile
CORS使用全局配置或注解方式

掌握这些配置,能够帮助开发者灵活应对各种Web开发场景,构建高效、安全的Web应用。


参考资料:

Spring Boot Web MVC配置详解 | 拦截器、视图解析、静态资源与CORS完整指南