背景 编写API
接口过程中,不可避免的会遇到一个问题,对于不同的接口,需要的字段不一样,但大多数情况下,使用的Service
层方法是相同的,也就是说,获取到的数据字段是一样的,但是往往不需要返回所有的字段。
解决方案 常用的解决思路有两种,一种是针对每个接口定义VO
类,在数据返回时,将Service
层查到的数据复制到VO
类后再返回,这样的话就可以返回需要的字段,但这样也有缺点,不同的接口,需要定义专属的VO
类,这样会使类的数量增多,后期如果需要添加一个通用字段,那么需要在每个VO
类都添加字段,否则无法返回,后期维护工作量大,不好维护,其次是性能问题,数据返回到浏览器之前,都需要将数据复制到VO
类,这样会产生许多的中间实例,影响性能;
第二种方案,在数据序列化为JSON
字符串的时候,只序列化需要返回的字段,这种方法相对第一种方法,可以很好的避免第一种方法出现的缺点,对于Jackson
原生的注解,无法实现动态过滤需求,如果把注解加在实体字段上,无法实现动态过滤,于是有了改进方案,自定义注解,通过自定义注解获取需要返回或需要过滤的字段,在序列化时处理。
代码实现 自定义注解
为了实现多注解,我们定义两个注解来实现,多注解可实现嵌套对象的字段过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.jeeplus.common.annotation;import java.lang.annotation.*;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Repeatable(JsonFieldFilters.class) public @interface JsonFieldFilter { Class<?> type(); String[] include() default {}; String[] exclude() default {"createBy" , "updateBy" }; String[] addExclude() default {}; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface JsonFieldFilters { JsonFieldFilter[] value(); }
自定义JSON
过滤器
由于Jackson
自带的过滤器com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter
不能满足需求,需要自定义过滤器,用于序列化时动态过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 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.*;@JsonFilter("JacksonJsonFilter") public class SuberJacksonFilterProvider extends FilterProvider { Map<Class<?>, Set<String>> includeMap = new HashMap<>(); Map<Class<?>, Set<String>> excludeMap = new HashMap<>(); public void include (Class<?> type, String... fields) { addToMap(includeMap, type, fields); } public void exclude (Class<?> type, String... fields) { addToMap(excludeMap, type, fields); } 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); } } }; } 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
序列化方法
我们自定义了过滤器,要想实现字段过滤,需要自定义序列化方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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;public class ResponseJsonFilterSerializer { JsonMapper mapper = JsonMapper.getInstance(); SuberJacksonFilterProvider filterProvider = new SuberJacksonFilterProvider(); 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()); } 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
处理器定义
以上准备就绪,我们需要定义一个处理器,用来调用我们的自定义序列化方法实现动态过滤字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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;public class JsonReturnFilterHandler implements HandlerMethodReturnValueHandler { @Override public boolean supportsReturnType (MethodParameter returnType) { 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 ); 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
处理后返回,我们需要在这个处理器前添加我们的自定义处理器,才能实现字段过滤,否则进行到这个过滤器后,就不会往下处理,直接返回数据了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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;@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 ; } 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++) { if (handlers.get(i) instanceof RequestResponseBodyMethodProcessor) { handlers.add(i, new JsonReturnFilterHandler()); break ; } } } }
使用示例
在方法上使用注解,填写只包含的字段或需要排除的字段,返回的JSON
数据就可以实现动态过滤了,对于实体无侵入,不影响之前的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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); }