JOSNVIEW更佳實踐

在使用SpringMVC進行開發時,使用JSONVIEW控制字段輸出雖然不難。但總感受應該有一種相對使用簡單、理解簡單的方法。本文在歷史項目實踐基礎上,嘗試找出一種更佳的實踐方法。java

項目源碼地址: https://github.com/mengyunzhi/springBootSampleCode/tree/master/jsonview

當前問題

咱們當前遇到的最大的問題是在實體中使用了大量的外部JSONVEIW
例:咱們輸出Student實體時,須要進行如下兩步操做:git

  • 定義相關的觸發器,例: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)註解。

存在問題也很明顯:github

  • Student實體的同一字段上,咱們使用了大量的JsonView,後期咱們進行維護時,只能增長新的,不敢刪除老的(由於咱們不知道誰會用這個JsonView)。不利於維護。
  • 違反了對修改關閉的原則。好比:A是負責實體類的,B是負責觸發器的。那麼B在進行觸發器開發時,須要修改A負責的實體類。而這並非咱們想要的。
  • 某個特定的JsonView具體須要了哪些實體、哪些字段,並不能一目瞭然。

解決方案

既然實體並不想並修改(哪怕是添加JsonView這樣並不影響實體結構的操做),那麼實體就要對擴展開放,以使其它調用者能夠順利的定義輸出字段。web

咱們嘗試作以下修改:spring

  • JsonView的定義移至實體類中,並在實體類中,使用實體內部定義的JsonView來進行修飾。
  • 爲了防止在json輸出時形成的死循環,凡事涉及到關聯的,單獨定義JsonView
  • 單獨定義的JsonView繼承關聯方實體內部的JsonView

示例代碼

pomapache

<?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 班級 兩個實體舉例,關係以下:json

  • 學生:班級 = n:1

學生api

@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
}

班級:app

@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
}

咱們在上述代碼中,主要作了兩件事:maven

  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輸出時的死循環問題。當前來看,不失爲一種更佳的實踐。

騏驥一躍,不能十步;駑馬十駕,功在不捨。
相關文章
相關標籤/搜索