背景

编写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.*;

/**
* 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 {};
}
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;

/**
* 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不能满足需求,需要自定义过滤器,用于序列化时动态过滤。

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.*;

/**
* 自定义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序列化方法

我们自定义了过滤器,要想实现字段过滤,需要自定义序列化方法。

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;

/**
* 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处理器定义

以上准备就绪,我们需要定义一个处理器,用来调用我们的自定义序列化方法实现动态过滤字段。

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;

/**
* 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处理后返回,我们需要在这个处理器前添加我们的自定义处理器,才能实现字段过滤,否则进行到这个过滤器后,就不会往下处理,直接返回数据了。

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;

/**
* 配置 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数据就可以实现动态过滤了,对于实体无侵入,不影响之前的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 分页查询建筑物信息
*
* @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);
}