web-dev-qa-db-ja.com

Spring Boot:動的親オブジェクトでのJSON応答のラップ

RESTバックエンドマイクロサービスと通信するAPI仕様があり、次の値を返します。

「コレクション」の応答(例:GET/users):

{
    users: [
        {
            ... // single user object data
        }
    ],
    links: [
        {
            ... // single HATEOAS link object
        }
    ]
}

「単一オブジェクト」応答(例:GET /users/{userUuid}):

{
    user: {
        ... // {userUuid} user object}
    }
}

このアプローチは、単一の応答が拡張可能になるように選択されました(たとえば、GET /users/{userUuid}?detailedView=trueなどの行で追加のクエリパラメータを取得した場合、追加の要求情報が得られる可能性があります)。

基本的には、API更新間の重大な変更を最小限に抑えるための適切なアプローチだと思います。ただし、このモデルをコードに変換することは非常に困難です。

シングルレスポンスの場合、シングルユーザー用に次のAPIモデルオブジェクトがあるとします。

public class SingleUserResource {
    private MicroserviceUserModel user;

    public SingleUserResource(MicroserviceUserModel user) {
        this.user = user;
    }

    public String getName() {
        return user.getName();
    }

    // other getters for fields we wish to expose
}

このメソッドの利点は、パブリックゲッターがある内部で使用されているモデルのフィールドのみのみを公開できることです。次に、コレクションレスポンスの場合、次のラッパークラスを使用します。

public class UsersResource extends ResourceSupport {

    @JsonProperty("users")
    public final List<SingleUserResource> users;

    public UsersResource(List<MicroserviceUserModel> users) {
        // add each user as a SingleUserResource
    }
}

単一オブジェクト応答の場合、次のようになります:

public class UserResource {

    @JsonProperty("user")
    public final SingleUserResource user;

    public UserResource(SingleUserResource user) {
        this.user = user;
    }
}

これにより、この投稿の上部にあるAPI仕様に従ってフォーマットされたJSON応答が生成されます。このアプローチの利点は、wantで公開したいフィールドのみを公開することです。重い欠点は、正しい形式の応答を生成するためにジャクソンによって読み取られることを除いて、認識できる論理的なタスクを実行しないラッパークラスがton飛んでいることです。 。

私の質問は次のとおりです。

  • このアプローチを一般化するにはどうすればよいですか?理想的には、すべてのモデルが拡張できる単一のBaseSingularResponseクラス(およびおそらくBaseCollectionsResponse extends ResourceSupportクラス)が欲しいのですが、Jacksonがオブジェクト定義からJSONキーをどのように派生しているかを見て、実行時に基本応答クラスにフィールドを追加するには、Javaassistのようなものを使用する必要があります-人間から可能な限り遠ざけたい汚いハック。

  • これを達成する簡単な方法はありますか?残念ながら、1年後の応答には可変数のトップレベルJSONオブジェクトが含まれる可能性があるため、ジャクソンのSerializationConfig.Feature.WRAP_ROOT_VALUEのようなものは使用できません。これは、everything(私が知る限り)単一のルートレベルのオブジェクトに。

  • (メソッドとフィールドレベルだけではなく)クラスレベルの@JsonPropertyのようなものはありますか?

15
user991710

いくつかの可能性があります。

Java.util.Map

List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));

ObjectWriterを使用して、以下のように使用できる応答をラップできます。

ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);

このシリアル化を一般化するための命題は次のとおりです。

単純なオブジェクトを処理するクラス

public abstract class BaseSingularResponse {

    private String root;

    protected BaseSingularResponse(String rootName) {
        this.root = rootName;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

コレクションを処理するクラス

public abstract class BaseCollectionsResponse<T extends Collection<?>> {
    private String root;
    private T collection;

    protected BaseCollectionsResponse(String rootName, T aCollection) {
        this.root = rootName;
        this.collection = aCollection;
    }

    public T getCollection() {
        return collection;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(collection);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

そしてサンプルアプリケーション

public class Main {

    private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> {
        public UsersResource() {
            super("users", new ArrayList<UserResource>());
        }
    }

    private static class UserResource extends BaseSingularResponse {

        private String name;
        private String id = UUID.randomUUID().toString();

        public UserResource(String userName) {
            super("user");
            this.name = userName;
        }

        public String getUserName() {
            return this.name;
        }

        public String getUserId() {
            return this.id;
        }
    }

    public static void main(String[] args) throws JsonProcessingException {
        UsersResource userCollection = new UsersResource();
        UserResource user1 = new UserResource("John");
        UserResource user2 = new UserResource("Jane");
        UserResource user3 = new UserResource("Martin");

        System.out.println(user1.serialize());

        userCollection.getCollection().add(user1);
        userCollection.getCollection().add(user2);
        userCollection.getCollection().add(user3);

        System.out.println(userCollection.serialize());
    }
}

ジャクソンアノテーションを使用することもできます@JsonTypeInfoクラスレベル

@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)
8
Arnaud Develay

個人的には、追加のDtoクラスを気にしません。作成する必要があるのは1回だけであり、メンテナンスコストはほとんどまたはまったくありません。また、MockMVCテストを実行する必要がある場合は、JSON応答をデシリアライズして結果を確認するためのクラスが必要になる可能性があります。

おそらくご存知のとおり、SpringフレームワークはHttpMessageConverterレイヤーでオブジェクトのシリアル化/非シリアル化を処理するため、オブジェクトのシリアル化方法を変更するのに適切な場所です。

応答をデシリアライズする必要がない場合は、汎用ラッパーとカスタムHttpMessageConverterを作成できます(メッセージコンバーターリストでMappingJackson2HttpMessageConverterの前に配置します)。このような:

_public class JSONWrapper {

    public final String name;
    public final Object object;

    public JSONWrapper(String name, Object object) {
        this.name = name;
        this.object = object;
    }
}


public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter {

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        // cast is safe because this is only called when supports return true.
        JSONWrapper wrapper = (JSONWrapper) object;
        Map<String, Object> map = new HashMap<>();
        map.put(wrapper.name, wrapper.object);
        super.writeInternal(map, type, outputMessage);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.equals(JSONWrapper.class);
    }
}
_

次に、configureMessageConverters()をオーバーライドしてWebMvcConfigurerAdapterを拡張するSpring構成にカスタムHttpMessageConverterを登録する必要があります。これを行うと、コンバーターのデフォルトの自動検出が無効になるので、おそらくデフォルトを自分で追加する必要があることに注意してください(WebMvcConfigurationSupport#addDefaultHttpMessageConverters()のSpringソースコードをチェックしてデフォルトを確認してください。WebMvcConfigurationSupportを拡張する場合代わりにWebMvcConfigurerAdapterを直接呼び出すことができますaddDefaultHttpMessageConvertersを直接呼び出すことができます(個人的には、何かをカスタマイズする必要がある場合はWebMvcConfigurationSupportよりもWebMvcConfigurerAdapterを使用することをお勧めしますが、実行にはいくつかの小さな影響がありますこれは、おそらく他の記事で読むことができます。

4
Klaus Groenbaek

私はあなたが探していると思います Custom Jackson Serializer 。単純なコード実装で、同じオブジェクトを異なる構造でシリアル化できます

いくつかの例: https://stackoverflow.com/a/10835504/814304http://www.davismol.net/2015/05/18/jackson-create-and-register -a-custom-json-serializer-with-stdserializer-and-simplemodule-classes /

1
iMysak

ジャクソンは動的/可変JSON構造をあまりサポートしていないため、このようなことを実現するソリューションは、あなたが言ったようにかなりハッキーです。私の知る限り、そして私が見た限りでは、標準的で最も一般的な方法は、現在のようなラッパークラスを使用することです。ラッパークラスは追加されますが、インヘレンスにクリエイティブになると、クラス間の共通点を見つけることができるため、ラッパークラスの量を減らすことができます。それ以外の場合は、カスタムフレームワークの作成を検討している可能性があります。

1
Trevor Bye