仅将Spring MVC中提到的字段序列化为JSON响应 [英] Serialize only mentioned fields in Spring MVC to JSON response

查看:125
本文介绍了仅将Spring MVC中提到的字段序列化为JSON响应的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用spring MVC 编写一个休息服务,它会生成 JSON 响应。它应该允许客户端在响应中仅选择给定的字段,这意味着客户端可以将他感兴趣的字段作为url参数提及,如?fields = field1,field2

I am writing a rest service using spring MVC which produces JSON response. It should allow client to select only the given fields in response, means client can mention the fields he is interested in as url parameter like ?fields=field1,field2.

使用杰克逊注释并没有提供我想要的东西,因为它不是动态的,杰克逊的过滤器似乎也没有足够的前景。
到目前为止,我正在考虑实现一个可以解决这个问题的自定义消息转换器。

Using Jackson annotations does not provide what I am looking for as it is not dynamic also the filters in Jackson doesnt seem to be promising enough. So far I am thinking to implement a custom message converter which can take care of this.

还有其他更好的方法来实现这个目标吗?我想如果这个逻辑没有与我的服务或控制器结合。

Is there any other better way to achieve this? I would like if this logic is not coupled with my services or controllers.

推荐答案

恕我直言,最简单的方法就是使用内省动态生成包含所选字段的哈希,然后使用Json序列化该哈希。您只需确定可用字段列表的内容(见下文)。

IMHO, the simplest way to do that would be to use introspection to dynamically generate a hash containing selected fields and then serialize that hash using Json. You simply have to decide what is the list of usable fields (see below).

以下是两个能够做到这一点的示例函数,首先获取所有公共字段和公共getter ,第二个获取当前类及其所有父类中的所有声明字段(包括私有字段):

Here are two example functions able to do that, first gets all public fields and public getters, the second gets all declared fields (including private ones) in current class and all its parent classes :

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序列化它。

Then you only have to get the result of one of this function (or of one you could adapt to your requirements) and serialize it with Jackson.

如果你有域对象的自定义编码,你必须维护两个不同的序列化规则地点:哈希生成和杰克逊序列化。在这种情况下,您可以简单地使用Jackson生成完整的类序列化,然后过滤生成的字符串。以下是此类过滤器函数的示例:

If you have custom encoding of you domain objects, you would have to maintain the serialization rules in two different places : hash generation and Jackson serialization. In that case, you could simply generate the full class serialization with Jackson and filter the generated string afterwards. Here is an example of such a filter function :

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中的集成

Edit : integration in Spring MVC

当你谈到网络服务和杰克逊时,我假设你使用Spring RestController ResponseBody 注释和(在引擎盖下) MappingJackson2HttpMessageConverter 。如果您使用Jackson 1,它应该是 MappingJacksonHttpMessageConverter

As you are speaking of a web service and of Jackson, I assume that you use Spring RestController or ResponseBody annotations and (under the hood) a MappingJackson2HttpMessageConverter. If you use Jackson 1 instead, it should be a MappingJacksonHttpMessageConverter.

我建议的只是添加一个新的 HttpMessageConverter 可以使用上述过滤功能之一,并将实际工作(以及辅助方法)委托给真正的 MappingJackson2HttpMessageConverter 。在该新转换器的 write 方法中,可以访问最终的字段请求参数,而无需感谢Spring RequestContextHolder 的显式ThreadLocal变量。那样:

What I propose is simply to add a new HttpMessageConverter that could make use of one of the above filtering functions, and delegate actual work (and also ancilliary methods) to a true MappingJackson2HttpMessageConverter. In the write method of that new converter, it is possible to have access to the eventual fields request parameter with no need for an explicit ThreadLocal variable thanks to Spring RequestContextHolder. That way :


  • 你保持明确的角色分离而不修改现有的控制器

  • 你在Jackson2配置中没有修改

  • 你不需要新的ThreadLocal变量,只需在已经绑定到Spring的类中使用Spring类,因为它实现了 HttpMessageConverter

  • you keep a clear separation of roles with no modification on existing controllers
  • you have no modification in Jackson2 configuration
  • you need no new ThreadLocal variable and simply use a Spring class in a class already tied to Spring since it implements HttpMessageConverter

以下是此类消息转换器的示例:

Here is an example of such a message converter :

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;
    }    
}

如果你想要转换器更通用,你可以定义一个接口

If you want the converter to be more versatile, you could define an interface

public interface FieldsFilter {
    Map<String, Object> getMap(Object obj, List<String> names)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}

并为其注入一个实现。

现在你必须要求Spring MVC使用那个自定义消息控制器。

Now you must ask Spring MVC to use that custom message controller.

如果你使用XML配置,你只需在<$中声明它c $ c>< mvc:annotation-driven> 元素:

If you use XML config, you simply declare it in the <mvc:annotation-driven> element :

<mvc:annotation-driven  >
    <mvc:message-converters>
        <bean id="jsonConverter" class="org.example.JsonConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

如果您使用Java配置,它几乎一样简单:

And if you use Java configuration, it is almost as simple :

@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());
  }
}

但是你必须明确地添加所有默认消息你需要的转换器。

but here you have to explicitely add all the default message converters that you need.

这篇关于仅将Spring MVC中提到的字段序列化为JSON响应的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆