Appearance
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 中的内容自动配置多个视图解析器:
视图解析器 | 用途 | 配置属性 |
---|---|---|
InternalResourceViewResolver | JSP 页面 | spring.mvc.view.prefix/suffix |
BeanNameViewResolver | Bean 名称视图 | 无需配置 |
ContentNegotiatingViewResolver | 内容协商 | 自动配置 |
ThymeleafViewResolver | Thymeleaf 模板 | spring.thymeleaf.* |
FreeMarkerViewResolver | FreeMarker 模板 | 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 开发能力:
核心优势
- 零配置启动 - 开箱即用的 REST 服务
- 灵活定制 - 从简单配置到完全控制
- 丰富功能 - JSON/XML 序列化、文件上传、错误处理
- 生产就绪 - 内置最佳实践和安全配置
最佳实践建议
开发建议
- 优先使用 Spring Boot 的自动配置
- 需要定制时,先尝试配置属性
- 复杂需求再考虑编程配置
- 保持代码简洁,注释清晰
生产环境注意事项
- 合理设置文件上传限制
- 实现全局异常处理
- 自定义错误页面提升用户体验
- 监控和日志记录
通过掌握这些 Spring MVC 的核心概念和实践技巧,你将能够构建出健壮、高效的 Web 应用程序! 🚀