Статьи

(Де) сериализация и проверка пользовательских примитивов и DTO

В последнее время мы представили вам наш новый HTTP-фреймворк — HttpMate. Во вступительной статье мы ссылались на сопоставление запросов и ответов на объекты домена как на «самую сложную техническую деталь», а также на то, как нам поможет другой помощник — MapMate.

Действительно, MapMate снимает нагрузку с HttpMate, когда дело доходит до сопоставления атрибутов запроса с объектами вашего домена. Он заботится о преобразовании ответов в соответствующий формат (JSON, XML, YAML и т. Д.), В основном выполняя десериализацию и сериализацию, а также многое другое.

В этой статье мы сосредоточимся на том, как MapMate помогает нам (де) сериализовать объекты запроса / ответа контролируемым и предсказуемым образом.

Пользовательские Примитивы

Давайте вспомним наш пример из предыдущей статьи; у нас есть простой UseCase для отправки электронной почты. Для этого нам нужен объект Email, который бы имел:

  • отправитель
  • Приемник
  • Тема
  • тело

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

1
2
3
public Object sendEmail(final Object sender, final Object receiver, final Object subject, final Object body) {
        ...
}

Это оставляет нам много открытых вопросов:

  • такое отправитель instanceOf String или byte []?
  • что такое кодировка?
  • тело сжато почтовым индексом?

Список можно продолжить. Хотя бывают случаи, когда это может быть уместно, я уверен, что вам будет удобнее:

1
2
3
public String sendEmail(final String sender, final String receiver, final String subject, final String body) {
        ...
}

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

Тем не менее, все еще не ясно, содержит ли поле отправителя имя пользователя или ее адрес электронной почты? Эта та же неоднозначность является причиной бесконечной неопределенности при написании модульных тестов … в той степени, в которой использование генераторов случайных строк для тестирования метода, который, как известно, должен принимать только адреса электронной почты.

Следующая сигнатура метода работает лучше с точки зрения неоднозначности, как для людей, так и для компиляторов:

1
2
3
public Receipt sendEmail(final EmailAddress sender, final EmailAddress receiver, final Subject subject, final Body body) {
        ...
}

Точно так же, как мы можем доверять, что String — это String, а Integer — это Integer, теперь мы можем верить, что EmailAddress — это адрес электронной почты, а Subject на самом деле является субъектом — они стали пользовательскими примитивами для метода отправки электронной почты.

Отправитель и Получатель не являются безликими «Струнами», и они очень сильно отличаются от «Субъекта» и «Тела». Они являются адресами электронной почты, и мы можем представлять их как таковые, проверяя их значение, например, с помощью некоторых нормальных регулярных выражений. (остерегайтесь ReDoS )

Рациональность использования фабричных методов как средства создания «всегда правильных» объектов широко обсуждалась и проверялась. Имея это в виду, мы создадим класс EmailAddress для нашего примера использования, который затем будет использоваться как пользовательский тип примитива для полей Sender и Receiver.

01
02
03
04
05
06
07
08
09
10
11
12
public final class EmailAddress {
    private final String value;
 
    private EmailAddress(final String value) {
        this.value = value;
    }
 
    public static EmailAddress fromStringValue(final String value) {
        final String validated = EmailAddressValidator.ensureEmailAddress(value, "emailAddress");
        return new EmailAddress(validated);
    }
}

Поскольку — единственная переменная экземпляра — private и final, она может быть назначена только с помощью приватного конструктора, который может быть вызван только снаружи класса с помощью общедоступного метода фабрики, который проверяет ввод перед передачей его конструктору — мы можем будьте уверены, что всякий раз, когда мы получаем экземпляр EmailAddress, он действителен.

Если вам интересно узнать о реализации EmailAddressValidator, ознакомьтесь с исходным кодом этого примера проекта.

Теперь наши доменные объекты могут использовать не только примитивы по умолчанию, такие как String, Double, Integer и т. Д., Но также и пользовательские примитивы, такие как EmailAddress и Body, Subject и т. Д. Обычно, хотя нам необходимо иметь возможность хранить объект домена в базе данных. или передать его другому сервису или пользовательскому интерфейсу. Ни одна другая сторона, тем не менее, не знает о Custom Primitive под названием EmailAddress Следовательно, нам нужно «представление» этого, что-то и HTTP, и постоянство, и дружественное человеку — строка.

01
02
03
04
05
06
07
08
09
10
11
12
public final class EmailAddress {
    private final String value;
 
    public static EmailAddress fromStringValue(final String value) {
        final String validated = EmailAddressValidator.ensureEmailAddress(value, "emailAddress");
        return new EmailAddress(validated);
    }
 
    public String stringValue() {
        return this.value;
    }
}

Метод «stringValue», который мы добавили, является строковым представлением нашего пользовательского примитива. Теперь мы можем отправить «stringValue» EmailAddress, а затем восстановить его на основе полученного значения. По сути, методы fromString и stringValue являются механизмами десериализации и сериализации EmailAddress соответственно.

Следуя этому подходу, мы можем также создавать пользовательские примитивы для тела и темы нашего электронного письма:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class Body {
    private final String value;
 
    public static Body fromStringValue(final String value) {
        final String emailAddress = LengthValidator.ensureLength(value, 1, 1000, "body");
        return new Body(emailAddress);
    }
 
    public String stringValue() {
        return this.value;
    }
}
 
public final class Subject {
    private final String value;
 
    public static Subject fromStringValue(final String value) {
        final String validated = LengthValidator.ensureLength(value, 1, 256, "subject");
        return new Subject(validated);
    }
 
    public String stringValue() {
        return this.value;
    }
}

Объекты передачи данных

Вооружившись нашими пользовательскими примитивами, мы теперь готовы создать надлежащий объект передачи данных — электронное письмо, что является довольно простой задачей, поскольку в основном это неизменяемая структура:

1
2
3
4
5
6
public final class Email {
    public final EmailAddress sender;
    public final EmailAddress receiver;
    public final Subject subject;
    public final Body body;
}

Тот же самый «всегда действительный» подход применим и к объектам передачи данных, за исключением того, что здесь нам проще, поскольку мы используем наши пользовательские примитивы.

Заводской метод DTO может быть таким же простым, как проверка наличия обязательных полей, или таким же сложным, как применение проверок между полями.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public final class Email {
    public final EmailAddress sender;
    public final EmailAddress receiver;
    public final Subject subject;
    public final Body body;
 
    public static Email restore(final EmailAddress sender,
                                final EmailAddress receiver,
                                final Subject subject,
                                final Body body) {
        RequiredParameterValidator.ensureNotNull(sender, "sender");
        RequiredParameterValidator.ensureNotNull(receiver, "receiver");
        RequiredParameterValidator.ensureNotNull(body, "body");
        return new Email(sender, receiver, subject, body);
}

К сожалению, современные (де) сериализационные и валидационные структуры не очень хорошо подходят для такого рода DTO.

Вот пример JSON, который вы получите в лучшем случае, если вы отправите DTO по электронной почте в такую ​​среду с конфигурацией по умолчанию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
{
  "sender": {
    "value": "[email protected]"
  },
  "receiver": {
    "value": "[email protected]"
  },
  "subject": {
    "value": "subject"
  },
  "body": {
    "value": "body"
  }
}

В то время как то, что можно было бы ожидать, это:

1
2
3
4
5
6
{
  "sender": "[email protected]",
  "receiver": "[email protected]",
  "subject": "subject",
  "body": "body"
}

Хотя эту проблему можно решить с помощью тонны стандартного кода, проверка — это другой тип зверя, который становится смертельно опасным в тот момент, когда вы хотите «сообщить обо всех ошибках проверки сразу» с вашего сервера. Почему бы сразу не сообщить пользователю, что отправитель и получатель недействительны, вместо того, чтобы отправить ее в поисках разрешения A38. Фактически, именно так мы себя чувствовали, когда пытались писать современные микросервисы, придерживаясь лучших практик чистого кода, «всегда действительного» подхода, основанного на домене, доменной безопасности…

И это проблема, которую должен решить MapMate.

MapMate

Как и в случае с HttpMate, мы позаботились о том, чтобы обеспечить легкий конструктор для начала, сохраняя возможность точной настройки зерна. Вот абсолютная минимальная конфигурация, которая позволит нашему примеру электронной почты сериализовать, десериализовать и проверять наши пользовательские примитивы и DTO.

1
2
3
4
5
public static MapMate mapMate() {
    return MapMate.aMapMate("com.envimate.examples.email_use_case")
            .usingJsonMarshallers(new Gson()::toJson, new Gson()::fromJson)
            .build();
}

Эта часть сделает следующий JSON действительным запросом:

1
2
3
4
5
6
{
    "sender": "[email protected]",
    "receiver": "[email protected]",
    "subject": "Hello world!",
    "body": "Hello from Sender to Receiver!"
}

Вы должны указать пакет для сканирования (рекурсивно) и пару (не) маршаллеров. Это может быть что угодно, что может сделать строку из карты и наоборот. Вот пример использования ObjectMapper:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
final ObjectMapper objectMapper = new ObjectMapper();
return MapMate.aMapMate("com.envimate.examples.email_use_case")
        .usingJsonMarshallers(value -> {
            try {
                return objectMapper.writeValueAsString(value);
            } catch (JsonProcessingException e) {
                throw new UnsupportedOperationException("Could not parse value " + value, e);
            }
        }, new Unmarshaller() {
            @Override
            public <t> T unmarshal(final String input, final Class<t> type) {
                try {
                    return objectMapper.readValue(input, type);
                } catch (final IOException e) {
                    throw new UnsupportedOperationException("Could not parse value " + input + " to type " + type, e);
                }
            }
        })
        .withExceptionIndicatingValidationError(CustomTypeValidationException.class)
        .build();
</t></t>

Как насчет обещанной агрегации исключений валидации?
В нашем примере все наши проверки возвращают экземпляр CustomTypeValidationException, если Custom Primitive или DTO недопустимы.

Добавьте следующую строку, чтобы MapMate распознал ваш класс Exception как признак ошибки проверки.

1
2
3
4
5
6
public static MapMate mapMate() {
    return MapMate.aMapMate("com.envimate.examples.email_use_case")
            .usingJsonMarshallers(new Gson()::toJson, new Gson()::fromJson)
            .withExceptionIndicatingValidationError(CustomTypeValidationException.class)
            .build();
}

Теперь, если мы попробуем следующий запрос:

1
2
3
4
5
6
{
  "sender": "not-a-valid-sender-value",
  "receiver": "not-a-valid-receiver-value",
  "subject": "Hello world!",
  "body": "Hello from Sender to Receiver!"
}

Мы получим следующий ответ:

1
2
3
4
5
HTTP/1.1 400 Bad Request
Date: Tue, 04 Jun 2019 18:30:51 GMT
Transfer-encoding: chunked
 
{"message":"receiver: Invalid email address: 'not-a-valid-receiver-value',sender: Invalid email address: 'not-a-valid-sender-value'"}

Заключительные слова

Представленный конструктор MapMate упрощает первоначальное использование. Однако все описанные значения по умолчанию являются настраиваемыми, кроме того, вы можете исключать пакеты и классы как из пользовательских примитивов, так и из DTO, вы можете настроить, какие исключения считаются ошибками проверки, и как они обрабатываются, вы можете указать другое имя метода для пользовательского Примитивную сериализацию или предоставьте свою лямбду для (де) сериализации обоих вместе.

Дополнительные примеры и сведения о MapMate можно найти в репозитории MapMate .

Дайте нам знать, что вы думаете и какие функции вы хотели бы видеть в MapMate дальше!

См. Оригинальную статью здесь: (Де) сериализация и проверка пользовательских примитивов и DTO. Мнения, высказанные участниками Java Code Geeks, являются их собственными.