JOSNVIEW更佳实践

在使用SpringMVC进行开发时,使用JSONVIEW控制字段输出虽然不难。但总感觉应该有一种相对使用简单、理解简单的方法。本文在历史项目实践基础上,尝试找出一种更佳的实践方法。

项目源码地址: https://github.com/mengyunzhi/springBootSampleCode/tree/master/jsonview

当前问题

我们当前遇到的最大的问题是在实体中使用了大量的外部JSONVEIW
例:我们输出Student实体时,需要进行以下两步操作:

  • 定义相关的触发器,例:class StudentController { public Student getById(Long id) { }
  • 定义相关的JsonView类或是接口,比如class StudentJsonView { public interface GetById{} }
  • 在触发器上加入@JsonView注解,并将刚刚定义的StudentJsonView.GetById.class加入其中。比如:@JsonView(StudentJsonView.GetById.class)
  • 修改Stduent实体,并将需要输出的字段,加入@JsonView(StudentJsonView.GetById.class)注解。

存在问题也很明显:

  • Student实体的同一字段上,我们使用了大量的JsonView,后期我们进行维护时,只能增加新的,不敢删除老的(因为我们不知道谁会用这个JsonView)。不利于维护。
  • 违反了对修改关闭的原则。比如:A是负责实体类的,B是负责触发器的。那么B在进行触发器开发时,需要修改A负责的实体类。而这并不是我们想要的。
  • 某个特定的JsonView具体需要了哪些实体、哪些字段,并不能一目了然。

解决方案

既然实体并不想并修改(哪怕是添加JsonView这样并不影响实体结构的操作),那么实体就要对扩展开放,以使其它调用者可以顺利的定义输出字段。

我们尝试做如下修改:

  • JsonView的定义移至实体类中,并在实体类中,使用实体内部定义的JsonView来进行修饰。
  • 为了防止在json输出时造成的死循环,凡事涉及到关联的,单独定义JsonView
  • 单独定义的JsonView继承关联方实体内部的JsonView

示例代码

pom



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.2.RELEASE
         
    
    com.mengyunzhi.springBootSampleCode
    jsonview
    0.0.1-SNAPSHOT
    jsonview
    Demo project for Spring Boot

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            com.h2database
            h2
            runtime
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            com.alibaba
            fastjson
            1.2.54
            test
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

    
        
            alimaven
            aliyun maven
            http://maven.aliyun.com/nexus/content/groups/public/
            
                true
            
            
                false
            
        
    

实体

实体依然采用我们熟悉的Student学生Klass 班级 两个实体举例,关系如下:

  • 学生:班级 = n:1

学生

@Entity
public class Student {
    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }


    interface base {
    }  // 基本字段

    interface klass extends Klass.base {
    } // 对应klass字段

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonView(base.class)
    private Long id;

    @JsonView(base.class)
    private String name;

    @JsonView(klass.class)
    @ManyToOne
    private Klass klass;
      
    // 省略set与get
}

班级:

@Entity
public class Klass {
    public Klass() {
    }

    public Klass(String name) {
        this.name = name;
    }


    interface base {
    }  // 基本字段

    interface students extends Student.base {
    }// 对应students字段

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonView(base.class)
    private String name;

    @JsonView(students.class)
    @OneToMany(mappedBy = "klass")
    private List students = new ArrayList<>();
    
    // 省略set与get
}

我们在上述代码中,主要做了两件事:

  1. 在内部定义了JsonView.
  2. 为关联字段单独定义了JsonView,并做了相应的继承,以使其显示关联实体的基本字段信息。

控制器

班级

package com.mengyunzhi.springBootSampleCode.jsonview;

import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("klass")
public class KlassController {

    // 这是关键!继承了两个interface,即显示这两个interface对应的字段。
    interface getById extends Klass.base, Klass.students {
    }

    @Autowired
    private KlassRepository klassRepository;

    @GetMapping("{id}")
    @JsonView(getById.class)
    public Klass getById(@PathVariable Long id) {
        return klassRepository.findById(id).get();
    }
}

学生

package com.mengyunzhi.springBootSampleCode.jsonview;

import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("student")
public class StudentController {

    // 这是关键!继承了两个interface,即显示这两个interface对应的字段。
    interface getById extends Student.base, Student.klass {
    }

    @Autowired
    private StudentRepository studentRepository;

    @GetMapping("{id}")
    @JsonView(getById.class)
    public Student getById(@PathVariable Long id) {
        return studentRepository.findById(id).get();
    }
}

如代码所示,我们进行输出时,并没有对实体进行任何的操作,却仍然达到了个性化输出字段的目的。

单元测试

班级:

package com.mengyunzhi.springBootSampleCode.jsonview;

import com.alibaba.fastjson.JSON;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;


@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
public class KlassControllerTest {
    @Autowired
    private KlassRepository klassRepository;
    @Autowired
    private StudentRepository studentRepository;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void getById() throws Exception {
        // 数据准备
        Klass klass = new Klass("测试班级");
        klassRepository.save(klass);
        Student student = new Student("测试学生");
        student.setKlass(klass);
        studentRepository.save(student);
        klass.getStudents().add(student);
        klassRepository.save(klass);

        // 模拟请求,将结果转化为字符化
        String result = this.mockMvc.perform(
                MockMvcRequestBuilders.get("/klass/" + klass.getId().toString())
                        .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andReturn().getResponse().getContentAsString();

        // 将字符串转换为实体,并断言
        Klass resultKlass = JSON.parseObject(result, Klass.class);
        Assertions.assertThat(resultKlass.getName()).isEqualTo("测试班级");
        Assertions.assertThat(resultKlass.getStudents().size()).isEqualTo(1);
        Assertions.assertThat(resultKlass.getStudents().get(0).getName()).isEqualTo("测试学生");
    }
}

学生:

package com.mengyunzhi.springBootSampleCode.jsonview;

import com.alibaba.fastjson.JSON;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;


@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentControllerTest {
    @Autowired
    private KlassRepository klassRepository;
    @Autowired
    private StudentRepository studentRepository;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void getById() throws Exception {
        // 数据准备
        Klass klass = new Klass("测试班级");
        klassRepository.save(klass);
        Student student = new Student("测试学生");
        student.setKlass(klass);
        studentRepository.save(student);

        // 模拟请求,将结果转化为字符化
        String result = this.mockMvc.perform(
                MockMvcRequestBuilders.get("/student/" + student.getId().toString())
                        .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andReturn().getResponse().getContentAsString();

        // 将字符串转换为实体,并断言
        Student resultStudent = JSON.parseObject(result, Student.class);
        Assertions.assertThat(resultStudent.getName()).isEqualTo("测试学生");
        Assertions.assertThat(resultStudent.getKlass().getName()).isEqualTo("测试班级");
    }
}

总结

我们将JsonView定义到相关的实体中,并使其与特定的字段进行关联。在进行输出时,采用继承的方法,来自定义输出字段。即达到了“对扩展开放,对修改关闭”的目标,也有效的防止了JSON输出时的死循环问题。当前来看,不失为一种更佳的实践。

骐骥一跃,不能十步;驽马十驾,功在不舍。

你可能感兴趣的:(jsonview)