概要
- 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 を記述する。
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 シリアライズ・デシリアライズをサポートするライブラリ。
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"}]
参考資料
- Spring Boot Reference Documentation - “How-to” Guides - 4.3. Customize the Jackson ObjectMapper
- Spring Boot Reference Documentation - Spring Boot Features - 2.8.6. Relaxed Binding
- hibernate - Converting Lazy Loaded object to JSON in spring boot jpa - Stack Overflow
- java - Strange Jackson exception being thrown when serializing Hibernate object - Stack Overflow