我正在使用spring MVC 编写一个休息服务,它会生成 JSON 响应。它应该允许客户端在响应中仅选择给定的字段,这意味着客户端可以将他感兴趣的字段作为url参数提及,如?fields=field1,field2
。
使用杰克逊注释并没有提供我正在寻找的东西,因为它不是动态的,杰克逊的过滤器似乎也没有足够的前景。 到目前为止,我正在考虑实现一个自定义消息转换器,可以解决这个问题。
还有其他更好的方法来实现这一目标吗?我想如果这个逻辑没有与我的服务或控制器耦合。
答案 0 :(得分:7)
从Spring 4.2开始,MappingJacksonValue
支持@JsonFilter
您可以直接将PropertyFilter注入控制器中的MappingJacksonValue。
@RestController
public class BookController {
private static final String INCLUSION_FILTER = "inclusion";
@RequestMapping("/novels")
public MappingJacksonValue novel(String[] include) {
@JsonFilter(INCLUSION_FILTER)
class Novel extends Book {}
Novel novel = new Novel();
novel.setId(3);
novel.setTitle("Last summer");
novel.setAuthor("M.K");
MappingJacksonValue res = new MappingJacksonValue(novel);
PropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept(include);
FilterProvider provider = new SimpleFilterProvider().addFilter(INCLUSION_FILTER, filter);
res.setFilters(provider);
return res;
}
或者您可以通过ResponseBodyAdvice声明全局策略。以下示例通过“exclude”参数实现过滤策略。
@ControllerAdvice
public class DynamicJsonResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
public static final String EXCLUDE_FILTER_ID = "dynamicExclude";
private static final String WEB_PARAM_NAME = "exclude";
private static final String DELI = ",";
private static final String[] EMPTY = new String[]{};
@Override
protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {
if (container.getFilters() != null ) {
// It will be better to merge FilterProvider
// If 'SimpleFilterProvider.addAll(FilterProvider)' is provided in Jackson, it will be easier.
// But it isn't supported yet.
return;
}
HttpServletRequest baseReq = ((ServletServerHttpRequest) req).getServletRequest();
String exclusion = baseReq.getParameter(WEB_PARAM_NAME);
String[] attrs = StringUtils.split(exclusion, DELI);
container.setFilters(configFilters(attrs));
}
private FilterProvider configFilters(String[] attrs) {
String[] ignored = (attrs == null) ? EMPTY : attrs;
PropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(ignored);
return new SimpleFilterProvider().addFilter(EXCLUDE_FILTER_ID, filter);
}
}
答案 1 :(得分:3)
我从未这样做过,但在查看此页http://wiki.fasterxml.com/JacksonFeatureJsonFilter之后,似乎可以通过这种方式做到你想做的事情:
1)创建一个自定义JacksonAnnotationIntrospector实现(通过扩展默认值),它将使用ThreadLocal变量为当前请求选择一个过滤器,并创建一个提供该过滤器的自定义FilterProvider。
2)配置消息转换器的ObjectMapper以使用自定义的introspector和过滤器提供程序
3)为REST服务创建MVC拦截器,检测fields
请求参数,并通过自定义过滤器提供程序为当前请求配置新过滤器(这应该是线程本地过滤器)。 ObjectMapper应该通过自定义的JacksonAnnotationIntrospector来获取它。
我并不是100%确定此解决方案是线程安全的(这取决于ObjectMapper如何在内部使用注释内部跟踪器和过滤器提供程序)。
- 编辑 -
好的,我做了一个测试实现,发现步骤1)不会起作用,因为Jackson会为每个类缓存AnnotationInterceptor的结果。我修改了只在注释的控制器方法上应用动态过滤的想法,并且只有当对象没有定义了JonFilter时才这样做。
这是解决方案(它非常冗长):
DynamicRequestJsonFilterSupport类管理要过滤的每个请求字段:
public class DynamicRequestJsonFilterSupport {
public static final String DYNAMIC_FILTER_ID = "___DYNAMIC_FILTER";
private ThreadLocal<Set<String>> filterFields;
private DynamicIntrospector dynamicIntrospector;
private DynamicFilterProvider dynamicFilterProvider;
public DynamicRequestJsonFilterSupport() {
filterFields = new ThreadLocal<Set<String>>();
dynamicFilterProvider = new DynamicFilterProvider(filterFields);
dynamicIntrospector = new DynamicIntrospector();
}
public FilterProvider getFilterProvider() {
return dynamicFilterProvider;
}
public AnnotationIntrospector getAnnotationIntrospector() {
return dynamicIntrospector;
}
public void setFilterFields(Set<String> fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(fieldsToFilter)));
}
public void setFilterFields(String... fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(fieldsToFilter))));
}
public void clear() {
filterFields.remove();
}
public static class DynamicIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(Annotated annotated) {
Object result = super.findFilterId(annotated);
if (result != null) {
return result;
} else {
return DYNAMIC_FILTER_ID;
}
}
}
public static class DynamicFilterProvider extends FilterProvider {
private ThreadLocal<Set<String>> filterFields;
public DynamicFilterProvider(ThreadLocal<Set<String>> filterFields) {
this.filterFields = filterFields;
}
@Override
public BeanPropertyFilter findFilter(Object filterId) {
return null;
}
@Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
if (filterId.equals(DYNAMIC_FILTER_ID) && filterFields.get() != null) {
return SimpleBeanPropertyFilter.filterOutAllExcept(filterFields.get());
}
return super.findPropertyFilter(filterId, valueToFilter);
}
}
}
JsonFilterInterceptor拦截使用自定义@ResponseFilter注释注释的控制器方法。
public class JsonFilterInterceptor implements HandlerInterceptor {
@Autowired
private DynamicRequestJsonFilterSupport filterSupport;
private ThreadLocal<Boolean> requiresReset = new ThreadLocal<Boolean>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
ResponseFilter filter = method.getMethodAnnotation(ResponseFilter.class);
String[] value = filter.value();
String param = filter.param();
if (value != null && value.length > 0) {
filterSupport.setFilterFields(value);
requiresReset.set(true);
} else if (param != null && param.length() > 0) {
String filterParamValue = request.getParameter(param);
if (filterParamValue != null) {
filterSupport.setFilterFields(filterParamValue.split(","));
}
}
}
requiresReset.remove();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Boolean reset = requiresReset.get();
if (reset != null && reset) {
filterSupport.clear();
}
}
}
这是自定义的@ResponseFilter注释。您可以定义静态过滤器(通过注释的值属性)或基于请求参数的过滤器(通过注释&#39;参数属性):
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseFilter {
String[] value() default {};
String param() default "";
}
您需要在config类中设置消息转换器和拦截器:
...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
}
@Bean
JsonFilterInterceptor jsonFilterInterceptor() {
return new JsonFilterInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jsonFilterInterceptor);
}
@Bean
DynamicRequestJsonFilterSupport filterSupport() {
return new DynamicRequestJsonFilterSupport();
}
@Bean
MappingJackson2HttpMessageConverter converter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(filterSupport.getAnnotationIntrospector());
mapper.setFilters(filterSupport.getFilterProvider());
converter.setObjectMapper(mapper);
return converter;
}
...
最后,您可以像这样使用过滤器:
@RequestMapping("/{id}")
@ResponseFilter(param = "fields")
public Invoice getInvoice(@PathVariable("id") Long id) { ... }
当请求/ invoices / 1?fields = id时,数字响应将是 过滤后,只返回id和number属性。
请注意我还没有彻底测试过,但它应该让你开始。
答案 2 :(得分:3)
以下是两个能够做到这一点的示例函数,首先获取所有公共字段和公共getter,第二个获取当前类及其所有父类中的所有声明字段(包括私有字段):
public Map<String, Object> getPublicMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
List<String> gettedFields = new ArrayList<String>();
Map<String, Object> values = new HashMap<String, Object>();
for (Method getter: obj.getClass().getMethods()) {
if (getter.getName().startsWith("get") && (getter.getName().length > 3)) {
String name0 = getter.getName().substring(3);
String name = name0.substring(0, 1).toLowerCase().concat(name0.substring(1));
gettedFields.add(name);
if ((names == null) || names.isEmpty() || names.contains(name)) {
values.put(name, getter.invoke(obj));
}
}
}
for (Field field: obj.getClass().getFields()) {
String name = field.getName();
if ((! gettedFields.contains(name)) && ((names == null) || names.isEmpty() || names.contains(name))) {
values.put(name, field.get(obj));
}
}
return values;
}
public Map<String, Object> getFieldMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if ((names == null) || names.isEmpty() || names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
然后你只需要得到这个函数之一的结果(或者你可以适应你的要求的结果)并用Jackson序列化它。
如果您拥有域对象的自定义编码,则必须在两个不同的位置维护序列化规则:哈希生成和杰克逊序列化。在这种情况下,您可以简单地使用Jackson生成完整的类序列化,然后过滤生成的字符串。以下是此类过滤功能的示例:
public String jsonSub(String json, List<String> names) throws IOException {
if ((names == null) || names.isEmpty()) {
return json;
}
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(json, HashMap.class);
for (String name: map.keySet()) {
if (! names.contains(name)) {
map.remove(name);
}
}
return mapper.writeValueAsString(map);
}
编辑:Spring MVC中的集成
当您谈到网络服务和杰克逊时,我假设您使用Spring RestController
或ResponseBody
注释和(引擎盖下)MappingJackson2HttpMessageConverter
。如果您使用Jackson 1,它应该是MappingJacksonHttpMessageConverter
。
我建议只是添加一个新的HttpMessageConverter
,它可以使用上述过滤功能之一,并将实际工作(以及辅助方法)委托给真正的MappingJackson2HttpMessageConverter
。在新转换器的write
方法中,由于Spring fields
,可以访问最终的RequestContextHolder
请求参数而不需要显式的ThreadLocal变量。那样:
HttpMessageConverter
以下是此类消息转换器的示例:
public class JsonConverter implements HttpMessageConverter<Object> {
private static final Logger logger = LoggerFactory.getLogger(JsonConverter.class);
// a real message converter that will respond to ancilliary methods and do the actual work
private HttpMessageConverter<Object> delegate =
new MappingJackson2HttpMessageConverter();
// allow configuration of the fields name
private String fieldsParam = "fields";
public void setFieldsParam(String fieldsParam) {
this.fieldsParam = fieldsParam;
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
@Override
public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
@Override
public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// is there a fields parameter in request
String[] fields = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getParameterValues(fieldsParam);
if (fields != null && fields.length != 0) {
// get required field names
List<String> names = new ArrayList<String>();
for (String field : fields) {
String[] f_names = field.split("\\s*,\\s*");
names.addAll(Arrays.asList(f_names));
}
// special management for Map ...
if (t instanceof Map) {
Map<?, ?> tmap = (Map<?, ?>) t;
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (Entry entry : tmap.entrySet()) {
String name = entry.getKey().toString();
if (names.contains(name)) {
map.put(name, entry.getValue());
}
}
t = map;
} else {
try {
Map<String, Object> map = getMap(t, names);
t = map;
} catch (Exception ex) {
throw new HttpMessageNotWritableException("Error in field extraction", ex);
}
}
}
delegate.write(t, contentType, outputMessage);
}
/**
* Create a Map by keeping only some fields of an object
* @param obj the Object
* @param names names of the fields to keep in result Map
* @return a map containing only requires fields and their value
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if (names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
}
如果您希望转换器更加通用,您可以定义一个接口
public interface FieldsFilter {
Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}
并为其注入一个实现。
现在您必须要求Spring MVC使用该自定义消息控制器。
如果您使用XML配置,只需在<mvc:annotation-driven>
元素中声明它:
<mvc:annotation-driven >
<mvc:message-converters>
<bean id="jsonConverter" class="org.example.JsonConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
如果您使用Java配置,那几乎就是这么简单:
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired JsonConverter jsonConv;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConv);
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
converters.add(new ByteArrayHttpMessageConverter());
converters.add(stringConverter);
converters.add(new ResourceHttpMessageConverter());
converters.add(new SourceHttpMessageConverter<Source>());
converters.add(new AllEncompassingFormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
}
但是在这里你必须明确地添加你需要的所有默认消息转换器。
答案 3 :(得分:1)
是否会从不符合要求的对象填充HashMap?然后你可以解析HashMap。我过去曾经做过类似GSON的事情,我必须提供一个简单的实体,最后只是填充一个HashMap然后序列化它,它比设计一个全新的系统更容易维护。