在使用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
}
我们在上述代码中,主要做了两件事:
- 在内部定义了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输出时的死循环问题。当前来看,不失为一种更佳的实践。
骐骥一跃,不能十步;驽马十驾,功在不舍。