Skip to content

Spring WebFlux 中的 Jackson JSON Views 详解 🎯

什么是 JSON Views?为什么需要它? 🤔

在现代 Web 开发中,我们经常遇到这样的场景:同一个数据对象需要在不同的接口中返回不同的字段。比如:

  • 用户详情接口:需要返回用户名、邮箱、创建时间等
  • 用户列表接口:只需要返回用户名和头像
  • 管理员接口:需要返回所有字段,包括敏感信息

IMPORTANT

如果没有 JSON Views,我们可能需要创建多个不同的 DTO 类,或者手动控制序列化过程,这会导致代码冗余和维护困难。

Jackson JSON Views 就是为了解决这个问题而生的!它允许我们在同一个实体类上定义多个"视图",然后在不同的接口中选择性地序列化不同的字段。

JSON Views 的核心原理 💡

JSON Views 的工作原理可以用下面的时序图来理解:

实战示例:用户信息接口设计 👨‍💻

让我们通过一个完整的示例来理解 JSON Views 的使用:

kotlin
@RestController
@RequestMapping("/api/users")
class UserController {

    // 公开接口 - 不返回敏感信息
    @GetMapping("/profile/{id}")
    @JsonView(User.PublicView::class) 
    fun getUserProfile(@PathVariable id: Long): User {
        return userService.findById(id)
    }

    // 管理员接口 - 返回完整信息
    @GetMapping("/admin/{id}")
    @JsonView(User.AdminView::class) 
    @PreAuthorize("hasRole('ADMIN')")
    fun getUserForAdmin(@PathVariable id: Long): User {
        return userService.findById(id)
    }

    // 用户列表 - 只返回基本信息
    @GetMapping("/list")
    @JsonView(User.ListView::class) 
    fun getUserList(): List<User> {
        return userService.findAll()
    }
}
kotlin
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonView(ListView::class) 
    val id: Long? = null,

    @JsonView(ListView::class) 
    val username: String,

    @JsonView(PublicView::class) 
    val email: String,

    @JsonView(AdminView::class) 
    val password: String, // 只有管理员才能看到

    @JsonView(PublicView::class) 
    val avatar: String? = null,

    @JsonView(AdminView::class) 
    val lastLoginIp: String? = null, // 敏感信息

    @JsonView(ListView::class) 
    val createdAt: LocalDateTime = LocalDateTime.now()
) {
    // 定义视图接口
    interface ListView // 列表视图:最基本信息
    interface PublicView : ListView // 公开视图:继承列表视图 + 更多公开信息
    interface AdminView : PublicView // 管理员视图:继承公开视图 + 敏感信息
}

TIP

注意视图接口的继承关系!AdminView 继承 PublicViewPublicView 继承 ListView。这样管理员视图就能看到所有字段,而公开视图只能看到非敏感字段。

不同接口的返回结果对比 📊

让我们看看同一个用户对象在不同视图下的序列化结果:

用户数据示例
kotlin
// 假设数据库中的用户对象
val user = User(
    id = 1L,
    username = "john_doe",
    email = "[email protected]",
    password = "encrypted_password_hash",
    avatar = "https://example.com/avatar.jpg",
    lastLoginIp = "192.168.1.100",
    createdAt = LocalDateTime.of(2024, 1, 15, 10, 30)
)
接口视图返回的JSON
/api/users/listListView{"id":1,"username":"john_doe","createdAt":"2024-01-15T10:30:00"}
/api/users/profile/1PublicView{"id":1,"username":"john_doe","email":"[email protected]","avatar":"https://example.com/avatar.jpg","createdAt":"2024-01-15T10:30:00"}
/api/users/admin/1AdminView{"id":1,"username":"john_doe","email":"[email protected]","password":"encrypted_password_hash","avatar":"https://example.com/avatar.jpg","lastLoginIp":"192.168.1.100","createdAt":"2024-01-15T10:30:00"}

高级用法:动态视图选择 🚀

有时候我们需要根据用户权限动态选择视图:

kotlin
@RestController
class DynamicViewController {

    @GetMapping("/user/{id}")
    fun getUser(
        @PathVariable id: Long,
        authentication: Authentication
    ): ResponseEntity<*> {
        val user = userService.findById(id)
        
        return when {
            // 管理员可以看到所有信息
            authentication.authorities.any { it.authority == "ROLE_ADMIN" } -> {
                ResponseEntity.ok()
                    .header("Content-Type", "application/json") 
                    .body(objectMapper.writerWithView(User.AdminView::class.java).writeValueAsString(user))
            }
            // 用户本人可以看到公开信息
            authentication.name == user.username -> {
                ResponseEntity.ok()
                    .body(objectMapper.writerWithView(User.PublicView::class.java).writeValueAsString(user)) 
            }
            // 其他人只能看到基本信息
            else -> {
                ResponseEntity.ok()
                    .body(objectMapper.writerWithView(User.ListView::class.java).writeValueAsString(user)) 
            }
        }
    }
}

最佳实践与注意事项 ⚠️

1. 视图继承设计

NOTE

合理设计视图继承关系,从最基础的视图开始,逐步扩展到更详细的视图。

kotlin
interface BaseView                    // 最基础的字段
interface PublicView : BaseView       // 公开可见的字段
interface OwnerView : PublicView      // 所有者可见的字段
interface AdminView : OwnerView       // 管理员可见的字段

2. 避免视图冲突

WARNING

每个控制器方法只能指定一个视图类。如果需要激活多个视图,请使用组合接口。

kotlin
// ❌ 错误:不能指定多个视图
@JsonView([User.PublicView::class, User.AdminView::class]) 

// ✅ 正确:使用组合接口
interface CompositeView : User.PublicView, User.AdminView
@JsonView(CompositeView::class) 

3. 性能考虑

性能优化建议

  • JSON Views 的序列化过程比普通序列化稍慢,但对于大多数应用来说影响可以忽略
  • 如果性能要求极高,可以考虑使用缓存或者预先序列化的方式
  • 避免在视图中包含过多的嵌套对象,这会增加序列化复杂度

与传统方案的对比 📈

kotlin
// 需要创建多个DTO类
data class UserListDto(
    val id: Long,
    val username: String,
    val createdAt: LocalDateTime
)

data class UserPublicDto(
    val id: Long,
    val username: String,
    val email: String,
    val avatar: String?,
    val createdAt: LocalDateTime
)

data class UserAdminDto(
    val id: Long,
    val username: String,
    val email: String,
    val password: String,
    val avatar: String?,
    val lastLoginIp: String?,
    val createdAt: LocalDateTime
)

// 需要手动转换
@GetMapping("/list")
fun getUserList(): List<UserListDto> {
    return userService.findAll().map { user ->
        UserListDto(
            id = user.id!!,
            username = user.username,
            createdAt = user.createdAt
        )
    }
}
kotlin
// 只需要一个实体类 + 视图接口
@Entity
data class User(
    @JsonView(ListView::class)
    val id: Long? = null,
    
    @JsonView(ListView::class)
    val username: String,
    
    @JsonView(PublicView::class)
    val email: String,
    
    @JsonView(AdminView::class)
    val password: String,
    
    @JsonView(PublicView::class)
    val avatar: String? = null,
    
    @JsonView(AdminView::class)
    val lastLoginIp: String? = null,
    
    @JsonView(ListView::class)
    val createdAt: LocalDateTime = LocalDateTime.now()
) {
    interface ListView
    interface PublicView : ListView
    interface AdminView : PublicView
}

// 控制器方法简洁明了
@GetMapping("/list")
@JsonView(User.ListView::class)
fun getUserList(): List<User> {
    return userService.findAll() // 直接返回实体
}

总结 🎉

JSON Views 是 Spring WebFlux 中一个强大而优雅的功能,它解决了以下核心问题:

代码复用:一个实体类满足多种序列化需求
安全性:轻松控制敏感信息的暴露
维护性:减少DTO类的数量,降低维护成本
灵活性:通过视图继承实现灵活的权限控制

IMPORTANT

JSON Views 特别适合于需要根据用户权限返回不同数据的场景,如用户管理系统、内容管理系统等。它让我们能够用最少的代码实现最灵活的数据序列化控制。

通过合理使用 JSON Views,我们可以构建出既安全又高效的 RESTful API,为不同的客户端和用户角色提供恰到好处的数据服务! 🚀