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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mengyunzhi.springBootSampleCode</groupId>
    <artifactId>jsonview</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jsonview</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

实体

实体依然采用我们熟悉的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<Student> 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输出时的死循环问题。当前来看,不失为一种更佳的实践。

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

相关推荐