纹理和基元
最近,我们为您提供了新的HTTP框架HttpMate。 在介绍性文章中 ,我们将请求和响应映射到域对象称为“最复杂的技术细节”,以及如何通过另一个伴侣MapMate帮助我们。
实际上,当将请求属性映射到您的域对象时,MapMate减轻了HttpMate的负担。 它负责将响应转换为适当的格式(JSON,XML,YAML等),本质上执行反序列化和序列化,但还有很多其他工作。
在本文中,我们将重点介绍MapMate如何以受控和可预测的方式帮助我们处理(反序列化)请求/响应对象。
自定义基元
让我们回顾一下上一篇文章中的示例; 我们有一个简单的UseCase发送电子邮件。 为此,我们需要一个Email对象,该对象应具有:
- 发件人
- 接收者
- 学科
- 身体
所有这些字段都可以表示为字符串,甚至可以表示为字节数组。 选择用来表示数据的通用类型越多,以后解释数据的可能性就越大。 想象一下以下方法:
public Object sendEmail(final Object sender, final Object receiver, final Object subject, final Object body) {
...
}
这给我们留下了很多未解决的问题:
- 是发件人instanceOf是字符串还是字节[]?
- 编码是什么?
- 拉链被压缩了吗?
清单继续。 尽管在某些情况下这可能是适当的,但我敢打赌,您会更满意:
public String sendEmail(final String sender, final String receiver, final String subject, final String body) {
...
}
后者留出了较少的解释空间:例如,我们不再需要假设编码或完全质疑参数的类型。
但是,它仍然是模棱两可的,发件人字段是否带有用户名或她的电子邮件地址? 同样的歧义是编写单元测试时产生无限不确定性的原因……在某种程度上,使用随机字符串生成器来测试一种人们只能接受电子邮件地址的方法。
对于人员和编译器,以下方法签名在歧义方面做得更好:
public Receipt sendEmail(final EmailAddress sender, final EmailAddress receiver, final Subject subject, final Body body) {
...
}
我们可以以相同的方式相信字符串是字符串,整数是整数,我们现在可以相信EmailAddress是电子邮件地址,主题实际上是主题–它们成为了send email方法的自定义原语。
发件人和接收者不是面面俱到的“字符串”,它们与“主题”和“正文”有很大不同。 它们是电子邮件地址,我们可以通过使用一些理智的正则表达式来验证其值来表示它们。 (谨防ReDoS )
使用工厂方法作为创建“始终有效”对象的方法的合理性已得到广泛讨论和验证。 考虑到这一点,我们将为示例用例创建一个EmailAddress类,然后将其用作Sender和Receiver字段的自定义原始类型。
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);
}
}
由于–唯一的实例变量是私有的且是最终变量,因此只能使用私有的构造函数进行分配,只有在将其传递给构造函数之前,可以使用验证输入的公共工厂方法从类外部调用该私有构造函数–我们可以请确保每当我们收到EmailAddress实例时,它都是有效的。
如果您现在对EmailAddressValidator实现感到好奇,请确保签出此示例项目的源代码 。
现在,我们的域对象不仅可以使用默认原语(例如String,Double,Integer等),还可以使用自定义原语(例如EmailAddress和Body,Subject等)。通常,尽管我们需要能够将域对象存储在数据库中或将其传达给其他服务或UI。 尽管没有其他方知道名为EmailAddress的自定义基元。 因此,我们需要它的“表示形式”,即HTTP,持久性和人性化的东西–字符串。
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”是自定义基元的字符串表示形式。 现在,我们可以发送EmailAddress的“ stringValue”,然后根据接收到的值对其进行重构。 本质上,“ fromString”和“ stringValue”方法分别是EmailAddress的“反序列化”和“序列化”机制。
按照这种方法,我们还可以为电子邮件的正文和主题创建自定义基元:
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;
}
}
数据传输对象
有了我们的自定义基元,我们现在可以创建适当的数据传输对象–电子邮件,这是一个非常简单的任务,因为它基本上是一个不变的结构:
public final class Email {
public final EmailAddress sender;
public final EmailAddress receiver;
public final Subject subject;
public final Body body;
}
相同的“始终有效”的方法也适用于数据传输对象,除了在这里,由于我们利用了自定义基元,因此时间更短。
DTO的工厂方法可以像验证必填字段的存在一样简单,也可以像应用跨字段验证一样复杂。
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馈送到这样的框架,则可能会获得最佳效果:
{
"sender": {
"value": "sender@example.com"
},
"receiver": {
"value": "receiver@example.com"
},
"subject": {
"value": "subject"
},
"body": {
"value": "body"
}
}
虽然人们期望的是:
{
"sender": "sender@example.com",
"receiver": "receiver@example.com",
"subject": "subject",
"body": "body"
}
尽管可以使用大量样板代码来缓解此问题,但是验证是另一种野兽,当您希望从服务器“一次报告所有验证错误”时,验证就变得致命。 为什么不立即告诉用户发送方和接收方均无效,而不是发送寻求许可A38的请求发送给用户。 实际上,这就是我们在尝试编写现代微服务时,同时又遵循Clean Code的最佳实践的感觉,Domain Driven Design,Domain Driven Security的“始终有效”方法……
这就是MapMate需要解决的问题。
MapMate
与HttpMate一样,我们确保提供一个易于构建的构建器,同时保留细粒度定制的可能性。 这是使我们的电子邮件示例序列化,反序列化和验证我们的自定义基元和DTO的绝对最低配置。
public static MapMate mapMate() {
return MapMate.aMapMate("com.envimate.examples.email_use_case")
.usingJsonMarshallers(new Gson()::toJson, new Gson()::fromJson)
.build();
}
这部分将使以下JSON成为有效请求:
{
"sender": "sender@example.com",
"receiver": "receiver@example.com",
"subject": "Hello world!",
"body": "Hello from Sender to Receiver!"
}
您必须指定要扫描(递归)的程序包和一对(非)编组器。 可以是任何可以从Map生成字符串的内容,反之亦然。 这是使用ObjectMapper的示例:
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 unmarshal(final String input, final Class
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();
承诺的验证异常聚合又如何呢?
在我们的示例中,如果自定义原语或DTO无效,则所有验证都返回CustomTypeValidationException的实例。
添加以下行,以指示MapMate将您的Exception类识别为验证错误的指示。
public static MapMate mapMate() {
return MapMate.aMapMate("com.envimate.examples.email_use_case")
.usingJsonMarshallers(new Gson()::toJson, new Gson()::fromJson)
.withExceptionIndicatingValidationError(CustomTypeValidationException.class)
.build();
}
现在,如果我们尝试以下请求:
{
"sender": "not-a-valid-sender-value",
"receiver": "not-a-valid-receiver-value",
"subject": "Hello world!",
"body": "Hello from Sender to Receiver!"
}
我们将收到以下答复:
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”中排除程序包和类,可以配置哪些异常被视为“验证错误”以及如何对其进行处理,还可以为“自定义”指定其他方法名称原始序列化,或者提供您的lambda来同时进行这两个序列的反序列化。
有关MapMate的更多示例和详细信息,请查看MapMate存储库 。
让我们知道您的想法以及您接下来想在MapMate中看到的功能!
纹理和基元