Json資料的重複引用/迴圈引用($ref)
引用符號
引用 | 描述 |
---|---|
"$ref":".." | 上一級 |
"$ref":"@" | 當前物件,也就是自引用 |
"$ref":"$" | 根物件 |
"$ref":"$.children.0" | 基於路徑的引用,相當於 root.getChildren().get(0) |
1、什麼是Json的重複引用和迴圈引用?
- 重複引用:一個物件的多個屬性同時引用同一個物件,或一個集合中同時添加了同一個物件。
在下方的程式碼中我們將同一個物件向一個集合中添加了兩次(實際開發會有這樣的需求),然後使用FastJson將集合轉換成Json字串。我們期待的結果應該是 [{"name":"test"},{"name":"test"},
[{"name":"test"},{"$ref":"$[0]"}]:表示當前集合的第二個元素與第一個元素相同,引用第一個元素。
-
$ref:當前元素引用其它元素
-
$[0]:引用當前集合的0號元素
// 預設開啟檢測 @Test public void test1() { // 儲存集合 ArrayList<One> ones = new ArrayList<>(); // 引用物件 One one = new One(); one.setName("test"); // 將同一個物件新增到集合中 ones.add(one); ones.add(one); // 使用FastJson將集合轉換成Json字串 String json = JSON.toJSONString(ones); System.out.println(json); // 列印Json結果:[{"name":"test"},{"$ref":"$[0]"}] } // 開啟引用檢測 @Test public void test1() { // 儲存集合 ArrayList<One> ones = new ArrayList<>(); // 引用物件 One one = new One(); one.setName("test"); // 將同一個物件新增到集合中 ones.add(one); ones.add(one); // 使用FastJson將集合轉換成Json字串 String json = JSON.toJSONString(ones, SerializerFeature.DisableCircularReferenceDetect); System.out.println(json); // 列印Json結果:[{"name":"test"},{"name":"test"}] }
- 迴圈引用:多個物件/集合之間存在相互引用,比如A物件引用B物件,同時B物件引用A物件。
建立兩個Map集合,使兩者之間相互引用,然後使用FastJson轉換成Json字串,在轉換成時就會出現兩者迴圈呼叫的問題。即在轉換Map1時,發現Map1引用著Map2,然後將Map2引入,引入Map2時發現Map2又引用著Map1......,從此進入死迴圈之中。在FastJson中,預設對這種死迴圈的相互呼叫進行了處理(預設開啟了迴圈引用的檢測,遇到迴圈引用就使用引用符號代替),如果關閉迴圈引用檢測,FastJson就不會再使用引用符號替代引用物件,這樣就會導致無限死迴圈的相互引用,會造成java.lang.StackOverflowError(棧記憶體溢位錯誤)。
迴圈引用在實際開發中是會使用到的,比如使用基於角色的訪問控制中,會涉及到使用者、角色、許可權這三者之間的多對多的對應的關係。此時,使用實體類建立三者之間的對應關係時,就會使用到迴圈引用:一個使用者可以擁有多個角色,一個角色可以被多個使用者擁有、同時一個角色也可以擁有多種許可權,一種許可權又可以被多個角色擁有。
// 預設開啟迴圈檢測
@Test
public void test3() {
Map map1 = new HashMap();
map1.put("test1", "Map1測試資料");
Map map2 = new HashMap();
map2.put("test2", "Map2測試資料");
// 迴圈引用
map1.put("Map1引用Map2", map2);
map2.put("Map2引用Map1", map1);
// 使用FastJson轉換成Json字串
String json = JSON.toJSONString(map1);
// {"引用map2":{"map2":"test2","引用map1":{"$ref":".."}},"map1":"test1"}
System.out.println(json);
}
// 關閉迴圈檢測
@Test
public void test4() {
Map<String, Object> map1 = new HashMap<>();
map1.put("map1", "test1");
Map<String, Object> map2 = new HashMap<>();
map2.put("map2", "test2");
// 迴圈引用
map1.put("引用map2", map2);
map2.put("引用map1", map1);
// SerializerFeature:序列化器特性;DisableCircularReferenceDetect:禁用迴圈引用檢測
String json = JSON.toJSONString(map1,SerializerFeature.DisableCircularReferenceDetect);
// java.lang.StackOverflowError
System.out.println(json);
}
2、怎麼解決Json的重複引用和迴圈引用問題?
在將資料轉換成Json資料時,可以通過關閉迴圈引用的檢測來消除Json資料的引用符號,而使用真正的物件資料來顯示。但是當遇到迴圈引用時,就會導致不斷的進行引用進入死迴圈,就會導致程式崩潰而丟擲棧記憶體溢位錯誤。
所以,FastJson的迴圈引用一般情況下因該使其保持預設的開啟檢測狀態。如果涉及到迴圈引用,因該將其拆分開來進行儲存。
下面使用基於角色的訪問控制種使用者和角色實體類之間的對應關係來演示引用問題的解決(簡單的對應關係)。
// 使用者實體類
public class User implements Serializable {
private String username; // 使用者名稱
private String password; // 密碼
private List<Role> roles; // 角色集合
}
// 角色實體類
public class Role implements Serializable {
private String roleName; // 許可權名稱
private List<User> users; // 對應的使用者集合
}
@Test
public void test() {
// 建立實體類物件
Use user = new Use("jack", "123"); // 使用者物件
Role role = new Role("管理員"); // 角色物件
// 建立關係
List<Role> roles = new ArrayList<>(); // 使用者的角色集合
roles.add(role);
user.setRoles(roles);
List<Use> users = new ArrayList<>(); // 角色的使用者集合
users.add(user);
role.setUsers(users);
// 轉換成Json資料
String json = JSON.toJSONString(user); // 預設開啟迴圈引用檢測,使用引用符號
// {"password":"123","roles":[{"roleName":"管理員","users":[{"$ref":"$"}]}],"username":"jack"}
System.out.println(json);
}
迴圈引用導致出現引用符號和棧記憶體溢位的問題根本原因就是(以User和Role為列):user物件引用了roles集合,而roles集合中的role物件中又引用了user物件。轉換過程如下:
- 先轉換user物件。
- 然後轉換roles集合,進而轉換roles集合中的role物件。
- 由於role物件又引用了user物件,所以再次轉換user物件。
- 重複1~3步。
從上可知,問題就出現在第三步的引用上,同時Json資料的轉換是根據物件的屬性進行轉換的。因此,我們只需要在第三步轉換role物件時,使其忽略role對現象中的對user引用,就可以對當前物件的引用和避免進入死迴圈的引用關係。
忽略物件屬性,在進行Json資料轉換時不進行轉換的方法:
1、@JSONField註解(靜態)
- 作用:fastJson提供的註解,為實體類的屬性進行一些Json轉換的配置。
- serialize屬性:標明該屬性在進行Json轉換時,是否進行轉換。預設為true進行轉換,false表示忽略不進行轉換。
- 弊端:直接使用註解修飾實體類的屬性,直接寫死在原始碼中,當需求變更時需要修改實體類的原始碼(實體類在不同的場景有不同的需求)。
如下方程式碼,使用@JSONField註解修飾Role物件的users屬性,並指定serialize屬性為false。然後在開啟迴圈檢測的情況下再次進行轉換,當轉換到role中的users物件時,就會自動忽略users物件不進行轉換,這樣就把不會出現引用問題了。
// 使用者實體類
public class User implements Serializable {
private String username; // 使用者名稱
private String password; // 密碼
private List<Role> roles; // 角色集合
}
// 角色實體類
public class Role implements Serializable {
private String roleName; // 許可權名稱
@JSONField(serialize = false) // 忽略屬性,不進行轉換。
private List<User> users; // 對應的使用者集合
}
@Test
public void test() {
// 建立實體類物件
Use user = new Use("jack", "123"); // 使用者物件
Role role = new Role("管理員"); // 角色物件
// 建立關係
List<Role> roles = new ArrayList<>(); // 使用者的角色集合
roles.add(role);
user.setRoles(roles);
List<Use> users = new ArrayList<>(); // 角色的使用者集合
users.add(user);
role.setUsers(users);
// 轉換成Json資料
String json = JSON.toJSONString(user); // {"password":"123","roles":[{"roleName":"管理員"}],"username":"jack"}
System.out.println(json);
}
2、使用過濾器指定需要轉換的屬性(動態)
定義一個過濾器(SimplePropertyPreFilter),指定我們需要進行Json轉換的實體類名稱。這樣,在使用FastJson轉換物件時,只有在過濾器中指定的屬性才會進行轉換,沒有指定的自動忽略。
注意:這種方式把動態的指定需要轉換的物件屬性,但是要求不同的屬性名稱不能相同。
@Test
public void test() {
// 建立實體類物件
Use user = new Use("jack", "123"); // 使用者物件
Role role = new Role("管理員"); // 角色物件
// 建立關係
List<Role> roles = new ArrayList<>(); // 使用者的角色集合
roles.add(role);
user.setRoles(roles);
List<Use> users = new ArrayList<>(); // 角色的使用者集合
users.add(user);
role.setUsers(users);
// 定義過濾器,指定需要進行轉換的屬性。
SimplePropertyPreFilter filter = new SimplePropertyPreFilter("username", "password", "roles", "roleName");
// 轉換成Json資料
String json = JSON.toJSONString(user, filter);
System.out.println(json); // {"password":"123","roles":[{"roleName":"管理員"}],"username":"jack"}
}
SimplePropertyPreFilter的建構函式的引數是可變引數型別的,可以直接傳遞引數,也可以將需要進行轉化的屬性儲存到String[]陣列中,然後將陣列傳遞進去。
public SimplePropertyPreFilter(String... properties){
this(null, properties);
}