Skip to content

Spring Boot 中的 Spring MVC 实战指南 🚀

前言:为什么需要了解 Spring MVC?

在现代 Web 开发中,我们经常需要构建 RESTful API 来提供数据服务。想象一下,如果没有 Spring MVC 这样的框架,我们需要手动处理 HTTP 请求解析、响应格式转换、错误处理等繁琐工作。Spring Boot 通过自动配置让 Spring MVC 的使用变得极其简单,让开发者能够专注于业务逻辑而非基础设施。

NOTE

Spring Boot 包含多个包含 Spring MVC 的 starter,有些是直接包含,有些是作为依赖引入。本文将解答关于 Spring MVC 和 Spring Boot 的常见问题。

1. 构建 JSON REST 服务 ⚙️

核心原理

Spring Boot 的魅力在于"约定优于配置"。当 Jackson2 在 classpath 中时,任何标注了 @RestController 的类都会自动将返回值序列化为 JSON 格式。

基础实现

kotlin
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class MyController {

    @RequestMapping("/thing") 
    fun thing(): MyThing {
        return MyThing()
    }
}

// 数据类
data class MyThing( 
    val name: String = "示例数据",
    val value: Int = 42,
    val timestamp: Long = System.currentTimeMillis()
)

TIP

只要 MyThing 能被 Jackson2 序列化(普通 POJO 或 Kotlin data class 都可以),访问 localhost:8080/thing 就会返回 JSON 格式的响应。

实际业务场景

kotlin
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {

    @GetMapping("/{id}")
    fun getUserById(@PathVariable id: Long): UserResponse { 
        val user = userService.findById(id)
        return UserResponse(
            id = user.id,
            username = user.username,
            email = user.email,
            createdAt = user.createdAt
        )
    }

    @PostMapping
    fun createUser(@RequestBody request: CreateUserRequest): UserResponse { 
        val user = userService.create(request)
        return UserResponse.from(user)
    }
}
kotlin
data class UserResponse(
    val id: Long,
    val username: String,
    val email: String,
    val createdAt: LocalDateTime
) {
    companion object {
        fun from(user: User): UserResponse { 
            return UserResponse(
                id = user.id,
                username = user.username,
                email = user.email,
                createdAt = user.createdAt
            )
        }
    }
}

请求响应流程

2. 构建 XML REST 服务 📄

两种实现方式

Spring Boot 支持两种 XML 序列化方式:Jackson XML 扩展和 JAXB。

方式一:Jackson XML 扩展

xml
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

方式二:JAXB 注解

kotlin
import jakarta.xml.bind.annotation.XmlRootElement

@XmlRootElement
data class MyThing(
    var name: String? = null,
    var value: Int = 0
)
xml
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
</dependency>

WARNING

要让服务器返回 XML 而非 JSON,需要发送 Accept: text/xml 请求头,或者在浏览器中访问(浏览器通常偏好 XML)。

实际应用场景

kotlin
@RestController
@RequestMapping("/api/reports")
class ReportController {

    @GetMapping(value = ["/sales"], produces = ["application/xml", "application/json"]) 
    fun getSalesReport(@RequestParam month: String): SalesReport {
        return SalesReport(
            month = month,
            totalSales = 150000.0,
            itemCount = 1250
        )
    }
}

@XmlRootElement
data class SalesReport( 
    var month: String = "",
    var totalSales: Double = 0.0,
    var itemCount: Int = 0
)

3. 自定义 Jackson ObjectMapper 🔧

为什么需要自定义?

默认的 Jackson 配置可能无法满足特定业务需求,比如:

  • 日期格式化方式
  • 空值处理策略
  • 属性命名规则
  • 序列化特性控制

Spring Boot 的默认配置

Spring Boot 自动配置的 ObjectMapper 具有以下特性:

特性状态说明
DEFAULT_VIEW_INCLUSION禁用不包含默认视图
FAIL_ON_UNKNOWN_PROPERTIES禁用忽略未知属性
WRITE_DATES_AS_TIMESTAMPS禁用日期不以时间戳格式输出
WRITE_DURATIONS_AS_TIMESTAMPS禁用持续时间不以时间戳格式输出

通过配置文件自定义

properties
# 启用美化输出
spring.jackson.serialization.indent_output=true
# 忽略空值
spring.jackson.default-property-inclusion=non_null
# 日期格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
yaml
spring:
  jackson:
    serialization:
      indent_output: true
    default-property-inclusion: non_null
    date-format: "yyyy-MM-dd HH:mm:ss"

编程方式自定义

kotlin
@Configuration
class JacksonConfig {

    @Bean
    @Primary
    fun objectMapper(): ObjectMapper { 
        return Jackson2ObjectMapperBuilder()
            .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .build()
    }

    @Bean
    fun jackson2ObjectMapperBuilderCustomizer(): Jackson2ObjectMapperBuilderCustomizer { 
        return Jackson2ObjectMapperBuilderCustomizer { builder ->
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            builder.serializationInclusion(JsonInclude.Include.NON_NULL)
        }
    }
}

自定义模块示例

kotlin
@Component
class CustomJacksonModule : SimpleModule() { 
    
    init {
        // 自定义 LocalDateTime 序列化
        addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer())
        addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer())
    }
}

class LocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
    override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) 
    }
}

IMPORTANT

任何 Module 类型的 Bean 都会自动注册到 Jackson2ObjectMapperBuilder,这提供了一个全局机制来为应用程序添加自定义功能。

4. 自定义 @ResponseBody 渲染 🎨

核心概念

Spring 使用 HttpMessageConverters 来渲染 @ResponseBody@RestController 的响应。理解这个机制有助于我们自定义响应格式。

添加自定义转换器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) { 
        // 添加自定义 CSV 转换器
        converters.add(CsvHttpMessageConverter())
    }
}

class CsvHttpMessageConverter : AbstractHttpMessageConverter<Any>(MediaType.parseMediaType("text/csv")) {

    override fun supports(clazz: Class<*>): Boolean {
        return List::class.java.isAssignableFrom(clazz) 
    }

    override fun readInternal(clazz: Class<out Any>, inputMessage: HttpInputMessage): Any {
        // CSV 读取逻辑
        throw UnsupportedOperationException("CSV reading not implemented")
    }

    override fun writeInternal(t: Any, outputMessage: HttpOutputMessage) { 
        if (t is List<*>) {
            val csvContent = convertToCsv(t)
            outputMessage.body.write(csvContent.toByteArray())
        }
    }

    private fun convertToCsv(data: List<*>): String {
        // 简单的 CSV 转换逻辑
        return data.joinToString("\n") { item ->
            // 假设对象有 toString 方法或反射获取属性
            item.toString()
        }
    }
}

实际业务场景

kotlin
@RestController
@RequestMapping("/api/export")
class ExportController {

    @GetMapping(value = ["/users"], produces = ["text/csv", "application/json"]) 
    fun exportUsers(): List<UserExportDto> {
        return listOf(
            UserExportDto("张三", "[email protected]", "2023-01-15"),
            UserExportDto("李四", "[email protected]", "2023-02-20")
        )
    }
}

data class UserExportDto(
    val name: String,
    val email: String,
    val joinDate: String
)

TIP

客户端通过设置不同的 Accept 头部可以获取不同格式的响应:

  • Accept: application/json → JSON 格式
  • Accept: text/csv → CSV 格式

5. 处理文件上传 📥

默认配置

Spring Boot 默认配置:

  • 单个文件最大 1MB
  • 单次请求最大 10MB
  • 使用 Servlet 5 的 Part API

自定义文件上传配置

properties
# 单个文件大小限制
spring.servlet.multipart.max-file-size=10MB
# 总请求大小限制
spring.servlet.multipart.max-request-size=50MB
# 临时文件存储位置
spring.servlet.multipart.location=/tmp
# 文件写入磁盘的阈值
spring.servlet.multipart.file-size-threshold=2KB
yaml
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB
      location: /tmp
      file-size-threshold: 2KB

文件上传实现

kotlin
@RestController
@RequestMapping("/api/files")
class FileUploadController {

    @PostMapping("/upload")
    fun uploadFile(@RequestParam("file") file: MultipartFile): UploadResponse { 
        
        // 文件验证
        if (file.isEmpty) {
            throw IllegalArgumentException("文件不能为空") 
        }
        
        // 文件类型检查
        val allowedTypes = setOf("image/jpeg", "image/png", "application/pdf")
        if (file.contentType !in allowedTypes) {
            throw IllegalArgumentException("不支持的文件类型: ${file.contentType}") 
        }

        try {
            // 生成唯一文件名
            val fileName = "${UUID.randomUUID()}_${file.originalFilename}"
            val uploadPath = Paths.get("uploads", fileName)
            
            // 确保目录存在
            Files.createDirectories(uploadPath.parent)
            
            // 保存文件
            file.transferTo(uploadPath.toFile()) 
            
            return UploadResponse(
                fileName = fileName,
                originalName = file.originalFilename ?: "",
                size = file.size,
                contentType = file.contentType ?: "",
                uploadTime = LocalDateTime.now()
            )
            
        } catch (e: IOException) {
            throw RuntimeException("文件上传失败", e) 
        }
    }

    @PostMapping("/batch-upload")
    fun uploadMultipleFiles(@RequestParam("files") files: Array<MultipartFile>): List<UploadResponse> { 
        return files.map { file -> uploadFile(file) }
    }
}

data class UploadResponse(
    val fileName: String,
    val originalName: String,
    val size: Long,
    val contentType: String,
    val uploadTime: LocalDateTime
)

文件上传流程

WARNING

建议使用容器内置的 multipart 支持,而不是引入额外的依赖如 Apache Commons File Upload。

6. 配置 DispatcherServlet ⚙️

自定义 Servlet 路径

默认情况下,所有内容都从应用根路径 (/) 提供服务。如果需要映射到不同路径:

properties
spring.mvc.servlet.path=/api
yaml
spring:
  mvc:
    servlet:
      path: "/api"

注册额外的 Servlet

kotlin
@Configuration
class ServletConfig {

    @Bean
    fun customServlet(): ServletRegistrationBean<HttpServlet> { 
        val servlet = CustomServlet()
        val registrationBean = ServletRegistrationBean(servlet, "/custom/*")
        registrationBean.setName("customServlet")
        return registrationBean
    }
}

class CustomServlet : HttpServlet() {
    override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { 
        resp.contentType = "text/plain"
        resp.writer.write("这是自定义 Servlet 的响应")
    }
}

NOTE

通过这种方式注册的 Servlet 可以映射到 DispatcherServlet 的子上下文,而无需调用它。

7. 关闭默认 MVC 配置 🚫

完全控制 MVC 配置

kotlin
@Configuration
@EnableWebMvc
class CustomMvcConfig : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        // 完全自定义消息转换器
        converters.add(MappingJackson2HttpMessageConverter()) 
    }

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        // 自定义静态资源处理
        registry.addResourceHandler("/static/**") 
            .addResourceLocations("classpath:/static/")
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // 自定义视图解析器
        registry.jsp("/WEB-INF/views/", ".jsp") 
    }
}

CAUTION

使用 @EnableWebMvc 会完全接管 MVC 配置,Spring Boot 的自动配置将不再生效。

8. 自定义视图解析器 👀

Spring Boot 默认的视图解析器

Spring Boot 根据 classpath 中的内容自动配置多个视图解析器:

视图解析器用途配置属性
InternalResourceViewResolverJSP 页面spring.mvc.view.prefix/suffix
BeanNameViewResolverBean 名称视图无需配置
ContentNegotiatingViewResolver内容协商自动配置
ThymeleafViewResolverThymeleaf 模板spring.thymeleaf.*
FreeMarkerViewResolverFreeMarker 模板spring.freemarker.*

自定义视图解析器示例

kotlin
@Configuration
class ViewResolverConfig {

    @Bean
    fun customViewResolver(): ViewResolver { 
        val resolver = InternalResourceViewResolver()
        resolver.setPrefix("/WEB-INF/views/")
        resolver.setSuffix(".jsp")
        resolver.order = 1 // 设置优先级
        return resolver
    }

    @Bean("errorView")
    fun errorView(): View { 
        return object : AbstractView() {
            override fun renderMergedOutputModel(
                model: MutableMap<String, Any>,
                request: HttpServletRequest,
                response: HttpServletResponse
            ) {
                response.contentType = "text/html;charset=UTF-8"
                response.writer.write("""
                    <html>
                    <body>
                        <h1>自定义错误页面</h1>
                        <p>发生了一个错误:${model["error"]}</p>
                    </body>
                    </html>
                """.trimIndent())
            }
        }
    }
}

9. 自定义错误页面 ⚠️

默认错误页面配置

Spring Boot 提供了一个"白标"错误页面。可以通过以下方式自定义:

properties
# 禁用默认错误页面
server.error.whitelabel.enabled=false

自定义错误页面实现

kotlin
@Controller
class CustomErrorController : ErrorController {

    @RequestMapping("/error")
    fun handleError(request: HttpServletRequest, model: Model): String { 
        val status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)
        val exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)
        
        model.addAttribute("status", status)
        model.addAttribute("error", exception?.toString() ?: "未知错误")
        model.addAttribute("timestamp", LocalDateTime.now())
        
        return when (status?.toString()) {
            "404" -> "error/404"
            "500" -> "error/500"
            else -> "error/default"
        }
    }
}
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>页面未找到</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
        .error-container { max-width: 600px; margin: 0 auto; }
    </style>
</head>
<body>
    <div class="error-container">
        <h1>404 - 页面未找到</h1>
        <p>抱歉,您访问的页面不存在。</p>
        <p th:text="'时间: ' + ${timestamp}"></p>
        <a href="/">返回首页</a>
    </div>
</body>
</html>

REST API 错误处理

kotlin
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handleIllegalArgument(ex: IllegalArgumentException): ErrorResponse { 
        return ErrorResponse(
            code = "INVALID_ARGUMENT",
            message = ex.message ?: "参数错误",
            timestamp = LocalDateTime.now()
        )
    }

    @ExceptionHandler(Exception::class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    fun handleGeneral(ex: Exception): ErrorResponse { 
        return ErrorResponse(
            code = "INTERNAL_ERROR",
            message = "服务器内部错误",
            timestamp = LocalDateTime.now()
        )
    }
}

data class ErrorResponse(
    val code: String,
    val message: String,
    val timestamp: LocalDateTime
)

总结 🎉

Spring Boot 的 Spring MVC 集成为我们提供了强大而灵活的 Web 开发能力:

核心优势

  1. 零配置启动 - 开箱即用的 REST 服务
  2. 灵活定制 - 从简单配置到完全控制
  3. 丰富功能 - JSON/XML 序列化、文件上传、错误处理
  4. 生产就绪 - 内置最佳实践和安全配置

最佳实践建议

开发建议

  • 优先使用 Spring Boot 的自动配置
  • 需要定制时,先尝试配置属性
  • 复杂需求再考虑编程配置
  • 保持代码简洁,注释清晰

生产环境注意事项

  • 合理设置文件上传限制
  • 实现全局异常处理
  • 自定义错误页面提升用户体验
  • 监控和日志记录

通过掌握这些 Spring MVC 的核心概念和实践技巧,你将能够构建出健壮、高效的 Web 应用程序! 🚀