MethodHandler in Hibernate using ByteBuddy proxies

2019-08-21 06:53发布

I'm currently migrating an old tool for Hibernate to automate prefetching based on entity statistics. The old tool used Hibernate 3.1, so there's some job to be done. Traditionally, Hibernate used CGlib proxies, but as I'm developing for the latest release of Hibernate I am changing the proxy generation to ByteBuddy.

However, since the change to ByteBuddy I've been having some problems with getting the MethodHandler to work. The methodhandler in my tool is supposed to handle all the calls to the proxies to enable the gathering of necessary statistics. Currently, my method handler looks like this:

public class EntityProxyMethodHandler implements ProxyConfiguration.Interceptor, Serializable {

private final EntityTracker entityTracker;
private final Object proxiedObject;
private final String proxiedClassName;

EntityProxyMethodHandler(
        Object proxiedObject,
        String proxiedClassName,
        Set<Property> persistentProperties,
        ExtentManager extentManager) {
    this.proxiedObject = proxiedObject;
    this.proxiedClassName = proxiedClassName;
    this.entityTracker = new EntityTracker(persistentProperties, extentManager );
}

@Override
@RuntimeType
public Object intercept(@This Object instance, @Origin Method method, @AllArguments Object[] arguments)
        throws Exception {
    final String methodName = method.getName();
    if ( "toString".equals( methodName ) ) {
        return proxiedClassName + "@" + System.identityHashCode( instance );
    }
    else if ( "equals".equals( methodName ) ) {
        return proxiedObject == instance;
    }
    else if ( "hashCode".equals( methodName ) ) {
        return System.identityHashCode( instance );
    }
    else if ( arguments.length == 0 ) {
        switch ( methodName ) {
            case "disableTracking": {
                boolean oldValue = entityTracker.isTracking();
                entityTracker.setTracking( false );
                return oldValue;
            }
            case "enableTracking": {
                boolean oldValue = entityTracker.isTracking();
                entityTracker.setTracking( true );
                return oldValue;
            }
            case "isAccessed":
                return entityTracker.isAccessed();

            default:
                break;
        }
    }
    else if ( arguments.length == 1 ) {
        if ( methodName.equals( "addTracker" ) && method.getParameterTypes()[0].equals( Statistics.class ) ) {
            entityTracker.addTracker( (Statistics) arguments[0] );
            return null;
        }
        else if ( methodName.equals( "addTrackers" ) && method.getParameterTypes()[0].equals( Set.class ) ) {
            @SuppressWarnings("unchecked")
            Set<Statistics> newTrackers = (Set) arguments[0];
            entityTracker.addTrackers( newTrackers );
            return null;
        }
        else if ( methodName
                .equals( "extendProfile" ) && method.getParameterTypes()[0].equals( Statistics.class ) ) {
            entityTracker.extendProfile( (Statistics) arguments[0], instance );
            return null;
        }
        else if ( methodName
                .equals( "removeTracker" ) && method.getParameterTypes()[0].equals( Statistics.class ) ) {
            entityTracker.removeTracker( (Statistics) arguments[0] );
            return null;
        }
    }
    entityTracker.trackAccess( instance );
    return method.invoke( instance, arguments ); // Gets stuck here, the method interception gets intercepted endlessly
}

The proxy factory looks like this:

public class EntityProxyFactory {
private static SessionFactoryImplementor sessionFactory = AutofetchIntegrator.getSessionFactory();
private static final ConcurrentMap<Class<?>, Constructor<?>> entityConstructorMap = new ConcurrentHashMap<>();

private static <T> Constructor<T> getDefaultConstructor(Class<T> clazz) throws NoSuchMethodException {
    Constructor<T> constructor = clazz.getDeclaredConstructor();
    if ( !constructor.isAccessible() ) {
        constructor.setAccessible( true );
    }

    return constructor;
}

static Object getProxyInstance(
        Class persistentClass,
        Set<Property> persistentProperties,
        ExtentManager extentManager)
        throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {

    if ( Modifier.isFinal( persistentClass.getModifiers() ) ) {
        // Use the default constructor, because final classes cannot be inherited.
        return useDefaultConstructor( persistentClass );
    }
    final ProxyConfiguration proxy = (ProxyConfiguration) Environment.getBytecodeProvider()
            .getProxyFactoryFactory()
            .buildBasicProxyFactory( persistentClass, new Class[] { TrackableEntity.class } )
            .getProxy();
    proxy.$$_hibernate_set_interceptor( new EntityProxyMethodHandler(proxy,persistentClass.getName(),
            persistentProperties,
            extentManager
    ) );
    return proxy;
}

private static Object useDefaultConstructor(Class<?> clazz)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    if ( !entityConstructorMap.containsKey( clazz ) ) {
        entityConstructorMap.put( clazz, getDefaultConstructor( clazz ) );
    }

    final Constructor<?> c = entityConstructorMap.get( clazz );
    return c.newInstance();
}

}

When I run a test containing accessing one of the fields of the proxies, I get an endless callstack of the following:

EntityProxyMethodHandler.intercept(EntityProxyMethodHandler.java:105)
Employee.$HibernateBasicProxy$Rbz7NDpP.getSupervisor(Unknown Source)

Caused by java.lang.reflect.InvocationTargetException:
EntityProxyMethodHandler.intercept(EntityProxyMethodHandler.java:105)
Employee.$HibernateBasicProxy$Rbz7NDpP.getSupervisor(Unknown Source)

For some reason, it does not call the real entity, but the proxy itself, getting stuck in an endless loop of intercepts. I have no idea how to resolve this, I've tried finding a way to call the "target" inside the method handler, but couldn't find anything since for some reason the proxies generated by the proxy factory is not implementing the HibernateProxy-interface. Maybe this is a problem that ByteBuddy has some ways to deal with, however since I'm rather inexperienced with the framework I haven't found a way yet.

Also, this was working flawlessly before when I used Javassist-proxies instead. Then I had similar classes:

public class EntityProxyFactory {

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

private static final MethodFilter FINALIZE_FILTER = new MethodFilter() {
    @Override
    public boolean isHandled(Method m) {
        // skip finalize methods
        return !(m.getParameterTypes().length == 0 && m.getName().equals("finalize"));
    }
};

private static final ConcurrentMap<Class<?>, Class<?>> entityFactoryMap = new ConcurrentHashMap<>();

private static final ConcurrentMap<Class<?>, Constructor<?>> entityConstructorMap = new ConcurrentHashMap<>();

//if entityFactoryMap doesnt contain that specific class, add it
private static Class<?> getProxyFactory(Class<?> persistentClass, String idMethodName) {
    // Not sure how enhancer work, but it seems like you tell enhancer what type of subclasses that can be created, and all the method calls will be intercepted by entitycallbackfilter
    if (!entityFactoryMap.containsKey(persistentClass)) {
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(persistentClass);
        factory.setInterfaces(new Class[]{TrackableEntity.class});
        factory.setFilter(FINALIZE_FILTER);
        entityFactoryMap.putIfAbsent(persistentClass, factory.createClass());
    }

    return entityFactoryMap.get(persistentClass);
}

private static <T> Constructor<T> getDefaultConstructor(Class<T> clazz) throws NoSuchMethodException {
    Constructor<T> constructor = clazz.getDeclaredConstructor();
    if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
    }

    return constructor;
}

public static Object getProxyInstance(Class persistentClass, String idMethodName, Set<Property> persistentProperties,
                                      ExtentManager extentManager)
        throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

    if (Modifier.isFinal(persistentClass.getModifiers())) {
        // Use the default constructor, because final classes cannot be inherited.
        return useDefaultConstructor(persistentClass);
    }

    Class<?> factory = getProxyFactory(persistentClass, idMethodName);
    try {
        final Object proxy = factory.newInstance();
        ((Proxy) proxy).setHandler(new EntityProxyMethodHandler(persistentProperties, extentManager));
        return proxy;
    } catch (IllegalAccessException | InstantiationException e) {
        return useDefaultConstructor(persistentClass);
    }
}

private static Object useDefaultConstructor(Class<?> clazz) throws NoSuchMethodException, InstantiationException,
        InvocationTargetException, IllegalAccessException {

    if (!entityConstructorMap.containsKey(clazz)) {
        entityConstructorMap.put(clazz, getDefaultConstructor(clazz));
    }

    final Constructor<?> c = entityConstructorMap.get(clazz);

    return c.newInstance((Object[]) null);
}

}

public class EntityProxyMethodHandler implements MethodHandler, Serializable {

private final EntityTracker entityTracker;

public EntityProxyMethodHandler(Set<Property> persistentProperties, ExtentManager extentManager) {
    this.entityTracker = new EntityTracker(persistentProperties, extentManager);
}

@Override
public Object invoke(Object obj, Method thisMethod, Method proceed, Object[] args) throws Throwable {
    if (args.length == 0) {
        if (thisMethod.getName().equals("disableTracking")) {
            boolean oldValue = entityTracker.isTracking();
            entityTracker.setTracking(false);
            return oldValue;
        } else if (thisMethod.getName().equals("enableTracking")) {
            boolean oldValue = entityTracker.isTracking();
            entityTracker.setTracking(true);
            return oldValue;
        } else if (thisMethod.getName().equals("isAccessed")) {
            return entityTracker.isAccessed();
        }
    } else if (args.length == 1) {
        if (thisMethod.getName().equals("addTracker") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
            entityTracker.addTracker((Statistics) args[0]);
            return null;
        } else if (thisMethod.getName().equals("addTrackers") && thisMethod.getParameterTypes()[0].equals(Set.class)) {
            @SuppressWarnings("unchecked")
            Set<Statistics> newTrackers = (Set) args[0];
            entityTracker.addTrackers(newTrackers);
            return null;
        } else if (thisMethod.getName().equals("extendProfile") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
            entityTracker.extendProfile((Statistics) args[0], obj);
            return null;
        } else if (thisMethod.getName().equals("removeTracker") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
            entityTracker.removeTracker((Statistics) args[0]);
            return null;
        }
    }

    entityTracker.trackAccess(obj);

    return proceed.invoke(obj, args);
}

Does anyone know how I could call the real entity object in my method handler here?

Here's a part of the stack tracea when I run a unit test, the caused by is being repeated "infinitely":

    java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.autofetch.hibernate.EntityProxyMethodHandler.intercept(EntityProxyMethodHandler.java:105)
    at org.autofetch.test.Employee$HibernateBasicProxy$dbvGRz97.getSupervisor(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.autofetch.hibernate.AutofetchLazyInitializer.intercept(AutofetchLazyInitializer.java:123)
    at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
    at org.autofetch.test.Employee$HibernateProxy$PajATc2c.getSupervisor(Unknown Source)
    at org.autofetch.test.ExtentTest.secondLevelSupervisorAccess(ExtentTest.java:533)
    at org.autofetch.test.ExtentTest.testSecondLevelSupervisorAccess(ExtentTest.java:246)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.hibernate.testing.junit4.ExtendedFrameworkMethod.invokeExplosively(ExtendedFrameworkMethod.java:45)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298)
    at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.autofetch.hibernate.EntityProxyMethodHandler.intercept(EntityProxyMethodHandler.java:105)
    at org.autofetch.test.Employee$HibernateBasicProxy$dbvGRz97.getSupervisor(Unknown Source)
    ... 30 more

1条回答
你好瞎i
2楼-- · 2019-08-21 07:35

If you want to invoke the original method, you can get hold of it using @SuperMethod Method m or @SuperCall Callable<?> c.

Using a callable proxy is the method with the least overhead, doing so, you neither need to get hold of the argument array.

查看更多
登录 后发表回答