在使用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
}
我们在上述代码中,主要做了两件事:
- 在内部定义了JsonView.
- 为关联字段单独定义了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输出时的死循环问题。当前来看,不失为一种更佳的实践。
骐骥一跃,不能十步;驽马十驾,功在不舍。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。