Prototype CGLIB replacement (Issue 1133)#1298
Prototype CGLIB replacement (Issue 1133)#1298mcculls wants to merge 125 commits intogoogle:masterfrom
Conversation
This avoids the need for a marker interface, which in turn reduces the need for bridge classloaders. Since we store bridge classloaders in a weak cache, we're effectively just trading one for another.
a sorted list of strings. It assumes only those strings will be queried and therefore may produce false-positive results for strings not in the list. This trie will be used in the replacement FastClass/Enhancer.
…onsistency with other args
markmarch
left a comment
There was a problem hiding this comment.
Thanks for addressing all my comments.
This LGTM but I'll have sameb@ have a second look to make sure I haven't missed anything. Once Sam has LGTMed this PR, I'll work on submitting it internal (as the repo is not properly set up to auto accept PR).
…ass-loading Note that package visibility wrt method intercepting is tested in BytecodeGenTest
…nd method parameters are also PUBLIC
| return new FastClassProxy<T>(injectionPoint, constructor, fastConstructor); | ||
| } | ||
| } catch (net.sf.cglib.core.CodeGenerationException e) { | ||
| } catch (Exception | LinkageError e) { |
There was a problem hiding this comment.
do we have any narrower type we can catch here, to avoid over-catching exceptions & suppressing real errors?
(same elsewhere we catch Exception or Throwable)
There was a problem hiding this comment.
This is meant to catch any exceptions that occurred while creating the fast constructor (not invocation as that happens inside FastClassProxy) and fall-through to use JDK reflection instead. CGLIB wrapped anything extending Exception inside CodeGenerationException so this just widens it to include linkage errors which can sometimes be thrown during class-loading. We'd want to fall back to JDK reflection in that case too.
We could consider logging the exception, but we'd still want to fall back to JDK reflection when we can't create the fast constructor.
| */ | ||
| ANONYMOUS, | ||
|
|
||
| /** Prefer Unsafe, but fall back to child class loaders if Unsafe is not available (Default) */ |
There was a problem hiding this comment.
can we clarify how "Prefer Unsafe" differs from "Unsafe.defineAnonymousClass"? what Unsafe method is it using? (or is it defineAnonymousClass?)
same in OFF's docs
There was a problem hiding this comment.
For the general Unsafe case we use Unsafe.defineAnonymousClass to get access to the host classloader's defineClass method. Once we have access we then define classes using that classloader. We only use defineAnonymousClass to define all classes when guice_custom_class_loading is set to ANONYMOUS.
I'll expand on this in the javadoc.
| try { | ||
| return (T) fastMethod.apply(instance, parameters); | ||
| } catch (Throwable e) { | ||
| throw new InvocationTargetException(e); |
There was a problem hiding this comment.
should the the FastClass/Enhancer code prewrap in InvocationTargetExceptions?
otherwise it looks like all callers need to remember to do that, and if they don't then (i'm assuming) checked exceptions from the user code can slip out? unless it's throwing the user-errors inside of some kind of runtime exception (to make it work w/ BiFunction, in which case do we need to unwrap from that?)
(also it looks like IllegalAccessException isn't possible anymore?)
same comment throughout where we do this.
There was a problem hiding this comment.
Enhanced classes don't pre-wrap exceptions with InvocationTargetException because they need to match the behaviour of the original type as closely as possible. To to keep class generation simple, fast-classes also don't do any pre-wrapping. In other words the fast-class behaves as if we were calling the original class directly (from an exception PoV).
Instead we apply InvocationTargetException to the few fast-constructor/fast-method places that need it to match JDK reflection behaviour. (This behaviour is checked by some internal tests.) This is also closer to the calling code that unwraps the invocation exception and reports the original cause, rather than have it hidden inside the generated code.
Wrt. checked exceptions from user code: those are propagated from the BiFunction by using a common trick to get the Java compiler to treat them as unchecked (all exceptions are unchecked from the perspective of the JVM.) This means the generated code doesn't need to do any wrapping itself, only the couple of places that need to match JDK reflection behaviour.
Also wrt. IllegalAccessException - that doesn't apply to the 'fast-class' case because we only use 'fast' invocation when we know we have enough access to benefit from it, and the generated code deliberately gives enough access to Guice to use the generated invoker.
There was a problem hiding this comment.
added a bit of javadoc in 1386c48 and removed IllegalAccessException for fast-invocation
| return fastClass.invoke(index, target, parameters); | ||
| try { | ||
| return fastMethod.apply(target, parameters); | ||
| } catch (Throwable e) { |
There was a problem hiding this comment.
(same q about wrapping w/ InvocationTargetException here)
| }; | ||
| } | ||
| } catch (net.sf.cglib.core.CodeGenerationException e) { | ||
| } catch (Exception | LinkageError e) { |
There was a problem hiding this comment.
(same q about catching narrower exceptions here)
There was a problem hiding this comment.
See above - we want to fall back to JDK reflection if any exception or class-loading linkage error occurs when creating the fast-method (note this doesn't cover the actual invocation.)
| try { | ||
| // pass this signature's index into the table function to retrieve the invoker | ||
| return (BiFunction) invokerTable.invokeExact(signatureTable.applyAsInt(signature)); | ||
| } catch (Throwable e) { |
There was a problem hiding this comment.
should WrongMethodTypeException be handled separately here? javadoc describes that as thrown when "if the target's type is not identical with the caller's symbolic type descriptor", as opposed to every other kind of exception, which is just propagated from the underlying call.
same below
There was a problem hiding this comment.
I don't believe we need to handle WrongMethodTypeException separately here.
| } | ||
|
|
||
| /** | ||
| * Generate trampoline that takes an index, along with a context object and array of argument |
There was a problem hiding this comment.
does "and array of argument objects" imply there's a penalty for using the trampoline, in order to allocate the array & stuff the args into it? is this a change from how FastClass or Enhancer worked?
[have we done any benchmarks on the old vs new AOP approaches?]
There was a problem hiding this comment.
For fast-invocation we are given an array of objects (which gets passed to either the fast-invoker or JDK reflection.)
Likewise for enhanced types, any intercepted methods need to convert their original arguments into an array of objects so they can be passed through an InvocationHandler and onto the method interceptor as MethodInvocation details. Methods that don't have any interceptors are not enhanced.
So the object array is a necessary detail of any solution (either having it passed in or expecting it to be provided for interception purposes.)
Re: benchmarks, I've run various tests that show performance is better using the new approach. For method interception the improvement per-invocation is small, but there's a much larger benefit when it comes to the up-front cost of interception when creating new injectors.
| protected byte[] generateGlue(Collection<Executable> members) { | ||
| ClassWriter cw = new ClassWriter(COMPUTE_MAXS); | ||
|
|
||
| cw.visit(V1_8, PUBLIC | ACC_SUPER, proxyName, null, hostName, null); |
There was a problem hiding this comment.
add a comment on why we're using v1_8 compatible classes (as opposed to anything older or newer)?
also: does this limit our ability to interact with host code that uses newer JDK features?
also: we use the constant in a bunch of places, should we extract to a shared constant so we can increment throughout the codebase more easily? or is that not a meaningful thing to do?
There was a problem hiding this comment.
We use V1_8 as a minimum because that's the base version that Guice currently supports. The bytecode generated for the fast-class and enhancer glue is simple and doesn't need anything more than Java8. This is because we're not copying the actual method code, only generating invokers that match the expected signature.
I'd prefer not to use a shared constant here because it makes it harder to see what level we're targeting. Also in the future we may decide we want to change the version for one of the cases, but not the others.
…atch JDK reflection behaviour
|
@mcculls |
…ype-cglib-replacement
Replace CGLIB with custom code to generate "enhancers" and "fast-classes". Some user visible changes from using cglib: - intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown. - Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy - Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable - Generated class name is slightly longer. Closes #1298 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=320433559
Replace CGLIB with custom code to generate "enhancers" and "fast-classes". Some user visible changes from using cglib: - intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown. - Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy - Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable - Generated class name is slightly longer. Closes #1298 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=320433559
Replace CGLIB with custom code to generate "enhancers" and "fast-classes". Some user visible changes from using cglib: - intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown. - Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy - Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable - Generated class name is slightly longer. Closes #1298 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=320433559
Replace CGLIB with custom code to generate "enhancers" and "fast-classes". Some user visible changes from using cglib: - intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown. - Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy - Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable - Generated class name is slightly longer. Closes #1298 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=320433559
This PR replaces CGLIB with prototype code to generate "enhancers" and "fast-classes".
I've tried this with a number of existing applications, however since this is new code and the whole area of proxying can be tricky (especially involving bridge methods) there will likely be tweaks and fixes needed :)
available for early testing, but not yet ready for review - more details to be added