JDK-8253870 : (proxy) Support for default methods
  • Type: CSR
  • Component: core-libs
  • Sub-Component: java.lang:reflect
  • Priority: P3
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 16
  • Submitted: 2020-09-30
  • Updated: 2020-11-25
  • Resolved: 2020-11-25
Related Reports
CSR :  
Description
Summary
-------

Provide a new static `InvocationHandler::invokeDefault` API to allow a default method
defined in a proxy interface directly or indirectly to be invoked.

Problem
-------

Default methods were introduced in Java SE 8.   It has been lacking of
support in `java.lang.reflect.Proxy` API of invocation of the default methods
defined in proxy interfaces.    A proxy class overrides all instance methods 
defined in the specified proxy interfaces and the implementation forwards to 
`InvocationHandler::invoke`.   It needs a mechanism equivalent to calling 
`X.super::m` to select a default method to be invoked.

Solution
--------

Define a static `InvocationHandler::invokeDefault(Object proxy, Method method, Object... args)` method that allows an invocation handler to specify a default method and invoke it with the specified proxy instance and arguments.

Specification
-------------

1) New static `InvocationHandler::invokeDefault` method

```
     /**
     * Invokes the specified default method on the given {@code proxy} instance with
     * the given parameters.  The given {@code method} must be a default method
     * declared in a proxy interface of the {@code proxy}'s class or inherited
     * from its superinterface directly or indirectly.
     * <p>
     * Invoking this method behaves as if {@code invokespecial} instruction executed
     * from the proxy class, targeting the default method in a proxy interface.
     * This is equivalent to the invocation:
     * {@code X.super.m(A* a)} where {@code X} is a proxy interface and the call to
     * {@code X.super::m(A*)} is resolved to the given {@code method}.
     * <p>
     * Examples: interface {@code A} and {@code B} both declare a default
     * implementation of method {@code m}. Interface {@code C} extends {@code A}
     * and inherits the default method {@code m} from its superinterface {@code A}.
     *
     * <blockquote><pre>{@code
     * interface A {
     *     default T m(A a) { return t1; }
     * }
     * interface B {
     *     default T m(A a) { return t2; }
     * }
     * interface C extends A {}
     * }</pre></blockquote>
     *
     * The following creates a proxy instance that implements {@code A}
     * and invokes the default method {@code A::m}.
     *
     * <blockquote><pre>{@code
     * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class },
     *         (o, m, params) -> {
     *             if (m.isDefault()) {
     *                 // if it's a default method, invoke it
     *                 return InvocationHandler.invokeDefault(o, m, params);
     *             }
     *         });
     * }</pre></blockquote>
     *
     * If a proxy instance implements both {@code A} and {@code B}, both
     * of which provides the default implementation of method {@code m},
     * the invocation handler can dispatch the method invocation to
     * {@code A::m} or {@code B::m} via the {@code invokeDefault} method.
     * For example, the following code delegates the method invocation
     * to {@code B::m}.
     *
     * <blockquote><pre>{@code
     * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class, B.class },
     *         (o, m, params) -> {
     *             if (m.getName().equals("m")) {
     *                 // invoke B::m instead of A::m
     *                 Method bMethod = B.class.getMethod(m.getName(), m.getParameterTypes());
     *                 return InvocationHandler.invokeDefault(o, bMethod, params);
     *             }
     *         });
     * }</pre></blockquote>
     *
     * If a proxy instance implements {@code C} that inherits the default
     * method {@code m} from its superinterface {@code A}, then
     * the interface method invocation on {@code "m"} is dispatched to
     * the invocation handler's {@link #invoke(Object, Method, Object[]) invoke}
     * method with the {@code Method} object argument representing the
     * default method {@code A::m}.
     *
     * <blockquote><pre>{@code
     * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { C.class },
     *        (o, m, params) -> {
     *             if (m.isDefault()) {
     *                 // behaves as if calling C.super.m(params)
     *                 return InvocationHandler.invokeDefault(o, m, params);
     *             }
     *        });
     * }</pre></blockquote>
     *
     * The invocation of method {@code "m"} on this {@code proxy} will behave
     * as if {@code C.super::m} is called and that is resolved to invoking
     * {@code A::m}.
     * <p>
     * Adding a default method, or changing a method from abstract to default
     * may cause an exception if an existing code attempts to call {@code invokeDefault}
     * to invoke a default method.
     *
     * For example, if {@code C} is modified to implement a default method
     * {@code m}:
     *
     * <blockquote><pre>{@code
     * interface C extends A {
     *     default T m(A a) { return t3; }
     * }
     * }</pre></blockquote>
     *
     * The code above that creates proxy instance {@code proxy} with
     * the modified {@code C} will run with no exception and it will result in
     * calling {@code C::m} instead of {@code A::m}.
     * <p>
     * The following is another example that creates a proxy instance of {@code C}
     * and the invocation handler calls the {@code invokeDefault} method
     * to invoke {@code A::m}:
     *
     * <blockquote><pre>{@code
     * C c = (C) Proxy.newProxyInstance(loader, new Class<?>[] { C.class },
     *         (o, m, params) -> {
     *             if (m.getName().equals("m")) {
     *                 // IllegalArgumentException thrown as {@code A::m} is not a method
     *                 // inherited from its proxy interface C
     *                 Method aMethod = A.class.getMethod(m.getName(), m.getParameterTypes());
     *                 return InvocationHandler.invokeDefault(o, aMethod params);
     *             }
     *         });
     * c.m(...);
     * }</pre></blockquote>
     *
     * The above code runs successfully with the old version of {@code C} and
     * {@code A::m} is invoked.  When running with the new version of {@code C},
     * the above code will fail with {@code IllegalArgumentException} because
     * {@code C} overrides the implementation of the same method and
     * {@code A::m} is not accessible by a proxy instance.
     *
     * @apiNote
     * The {@code proxy} parameter is of type {@code Object} rather than {@code Proxy}
     * to make it easy for {@link InvocationHandler#invoke(Object, Method, Object[])
     * InvocationHandler::invoke} implementation to call directly without the need
     * of casting.
     *
     * @param proxy   the {@code Proxy} instance on which the default method to be invoked
     * @param method  the {@code Method} instance corresponding to a default method
     *                declared in a proxy interface of the proxy class or inherited
     *                from its superinterface directly or indirectly
     * @param args    the parameters used for the method invocation; can be {@code null}
     *                if the number of formal parameters required by the method is zero.
     * @return the value returned from the method invocation
     *
     * @throws IllegalArgumentException if any of the following conditions is {@code true}:
     *         <ul>
     *         <li>{@code proxy} is not {@linkplain Proxy#isProxyClass(Class)
     *             a proxy instance}; or</li>
     *         <li>the given {@code method} is not a default method declared
     *             in a proxy interface of the proxy class and not inherited from
     *             any of its superinterfaces; or</li>
     *         <li>the given {@code method} is overridden directly or indirectly by
     *             the proxy interfaces and the method reference to the named
     *             method never resolves to the given {@code method}; or</li>
     *         <li>the length of the given {@code args} array does not match the
     *             number of parameters of the method to be invoked; or</li>
     *         <li>any of the {@code args} elements fails the unboxing
     *             conversion if the corresponding method parameter type is
     *             a primitive type; or if, after possible unboxing, any of the
     *             {@code args} elements cannot be assigned to the corresponding
     *             method parameter type.</li>
     *         </ul>
     * @throws IllegalAccessException if the declaring class of the specified
     *         default method is inaccessible to the caller class
     * @throws NullPointerException if {@code proxy} or {@code method} is {@code null}
     * @throws Throwable anything thrown by the default method

     * @since 16
     * @jvms 5.4.3. Method Resolution
     */
    @CallerSensitive
    public static Object invokeDefault(Object proxy, Method method, Object... args)
            throws Throwable 

```
2) Proxy class spec diff:

```
--- a/src/java.base/share/classes/java/lang/reflect/Proxy.java
+++ b/src/java.base/share/classes/java/lang/reflect/Proxy.java
@@ -144,6 +151,12 @@ 
 * InvocationHandler#invoke invoke} method as described in the
 * documentation for that method.
 *  * * <li>A proxy interface may define a default method or inherit
+ * <li>A proxy interface may define a default method or inherit
+ * a default method from its superinterface directly or indirectly.
+ * An invocation handler can invoke a default method of a proxy interface
+ * by calling {@link InvocationHandler#invokeDefault(Object, Method, Object...)
+ * InvocationHandler::invokeDefault}.
+ *
 * <li>An invocation of the {@code hashCode},
 * {@code equals}, or {@code toString} methods declared in
 * {@code java.lang.Object} on a proxy instance will be encoded and
@@ -172,9 +185,8 @@ import static java.lang.module.ModuleDescriptor.Modifier.SYNTHETIC;
 *     packages:
 * <ol type="a">
 * <li>if all the proxy interfaces are <em>public</em>, then the proxy class is
- *     <em>public</em> in a package exported by the
- *     {@linkplain ClassLoader#getUnnamedModule() unnamed module} of the specified
- *     loader. The name of the package is unspecified.</li>
+ *     <em>public</em> in an unconditionally exported but non-open package.
+ *     The name of the package and the module are unspecified.</li>
 *
 * <li>if at least one of all the proxy interfaces is <em>non-public</em>, then
 *     the proxy class is <em>non-public</em> in the package and module of the
```

A proxy class, used to be defined in an unnamed module i.e. in a exported 
and open package, is changed to be defined in an unconditionally
exported but non-open package.  The module is unspecified. 
Programs that assume it to be open unconditionally may be affected and 
cannot do deep reflection on such proxy classes.

Defining a proxy class in an unconditionally exported but non-open package 
will strengthen encapsulation such that private members of the proxy class 
cannot be accessed via core reflection.  This only applies to the case if all 
the proxy interfaces are public and in a package that is exported or open. 
This spec change is for defense-in-depth as the proxy support for default 
method requires to access the `Lookup` of a proxy class for the proxy 
machinery so that it can invoke the default method on behalf of the proxy 
class.

Implementation-specific: one dynamic module is created for each class
loader that defines proxies.  The implementation changes the dynamic 
module to contain another package (same name as the module) that is 
unconditionally exported and is qualifiedly opened to `java.base`.  

There is no change to the package and module of the proxy class for
the following cases:

   - if at least one proxy interface is non-public, then the proxy class is defined
     in the package and module of the non-public interfaces.
   - if at least one proxy is in a package that is non-exported and non-open,
     if all proxy interfaces are public, then the proxy class is defined in
     a non-exported, non-open package of a dynamic module.


Comments
Moving to Approved.
25-11-2020

Yes if an invocation handler prepares for the change of the default method resolved for a proxy interface (A::m or B::m in this example). The `Method` object passed to `invokeDefaultMethod` will need to be dynamically looked up at runtime for example if a proxy implements C, B, it should call `invokeDefaultMethod` with B.class.getMethod("m") but neither A.class.getMethod("m") nor the method parameter passed to `InvocationHandler::invoke`. Here is the updated example about the binary compatibility: ``` * For example, interface {@code A} and {@code B} both declare a default * implementation of method {@code m}. Interface {@code C} extends {@code A} * and it inherits the default method {@code m} from its superinterface {@code A}. * * <blockquote><pre>{@code * interface A { * default T m(A a) { return t1; } * } * interface B { * default T m(A a) { return t2; } * } * interface C extends A {} * }</pre></blockquote> * * The following creates a proxy instance that implements {@code A} * and invokes the default method {@code A::m}. * * <blockquote><pre>{@code * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class }, * (o, m, params) -> { * assert m.getDeclaringClass() == A.class && m.isDefault(); * return InvocationHandler.invokeDefaultMethod(o, m, params); * }); * }</pre></blockquote> * * If a proxy instance implements both {@code A} and {@code B}, both * of which provide the default implementation of the same method {@code m}, * the invocation handler can dispatch the method invocation to * {@code A::m} or {@code B::m} via {@code InvocationHandler::invokeDefaultMethod}. * For example, the following code delegates the method invocation * to {@code B::m}. * * <blockquote><pre>{@code * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class, B.class }, * (o, m, params) -> { * // delegate to invoking B::m * Method selectedMethod = B.class.getMethod(m.getName(), m.getParameterTypes()); * return InvocationHandler.invokeDefaultMethod(o, selectedMethod, params); * }); * }</pre></blockquote> * * If a proxy instance implements {@code C} that inherits the default * method {@code m} from its superinterface {@code A}, then * the interface method invocation on {@code "m"} is dispatched to * the invocation handler's {@link #invoke(Object, Method, Object[]) invoke} * method with the {@code Method} object argument representing the * default method {@code A::m}. * * <blockquote><pre>{@code * Object c = Proxy.newProxyInstance(loader, new Class<?>[] { C.class }, * (o, m, params) -> { * assert m.isDefault(); * return InvocationHandler.invokeDefaultMethod(o, m, params); * }); * }</pre></blockquote> * * The invocation of method {@code "m"} on the {@code c} object will behave * as if the invocation of {@code C.super::m} which is resolved to invoking * {@code A::m}. * <p> * Note that if {@code C} is modified to override {@code m} with its implementation, * then the invocation of method {@code "m"} on the {@code c} object will * behave differently and result in invoking {@code C::m} instead of {@code A::m}. * If {@code C} is modified as below, {@code C.super::m} is resolved to {@code C::m}. * * <blockquote><pre>{@code * interface C extends A { * default T m(A a) { return t3; } * } * }</pre></blockquote> * * If an invocation handler dispatches the method invocation by calling * {@code InvocationHandler::invokeDefaultMethod} with the {@code Method} object * representing {@code A::m}, then the invocation on {@code "m"} with this proxy * instance will result in an {@code IllegalArgumentException} because {@code C} * overrides the implementation of the same method and {@code A::m} is * not accessible by a proxy instance. * * <blockquote><pre>{@code * Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { C.class }, * (o, m, params) -> { * // IllegalArgumentException thrown as {@code A::m} is not a method * // inherited from its proxy interface C * return InvocationHandler.invokeDefaultMethod(o, A.class.getMethod("m"), params); * }); * }</pre></blockquote> ```
09-10-2020

Hi [~mchung], unfortunately I had a typo in my earlier comment. I meant to ask: "Does the current spec handle a case like interface A declares a default method m, interface B extends A and separately interface C declare a default method m. Would invokeDefaultMethod on B::m do-what-is-wanted by call the default method inherited from A? In text: interface A { default void m() {...} } interface B extends A { // inherit m } interface C { default void m() {...} } Perhaps this arrangement is changed in the next release of the library where B would declare its own default method. Another situation would be the default method starts out in B in the first version of a library and get hoisted up to A in a second version of the library. If there a call to invokeDefaultMethod that would work across both versions of the library?
08-10-2020

Joe, thanks for the feedback. > should this functionality be housed in the InvocationHandler interface rather than the Proxy class? If I understand the intended usage of the new method, it should only be done in an InvocationHandler. The primary usage of this new method is InvocationHandler. I agree that InvocationHandler is a more appropriate home for it. > Does the current spec handle a case like interface A declares a default method m, interface B extends A and separately interface B declare a default method m. Yes > Would invokeDefaultMethod on A::m do-what-is-wanted by call the default method inherited from A? Indirectly, this is also a question if the operation that are binary compatible with respect to moving around default methods and behaviorally compatible with the semantics of the new method. If a proxy instance implements both A and B, `invokeDefaultMethod(o, A.class.getMethod("m"), params)` will invoke A::m. If a proxy instance implements B only, `invokeDefaultMethod(o, A.class.getMethod("m"), params)` will get IAE thrown. This is consistent with a concrete class implements B that can only call B.super::m but not A::m. > Any special cases for hidden classes? No special case for hidden classes needed. A proxy interface must not be hidden as specified in `Proxy::newProxyInstance` as follows: ``` IllegalArgumentException will be thrown if any of the following restrictions is violated: - All of Class objects in the given interfaces array must represent non-hidden interfaces, not classes or primitive types. ```
08-10-2020

Moving to Provisional. I have not read the entire review thread so perhaps this point was already considered the rejected: should this functionality be housed in the InvocationHandler interface rather than the Proxy class? If I understand the intended usage of the new method, it should only be done in an InvocationHandler. Does the current spec handle a case like interface A declares a default method m, interface B extends A and separately interface B declare a default method m. Would invokeDefaultMethod on A::m do-what-is-wanted by call the default method inherited from A? Indirectly, this is also a question if the operation that are binary compatible with respect to moving around default methods and behaviorally compatible with the semantics of the new method. Any special cases for hidden classes?
06-10-2020