Статьи

Пользовательский десериализатор в Джексоне и проверка

tl; dr: важно добавить проверку ввода для пользовательских десериализаторов json в Джексоне.

В RHQ мы используем разбор Json в нескольких местах — будь то прямо в плагине as7 / Wildfly, будь то в REST-api косвенно через RESTEasy 2.3.5, который уже выполняет тяжелую работу.

Теперь у нас есть Link на компонент:

1
2
3
4
public class Link {
   String rel;
   String href;
}

Стандартный способ сериализации это

1
{ "rel":"edit", "href":"http://acme.org" }

Поскольку нам нужен другой формат, я написал собственный сериализатор и прикрепил его к классу.

1
2
3
4
5
6
7
@JsonSerialize(using = LinkSerializer.class)
@JsonDeserialize(using = LinkDeserializer.class)
@Produces({"application/json","application/xml"})
public class Link {
 
    private String rel;
    private String href;

Этот пользовательский формат выглядит так:

1
2
3
4
5
{
    "edit": {
        "href": "http://acme.org"
    }
}

Поскольку клиент также может отправлять ссылки, должна произойти некоторая пользовательская десериализация. Первый срез десериализатора выглядел так и работал хорошо:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class LinkDeserializer extends JsonDeserializer<link>{
 
    @Override
    public Link deserialize(JsonParser jp,
        DeserializationContext ctxt) throws IOException
    {
        String tmp = jp.getText(); // {
        jp.nextToken(); // skip over { to the rel
        String rel = jp.getText();
        jp.nextToken(); // skip over  {
        […]
 
        Link link = new Link(rel,href);
 
        return link;
    }

На днях произошло то, что в некоторых тестах я отправлял данные, и наш сервер ужасно взорвался. Использование памяти возросло, сборщик мусора занял огромное количество процессорного времени, и вызов в итоге завершился с OutOfMemoryException .

После некоторого расследования я обнаружил, что клиент отправлял объект Link в нашем специальном формате, а в исходном формате, который я показал первым. Дальнейшие исследования показали, что на самом деле LinkDeserializer потреблял токены из потока, как показано выше, а затем глотал последующие токены из входных данных. Поэтому, когда он вернулся, весь анализатор был в плохом состоянии и затем пытался копировать большие массивы, пока мы не увидели OOME.

После того, как я получил это, я изменил реализацию, добавив проверку и заблаговременно выручив при неверном вводе, чтобы синтаксический анализатор не примет неправильный результат при неверном вводе:

01
02
03
04
05
06
07
08
09
10
11
12
13
public Link deserialize(JsonParser jp,
        DeserializationContext ctxt) throws IOException
    {
 
        String tmp = jp.getText(); // {
        validate(jp, tmp,"{");
        jp.nextToken(); // skip over { to the rel
        String rel = jp.getText();
        validateText(jp, rel);
        jp.nextToken(); // skip over  {
        tmp = jp.getText();
        validate(jp, tmp,"{");
        […]

Они validate*() затем просто сравнивают токен с переданным ожидаемым значением и выдают исключение при неожиданном вводе:

1
2
3
4
5
6
7
private void validate(JsonParser jsonParser, String input,
        String expected) throws JsonProcessingException {
        if (!input.equals(expected)) {
            throw new JsonParseException("Unexpected token: " + input,
               jsonParser.getTokenLocation());
        }
    }

Проверка может быть улучшена еще больше, но вы поняли идею.