springboot2 jackson實(shí)現(xiàn)動(dòng)態(tài)返回類字段方式
問(wèn)題與需求
自從前后端分離的開(kāi)發(fā)模式廣泛普及之后,JSON 便成為了端到端交互的首選數(shù)據(jù)結(jié)構(gòu)。
我們?cè)谑褂?java 開(kāi)發(fā)后端接口的時(shí)候,往往會(huì)出現(xiàn)我們一個(gè)類有十來(lái)個(gè)字段,但是前端使用到的可能就兩三個(gè)字段,產(chǎn)生大量冗余字段的情況,雖然對(duì)開(kāi)發(fā)沒(méi)什么影響,但是感覺(jué)上就很不爽,并且好些敏感字段返回出去,會(huì)降低程序的安全性。
比如下面這個(gè)典型的例子:
@Getter
@Setter
public class User extends GlobalEntity {
private Integer id; // 主鍵
private String roleCode; // 外鍵,角色代碼
private String name; // 用戶昵稱
private String account; // 賬號(hào)
private String pwd; // 密碼
}- 我們?cè)诮o前端返回用戶信息的時(shí)候,pwd 密碼字段肯定要過(guò)濾掉,不然會(huì)有安全隱患
- 我們?cè)谀承┙壎▓?chǎng)景,比如一個(gè)下拉選擇框選擇綁定用戶,這時(shí)候只需要 id 和 name 字段,其它字段就比較多余
- 但是我們?cè)?service 層肯定都是調(diào)用相同的方法,這時(shí)候我們就會(huì)想要 在控制器層 控制該實(shí)體類 需要返回的字段
修改 @JsonFilter 的執(zhí)行機(jī)制
Jackson 本身支持
@JsonIgnore 只要在字段上加上該注解,就會(huì)被過(guò)濾掉,但是無(wú)法做到動(dòng)態(tài),比如同一個(gè)類在 A 接口需要一個(gè)字段在B接口時(shí)不需要這個(gè)字段,該注解就無(wú)法實(shí)現(xiàn)
@JsonFilter 雖然支持動(dòng)態(tài)配置過(guò)濾規(guī)則,但是這要求我們針對(duì)不同的類,寫入不同的過(guò)濾規(guī)則,而且像 springboot 程序中,我們一般是全局共享一個(gè) ObjectMapper 對(duì)象,如果要對(duì)同一個(gè)類實(shí)現(xiàn)不同過(guò)濾規(guī)則,多線程情況下,會(huì)出現(xiàn)線程安全問(wèn)題
筆者針對(duì)以上需求,結(jié)合 @JsonFilter 注解,修改并實(shí)現(xiàn)了自己過(guò)濾機(jī)制,從而實(shí)現(xiàn)動(dòng)態(tài)過(guò)濾字段功能,廢話不多說(shuō),上代碼
實(shí)現(xiàn)一個(gè)自己的過(guò)濾規(guī)則類
package com.hwq.common.api.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.BeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import java.util.HashMap;
import java.util.Map;
public class JsonFilter extends FilterProvider {
/**
* 對(duì)于規(guī)則我們采用 ThreadLocal 封裝,防止出現(xiàn)線程安全問(wèn)題
*/
private static final ThreadLocal<Map<Class<?>, String[]>> include = new ThreadLocal<>();
/**
* 清空規(guī)則
*/
public static void clear() {
include.remove();
}
/**
* 設(shè)置過(guò)濾規(guī)則
* @param clazz 規(guī)則
*/
public static void add(Class<?> clazz, String ... fields) {
Map<Class<?>, String[]> map = include.get();
if (map == null) {
map = new HashMap<>();
include.set(map);
}
map.put(clazz, fields);
}
/**
* 一個(gè)將過(guò)期的方法,但是目前還是需要實(shí)現(xiàn),拋個(gè)異常即可
*/
@Deprecated
@Override
public BeanPropertyFilter findFilter(Object filterId) {
throw new UnsupportedOperationException("不支持訪問(wèn)即將過(guò)期的過(guò)濾器");
}
/**
* 重寫規(guī)律規(guī)則
*/
@Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
return new SimpleBeanPropertyFilter() {
@Override
public void serializeAsField(
Object pojo,
JsonGenerator jg,
SerializerProvider sp,
PropertyWriter pw
) throws Exception {
if (apply(pojo.getClass(), pw.getName())) {
pw.serializeAsField(pojo, jg, sp);
} else if (!jg.canOmitFields()) {
pw.serializeAsOmittedField(pojo, jg, sp);
}
}
};
}
/**
* 判斷該字段是否需要,返回 true 序列化,返回 false 則過(guò)濾
* @param type 實(shí)體類類型
* @param name 字段名
*/
public boolean apply(Class<?> type, String name) {
Map<Class<?>, String[]> map = include.get();
if (map == null) {
return true;
}
String[] fields = map.get(type);
for (String field : fields) {
if (field.equals(name)) {
return true;
}
}
return false;
}
}將實(shí)現(xiàn)的過(guò)濾規(guī)則注入到 ObjectMapper
我們直接在啟動(dòng)類中,注入自己的過(guò)濾規(guī)則
package com.hwq.admin.back;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hwq.common.api.config.JsonFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication // 注明為微服務(wù)程序
public class AdminBackApp {
public static void main(String[] args) {
ConfigurableApplicationContext app = SpringApplication.run(AdminBackApp.class);
ObjectMapper objectMapper = app.getBean(ObjectMapper.class);
objectMapper.setFilterProvider(new JsonFilter());
}
}
測(cè)試使用
- 實(shí)體類,需要加上 @JsonFilter(“f”) 注解,f 內(nèi)容隨便填寫
package com.hwq.common.api.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.hwq.common.api.model.common.GlobalEntity;
import lombok.Getter;
import lombok.Setter;
@JsonFilter("f")
@TableName("t_menu")
@Getter
@Setter
public class Menu extends GlobalEntity {
@TableId(value = "id", type = IdType.AUTO)
private Integer id; // 主鍵
private Integer pid; // 菜單的主鍵,一級(jí)菜單為0
private String name; // 菜單名稱
private String path; // 菜單路徑
private String icon; // 菜單圖標(biāo)
private Integer ordered; // 菜單序號(hào)
}- 接口使用
@PostMapping("menu/list")
public ResultVO<Object> list() {
// 這里配置某個(gè)類需要返回的字段,如果有多個(gè)類,可以多次 add
JsonFilter.add(Menu.class, "id", "name", "icon", "path");
List<Menu> list = menuService.list();
return ResultVO.ok("查詢成功", list);
}- 查詢結(jié)果展示

抗干擾
上面的方式雖然實(shí)現(xiàn)了線程隔離,防止了線程安全問(wèn)題,但是 springboot 的接口是以線程池的方式運(yùn)行的,如果我們?cè)谝粋€(gè)線程給某個(gè)類設(shè)置了過(guò)濾的字段,下一次訪問(wèn)如果也用到了該線程,并且沒(méi)對(duì)之前的規(guī)則做清理操作,那么他就會(huì)使用上一次的過(guò)濾規(guī)則,使接口出現(xiàn)奇怪的現(xiàn)象
解決方式一:
在所有接口前面,執(zhí)行 JsonFilter 的 clear 方法;
@PostMapping("menu/tree")
public ResultVO<List<Menu>> tree() {
JsonFilter.clear(); // 清理之前的過(guò)濾設(shè)置
List<Menu> vos = menuService.tree();
return ResultVO.ok("查詢成功", vos);
}這種方式就要求我們?cè)谒锌刂茖拥姆椒ǘ家獔?zhí)行 JsonFilter.clear(); 語(yǔ)句,明顯代碼不夠優(yōu)雅
解決方法二:
使用 AOP 的前置攔截,執(zhí)行該代碼
package com.hwq.admin.back.config;
import com.hwq.common.api.config.JsonFilter;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class JsonFilterAop {
/**
* (1)@annotation:用來(lái)攔截所有被某個(gè)注解修飾的方法
* (2)@within:用來(lái)攔截所有被某個(gè)注解修飾的類
* (3)within:用來(lái)指定掃描的包的范圍
*/
@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void doBefore() {
JsonFilter.clear();
}
}@within(org.springframework.web.bind.annotation.RestController)
表示攔截所有帶 @RestController 注解的類,如果有其他需求,可以做適當(dāng)修改
擴(kuò)展
JsonFilter.add(Menu.class, "id", "name");
- 針對(duì)這里的情況,我們還可以讓前端傳遞過(guò)來(lái),實(shí)現(xiàn)一個(gè)接口讓前端控制返回哪些字段的功能
- 當(dāng)前筆者實(shí)現(xiàn)的是包含關(guān)系,只有被配置的字段才會(huì)出現(xiàn),如果出現(xiàn)一個(gè)類需要很多字段,只過(guò)濾一兩個(gè)字段,我們也可以通過(guò)修改 過(guò)濾規(guī)則類 JsonFilter 實(shí)現(xiàn),就擴(kuò)展一下 apply 方法就好,很簡(jiǎn)單。不過(guò)筆者不推薦,如果一個(gè)接口大部分字段都需要,那就全部返回好了,冗余一兩個(gè)字段也不是不能接受的
- 如果覺(jué)得在業(yè)務(wù)代碼里寫上上面的代碼不好看什么的,也可以通過(guò) 注解的方式實(shí)現(xiàn),然后用 AOP 攔截實(shí)現(xiàn),代碼邏輯差不多
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
如何利用Spring?MVC實(shí)現(xiàn)RESTful風(fēng)格
這篇文章主要介紹了如何利用Spring?MVC實(shí)現(xiàn)RESTful風(fēng)格,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
Java+opencv3.2.0實(shí)現(xiàn)hough圓檢測(cè)功能
這篇文章主要為大家詳細(xì)介紹了Java+opencv3.2.0實(shí)現(xiàn)hough圓檢測(cè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
JAVA String轉(zhuǎn)化成java.sql.date和java.sql.time方法示例
這篇文章主要給大家分享了關(guān)于JAVA String轉(zhuǎn)化成java.sql.date和java.sql.time的方法,文中給出了詳細(xì)的示例代碼,相信對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03

