6
6

More than 3 years have passed since last update.

Spring Data JPA で hibernateLazyInitializer を Jackson が JSON シリアライズできなくてエラーになる

Posted at

概要

  • Spring Data JPA 使用時に Hibernate がエンティティのプロキシオブジェクトに hibernateLazyInitializer というフィールドを追加してしまう
  • Jackson が hibernateLazyInitializer を JSON シリアライズできずにエラーになってしまう
  • いくつかの対策方法を示す

エラーメッセージ例

環境: Java 11 + Spring Boot 2.2.6 + Spring Data JPA 2.2.6 + Hibernate ORM 5.4.12.Final

Jackson が hibernateLazyInitializer を JSON にシリアライズできなくてエラーになっている。

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->com.example.Counter\$HibernateProxy$YAFGFxdB["hibernateLazyInitializer"])] with root cause

今回の JPA エンティティクラス

package com.example;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Data // Lombok で getter setter など便利なメソッドを自動生成
@Entity // JPA エンティティとして扱う
public class Counter {

  // カウンター名
  @Id // JPA にこの変数をオブジェクトの ID だと認識させる
  private String name;

  // カウント数
  private int count;

  // 更新日時
  private LocalDateTime updatedAt;
}

Hibernate によるプロキシクラスの中身を調べる

以下のようにクラスを調べるメソッドを作成。

private static String getClassInfo(Class cls) {
  var s = new ArrayList<String>();
  s.add("Class Name: " + cls.getName());
  s.add("Super Class: " + cls.getSuperclass().getName());
  s.add("Methods: " + Arrays.stream(cls.getMethods()).map(m -> m.getName()).collect(Collectors.joining(", ")));
  s.add("Fields: " + Arrays.stream(cls.getFields()).map(f -> f.getName()).collect(Collectors.joining(", ")));
  s.add("Declared Methods: " + Arrays.stream(cls.getDeclaredMethods()).map(m -> m.getName()).collect(Collectors.joining(", ")));
  s.add("Declared Fields: " + Arrays.stream(cls.getDeclaredFields()).map(f -> f.getName()).collect(Collectors.joining(", ")));
  return String.join("\n", s);
}

このメソッドに Hibernate が生成したオブジェクトの Class オブジェクトを指定した結果の出力例。
エンティティクラスを親クラスとして、getHibernateLazyInitializer メソッドが増えているのがわかる。

Class Name: com.example.Counter$HibernateProxy$AkTOy5Fs
Super Class: com.example.Counter
Methods: equals, toString, hashCode, getName, setName, writeReplace, getUpdatedAt, getCount, getHibernateLazyInitializer, setCount, setUpdatedAt, $$_hibernate_set_interceptor, wait, wait, wait, getClass, notify, notifyAll
Fields: INTERCEPTOR_FIELD_NAME
Declared Methods: equals, toString, hashCode, clone, getName, setName, writeReplace, getUpdatedAt, getCount, canEqual, getHibernateLazyInitializer, setCount, setUpdatedAt, $$_hibernate_set_interceptor
Declared Fields: $$_hibernate_interceptor, cachedValue$qiSxWcNz$dgag0r1, cachedValue$qiSxWcNz$o23rrk2, cachedValue$qiSxWcNz$fktln90, cachedValue$qiSxWcNz$7m9oaq0, cachedValue$qiSxWcNz$1ev2u10, cachedValue$qiSxWcNz$bolum62, cachedValue$qiSxWcNz$csesgl2, cachedValue$qiSxWcNz$iiddvi2, cachedValue$qiSxWcNz$14i7kh1, cachedValue$qiSxWcNz$hm0jak2, cachedValue$qiSxWcNz$c92ibj2, cachedValue$qiSxWcNz$gpia792, cachedValue$qiSxWcNz$ca4pq83

対策1: spring.jackson.serialization.fail-on-empty-beans=false

application.properties に spring.jackson.serialization.fail-on-empty-beans=false を記述する。

application.properties
spring.jackson.serialization.fail-on-empty-beans=false

例外が発生せず hibernateLazyInitializer が空のオブジェクトで出力されるようになる。

JSON 出力例。

[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T18:39:57.861259","hibernateLazyInitializer":{}}]  

SerializationFeature (jackson-databind 2.10.0 API)

public static final SerializationFeature FAIL_ON_EMPTY_BEANS

Feature that determines what happens when no accessors are found for a type (and there are no annotations to indicate it is meant to be serialized). If enabled (default), an exception is thrown to indicate these as non-serializable types; if disabled, they are serialized as empty Objects, i.e. without any properties.
Note that empty types that this feature has only effect on those "empty" beans that do not have any recognized annotations (like @JsonSerialize): ones that do have annotations do not result in an exception being thrown.

Feature is enabled by default.

対策2: @JsonIgnoreProperties アノテーション

エンティティクラスに @JsonIgnoreProperties({"hibernateLazyInitializer"}) を付加することで該当の項目を JSON シリアライズしないようにする。

package com.example;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Data // Lombok で getter setter など便利なメソッドを自動生成
@Entity // JPA エンティティとして扱う
@JsonIgnoreProperties({"hibernateLazyInitializer"})
public class Counter {

  // カウンター名
  @Id // JPA にこの変数をオブジェクトの ID だと認識させる
  private String name;

  // カウント数
  private int count;

  // 更新日時
  private LocalDateTime updatedAt;
}

JsonIgnoreProperties (Jackson-annotations 2.10.0 API)

Annotation that can be used to either suppress serialization of properties (during serialization), or ignore processing of JSON properties read (during deserialization).

JSON 出力例。

[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T18:45:05.009029"}]

対策3: jackson-datatype-hibernate を導入する

jackson-datatype-hibernate は Hibernate の JSON シリアライズ・デシリアライズをサポートするライブラリ。

GitHub - FasterXML/jackson-datatype-hibernate: Add-on module for Jackson JSON processor which handles Hibernate (http://www.hibernate.org/) datatypes; and specifically aspects of lazy-loading

Project to build Jackson module (jar) to support JSON serialization and deserialization of Hibernate (http://hibernate.org) specific datatypes and properties; especially lazy-loading aspects.

Gradle の場合は build.gradle の dependencies に jackson-datatype-hibernate5 を追加することで Hibernate 5 に対応したライブラリを導入できる。

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

また Jackson の ObjectMapper オブジェクトを生成するための設定クラスが必要。
jackson-datatype-hibernate5 を導入することで Hibernate5Module クラスが使えるようになる。

package com.example;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class MyConfig {

  @Bean
  public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {

    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();

    // Hibernate 5 使用時のサポート
    builder.modulesToInstall(new Hibernate5Module());

    // Spring Boot のデフォルト設定に合わせる
    builder.featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION);
    builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    return builder;
  }
}

Spring Boot が ObjectMapper を生成する際のデフォルト設定値。

Spring Boot Reference Documentation - “How-to” Guides - 4.3. Customize the Jackson ObjectMapper

The ObjectMapper (or XmlMapper for Jackson XML converter) instance (created by default) has the following customized properties:

・MapperFeature.DEFAULT_VIEW_INCLUSION is disabled
・DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES is disabled
・SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is disabled

JSON 出力例。

[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T20:10:59.749087"}]

参考資料

6
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6