package tc.oc.evil; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.concurrent.ExecutionException; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import tc.oc.commons.core.reflect.MethodResolver; import tc.oc.commons.core.reflect.Methods; public class LibCGDecoratorGenerator implements DecoratorGenerator { private static class CGMeta<T, D extends Decorator<T>> extends DecoratorGenerator.Meta<T, D> { final Enhancer enhancer; public CGMeta(Class<T> type, Class<D> decorator, Class implementation, Enhancer enhancer) { super(type, decorator, implementation); this.enhancer = enhancer; } @Override public D newInstance() throws Exception { return (D) enhancer.create(); } @Override public D newInstance(Class[] parameterTypes, Object[] arguments) throws Exception { return (D) enhancer.create(parameterTypes, arguments); } } private static class Callback<T, D extends Decorator<T>> implements MethodInterceptor { final Class<T> base; final Class<D> decorator; final MethodResolver resolver; final Cache<Method, Boolean> isDecorated = CacheBuilder.newBuilder().build(); final Cache<Method, MethodHandle> methodHandles = CacheBuilder.newBuilder().build(); private Callback(Class<T> base, Class<D> decorator) { this.base = base; this.decorator = decorator; this.resolver = new MethodResolver(decorator); } boolean isDecorated(Method method) throws ExecutionException { final Boolean yes = isDecorated.getIfPresent(method); if(yes != null) return yes; synchronized(isDecorated) { return isDecorated.get(method, () -> { // This may look simple, but it was absurdly difficult to get right. // There are a lot of subtleties involved in method dispatch. // // Note, for example, that we completely ignore the declaring class // of all methods involved. The only thing that matters is the // name and signature of the method. This is absolutely necessary // in order to handle all cases properly. // First of all, make sure the delegate() method always goes to the decorator. if("delegate".equals(method.getName()) && method.getParameterTypes().length == 0) return true; // Look for a matching method in the base class (which may be abstract). // If the method is completely absent from the base, send it to the decorator. final Method baseMethod = Methods.accessibleMethod(base, method); if(baseMethod == null) return true; // Now look for a callable match in the decorator. If we find one, // and it's different from the base method, and it doesn't come from // an interface, then assume it's an override and call it. // // To be consistent with Java, we don't allow interface default methods // to override methods in the base class. final Method decoMethod = Methods.callableMethod(decorator, method); return decoMethod != null && !decoMethod.equals(baseMethod) && !decoMethod.getDeclaringClass().isInterface(); }); } } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { if(isDecorated(method)) { // Decorated method return proxy.invokeSuper(obj, args); } else { final T t = ((Decorator<T>) obj).delegate(); if(method.getDeclaringClass().isInstance(t)) { // Forwarded method return proxy.invoke(t, args); } else { // Forwarded method shadowed by an interface method in the decorator. // // This can happen if the decorator implements an interface that the // base class doesn't, and that interface contains a method that shadows // one on the base class. Java would allow the method to be called on the // base anyway, but MethodProxy refuses to invoke it on something that // is not assignable to the method's declaring type. So, unfortunately, // we have to fall back to the JDK to handle this case. return methodHandles.get(method, () -> resolver.virtualHandle(t.getClass(), method).bindTo(t) ).invokeWithArguments(args); } } } } @Override public <T, D extends Decorator<T>> DecoratorGenerator.Meta<T, D> implement(Class<T> type, Class<D> decorator) { final Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(decorator); enhancer.setCallbackType(MethodInterceptor.class); final Class<? extends D> impl = enhancer.createClass(); enhancer.setCallback(new Callback<>(type, decorator)); return new CGMeta(type, decorator, impl, enhancer); } }