Здравствуйте!

Недавно в своем учебном Spring проекте Java Enterprise (Topjava) столкнулся с задачей каcтомизации сериализации объекта User в JSON в зависимости от контроллера: для REST API контроллера нужно было возвращать хешированный пароль (поле user.password), а для контроллера отображения на UI- нет. Можно решить задачу в «лоб», сделав нестолько TO (Data Transfer Object), но в Spring 4.2+/Jackson 2.6 появилась возможность использовать Jackson’s Serialization Views. Однако с статье есть подвох, и для невнимательных читателей вьюхи работают не так, как он ожидает.

В результате мне пришлось немного покопаться в реализации Jackson, чтобы понять, как все это работает. Коротко об этом:

MapperFeature.DEFAULT_VIEW_INCLUSION



В статье есть небольшое упоминание
In Spring MVC default configuration, MapperFeature.DEFAULT_VIEW_INCLUSION is set to false.

Это означает, что по умолчанию поля, не помеченные аннотацией @JsonView, исключаются. Но если посмотреть в код MapperFeature, то увидим:

    ...
    * Default value is enabled, meaning that non-annotated
    * properties are included in all views if there is no
    * {@link com.fasterxml.jackson.annotation.JsonView} annotation.
    *
    * Feature is enabled by default.
    */
    DEFAULT_VIEW_INCLUSION(true),

Т.е все с точностью до наоборот — все, что не помечено, включается. И если пометить только нужные для UI поля User:

public class User
    ...
    @JsonView(View.UI.class)
    protected String email;

    @JsonView(View.UI.class)
    protected boolean enabled = true;

    protected String password;

и вызвать помеченный @JsonView метод контроллера

@JsonView(View.UI.class)
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public User get(@PathVariable("id") int id) {
   return ...;
}

То в результат войдут как помеченные поля User (email, enabled, ..), так и все остальные (password).

FilteredBeanPropertyWriter


Т.к. хочется исключить из контроллера UI только одно поле password, логично будет пометить только его. Смотрим в код jackson-databind-2.8.0: если запрос контроллера и поля его результата аннотированы @JsonView, Jackson сериализует через FilteredBeanPropertyWriter.serializeAsField

    final Class<?> activeView = prov.getActiveView();
    if (activeView != null) {
        int i = 0, len = _views.length;
        for (; i < len; ++i) {
           if (_views[i].isAssignableFrom(activeView)) break;
        }
        // not included, bail out:
        if (i == len) {
            _delegate.serializeAsOmittedField(bean, jgen, prov);
            return;
        }
    }
    _delegate.serializeAsField(bean, jgen, prov);

Т.е. если View, которым помечено поле объекта, совпадает или является суперклассом от View метода контроллера, поле сериализуется. Иначе оно пропускается (serializeAsOmittedField).

Решение


В итоге:

  • создаем по одному View для каждого контекста сериализации

    
    public class View {
        public static class REST {}
        public static class UI {}
    }

  • помечаем в User исключаемые в UI поля тем View, в котором они должны присутствовать (REST)

    
    public class User
        ...
    
        protected String email;
    
        protected boolean enabled = true;
    
        @JsonView(View.REST.class)
        protected String password;

  • аннотируем метод контроллера UI соответствующим контекстом

    @JsonView(View.UI.class)
    @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public User get(@PathVariable("id") int id) {
       return ...;
    }

Теперь поле password в результат не попадет. В контроллере REST можно обойтись без @JsonView, т.к. туда включаются все поля User.

Спасибо за внимание! Надеюсь @JsonView сделают Ваши Spring приложения более красивыми и компактными.
Поделиться с друзьями
-->

Комментарии (5)


  1. dim_s
    09.08.2016 10:21

    Интересно стало, для каких целей отправляете хеш пароля в REST пользователю? Или это просто пример?


    1. gkislin
      09.08.2016 14:27

      Скорее да, как пример. Но вообще безнес требования бывают совершенно разные, и это больше задача ПМ.


  1. serf
    09.08.2016 10:48

    Старый добрый TO подход все же луше как по мне, он универсален, из TO потом в любой формат просто транслировать, а здесь частный случай — JSON.


    1. gkislin
      09.08.2016 14:33

      1. Счас достаточно много примеров проектов, в которых вообще отсутствует TO
      2. Мы же можем TO также аннотировать @JsonView. Если контекстов много, то придется на каждый из них делать свои методы в сервисе, или возможно — даже свой сервис. Или пергонять в TO в контроллере, что возможно блюстетелям строгих правил тоже может не понравится.


      1. serf
        12.08.2016 11:01

        Я уже использовал эти JsonView в нескольких проектах (никаких TO, аннотируются прямо JPA модели), пока что мне такой подход не слишком нравится, но для простых проектов жизнеспособно.