前端妹子说:你是个好人,但我们不合适!
雷猴,我是一航,一个爱叨逼叨的程序员;
某天和前端妹子联调接口时被嫌弃我给的数据太多了,让我给去掉。
哦豁!!!
我倾尽全力给了你 我能给的所有,你竟然说不合适,还让我拿回去!难不成还想发我一张好人卡?
不过这个好人卡没啥毛病!不用的数据确实不该存在,白白浪费了IO、占用了内存,却没起到任何作用;服务器的资源,每个bit都得珍惜;
但是好像有有点毛病。我该以什么样的方式去减少返回的数据呢?
比如一个用户对象,在查询列表的时候,不需要返回密码;但是查询详情的时候就得返回密码信息(这里仅仅是打个比方,密码数据一般都不会返回到客户端);
他两返回的只是List<User>和User的区别,那如何做到List中用户数据没有密码,而详情里面就带着密码呢?
方案也有好几种,可以针对场景做合适的选择;
数据库按需查询【推荐】
Dao查询列表的时候,仅仅查询基础信息,不包含密码信息;查询详情的时候,就把更详细的详细查询并返回;
定义不同的前端视图对象
查询的时候,都把详细的查询出来,定义不同的响应对象并赋值返回,List<UserSimpleInfo>
和UserDetailsInfo 定义特定的对象转换工具
业务对象到响应的视图对象转换时,定义一个特殊的转换工具类,根据需要,忽略掉对应的值,比如之前介绍的:【还用 BeanUtils 拷贝对象?MapStruct 才是王者!】就可以实现
@JsonView【推荐】
同一个响应对象,通过指定不同的Json视图,来达到响应不同数据结构的目的
前几种方式比较好理解,今天要讲的就是最后一种@JsonView
好了,上Bug!!!
基础实现
没有JsonView基础的功能
用户对象
@Data
@AllArgsConstructor
public class User {
// 名字
private String userName;
// 年龄
private Integer age;
// 密码
private String pwd;
}测试接口
本文主要将响应部分,为了简化,就直接在Controller层模拟了响应数据。
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/list")
public ResponseEntity list() {
// 这里不做Dao层的数据查询
// 直接模拟一点数据并返回
List<User> users = new ArrayList<>();
users.add(new User("a", 10, "123"));
users.add(new User("b", 20, "456"));
users.add(new User("c", 70, "789"));
return new ResponseEntity(users, HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity userDetails(@PathVariable Integer id) {
// 同样直接构造一个数据并返回
User user = new User("a", 10, "123");
return new ResponseEntity(user, HttpStatus.OK);
}
}测试用例
通过MockMvc构建
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserTests {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setup(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void list() throws Exception {
String contentAsString = mockMvc.perform(get("/user/list")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
log.info(contentAsString);
}
@Test
public void userDetails() throws Exception{
String contentAsString = mockMvc.perform(get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
log.info(contentAsString);
}
}运行结果如下:
list()输出:[{"userName":"a","age":10,"pwd":"123"},{"userName":"b","age":20,"pwd":"456"},{"userName":"c","age":70,"pwd":"789"}]
userDetails()输出:{"userName":"a","age":10,"pwd":"123"}添加JsonView
上面的测试结果都带有
pwd
字段,下面就通过JsonView将列表查询中的pwd字段给去掉定义不同的视图对象
@Data
@AllArgsConstructor
public class User {
// 用户名
@JsonView(UserSimpleView.class)
private String userName;
// 年龄
@JsonView(UserSimpleView.class)
private Integer age;
// 密码
@JsonView(UserDetailsView.class)
private String pwd;
// 基础的简单详细视图
public interface UserSimpleView {
}
// 详情视图
// 详情视图继承自基础视图,意味着详情视图中包含了所有的基础视图数据
public interface UserDetailsView extends userSimpleView {
}
}第一步:定义不同的视图对象
第二步:在属性的get方法上面指定不同的视图
@JsonView(xxx.class)
;由于这里使用了Lombok,所以@JsonView注解直接添加在属性之上接口(controller)指定不同的视图
@JsonView(xxx.class)
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/list")
@JsonView(User.UserSimpleView.class)
public ResponseEntity list() {
List<User> users = new ArrayList<>();
users.add(new User("a", 10, "123"));
users.add(new User("b", 20, "456"));
users.add(new User("c", 70, "789"));
return new ResponseEntity(users, HttpStatus.OK);
}
@GetMapping("/{id}")
@JsonView(User.UserDetailsView.class)
public ResponseEntity userDetails(@PathVariable Integer id) {
User user = new User("a", 10, "123");
return new ResponseEntity(user, HttpStatus.OK);
}
}测试
list()输出:[{"userName":"a","age":10},{"userName":"b","age":20},{"userName":"c","age":70}]
userDetails()输出:{"userName":"a","age":10,"pwd":"123"}可以看出,page接口中返回的User对象已经不包含pwd字段了;但是userDetails()依然包含
嵌套对象
上面详细展示了单个对象通过@JsonView
指定了返回结构,但是实际业务中,很多场景都涉及到了对象嵌套;
比如很多系统都会在响应数据的最外层再做一次状态的封装,如下:
封装前:
{
"userName": "a",
"age": 10,
"pwd": "123"
}
封装后:
{
"code": 0,
"msg": "成功",
"data": {
"userName": "a",
"age": 10,
"pwd": "123"
}
}
根据上面封装的结构,来做如下的改进
定义基础的外层结构
@Data
@AllArgsConstructor
public class BaseVO<T> {
/**
* 状态码
*/
private Integer code;
/**
* 状态描述
*/
private String msg;
/**
* 数据
*/
private T data;
}定义基础的视图对象
@Data
@AllArgsConstructor
public class BaseVO<T> {
/**
* 状态码
*/
@JsonView(BaseResponceView.class)
private Integer code;
/**
* 状态描述
*/
@JsonView(BaseResponceView.class)
private String msg;
/**
* 数据
*/
@JsonView(BaseResponceView.class)
private T data;
// 基础的视图对象
public interface BaseResponceView {
}
}业务的视图继承自基础视图
public interface UserSimpleView extends BaseVO.BaseResponceView
@Data
@AllArgsConstructor
public class User {
@JsonView(UserSimpleView.class)
private String userName;
@JsonView(UserSimpleView.class)
private Integer age;
@JsonView(UserDetailsView.class)
private String pwd;
public interface UserSimpleView extends BaseVO.BaseResponceView {
}
public interface UserDetailsView extends UserSimpleView {
}
}调整controller,添加基础封装
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/list")
@JsonView(User.UserSimpleView.class)
public ResponseEntity list() {
List<User> users = new ArrayList<>();
users.add(new User("a", 10, "123"));
users.add(new User("b", 20, "456"));
users.add(new User("c", 70, "789"));
BaseVO<List<User>> re = new BaseVO<>(0, "成功", users);
return new ResponseEntity(re, HttpStatus.OK);
}
@GetMapping("/{id}")
@JsonView(User.UserDetailsView.class)
public ResponseEntity userDetails(@PathVariable Integer id) {
User user = new User("a", 10, "123");
BaseVO<User> re = new BaseVO<>(0, "成功", user);
return new ResponseEntity(re, HttpStatus.OK);
}
}测试用例
list()输出:{"code":0,"msg":"成功","data":[{"userName":"a","age":10},{"userName":"b","age":20},{"userName":"c","age":70}]}
userDetails()输出:{"code":0,"msg":"成功","data":{"userName":"a","age":10,"pwd":"123"}}业务对象的嵌套
上面列举了基础结构嵌套业务数据的示例,实际的开发中同样存在多个业务对象间的嵌套,不同的场景,返回的嵌套对象不同,对于
JsonView的配置也上面展示的基础结构配置没啥差异,举一反三即可实现了。
如何选择最好的方式
文章一开头列举了几种不同的方式,来满足不同场景下返回不同数据结构的问题,并没有说哪一种就是最优的解决方案;需要根据不同的业务场景,来针对性选择;如果说单表的操作,可能直接通Dao层按需求查询对应的字段就能好了;如果业务逻辑比较复杂,最终数据来源于多个地方,通过数据库的方式会导致Dao越来越庞大,使用JsonView的方式可能很轻松就满足了需求;
我们最终目的是让结构更清晰,代码更合理,维护更容易,所以合适才是最重要。
注意
JsonView仅支持jackson框架;SpringBoot默认使用的框架就是jackson;如果你将Http的消息转换对象由jackson配置成了FastJson,那么所有的@JsonView配置将全部失效
,所以务必注意;替换的具体配置如下:
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters(){
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue,SerializerFeature.WriteNullStringAsEmpty,SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastJsonHttpMessageConverter;
return new HttpMessageConverters(converter);
}
相信这回,前端妹子应该觉得合适了吧...
END
精品资料,超赞福利,免费领
最近开发整理了一个用于速刷面试题的小程序;其中收录了上千道常见面试题及答案(包含基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型),欢迎您的使用。QQ交流群:912509560