API介面使用Jackson序列化JSON資料動態過濾欄位
技術標籤:經驗分享Java後端SpringBootjavaspringbootjacksonjsonapi欄位過濾
背景
編寫API
介面過程中,不可避免的會遇到一個問題,對於不同的介面,需要的欄位不一樣,但大多數情況下,使用的Service
層方法是相同的,也就是說,獲取到的資料欄位是一樣的,但是往往不需要返回所有的欄位。
解決方案
常用的解決思路有兩種,一種是針對每個介面定義VO
類,在資料返回時,將Service
層查到的資料複製到VO
類後再返回,這樣的話就可以返回需要的欄位,但這樣也有缺點,不同的介面,需要定義專屬的VO
類,這樣會使類的數量增多,後期如果需要新增一個通用欄位,那麼需要在每個VO
VO
類,這樣會產生許多的中間例項,影響效能;
第二種方案,在資料序列化為JSON
字串的時候,只序列化需要返回的欄位,這種方法相對第一種方法,可以很好的避免第一種方法出現的缺點,對於Jackson
原生的註解,無法實現動態過濾需求,如果把註解加在實體欄位上,無法實現動態過濾,於是有了改進方案,自定義註解,通過自定義註解獲取需要返回或需要過濾的欄位,在序列化時處理。
程式碼實現
自定義註解
為了實現多註解,我們定義兩個註解來實現,多註解可實現巢狀物件的欄位過濾。
package com. jeeplus.common.annotation;
import java.lang.annotation.*;
/**
* JSON 返回欄位過濾
*
* @author zhufeihong
* @since 2020/12/28 14:32
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(JsonFieldFilters.class)
public @interface JsonFieldFilter {
/**
* 物件類
*/
Class< ?> type();
/**
* 只包含的欄位
*/
String[] include() default {};
/**
* 不包含的欄位,如果重新賦值那麼預設值失效
*/
String[] exclude() default {"createBy", "updateBy"};
/**
* 不包含的欄位,在exclude()預設值的條件下繼續新增排除欄位
*/
String[] addExclude() default {};
}
package com.jeeplus.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* JSON 返回欄位過濾
*
* @author zhufeihong
* @since 2020/12/28 14:32
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFieldFilters {
JsonFieldFilter[] value();
}
自定義JSON
過濾器
由於
Jackson
自帶的過濾器com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter
不能滿足需求,需要自定義過濾器,用於序列化時動態過濾。
package com.jeeplus.config.handler;
import com.fasterxml.jackson.annotation.JsonFilter;
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.*;
/**
* 自定義JSON序列化過濾器
*
* @author zhufeihong
* @since 2021/1/4 16:30
*/
@JsonFilter("JacksonJsonFilter")
public class SuberJacksonFilterProvider extends FilterProvider {
/**
* 包含欄位 Map
*/
Map<Class<?>, Set<String>> includeMap = new HashMap<>();
/**
* 排除欄位 Map
*/
Map<Class<?>, Set<String>> excludeMap = new HashMap<>();
/**
* 新增包含欄位
*
* @param type 欄位所屬類
* @param fields 欄位名陣列
* @since 2021/1/4 17:03
*/
public void include(Class<?> type, String... fields) {
addToMap(includeMap, type, fields);
}
/**
* 新增排除欄位
*
* @param type 欄位所屬類
* @param fields 欄位名陣列
* @since 2021/1/4 17:03
*/
public void exclude(Class<?> type, String... fields) {
addToMap(excludeMap, type, fields);
}
/**
* 實際執行新增包含/排除欄位進對應Map的方法
*
* @param map 包含欄位Map OR 排除欄位Map
* @param type 欄位所屬類
* @param fields 欄位名稱陣列
* @since 2021/1/4 17:04
*/
private void addToMap(Map<Class<?>, Set<String>> map, Class<?> type, String... fields) {
Set<String> fieldSet = map.getOrDefault(type, new HashSet<>());
fieldSet.addAll(Arrays.asList(fields));
map.put(type, fieldSet);
}
@Deprecated
@Override
public BeanPropertyFilter findFilter(Object filterId) {
throw new UnsupportedOperationException("Access to deprecated filters not supported");
}
@Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
return new SimpleBeanPropertyFilter() {
@Override
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider prov, PropertyWriter writer)
throws Exception {
if (apply(pojo.getClass(), writer.getName())) {
writer.serializeAsField(pojo, jgen, prov);
} else if (!jgen.canOmitFields()) {
writer.serializeAsOmittedField(pojo, jgen, prov);
}
}
};
}
/**
* 判斷是否序列化當前欄位,在includeMap或不在excludeMap中的欄位進行序列化
*
* @param type 欄位所屬類
* @param name 欄位名稱
* @return boolean 是否序列化
* @since 2021/1/4 17:09
*/
public boolean apply(Class<?> type, String name) {
Set<String> includeFields = includeMap.get(type);
Set<String> excludeFields = excludeMap.get(type);
if (includeFields != null && includeFields.contains(name)) {
return true;
} else if (excludeFields != null && !excludeFields.contains(name)) {
return true;
} else {
return includeFields == null && excludeFields == null;
}
}
}
自定義JSON
序列化方法
我們自定義了過濾器,要想實現欄位過濾,需要自定義序列化方法。
package com.jeeplus.core.mapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ObjectArrays;
import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.annotation.JsonFieldFilters;
import com.jeeplus.config.handler.SuberJacksonFilterProvider;
/**
* JSON 序列化,用於API返回欄位過濾
*
* @author zhufeihong
* @since 2020/12/28 15:11
*/
public class ResponseJsonFilterSerializer {
// JsonMapper 繼承自com.fasterxml.jackson.databind.ObjectMapper
JsonMapper mapper = JsonMapper.getInstance();
SuberJacksonFilterProvider filterProvider = new SuberJacksonFilterProvider();
/**
* json資料返回時過濾欄位
*
* @param clazz 需要設定規則的Class
* @param include 轉換時包含哪些欄位
* @param exclude 轉換時過濾哪些欄位
*/
public void filter(Class<?> clazz, String[] include, String[] exclude) {
if (clazz == null) {
return;
}
if (include != null && include.length > 0) {
filterProvider.include(clazz, include);
} else if (exclude != null && exclude.length > 0) {
filterProvider.exclude(clazz, exclude);
}
mapper.addMixIn(clazz, filterProvider.getClass());
}
/**
* json資料返回時過濾欄位
*
* @param fieldFilters 註解陣列進行過濾
* @since 2021/1/4 17:26
*/
public void filter(JsonFieldFilters fieldFilters) {
for (JsonFieldFilter json : fieldFilters.value()) {
this.filter(json.type(), json.include(),
ObjectArrays.concat(json.exclude(), json.addExclude(), String.class));
}
}
public String toJson(Object object) throws JsonProcessingException {
mapper.setFilterProvider(filterProvider);
return mapper.toJson(object);
}
}
ResponseJson
處理器定義
以上準備就緒,我們需要定義一個處理器,用來呼叫我們的自定義序列化方法實現動態過濾欄位。
package com.jeeplus.config.handler;
import com.google.common.collect.ObjectArrays;
import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.annotation.JsonFieldFilters;
import com.jeeplus.core.mapper.ResponseJsonFilterSerializer;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Objects;
/**
* JSON返回欄位過濾處理器
*
* @author zhufeihong
* @since 2020/12/28 14:52
*/
public class JsonReturnFilterHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 如果有自定義的 @JsonFieldFilter 註解 就用我們這個Handler 來處理
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), JsonFieldFilters.class)
|| returnType.hasMethodAnnotation(JsonFieldFilters.class)
|| AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), JsonFieldFilter.class)
|| returnType.hasMethodAnnotation(JsonFieldFilter.class);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 設定這個就是最終的處理類了,處理完不再去找下一個類進行處理
mavContainer.setRequestHandled(true);
// 獲得註解並執行filter方法 最後返回
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
Annotation[] annotations = returnType.getMethodAnnotations();
ResponseJsonFilterSerializer jsonFilterSerializer = new ResponseJsonFilterSerializer();
Arrays.asList(annotations).forEach(a -> {
if (a instanceof JsonFieldFilter) {
JsonFieldFilter json = (JsonFieldFilter) a;
jsonFilterSerializer.filter(json.type(), json.include(),
ObjectArrays.concat(json.exclude(), json.addExclude(), String.class));
} else if (a instanceof JsonFieldFilters) {
jsonFilterSerializer.filter((JsonFieldFilters) a);
}
});
Objects.requireNonNull(response).setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String json = jsonFilterSerializer.toJson(returnValue);
response.getWriter().write(json);
}
}
註冊處理器物件
處理器定義好了,我們需要註冊它,才能使用,在
Spring Boot
專案中,對於API
介面返回的JSON
資料,交由org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
處理後返回,我們需要在這個處理器前新增我們的自定義處理器,才能實現欄位過濾,否則進行到這個過濾器後,就不會往下處理,直接返回資料了。
package com.jeeplus.config.handler;
import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.utils.collection.CollectionUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import java.util.ArrayList;
import java.util.List;
/**
* 配置 HandlerMethodReturnValueHandler
* 在此配置 API 介面返回的 JSON 欄位過濾處理器
* 在處理器 {@link RequestResponseBodyMethodProcessor} 前新增 {@link JsonReturnFilterHandler}
* 預設的返回值處理配置 {@link RequestMappingHandlerAdapter#getDefaultReturnValueHandlers()}
*
* @author zhufeihong
* @see JsonFieldFilter
* @since 2020/12/30 10:10
*/
@Configuration
public class InitializingRequestMappingHandler implements InitializingBean {
@Autowired
private RequestMappingHandlerAdapter adapter;
@Override
public void afterPropertiesSet() throws Exception {
List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
if (CollectionUtil.isEmpty(returnValueHandlers)) {
return;
}
// 不能直接使用 returnValueHandlers集合,因為此集合被方法unmodifiableList設定為不可修改
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(returnValueHandlers);
this.decorateHandlers(handlers);
adapter.setReturnValueHandlers(handlers);
}
private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
for (int i = 0; i < handlers.size(); i++) {
// 在RequestResponseBodyMethodProcessor前新增自定義json資料返回處理器,用於返回欄位過濾
if (handlers.get(i) instanceof RequestResponseBodyMethodProcessor) {
handlers.add(i, new JsonReturnFilterHandler());
break;
}
}
}
}
使用示例
在方法上使用註解,填寫只包含的欄位或需要排除的欄位,返回的
JSON
資料就可以實現動態過濾了,對於實體無侵入,不影響之前的方法。
/**
* 分頁查詢建築物資訊
*
* @param queryParam 查詢引數
* @param page 分頁引數
* @return java.util.Map<java.lang.String, java.lang.Object>
* @author zhufeihong
* @date 2020/11/4 16:48
*/
@Api
@RequestMapping("/findPage")
@JsonFieldFilter(type = BuildLocation.class, include = {"createDate", "updateDate", "id"})
@JsonFieldFilter(type = Page.class, include = {"pageSize", "pageNo", "count", "list"})
public Map<String, Object> findPage(@RequestAttribute BuildLocation queryParam,
@RequestAttribute Page<BuildLocation> page) {
Page<BuildLocation> pageList;
if (Strings.isBlank(queryParam.getAddressId())) {
pageList = new Page<>();
} else {
pageList = buildLocationService.findPage(page, queryParam);
}
return super.success(pageList);
}