Slightly customized LazyInitializer returns null in method.invoke()


#1

Hi,

In my project, we are using a custom LazyInitializer to intercept certain method calls. However, since we changed from Javassist proxies to Hibernates custom LazyInitializer from Hibernate 4.3 to 5.3, we have had problems to retrieve data from objects. For some reason, after loading an object and trying to get specific properties from that object, the method.invoke( target, arguments ) methods always return null. While debugging I’ve noticed that the target shows all the values, so the values actually exist.

Does anyone have an idea what could be the reason why we can’t retrieve values? In the test case, we will get a NullpointerException in Assert.assertNotNull( dave.getAddress().getStreet());

public class AutofetchLazyInitializer extends BasicLazyInitializer {

	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( AutofetchLazyInitializer.class );

	private final EntityTracker entityTracker;

	private final Class[] interfaces;

	private boolean entityTrackersSet;

	private boolean constructed;

	private AutofetchLazyInitializer(
			String entityName,
			Class persistentClass,
			Class[] interfaces,
			Serializable id,
			Method getIdentifierMethod,
			Method setIdentifierMethod,
			CompositeType componentIdType,
			SharedSessionContractImplementor session,
			Set<Property> persistentProperties,
			boolean classOverridesEquals) {

		super( entityName, persistentClass, id, getIdentifierMethod, setIdentifierMethod,
			   componentIdType, session, classOverridesEquals
		);

		this.interfaces = interfaces;

		AutofetchService autofetchService = session.getFactory()
				.getServiceRegistry()
				.getService( AutofetchService.class );
		this.entityTracker = new EntityTracker( persistentProperties, autofetchService.getExtentManager() );
		this.entityTrackersSet = false;
	}

	@Override
	protected Object serializableProxy() {
		return new AutofetchSerializableProxy(
				getEntityName(),
				this.persistentClass,
				this.interfaces,
				getIdentifier(),
				( isReadOnlySettingAvailable() ?
						Boolean.valueOf( isReadOnly() ) :
						isReadOnlyBeforeAttachedToSession() ),
				this.getIdentifierMethod,
				this.setIdentifierMethod,
				this.componentIdType,
				this.entityTracker.getPersistentProperties()
		);
	}

	public static HibernateProxy getProxy(
			final String entityName,
			final Class persistentClass,
			final Class[] interfaces,
			final Method getIdentifierMethod,
			final Method setIdentifierMethod,
			final CompositeType componentIdType,
			final Serializable id,
			final SessionImplementor session,
			final Set<Property> persistentProperties) throws HibernateException {

		// note: interface is assumed to already contain HibernateProxy.class
		try {
			final AutofetchLazyInitializer lazyInitializer = new AutofetchLazyInitializer(
					entityName,
					persistentClass,
					interfaces,
					id,
					getIdentifierMethod,
					setIdentifierMethod,
					componentIdType,
					session,
					persistentProperties,
					ReflectHelper.overridesEquals( persistentClass )
			);
			final HibernateProxy proxy = (HibernateProxy) Environment.getBytecodeProvider().getProxyFactoryFactory()
					.buildBasicProxyFactory( interfaces.length == 1 ? persistentClass : null, interfaces )
					.getProxy();
			final Interceptor interceptor = new Interceptor( proxy, entityName, lazyInitializer );
			( (ProxyConfiguration) proxy ).$$_hibernate_set_interceptor( interceptor );
			lazyInitializer.constructed = true;
			return proxy;
		}
		catch (Throwable t) {
			final String message = LOG.bytecodeEnhancementFailed( entityName );
			LOG.error( message, t );
			throw new HibernateException( message, t );
		}
	}

	public static HibernateProxy getProxy(
			final BasicProxyFactory factory,
			final String entityName,
			final Class persistentClass,
			final Class[] interfaces,
			final Method getIdentifierMethod,
			final Method setIdentifierMethod,
			final CompositeType componentIdType,
			final Serializable id,
			final SharedSessionContractImplementor session,
			final Set<Property> persistentProperties) throws HibernateException {

		// note: interfaces is assumed to already contain HibernateProxy.class
		final AutofetchLazyInitializer lazyInitializer = new AutofetchLazyInitializer(
				entityName,
				persistentClass,
				interfaces,
				id,
				getIdentifierMethod,
				setIdentifierMethod,
				componentIdType,
				session,
				persistentProperties,
				ReflectHelper.overridesEquals( persistentClass )
		);

		final HibernateProxy proxy;
		try {
			proxy = (HibernateProxy) factory.getProxy();
		}
		catch (Exception e) {
			throw new HibernateException( "Bytecode Enhancement failed: " + persistentClass.getName(), e );
		}

		final Interceptor interceptor = new Interceptor( proxy, entityName, lazyInitializer );
		( (ProxyConfiguration) proxy ).$$_hibernate_set_interceptor( interceptor );
		lazyInitializer.constructed = true;

		return proxy;
	}

	private static class Interceptor extends PassThroughInterceptor {

		private final AutofetchLazyInitializer lazyInitializer;

		Interceptor(
				Object proxiedObject,
				String proxiedClassName,
				AutofetchLazyInitializer lazyInitializer) {
			super( proxiedObject, proxiedClassName );

			this.lazyInitializer = lazyInitializer;
		}

		@Override
		@RuntimeType
		public Object intercept(@This Object instance, @Origin Method method, @AllArguments Object[] arguments)
				throws Exception {
			final String methodName = method.getName();

			if ( lazyInitializer.constructed ) {
				Object result;
				try {
					result = lazyInitializer.invoke( method, arguments, instance );
				}
				catch (Throwable t) {
					throw new Exception( t.getCause() );
				}

				if ( result == INVOKE_IMPLEMENTATION ) {
					if ( arguments.length == 0 ) {
						switch ( methodName ) {
							case "enableTracking":
								return handleEnableTracking();
							case "disableTracking":
								return handleDisableTracking();
							case "isAccessed":
								return lazyInitializer.entityTracker.isAccessed();
						}
					}
					else if ( arguments.length == 1 ) {
						if ( methodName.equals( "addTracker" ) && method.getParameterTypes()[0].equals(
								Statistics.class ) ) {
							return handleAddTracked( arguments[0] );
						}
						else if ( methodName.equals( "addTrackers" ) && method.getParameterTypes()[0].equals(
								Set.class ) ) {
							return handleAddTrackers( arguments[0] );
						}
						else if ( methodName
								.equals( "removeTracker" ) && method.getParameterTypes()[0].equals( Statistics.class ) ) {
							lazyInitializer.entityTracker.removeTracker( (Statistics) arguments[0] );
							return handleRemoveTracker( arguments );
						}
						else if ( methodName
								.equals( "extendProfile" ) && method.getParameterTypes()[0].equals( Statistics.class ) ) {
							return extendProfile( arguments );
						}
					}

					final Object target = lazyInitializer.getImplementation();
					final Object returnValue;

					try {
						if ( ReflectHelper.isPublic( lazyInitializer.persistentClass, method ) ) {
							if ( !method.getDeclaringClass().isInstance( target ) ) {
								throw new ClassCastException(
										target.getClass().getName() + " incompatible with " + method.getDeclaringClass()
												.getName()
								);
							}
						}
						else {
							method.setAccessible( true );
						}

						returnValue = method.invoke( target, arguments );
						if ( returnValue == target ) {
							if ( returnValue.getClass().isInstance( instance ) ) {
								return instance;
							}
							else {
								LOG.narrowingProxy( returnValue.getClass() );
							}
						}

						return returnValue;
					}
					catch (InvocationTargetException ite) {
						throw new RuntimeException( ite.getTargetException() );
					}
					finally {
						if ( !lazyInitializer.entityTrackersSet && target instanceof Trackable ) {
							lazyInitializer.entityTrackersSet = true;
							Trackable entity = (Trackable) target;
							entity.addTrackers( lazyInitializer.entityTracker.getTrackers() );
							if ( lazyInitializer.entityTracker.isTracking() ) {
								entity.enableTracking();
							}
							else {
								entity.disableTracking();
							}
						}
					}
				}
				else {
					return result;
				}
			}
			else {
				// while constructor is running
				if ( methodName.equals( "getHibernateLazyInitializer" ) ) {
					return this;
				}
				else {
					return super.intercept( instance, method, arguments );
				}
			}
		}

		private Object handleDisableTracking() {
			boolean oldValue = lazyInitializer.entityTracker.isTracking();
			this.lazyInitializer.entityTracker.setTracking( false );
			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof Trackable ) {
					Trackable entity = (Trackable) o;
					entity.disableTracking();
				}
			}

			return oldValue;
		}

		private Object handleEnableTracking() {
			boolean oldValue = this.lazyInitializer.entityTracker.isTracking();
			this.lazyInitializer.entityTracker.setTracking( true );

			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof Trackable ) {
					Trackable entity = (Trackable) o;
					entity.enableTracking();
				}
			}

			return oldValue;
		}

		private Object extendProfile(Object[] params) {
			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof TrackableEntity ) {
					TrackableEntity entity = (TrackableEntity) o;
					entity.extendProfile( (Statistics) params[0] );
				}
			}
			else {
				throw new IllegalStateException( "Can't call extendProfile on unloaded self." );
			}

			return null;
		}

		private Object handleRemoveTracker(Object[] params) {
			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof Trackable ) {
					Trackable entity = (Trackable) o;
					entity.removeTracker( (Statistics) params[0] );
				}
			}
			return null;
		}

		@SuppressWarnings("unchecked")
		private Object handleAddTrackers(Object param) {
			Set<Statistics> newTrackers = (Set<Statistics>) param;
			this.lazyInitializer.entityTracker.addTrackers( newTrackers );
			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof Trackable ) {
					Trackable entity = (Trackable) o;
					entity.addTrackers( newTrackers );
				}
			}

			return null;
		}

		private Object handleAddTracked(Object param) {
			this.lazyInitializer.entityTracker.addTracker( (Statistics) param );
			if ( !lazyInitializer.isUninitialized() ) {
				Object o = lazyInitializer.getImplementation();
				if ( o instanceof Trackable ) {
					Trackable entity = (Trackable) o;
					entity.addTracker( (Statistics) param );
				}
			}
			return null;
		}
	}
}


@Entity
public class Employee {

	@Id
	@Column(name = "employee_id")
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long m_id;

	@Column(name = "name")
	private String m_name;

	@JoinColumn(name = "supervisor_id")
	@ManyToOne(cascade = { CascadeType.ALL })
	private Employee m_supervisor;

	@JoinColumn(name = "supervisor_id")
	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.LAZY)
	private Set<Employee> m_subordinates;

	@Embedded
	@Basic(fetch = FetchType.LAZY)
	private Address m_address;

	@JoinColumn(name = "mentor_id")
	@ManyToOne(cascade = { CascadeType.ALL })
	private Employee m_mentor;

	@ManyToMany(cascade = { CascadeType.ALL })
	@JoinTable(name = "friends", joinColumns = { @JoinColumn(name = "friend_id") }, inverseJoinColumns = { @JoinColumn(name = "befriended_id") })
	private Set<Employee> m_friends;

	/**
	 * Default constructor. Needed by Hibernate
	 */
	protected Employee() {
		// Nothing
	}

	/**
	 * Creates employee with name and supervisor and no subordinates
	 *
	 * @param name should be unique and not null
	 * @param supervisor employee supervisor, null indicates no supervisor.
	 */
	public Employee(String name, Employee supervisor, Employee mentor, Address addr) {
		m_name = name;
		m_supervisor = supervisor;
		m_subordinates = new HashSet<>();
		m_friends = new HashSet<>();
		m_mentor = mentor;
		m_address = addr;
	}

	public String getName() {
		return m_name;
	}

	public void setName(String name) {
		m_name = name;
	}

	public Set<Employee> getSubordinates() {
		return m_subordinates;
	}

	public void setSubordinates(Set<Employee> subordinates) {
		m_subordinates = subordinates;
	}

	public void addSubordinate(Employee e) {
		m_subordinates.add( e );
	}

	public Employee getSupervisor() {
		return m_supervisor;
	}

	public void setSupervisor(Employee supervisor) {
		m_supervisor = supervisor;
	}

	public Long getId() {
		return m_id;
	}

	public void setId(Long id) {
		m_id = id;
	}

	public Address getAddress() {
		return m_address;
	}

	public void setAddress(Address address) {
		m_address = address;
	}

	public Employee getMentor() {
		return m_mentor;
	}

	public void setMentor(Employee mentor) {
		m_mentor = mentor;
	}

	public Set<Employee> getFriends() {
		return m_friends;
	}

	public void setFriends(Set<Employee> friends) {
		m_friends = friends;
	}

	public void addFriend(Employee friend) {
		m_friends.add( friend );
	}

	@Override
	public int hashCode() {
		return m_name.hashCode();
	}

	@Override
	public boolean equals(Object other) {
		return ( other instanceof Employee )
				&& ( (Employee) other ).m_name.equals( m_name );
	}

	@Override
	public String toString() {
		return m_name;
	}
}

	private void addressAccess() {
		Long daveId = createObjectGraph( true );
		try (Session sess = openSession()) {
			Transaction tx = sess.beginTransaction();
			Employee dave = sess.load( Employee.class, daveId );
			Assert.assertNotNull( dave.getAddress().getStreet() );
			Assert.assertNotNull( dave.getAddress().getCity() ); // Second access shouldn't make a difference
			tx.commit();
			sess.close();
		}
	}
private Long createObjectGraph(boolean returnLeaf) {
		Session sess;
		Transaction tx = null;
		try {
			sess = openSession();
			tx = sess.beginTransaction();
			Employee root = new Employee( "Root", null, null, new Address( "", "", "" ) );
			Employee e4 = new Employee( "Ali", null, null, new Address( "104 Main St.", "Austin", "Texas" ) );
			Employee e5 = new Employee( "Ben", null, null, new Address( "105 Main St.", "Austin", "Texas" ) );
			Employee e0 = new Employee( "John", root, e5, new Address( "100 Main St.", "Austin", "Texas" ) );
			Employee e1 = new Employee( "Mike", e0, null, new Address( "101 Main St.", "Austin", "Texas" ) );
			Employee e2 = new Employee( "Sam", e0, e4, new Address( "102 Main St.", "Austin", "Texas" ) );
			Employee e3 = new Employee( "Dave", e1, e2, new Address( "103 Main St.", "Austin", "Texas" ) );
			root.addSubordinate( e0 );
			e0.addSubordinate( e1 );
			e0.addSubordinate( e2 );
			e1.addSubordinate( e3 );

			// Make some friends for e3
			for ( int i = 0; i < NUM_FRIENDS; i++ ) {
				Employee friend = new org.autofetch.test.Employee( "Friend" + i, e2, e0,
																   new Address( "100 Friend St.", "Austin", "Texas" )
				);
				e3.addFriend( friend );
			}

			sess.save( root );
			tx.commit();
			sess.close();
			tx = null;
			if ( returnLeaf ) {
				return e3.getId();
			}
			else {
				return e0.getId();
			}
		}
		finally {
			if ( tx != null ) {
				tx.rollback();
			}
		}
	}

#2

Hi,

Could you create a test case based on our template here: https://github.com/hibernate/hibernate-test-case-templates/tree/master/orm/hibernate-orm-5 ?

There’s an upload feature in the toolbar just above the message box.

Thanks!


#3

I could but I seriously doubt that it’s an error from hibernate, but rather because I am customizing the proxies/lazyInitialization. If you happen to have a clue what it potentially could be about, I would be very happy! The strange thing is that if something went wrong with for example the reflection in the invoke-call, then I would expect that it would throw an exception. As it is now, it seems like it is something else, but what is the question… :wink:


#4

Here’s a picture while debugging, the target is right, but still we return null.


#5

Sounds like the Proxy was not initialized properly. Try to compare the AutofetchLazyInitializer with the standard one and see if there are some notable differences. Try switching to trace in debug to see where they diverge.


#6

In the end, the cause was that we extended PassThroughInterceptor, which returns null for methods that are not setters, getters, hashCode or equals. Instead now we only implement the ProxyConfiguration.Interceptor interface. Thanks for the help everyone.