2

我有一个 Spring Boot 应用程序,我想向客户端发送 DTO 验证约束和字段值。

拥有 DTO

class PetDTO {
  @Length(min=5, max=15)
  String name;
}

其中名称恰好是“Leviathan”,应该会导致这个 JSON 被发送到客户端:

{
    name: 'Leviathan'
    name_constraint: { type: 'length', min:5, max: 15},
}

推理是为验证提供单一的事实来源。这可以通过合理的工作量来完成吗?

4

2 回答 2

1

为了扩展 Frederik 的回答,我将展示一个小示例代码,该代码将对象转换为映射并序列化它。

所以这里是用户pojo:

import org.hibernate.validator.constraints.Length;

public class User {

    private String name;

    public User(String name) {
        this.name = name;
    }

    @Length(min = 5, max = 15)
    public String getName() {
        return name;
    }
}

然后是实际的序列化器:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toMap;

public class UserSerializer extends StdSerializer<User> {

    public UserSerializer(){
        this(User.class);
    }

    private UserSerializer(Class t) {
        super(t);
    }

    @Override
    public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
        Map<String, Object> properties = beanProperties(bean);
        gen.writeStartObject();
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            gen.writeObjectField(entry.getKey(), entry.getValue());
        }
        gen.writeEndObject();
    }

    private static Map<String, Object> beanProperties(Object bean) {
        try {
            return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
                    .filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
                    .flatMap(descriptor -> {
                        String name = descriptor.getName();
                        Method getter = descriptor.getReadMethod();
                        Object value = ReflectionUtils.invokeMethod(getter, bean);
                        Property originalProperty = new Property(name, value);

                        Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
                                .map(anno -> new Property(name + "_constraint", annotationProperties(anno)));

                        return Stream.concat(Stream.of(originalProperty), constraintProperties);
                    })
                    .collect(toMap(Property::getName, Property::getValue));
        } catch (Exception e) {
            return Collections.emptyMap();
        }
    }

    // Methods from Annotation.class
    private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");

    private static Map<String, Object> annotationProperties(Annotation anno) {
        try {
            Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
                    .filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
                    .map(descriptor -> {
                        String name = descriptor.getName();
                        Method method = descriptor.getMethod();
                        Object value = ReflectionUtils.invokeMethod(method, anno);
                        return new Property(name, value);
                    });
            Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
            return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
        } catch (IntrospectionException e) {
            return Collections.emptyMap();
        }
    }

    private static class Property {
        private String name;
        private Object value;

        public Property(String name, Object value) {
            this.name = name;
            this.value = value;
        }

        public String getName() {
            return name;
        }

        public Object getValue() {
            return value;
        }
    }
}

最后我们需要注册这个序列化器以供 Jackson 使用:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {

    @Bean
    public Jackson2ObjectMapperBuilder mapperBuilder(){
        Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
        jackson2ObjectMapperBuilder.serializers(new UserSerializer());
        return jackson2ObjectMapperBuilder;
    }

    public static void main(String[] args) {
        SpringApplication.run(SerializationApp.class, args);
    }
}

@RestController
class SerializationController {
    @GetMapping("/user")
    public User user() {
        return new User("sample");
    }
}

将发出的 Json:

{  
   "name_constraint":{  
      "min":5,
      "max":15,
      "payload":[],
      "groups":[],
      "message":"{org.hibernate.validator.constraints.Length.message}",
      "type":"org.hibernate.validator.constraints.Length"
   },
   "name":"sample"
}

希望这可以帮助。祝你好运。

于 2017-02-07T22:06:32.637 回答
1

为此,您始终可以使用自定义 Jackson 序列化器。可以在互联网上找到大量这样做的文档,可能看起来像这样:

public void serialize(PetDTO value, JsonGenerator jgen, ...) {

    jgen.writeStartObject();
    jgen.writeNumberField("name", value.name);
    jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}

public ConstaintDTO getConstraintValue(PetDTO value) {

    // Use reflection to check if the name field on the PetDTO is annotated
    // and extract the min, max and type values from the annotation
    return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}

您可能想要创建一个基本 DTO 类,转换器为其启动,因此您不必为需要公开约束的所有域对象创建自定义转换器。

通过结合反射和对书写字段的智能使用,您可以接近。缺点是您无法利用域对象上的@JsonXXX注释,因为您自己编写 JSON。

更理想的解决方案应该是让 Jackson 转换,但有某种转换后调用以XX_condtion向对象添加其他属性。也许从覆盖默认的对象序列化程序开始(如果可能的话)?

于 2017-02-07T20:32:03.303 回答