복붙노트

[SPRING] POST / PUT 중에 모든 객체에 대한 단일 사용자 정의 디시리얼라이저 또는 해당 객체로 삽입 된 전체 객체

SPRING

POST / PUT 중에 모든 객체에 대한 단일 사용자 정의 디시리얼라이저 또는 해당 객체로 삽입 된 전체 객체

@Entity
public Product {
   @Id
   public int id;

   public String name;

   @ManyToOne(cascade = {CascadeType.DETACH} )
   Category category

   @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH} )
   Set<Category> secondaryCategories;


}

이 엔티티 :

@Entity
public Category {
   @Id
   public int id;

   public String name;
}

json과 함께 POST를 보내고 싶습니다.

{name : "name", category : 2, secondaryCategories : [3,4,5]} 클라이언트 쪽에서

다음과 같이 deserialize 할 수 있습니다.

{ name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

송신 된 경우

 { name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

나는 지금도 여전히 일하고 싶다.

어떤 종류의 주석과 맞춤 디시리얼라이저가 필요한가요? deserializer는 ID를 속성으로 가질 수있는 모든 가능한 객체에 대해 작업 할 수 있기를 바랍니다.

감사!

편집하다

해결법

  1. ==============================

    1.또 다른 방법은 엔티티를 수정할 수있는 경우 @JsonCreator 팩토리 메서드를 사용하는 것입니다.

    또 다른 방법은 엔티티를 수정할 수있는 경우 @JsonCreator 팩토리 메서드를 사용하는 것입니다.

    private class Product {
        @JsonProperty("category")
        private Category category;
    
        @JsonProperty("secondaryCategories")
        private List<Category> secondaryCategories;
    }
    
    
    private class Category {
        @JsonProperty("id")
        private int id;
    
        @JsonCreator
        public static Category factory(int id){
            Category p = new Category();
            p.id = id;
            // or some db call 
            return p;
        }
    }
    

    또는 이와 유사한 것조차도 작동해야합니다.

    private class Category {
        private int id;
    
        public Category() {}
    
        @JsonCreator
        public Category(int id) {
            this.id = id;
        }
    }
    
  2. ==============================

    2.시도 할 수있는 몇 가지 옵션이 있습니다. 실제로 사용자 정의 디시리얼라이저 / 시리얼 라이저가 적합 할 수 있지만, @JsonIdentityInfo (역 직렬화 용) + @JsonIdentityReference (정수로 직렬화해야하는 경우) 주석을 사용하여이 작업을 수행 할 수도 있습니다.

    시도 할 수있는 몇 가지 옵션이 있습니다. 실제로 사용자 정의 디시리얼라이저 / 시리얼 라이저가 적합 할 수 있지만, @JsonIdentityInfo (역 직렬화 용) + @JsonIdentityReference (정수로 직렬화해야하는 경우) 주석을 사용하여이 작업을 수행 할 수도 있습니다.

    Work both for 
    { "category":1 }
    { "category":{ "id":1 }
    

    그래서 당신은 @JsonIdentityInfo로 ID에서 직렬화 될 수있는 모든 클래스에 주석을 달 필요가있다.

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id", 
            scope = Product.class,  // different for each class
            resolver = MyObjectIdResolver.class)
    

    여기서 어려운 부분은 실제로 db / other 소스의 객체를 확인할 수있는 사용자 정의 ObjectIdResolver를 작성해야한다는 것입니다. 아래 예제에서 MyObjectIdResolver.resolveId 메서드의 간단한 리플렉션 버전을 살펴보십시오.

    private static class MyObjectIdResolver implements ObjectIdResolver {
        private Map<ObjectIdGenerator.IdKey,Object> _items  = new HashMap<>();
    
        @Override
        public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
            if (!_items.containsKey(id)) _items.put(id, pojo);
        }
    
        @Override
        public Object resolveId(ObjectIdGenerator.IdKey id) {
            Object object = _items.get(id);
            return object == null ? getById(id) : object;
        }
    
        protected Object getById(ObjectIdGenerator.IdKey id){
            Object object = null;
            try {
                // todo objectRepository.getById(idKey.key, idKey.scope)
                object = id.scope.getConstructor().newInstance(); // create instance
                id.scope.getField("id").set(object, id.key);  // set id
                bindItem(id, object);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return object;
        }
    
        @Override
        public ObjectIdResolver newForDeserialization(Object context) {
            return this;
        }
    
        @Override
        public boolean canUseFor(ObjectIdResolver resolverType) {
            return resolverType.getClass() == getClass();
        }
    }
    
    Default behavior
    { "category":{ "id":1 , "name":null} , secondaryCategories: [1 , { { "id":2 , "name":null} ]}
    

    기본 동작은 다음에서 설명합니다. https://github.com/FasterXML/jackson-databind/issues/372 첫 번째 요소에 대한 객체와 각 요소에 대한 ID를 생성합니다. Jackson의 ID / 참조 메커니즘은 객체 인스턴스가 한 번만 완전히 일련 화되고 ID로 다른 곳에서 참조되도록 작동합니다.

    옵션 1 (항상 ID로)

    Works for 
    { "category":1 , secondaryCategories:[1 , 2]}
    

    각 객체 필드 위에 @JsonIdentityReference (alwaysAsId = true)를 사용해야합니다 (페이지 하단의 데모에서 주석을 제거 할 수 있음)

    옵션 2 (항상 완전한 객체 표현으로)

    Works for 
    { "category" : { "id":1 , "name":null} , secondaryCategories: [{ "id":1 , "name":null} , { "id":2 , "name":null}]}
    

    이 옵션은 직렬화를 위해 모든 IdentityInfo를 어떻게 든 제거해야하기 때문에 까다로운 작업입니다. 하나의 옵션은 2 개의 객체 매퍼를 가질 수 있습니다. 직렬화를 위해서는 1을, 직렬화를 위해서는 2를, 믹스 인이나 @JsonView를 구성하십시오.

    더 쉽게 구현할 수있는 또 다른 접근법은 SerializationConfig를 사용하여 @JsonIdentityInfo 주석을 완전히 무시하는 것입니다

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
    
        SerializationConfig config = mapper.getSerializationConfig()
                .with(new JacksonAnnotationIntrospector() {
            @Override
            public ObjectIdInfo findObjectIdInfo(final Annotated ann) {
                return null;
            }
        });
    
        mapper.setConfig(config);
    
        return mapper;
    }
    

    아마도 더 나은 접근법은 실제로 같은 방식으로 deserializerconfig에 대한 @JsonIdentityInfo를 정의하고 클래스 위에있는 모든 주석을 제거하는 것입니다. 이 같은

    이 시점에서 사용자 정의 serializer / deserializer

    여기에 일하고있다 (봄이없는 간단한 Jackson) 데모 :

    import com.fasterxml.jackson.annotation.*;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.Set;
    
    public class Main {
    
        @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                property = "id",
                resolver = MyObjectIdResolver.class,
                scope = Category.class)
        public static class Category {
            @JsonProperty("id")
            public int id;
            @JsonProperty("name")
            public String name;
    
            public int getId() {
                return id;
            }
    
            public void setId(int id) {
                this.id = id;
            }
    
            @Override
            public String toString() {
                return "Category{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        '}';
            }
        }
    
        @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                property = "id",
                resolver = MyObjectIdResolver.class,
                scope = Product.class)
        public static class Product {
            @JsonProperty("id")
            public int id;
            @JsonProperty("name")
            public String name;
    
            // Need @JsonIdentityReference only if you want the serialization
            // @JsonIdentityReference(alwaysAsId = true)
            @JsonProperty("category")
            Category category;
    
            // Need @JsonIdentityReference only if you want the serialization
            // @JsonIdentityReference(alwaysAsId = true)
            @JsonProperty("secondaryCategories")
            Set<Category> secondaryCategories;
    
            public int getId() {
                return id;
            }
    
            public void setId(int id) {
                this.id = id;
            }
    
            @Override
            public String toString() {
                return "Product{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        ", category=" + category +
                        ", secondaryCategories=" + secondaryCategories +
                        '}';
            }
        }
    
        private static class MyObjectIdResolver implements ObjectIdResolver {
    
           private Map<ObjectIdGenerator.IdKey,Object> _items;
    
            @Override
            public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
                if (_items == null) {
                    _items = new HashMap<ObjectIdGenerator.IdKey,Object>();
                } if (!_items.containsKey(id))
                    _items.put(id, pojo);
            }
    
            @Override
            public Object resolveId(ObjectIdGenerator.IdKey id) {
                Object object = (_items == null) ? null : _items.get(id);
                if (object == null) {
                    try {
    
                        // create instance
                        Constructor<?> ctor = id.scope.getConstructor();
                        object = ctor.newInstance();
    
                        // set id
                        Method setId = id.scope.getDeclaredMethod("setId", int.class);
                        setId.invoke(object, id.key);
                        // https://github.com/FasterXML/jackson-databind/issues/372
                        // bindItem(id, object); results in strange behavior
    
                    } catch (NoSuchMethodException | IllegalAccessException
                            | InstantiationException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
                return object;
            }
    
            @Override
            public ObjectIdResolver newForDeserialization(Object context) {
                return new MyObjectIdResolver();
            }
    
            @Override
            public boolean canUseFor(ObjectIdResolver resolverType) {
                return resolverType.getClass() == getClass();
            }
        }
    
        public static void main(String[] args) throws Exception {
            String str = "{ \"name\": \"name\", \"category\": {\"id\": 2 }, " +
                    "\"secondaryCategories\":[{\"id\":3},{\"id\":4},{\"id\":5}]}";
    
            // from  str
            Product product = new ObjectMapper().readValue(str, Product.class);
            System.out.println(product);
    
            // to json
            String productStr = new ObjectMapper().writeValueAsString(product);
            System.out.println(productStr);
    
            String str2 = "{ \"name\": \"name\", \"category\":  2, " +
                    "\"secondaryCategories\": [ 3,  4,  5] }";
    
            // from  str2
            Product product2 = new ObjectMapper().readValue(str2, Product.class);
            System.out.println(product2);
    
            // to json
            String productStr2 = new ObjectMapper().writeValueAsString(product2);
            System.out.println(productStr2);
        }
    }
    
  3. ==============================

    3.많은 어려움을 겪은 후 완벽한 해결책은 https://stackoverflow.com/users/1032167/varren의 의견과 https://stackoverflow.com/a/16825934/986160 덕분이었습니다. (로컬을 통해 기본 직렬화를 사용할 수있었습니다. new objectMapper) 내 대답을 방해하지 않고 내 StdDeserializer에서 : https://stackoverflow.com/a/18405958/986160

    많은 어려움을 겪은 후 완벽한 해결책은 https://stackoverflow.com/users/1032167/varren의 의견과 https://stackoverflow.com/a/16825934/986160 덕분이었습니다. (로컬을 통해 기본 직렬화를 사용할 수있었습니다. new objectMapper) 내 대답을 방해하지 않고 내 StdDeserializer에서 : https://stackoverflow.com/a/18405958/986160

    코드는 int를 구문 분석하려고 시도하고 전체 객체 인 경우이를 전달합니다. 따라서 Category의 POST / PUT 요청을 만들거나 Category가 포함되지 않은 경우에는 여전히 작동합니다.

    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
    import org.springframework.context.annotation.Bean;
    
    import java.io.IOException;
    
    public class IdWrapperDeserializer<T> extends StdDeserializer<T> {
    
        private Class<T> clazz;
    
        public IdWrapperDeserializer(Class<T> clazz) {
            super(clazz);
            this.clazz = clazz;
        }
    
        @Bean
        public ObjectMapper objectMapper() {
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
            mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
            mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
            return mapper;
        }
    
        @Override
        public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
            String json = jp.readValueAsTree().toString();
    
            T obj = null;
            int id = 0;
            try {
                id = Integer.parseInt(json);
            }
            catch( Exception e) {
                obj = objectMapper().readValue(json, clazz);
                return obj;
            }
            try {
                obj = clazz.newInstance();
                ReflectionUtils.set(obj,"id",id);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return obj;
        }
    
    }
    

    설명 된대로 동작해야하는 각 엔티티에 대해 Spring Boot 응용 프로그램의 전역 ObjectMapper Bean에 구성해야합니다.

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    
        SimpleModule testModule = new SimpleModule("MyModule")
                .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))
    
        mapper.registerModule(testModule);
    
        return mapper;
    }
    

    이것은 https://stackoverflow.com/a/14374995/986160의 ReflectionUtils입니다.

    public class ReflectionUtils {
        // 
        public static boolean set(Object object, String fieldName, Object fieldValue) {
            Class<?> clazz = object.getClass();
            while (clazz != null) {
                try {
                    Field field = clazz.getDeclaredField(fieldName);
                    field.setAccessible(true);
                    field.set(object, fieldValue);
                    return true;
                } catch (NoSuchFieldException e) {
                    clazz = clazz.getSuperclass();
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
            }
            return false;
        }
    }
    
  4. from https://stackoverflow.com/questions/46603075/single-custom-deserializer-for-all-objects-as-their-ids-or-embedded-whole-object by cc-by-sa and MIT license