Hibernate toString stackOverflowError
logback 을 DEBUG 로 설정했을때 stackOverflowError 가 발생했다.
Entity 의 toString 은 Lombok 의 @Data Annotation 을 이용해서 생성되어있다.
실제로 브레이크 포인트를 StringBuilder 와 String 의 valueOf 에 잡아보면 Request 를 처리하는 쓰레드에서
toString 메서드가 재귀적으로 호출되며 종료조건없이 계속해서 호출되는것을 확인할 수 있다.
Entity.toString() 이 제귀적으로 호출되고 있다.
Entity 에는 3가지 @OneToMany 필드가 있고 모두 fetch = fetchType.Lazy 이다.
thread stack trace 를 보면 LazyFetch 인 Entity 필드의 toString 을 가져오기 위해서 PersistentBag 의 toString 이 호출된것을 확인할 수 있다.
persistentBag.toString()
public String toString() {
read();
return bag.toString();
}
Persistent 의 toString() 내부에서는 toString 을 호출하기전에 LazyFetch 인 Entity 들을 가져오기 위해 read() 를 호출한다.
AbstractPersistentCollection.read()
protected final void read() {
initialize( false );
}
read() 에서 LazyFetch 를 initialize 를 진행하면서 LogBack 설정이 Debug 일 경우에 Entity 의 toString 을 호출하는 로직이 존재한다.
Loader.java
/**
* Called by subclasses that initialize collections
*/
public final void loadCollection(
final SessionImplementor session,
final Serializable id,
final Type type) throws HibernateException {
if ( LOG.isDebugEnabled() )
LOG.debugf( "Loading collection: %s",
MessageHelper.collectionInfoString( getCollectionPersisters()[0], id, getFactory() ) );
Serializable[] ids = new Serializable[]{id};
try {
doQueryAndInitializeNonLazyCollections(
session,
new QueryParameters( new Type[]{type}, ids, ids ),
true
);
}
catch ( SQLException sqle ) {
throw factory.getSQLExceptionHelper().convert(
sqle,
"could not initialize a collection: " +
MessageHelper.collectionInfoString( getCollectionPersisters()[0], id, getFactory() ),
getSQLString()
);
}
LOG.debug( "Done loading collection" );
}
LOG.isDebugEnabled() 일 경우에 아래 명령이 수행된다.
LOG.debugf( "Loading collection: %s", MessageHelper.collectionInfoString( getCollectionPersisters()[0], id, getFactory() ) );
MessageHelper.collectionInfoString()
public static String collectionInfoString(
CollectionPersister persister,
Serializable id,
SessionFactoryImplementor factory) {
StringBuilder s = new StringBuilder();
s.append( '[' );
if ( persister == null ) {
s.append( "<unreferenced>" );
}
else {
s.append( persister.getRole() );
s.append( '#' );
if ( id == null ) {
s.append( "<null>" );
}
else {
addIdToCollectionInfoString( persister, id, factory, s );
}
}
s.append( ']' );
return s.toString();
}
위 메서드에서 persister 가 null 이 아니고 id 가 null 이 아니면 addIdToCollectionInfoString() 를 호출한다
MessageHelper.addIdToCollectionInfoString()
private static void addIdToCollectionInfoString(
CollectionPersister persister,
Serializable id,
SessionFactoryImplementor factory,
StringBuilder s ) {
// Need to use the identifier type of the collection owner
// since the incoming is value is actually the owner's id.
// Using the collection's key type causes problems with
// property-ref keys.
// Also need to check that the expected identifier type matches
// the given ID. Due to property-ref keys, the collection key
// may not be the owner key.
Type ownerIdentifierType = persister.getOwnerEntityPersister()
.getIdentifierType();
if ( id.getClass().isAssignableFrom(
ownerIdentifierType.getReturnedClass() ) ) {
s.append( ownerIdentifierType.toLoggableString( id, factory ) );
} else {
// TODO: This is a crappy backup if a property-ref is used.
// If the reference is an object w/o toString(), this isn't going to work.
s.append( id.toString() );
}
}
addIdToCollectionInfoString() 클래스 내에서 조건에 따라 id.toString() 을 호출하고 있다.
id 는 Entity 이기 때문에 Entity.toString() 이 다시 불리게 된다....
즉 Entity.toString() 이 다시 호출되게 된다.
Entity.toString() → LazyFetch Initialize → if (LOG.debugMode == true) → Entity.toString()
무한 호출이 되게 된다...
LazyFetch Entity 에 대해서 toString 호출하는건 바람직 하지 않아 보인다..