查看原文
其他

前端妹子说:你是个好人,但我们不合适!

一航 一行Java 2022-08-09

雷猴,我是一航,一个爱叨逼叨的程序员;

某天和前端妹子联调接口时被嫌弃我给的数据太多了,让我给去掉。

哦豁!!!

我倾尽全力给了你 我能给的所有,你竟然说不合适,还让我拿回去!难不成还想发我一张好人卡?

不过这个好人卡没啥毛病!不用的数据确实不该存在,白白浪费了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

精品资料,超赞福利,免费领


点击👇名片,关注公众号,回复【  资料  
获取大厂面试资料2T+视频教程10G+电子书
各类精品资料。

注:资料太多,截图为其中部分

最近开发整理了一个用于速刷面试题的小程序;其中收录了上千道常见面试题及答案(包含基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型),欢迎您的使用。QQ交流群:912509560



20 个实例轻松玩转 Java 8 Stream
Excel大批量导入导出解决方案,太NB...
聊一聊前后分离接口规范
Redis 读写分离技术,你了解多少?
Spring Boot 项目脚本【启动、停止、重启、状态】
服务器 CPU 告警,这个神器一键轻松排查
System.currentTimeMillis的性能,真有如此不堪吗?

👇👇
👇点击"阅读原文",获取更多资料(持续更新中)

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存