JOSNVIEW更佳實踐
在使用SrpingMVC進行開發時,如何使用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
來進行修飾。 -
為了防止在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; } /** * 定義JSON輸出 */ static abstract class Json { interface base {}// 基本欄位 interface klass extends Klass.Json.base {} // 對應klass欄位 } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @JsonView(Json.base.class) private Long id; @JsonView(Json.base.class) private String name; @JsonView(Json.klass.class) @ManyToOne private Klass klass; // 省略set與get }
班級:
@Entity public class Klass { public Klass() { } public Klass(String name) { this.name = name; } /** * 定義JSON輸出 */ static abstract class Json { interface base {}// 基本欄位 interface students extends Student.Json.base {}// 對應students欄位 } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @JsonView(Json.base.class) private String name; @JsonView(Json.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 { static abstract class Json { interface getById extends Klass.Json.base, Klass.Json.students {} } @Autowired private KlassRepository klassRepository; @GetMapping("{id}") @JsonView(Json.getById.class) public Klass getById(@PathVariableLong 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 { static abstract class Json { interface getById extends Student.Json.base, Student.Json.klass {} } @Autowired private StudentRepository studentRepository; @GetMapping("{id}") @JsonView(Json.getById.class) public Student getById(@PathVariableLong 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("測試班級"); } }
總結
我們將JosnView
定義到相關的實體中,並使其與特定的欄位進行關聯。在進行輸出時,採用繼承的方法,來自定義輸出欄位。即達到了“對擴充套件開放,對修改關閉”的目標,也有效的防止了JSON輸出時的死迴圈問題。當前來看,不失為一種更佳的實踐。
騏驥一躍,不能十步;駑馬十駕,功在不捨。