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
|
|
Поскольку нам нужен другой формат, я написал собственный сериализатор и прикрепил его к классу.
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
|
Поскольку клиент также может отправлять ссылки, должна произойти некоторая пользовательская десериализация. Первый срез десериализатора выглядел так и работал хорошо:
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()); } } |
Проверка может быть улучшена еще больше, но вы поняли идею.