Spring Boot版本:2.3.0
JSON序列化库:Jackson
在日常的API开发中我们可能会有这样的需求:将一个对象转为JSON数据返回给前端时,里面的某些字段需要根据不同的业务场景进行屏蔽。
假设现在这个需求是这样的,用户的个人信息保存在UserProfile对象中:
public class UserProfile {
// 用户名
private String username;
// 地址
private Address address;
public UserProfile(String username, Address address) {
this.username = username;
this.address = address;
}
public String getUsername() {
return username;
}
public Address getAddress() {
return address;
}
}
我们希望在Address对象中的家庭详细地址只有用户本人才能看到,而用户所在的身份与城市则是公开的,任何人都可以看到:
public class Address {
// 省
private String province;
// 市
private String city;
// 家庭详细地址
private String detailed;
public Address(String province, String city, String detailed) {
this.province = province;
this.city = city;
this.detailed = detailed;
}
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
public String getDetailed() {
return detailed;
}
}
然后我们编写下面的Controller用于返回UserProfile:
public class UserController {
private final UserProfile userProfile = new UserProfile("sword4j", new Address("湖北", "武汉市", "光谷大道xx小区xx号"));
@GetMapping("/profile")
public UserProfile getDefaultInfo() {
return userProfile;
}
}
默认情况下访问/profile,它会返回所有字段给前端:
{"username":"sword4j","address":{"province":"湖北","city":"武汉市","detailed":"光谷大道xx小区xx号"}}
现在我们开始实现前面提出的需求,对于该需求至少有三个方式可以实现。
方式1:DTO对象
第一个方法是对detailed字段进行“物理阉割”,我们通过新增一个与前端要求JSON数据结构一致的DTO对象,然后在这个DTO对象中去掉detailed字段:
public class UserProfileWithoutDetailedAddressDTO {
private String username;
private AddressDTO address;
public static class AddressDTO {
private String province;
private String city;
public AddressDTO(String province, String city) {
this.province = province;
this.city = city;
}
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
}
private UserProfileWithoutDetailedAddressDTO(String username, AddressDTO address) {
this.username = username;
this.address = address;
}
public static UserProfileWithoutDetailedAddressDTO from(UserProfile userProfile) {
Address address = userProfile.getAddress();
return new UserProfileWithoutDetailedAddressDTO(userProfile.getUsername(),
new AddressDTO(address.getProvince(), address.getCity()));
}
public String getUsername() {
return username;
}
public AddressDTO getAddress() {
return address;
}
}
在Controller中的改动如下:
@RestController
public class UserController {
private final UserProfile userProfile = new UserProfile("sword4j", new Address("湖北", "武汉市", "光谷大道xx小区xx号"));
@GetMapping("/profile-using-dto")
public UserProfileWithoutDetailedAddressDTO getProfileUsingDTO() {
return UserProfileWithoutDetailedAddressDTO.from(userProfile);
}
}
通过访问/profile-using-dto,可以看到输出的JSON数据满足我们的需求:
{"username":"sword4j","address":{"province":"湖北","city":"武汉市"}}
方式2:清空字段值
第二个方法是对detailed字段进行“化学阉割”,我们复用之前的UserProfile和Address对象,但是在返回数据给前端之前需要把detailed字段设为null:
@RestController
public class UserController {
private final UserProfile userProfile = new UserProfile("sword4j", new Address("湖北", "武汉市", "光谷大道xx小区xx号"));
@GetMapping("/profile-using-null")
public UserProfile getProfileUsingNull() {
// 把detailed字段设为null
Address address = new Address(userProfile.getAddress().getProvince(),
userProfile.getAddress().getCity(), null);
return new UserProfile(userProfile.getUsername(), address);
}
}
通过访问/profile-using-null,可以看到输出的JSON数据也满足需求:
{"username":"sword4j","address":{"province":"湖北","city":"武汉市","detailed":null}}
但是响应结果中的detailed只能看不能用,有点讨厌,下面我们把它隐藏掉。
我们在Address类上加上注解@JsonInclude(JsonInclude.Include.NON_NULL)。它表示把对象转为JSON时,只处理值不为null的字段。
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Address {
// 省略内部代码
}
上面这种方式可以实现在局部隐藏值为null的字段,如果想进行全局的配置,我们可以在application.properties文中添加:
spring.jackson.default-property-inclusion=non_null
我们再来看下响应结果,现在detailed字段也消失了:
{"username":"sword4j","address":{"province":"湖北","city":"武汉市"}}
方式3:@JsonView
第三个方法就是Jackson提供的@JsonView注解,下面介绍如何使用它实现我们的需求。
@JsonView的定义如下:
public @interface JsonView {
Class<?>[] value() default {};
}
我们对UserProfile类做一些改变:
public class UserProfile {
public interface PublicView {}
public interface PrivateView extends PublicView {}
// 用户名所有人可见
@JsonView(PublicView.class)
private String username;
// 支持对象嵌套
@JsonView(PublicView.class)
private Address address;
// 省略构造器与Getter方法
}
在上面的代码中,我们添加了两个空接口:PublicView和PrivateView。
PublicView用于标识可以被所有人访问的字段。
PrivateView用于表示自能被自己访问的字段。因为它继承自PublicView,所以它也包括了被PublicView标记的字段,换句话说,它可以看见所有字段。
我们在字段username上用注解@JsonView(PublicView.class)进行标记。
另外笔者发现,在字段address上用@JsonView(PublicView.class)或者@JsonView(PrivateView.class)标记都可以实现对address字段的正确解析。但是仅仅使用不带参数的@JsonView或者不对这个字段进行标记都会导致address字段被屏蔽掉。
此外,我们也要对Address类进行一些改变:
public class Address {
@JsonView(UserProfile.PublicView.class)
private String province;
@JsonView(UserProfile.PublicView.class)
private String city;
@JsonView(UserProfile.PrivateView.class)
private String detailed;
// 省略构造器与Getter方法
}
最后在Controller中对于的方法上也要标记@JsonView:
@RestController
public class UserController {
private final UserProfile userProfile = new UserProfile("sword4j", new Address("湖北", "武汉市", "光谷大道xx小区xx号"));
@GetMapping("/profile-public-view")
@JsonView(UserProfile.PublicView.class)
public UserProfile getPublicInfo() {
return userProfile;
}
@GetMapping("/profile-private-view")
@JsonView(UserProfile.PrivateView.class)
public UserProfile getPrivateInfo() {
return userProfile;
}
@GetMapping("/profiles/public-view")
@JsonView(UserProfile.PublicView.class)
public List<UserProfile> getPublicInfoOnList() {
return Collections.singletonList(userProfile);
}
@GetMapping("/profiles/private-view")
@JsonView(UserProfile.PrivateView.class)
public List<UserProfile> getPrivateInfoOnList() {
return Collections.singletonList(userProfile);
}
}
访问被@JsonView(UserProfile.PublicView.class)标注的/profile-public-view会返回结果:
{"username":"sword4j"}
访问被@JsonView(UserProfile.PrivateView.class)标注的/profile-private-view会返回结果:
{"username":"sword4j","address":{"province":"湖北","city":"武汉市","detailed":"光谷大道xx小区xx号"}}
另外对于返回List<UserProfile>的情况,@JsonView也是可以生效的。
访问/profiles/public-view会返回结果:
[{"username":"sword4j"}]
访问/profiles/private-view会返回结果:
[{"username":"sword4j","address":{"province":"湖北","city":"武汉市","detailed":"光谷大道xx小区xx号"}}]
总结
本文介绍了对API返回的JSON数据字段进行屏蔽的3种方式,在使用的场景与便利性方面这三种方式各有不同:
- 第一个方式需要新增一个或多个DTO对象,这种方式在多种使用场景下都可以使用,不仅可以用于屏蔽字段,还可用于:重组字段、字段重命名、展平字段等场景,它主要用于一个DTO对象是由多个业务值对象组合而成的情况。这种方式目前笔者最近在项目中用的比较多,它的缺点是编码工作较多,但是在配合上Lombok时也还不错;
- 第二种方式里面有点“打补丁”的意味,手动编码实现的方式有点丑陋,这种方式笔者不是很喜欢,笔者认为它只适用于一些简单的业务场景;
- 第三种方式笔者把它理解为是第二种方式的一种自动化流程,它与第二种方式的使用场景类似,但是在编码实现上要比第二种方式漂亮一些。