本周需要完成一项工作:在单元测试中完成后端返回字段的 断言 。
换句话说,需要断言后端向前端返回了哪些字段。
由于对JsonView的了解不足,在找字段的时候花费了较多时间。
因此本文将从实践的角度,阐述JsonView的作用和用法。
在说作用之前,我们已经知道:前后端分离的项目中,使用 Json字符串 来完成前后端之间的通信。
在默认情况下,只要前端发起请求,就会返回对象的 所有 字段。但有的时候,后端不能把所有字段全部返回。
一方面,是因为有些字段前端不需要,返回过多的数据会占用网络带宽;另一方面是出于 安全性 考虑,比如,不可以将密码返回给前端,否则,网站攻击者可以用REST工具直接获取密码。
而JsonView的作用,就是用来 控制C层返回哪些字段 的。
通过以下几个实例的对比,来展示JsonView的效果。
以下的代码,我们通过一个小Demo来演示:
在这个小小的教务系统中,有三种实体—— 教师、学生、班级 ,
为了清晰的展示实体关系,提供简单的E-R图:
  
 
由图可知:
班级和教师是 多对一 的关系,
学生和班级是 多对一 的关系
因此,如果查询学生,班级会包含在学生的字段中,教师会包含在班级的字段中。
这是典型的“对象套对象套对象”的例子。
下面实体的代码供参考(可略过):
/**
 * 班级实体
 * 包含字段 id、 name、
 * 外键 teacher
 */
@Entity
public class Klass {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    private Teacher teacher;
    private String name;
} 
 /**
 * 学生实体
 * 包含字段 id、 name、 sno
 * 外键 klass
 */
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String name;
    @Column(nullable = false, unique = true)
    private String sno;
    @ManyToOne
    @JoinColumn(nullable = false)
    private Klass klass; 
 /**
 * 教师实体
 * 包含字段 id、 name、 sex、 username、 email
 */
@Entity
public class Teacher {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Boolean sex;
    private String username;
    private String email; 
 在不使用JsonView的情况下,使用REST工具直接取出一个学生,返回了学生的所有字段,也包含 关联查询 得到的对象:
{
    "id":1,
    "name":"学生1",
    "sno":"123456",
    "klass":{
        "id":1,
        "teacher":{
            "id":1,
            "name":"张三",
            "sex":false,
            "username":"zhangsan",
            "email":"123@123.com"
        },
        "name":"班级1"
    }
} 
 因此,很容易得到结论一:
在原来的基础上,对姓名和学号字段分别使用JsonView,同时,定义对应的接口:
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // 姓名字段使用JsonView
    @Column(nullable = false)
    @JsonView(NameJsonView.class)
    private String name;
    // 学号字段使用JsonView
    @Column(nullable = false, unique = true)
    @JsonView(SnoJsonView.class)
    private String sno;
    // 班级外键使用JsonView
    @ManyToOne
    @JsonView(KlassJsonView.class)
    @JoinColumn(nullable = false)
    private Klass klass;
    
    ...
    // 姓名字段的接口
    public interface NameJsonView {}
    // 学号字段的接口
    public interface SnoJsonView {}
    // 班级外键对应的实体的接口
    public interface KlassJsonView {}
} 
 在C层控制器中加入NameJsowView:
(注意:此时只有 姓名 ,并没有加入学号和班级)
/**
     * 通过ID查询学生
     * @param id 学生ID
     * @return 学生
     */
    @GetMapping("{id}")
    @JsonView(Student.NameJsonView.class)
    public Student getById(@PathVariable Long id) {
        return this.studentService.findById(id);
    } 
 返回结果如下:
{
    "name":"学生1"
} 
 由于我们在C层只使用了姓名的字段,除了姓名,其他字段均不返回。
因此可以得出结论二:
对于已经定义JsowView的对象,C层只返回注解中的JsonView接口里面包含的字段,其他字段一概不返回。
前一节的基础上,把C层的注解由 @JsonView(Student.NameJsonView.class)
改为
@JsonView(Student.KlassJsonView.class)
来返回学生对象关联的班级。
/**
     * 通过ID查询学生
     * @param id 学生ID
     * @return 学生
     */
    @GetMapping("{id}")
    @JsonView(Student.KlassJsonView.class)
    public Student getById(@PathVariable Long id) {
        return this.studentService.findById(id);
    } 
 这次的返回结果比较有意思,只返回了空的Klass,里面一个字段也没有:
{
    "klass":
        {
            
        }
} 
 所以,结论三:
为了解决结论三的问题,我们需要像上文一样,在学生关联的 班级 实体中也启用JsonView。
然后新建一个接口,分别继承 班级 字段以及班级实体中的 姓名 、 教师 等其他字段:
public interface GetByIdJsonView extends Student.KlassJsonView, Klass.NameJsonView, Klass.TeacherJsonView {} 
 把这个新接口写到C层方法的注解上:
@GetMapping("{id}")
    @JsonView(GetByIdJsonView.class)
    public Student getById(@PathVariable Long id) {
        return this.studentService.findById(id);
    } 
 再次运行,查看返回结果:
{
    "klass":
    {
        "teacher":{ },
        "name":"班级1"
    }
} 
 符合预期,因此,结论四:
细心的你可以发现,Teacher中依然没有字段,如果也想返回Teacher的字段,只需要在接口中继续继承即可。
接下来说具体如何在Spring的项目中应用JsonView。
需要记住接口名称
// 定义了一个接口,用于JsonView控制返回字段
    public interface SnoJsonView {} 
 找到一个字段,加入 @JsonView(XXXJsonView.class) ,名称与刚才写的接口名称相同。
@JsonView(SnoJsonView.class)
    private String sno; 
 实际的项目中,不可能只返回一个字段,如果返回多个字段,那就在C层再定一个接口,继承所以要返回字段的接口即可。
原则上,每个控制器方法,都必须有唯一的JsonView接口,接口名与方法名相同,不能混用。
定义一个与C层方法名相同的接口,继承业务逻辑中需要返回的所有字段:
public interface GetByIdJsonView extends Student.KlassJsonView, Student.NameJsonView, Student.SnoJsonView {} 
 最后一步,就是把刚才的接口,加到要控制字段的C层方法上:
@GetMapping("{id}")
    @JsonView(GetByIdJsonView.class)
    public Student getById(@PathVariable Long id) {
        return this.studentService.findById(id);
    } 
 到此,就可以实现用JsonView控制返回字段了。
这种做法的优点在于:
实体层中, 接口名 和 字段名 一致,到C层引用时,就可以根据名称知道这个接口控制哪个字段;
控制器中, 接口名 与 方法名 一致,通过接口名可以知道是这个方法返回哪些字段。
前后端分离的项目中,使用 Json字符串 来完成前后端之间的通信,但有的时候,后端不能把所有字段全部返回,因此可以使用JsonView,来 控制C层返回哪些字段 。
如果不使用JsonView,默认返回 所有字段 ,包括外键关联对象的所有信息;
如果使用JsonView,只返回接口中 声明 的所有字段,如果出现关联对象,只返回关联对象本身,而不返回其中的字段。
JsonView接口可以通过 继承 ,来实现返回不同字段的组合。
本文作者: 河北工业大学梦云智开发团队 -刘宇轩