Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ private boolean requiresCleanup() {
// single, array-typed parameter at the end for variable arity methods
mh = mh.asFixedArity();

// Check instance is not null
if (!instanceLookup && !isStaticMethod) {
Class<?> instanceType = mh.type().parameterType(0);
Class<?> returnType = mh.type().returnType();
MethodHandle checkInstanceNotNull = MethodHandles.insertArguments(MethodHandleUtils.CHECK_INSTANCE_NOT_NULL, 0,
reflectionMethod);
checkInstanceNotNull = checkInstanceNotNull
.asType(checkInstanceNotNull.type().changeParameterType(0, instanceType));
MethodHandle npeCatch = MethodHandles.throwException(returnType, NullPointerException.class);
npeCatch = MethodHandles.collectArguments(npeCatch, 1, checkInstanceNotNull);
mh = MethodHandles.catchException(mh, NullPointerException.class, npeCatch);
}

// instance transformer
if (instanceTransformer != null && !isStaticMethod) {
MethodHandle instanceTransformerMethod = MethodHandleUtils.createMethodHandleFromTransformer(reflectionMethod,
Expand All @@ -155,8 +168,10 @@ private boolean requiresCleanup() {

// argument transformers
// backwards iteration for correct construction of the resulting parameter list
Class<?>[] transformerArgTypes = new Class<?>[argTransformers.length];
for (int i = argTransformers.length - 1; i >= 0; i--) {
if (argTransformers[i] == null) {
transformerArgTypes[i] = reflectionMethod.getParameterTypes()[i];
continue;
}
int position = instanceArguments + i;
Expand All @@ -172,6 +187,7 @@ private boolean requiresCleanup() {
// internal error, this should not pass validation
throw InvokerLogger.LOG.invalidTransformerMethod("argument", argTransformers[i]);
}
transformerArgTypes[i] = argTransformerMethod.type().parameterType(0);
}

// return type transformer
Expand Down Expand Up @@ -323,6 +339,61 @@ private boolean requiresCleanup() {
mh = MethodHandles.foldArguments(mh, MethodHandleUtils.CLEANUP_ACTIONS_CTOR);
}

Class<?>[] expectedTypes = new Class<?>[transformerArgTypes.length];
for (int i = 0; i < transformerArgTypes.length; i++) {
if (argLookup[i]) {
expectedTypes[i] = null;
} else {
expectedTypes[i] = transformerArgTypes[i];
}
}

if (reflectionMethod.getParameterCount() > 0) {
// Catch NullPointerException and check whether it's caused by any arguments being null:
Class<?> instanceType = mh.type().parameterType(0);
MethodHandle checkArgumentsNotNull = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARGUMENTS_NOT_NULL, 0,
reflectionMethod, expectedTypes);
checkArgumentsNotNull = MethodHandles.dropArguments(checkArgumentsNotNull, 0, instanceType);
MethodHandle npeCatch = MethodHandles.throwException(mh.type().returnType(), NullPointerException.class);
npeCatch = MethodHandles.collectArguments(npeCatch, 1, checkArgumentsNotNull);
mh = MethodHandles.catchException(mh, NullPointerException.class, npeCatch);

// Catch IllegalArgumentException and check whether it's caused by the args array being too short
MethodHandle checkArgCountAtLeast = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARG_COUNT_AT_LEAST, 0,
reflectionMethod, reflectionMethod.getParameterCount());
checkArgCountAtLeast = MethodHandles.dropArguments(checkArgCountAtLeast, 0, instanceType);
MethodHandle iaeCatch = MethodHandles.throwException(mh.type().returnType(), IllegalArgumentException.class);
iaeCatch = MethodHandles.collectArguments(iaeCatch, 1, checkArgCountAtLeast);
mh = MethodHandles.catchException(mh, IllegalArgumentException.class, iaeCatch);
}

if ((!isStaticMethod && !instanceLookup) || reflectionMethod.getParameterCount() > 0) {
// Catch ClassCastException and check whether it's caused by either the instance or the arguments being the wrong type
Class<?> instanceType = mh.type().parameterType(0);
MethodHandle checkTypes = null;
if (reflectionMethod.getParameterCount() > 0) {
checkTypes = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARGUMENTS_HAVE_CORRECT_TYPE, 0,
reflectionMethod, expectedTypes);
checkTypes = MethodHandles.dropArguments(checkTypes, 0, Object.class);
}

if (!isStaticMethod && !instanceLookup) {
MethodHandle checkInstanceType = MethodHandles.insertArguments(MethodHandleUtils.CHECK_INSTANCE_HAS_TYPE, 0,
reflectionMethod, instanceType);
if (checkTypes == null) {
checkTypes = checkInstanceType;
} else {
checkTypes = MethodHandles.foldArguments(checkTypes, checkInstanceType);
}
}

MethodHandle cceCatch = MethodHandles.throwException(mh.type().returnType(), ClassCastException.class);
cceCatch = MethodHandles.collectArguments(cceCatch, 1, checkTypes);

mh = mh.asType(mh.type().changeParameterType(0, Object.class)); // Defer the casting the instance to its expected type until we're inside the ClassCastException try block
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it correctly that this is needed strictly because the adapter method handle (cceCatch) must have the exact same type as the target method handle? I am a bit surprised this doesn't cause issues when invoking the method handle for the original (user defined) method.

Also, a nitpicker note - the comment has a superflous the in it :)

Copy link
Copy Markdown
Contributor Author

@Azquelt Azquelt Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it correctly that this is needed strictly because the adapter method handle (cceCatch) must have the exact same type as the target method handle?

Yes, this is a requirement of MethodHandles.catchException.

I am a bit surprised this doesn't cause issues when invoking the method handle for the original (user defined) method.

I think this works because MethodHandle.invoke will attempt to adapt the arguments to the types expected by the method handle, in the same way that MethodHandle.asType would.

Without this line which changes the parameter type, the final MethodHandle will have:

  • return type the same as the user's method
  • first parameter type equal to the declaring class of the user's method (or Object for static methods)
  • second parameter of Object[]

This will cast the instance to the expected type when invoke is called, so when we're trying to catch and handle the ClassCastException ourselves, we need to ensure that the first parameter has type Object and that the cast to the expected type is done within our catch block.

Otherwise, it would also be possible satisfy the requirement that parameter types match exactly by adapting the cceCatch method handle to match the user's method:

            MethodHandle cceCatch = MethodHandles.throwException(mh.type().returnType(), ClassCastException.class);
            cceCatch = MethodHandles.collectArguments(cceCatch, 1, checkTypes);
            cceCatch = cceCatch.asType(cceCatch.type().changeParameterType(1, mh.type().parameterType(0)));

            mh = MethodHandles.catchException(mh, ClassCastException.class, cceCatch);

However, this results in the invoke call doing the cast which means we can't catch the exception.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this works because MethodHandle.invoke will attempt to adapt the arguments to the types expected by the method handle, in the same way that MethodHandle.asType would.

Ah, right, that's it.
I keep thinking along the lines of invokeExact which is what probably wouldn't work.

Thanks for detailed explanation.

mh = MethodHandles.catchException(mh, ClassCastException.class, cceCatch);
}

// create an inner invoker and pass it to wrapper
if (invocationWrapper != null) {
InvokerImpl<?, ?> invoker = new InvokerImpl<>(mh);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.jboss.weld.invokable;

import java.lang.reflect.Method;

import jakarta.enterprise.invoke.Invoker;

import org.jboss.weld.exceptions.IllegalArgumentException;
import org.jboss.weld.logging.InvokerLogger;

/**
* Utility methods for checking the arguments and instances being used by an {@link Invoker}.
* <p>
* Handles to these methods are obtained via {@link MethodHandleUtils}.
*/
public class InvokerValidationUtils {

private InvokerValidationUtils() {
}

/**
* Validate that an instance being called by an {@link Invoker} has the expected type or is {@code null}.
*
* @param invokerMethod the method being invoked
* @param type the expected type of the instance
* @param instance the instance the method is being invoked on
* @throws ClassCastException if {@code instance} is not {@code null} and not of the expected type
*/
static void instanceHasType(Method invokerMethod, Class<?> type, Object instance) {
if (instance != null && !type.isInstance(instance)) {
throw InvokerLogger.LOG.wrongInstanceType(invokerMethod, instance.getClass(), type);
}
}

/**
* Validate that an instance being called by an {@link Invoker} is not {@code null}.
*
* @param invokerMethod the method being invoked
* @param instance the instance to check
* @throws NullPointerException if {@code instance} is {@code null}
*/
static void instanceNotNull(Method invokerMethod, Object instance) {
if (instance == null) {
throw InvokerLogger.LOG.nullInstance(invokerMethod);
}
}

/**
* Validate that if arguments are required then the arguments array is not {@code null} and any primitive arguments are not
* {@code null}.
*
* @param invokerMethod the method being invoked
* @param expectedTypes the expected type of each argument, a {@code null} entry indicates that the type of that parameter
* should not be checked
* @param args the array of arguments
* @throws NullPointerException if arguments are required and {@code args} is {@code null} or a primitive argument is
* {@code null}
*/
static void argumentsNotNull(Method invokerMethod, Class<?>[] expectedTypes, Object[] args) {
if (invokerMethod.getParameterCount() == 0) {
return; // If there are no arguments expected then there's nothing to check
}
if (args == null) {
throw InvokerLogger.LOG.nullArgumentArray(invokerMethod);
}
for (int i = 0; i < expectedTypes.length; i++) {
Class<?> expectedType = expectedTypes[i];
Object arg = args[i];
if (expectedType != null && arg == null && expectedType.isPrimitive()) {
throw InvokerLogger.LOG.nullPrimitiveArgument(invokerMethod, i + 1);
}
}
}

/**
* Validate that an array of arguments for an {@link Invoker} has at least an expected number of elements
*
* @param invokerMethod the method being invoked
* @param requiredArgs the expected number of arguments
* @param args the array of arguments
* @return {@code args}
* @throws IllegalArgumentException if the length of {@code args} is less than {@code requiredArgs}
*/
static void argCountAtLeast(Method invokerMethod, int requiredArgs, Object[] args) {
int actualArgs = args == null ? 0 : args.length;
if (actualArgs < requiredArgs) {
throw InvokerLogger.LOG.notEnoughArguments(invokerMethod, requiredArgs, actualArgs);
}
}

/**
* Validate that each of the arguments being passed passed to a method by an {@link Invoker} has the correct type.
* <p>
* For each pair if type and argument from {@code expectedTypes} and {@code args}:
* <ul>
* <li>if the expected type is {@code null}, no validation is done
* <li>if the expected type is a primitive type, check that the argument is not {@code null} and can be converted to that
* primitive type using boxing and primitive widening conversions
* <li>otherwise, check that the argument is an instance of the expected type
* </ul>
*
* @param invokerMethod the method being invoked
* @param expectedTypes an array of the expected type of each argument. May contain {@code null} to indicate that that
* argument should not be validated.
* @param args the array of values being passed as arguments
* @throws ClassCastException if any of the arguments are not valid for their expected type
* @throws NullPointerException if an argument for a primitive-typed parameter is not null
*/
static void argumentsHaveCorrectType(Method invokerMethod, Class<?>[] expectedTypes, Object[] args) {
if (args == null) {
// Not expected since we're in the catch block of ClassCastException, but guarantees we can safely access args
throw InvokerLogger.LOG.nullArgumentArray(invokerMethod);
}
for (int i = 0; i < expectedTypes.length; i++) {
Class<?> expectedType = expectedTypes[i];
Object arg = args[i];
if (expectedType != null) {
int pos = i + 1; // 1-indexed argument position
if (expectedType.isPrimitive()) {
if (arg == null) {
// Not expected since we're in the catch block of ClassCastException, but guarantees we can safely call methods on arg
throw InvokerLogger.LOG.nullPrimitiveArgument(invokerMethod, pos);
}
if (!primitiveConversionPermitted(expectedType, arg.getClass())) {
throw InvokerLogger.LOG.wrongArgumentType(invokerMethod, pos, arg.getClass(), expectedType);
}
} else {
if (arg != null && !expectedType.isInstance(arg)) {
throw InvokerLogger.LOG.wrongArgumentType(invokerMethod, pos, arg.getClass(), expectedType);
}
}
}
}
}

/**
* Validate whether a reference type can be converted to a primitive type via an unboxing and primitive widening conversion.
*
* @param primitive the target primitive type
* @param actual the reference type to test
* @return {@code true} if {@code actual} can be converted to {@code primitive} via an unboxing and primitive widening
* conversion, otherwise {@code false}
*/
private static boolean primitiveConversionPermitted(Class<?> primitive, Class<? extends Object> actual) {
if (primitive == Integer.TYPE) {
return actual == Integer.class
|| actual == Character.class
|| actual == Short.class
|| actual == Byte.class;
} else if (primitive == Long.TYPE) {
return actual == Long.class
|| actual == Integer.class
|| actual == Character.class
|| actual == Short.class
|| actual == Byte.class;
} else if (primitive == Boolean.TYPE) {
return actual == Boolean.class;
} else if (primitive == Double.TYPE) {
return actual == Double.class
|| actual == Float.class
|| actual == Long.class
|| actual == Integer.class
|| actual == Character.class
|| actual == Short.class
|| actual == Byte.class;
} else if (primitive == Float.TYPE) {
return actual == Float.class
|| actual == Long.class
|| actual == Integer.class
|| actual == Character.class
|| actual == Short.class
|| actual == Byte.class;
} else if (primitive == Short.TYPE) {
return actual == Short.class
|| actual == Byte.class;
} else if (primitive == Character.TYPE) {
return actual == Character.class;
} else if (primitive == Byte.TYPE) {
return actual == Byte.class;
}
throw new RuntimeException("Unhandled primitive type: " + primitive);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ private MethodHandleUtils() {
static final MethodHandle REPLACE_PRIMITIVE_LOOKUP_NULLS;
static final MethodHandle THROW_VALUE_CARRYING_EXCEPTION;
static final MethodHandle TRIM_ARRAY_TO_SIZE;
static final MethodHandle CHECK_INSTANCE_HAS_TYPE;
static final MethodHandle CHECK_INSTANCE_NOT_NULL;
static final MethodHandle CHECK_ARG_COUNT_AT_LEAST;
static final MethodHandle CHECK_ARGUMENTS_HAVE_CORRECT_TYPE;
static final MethodHandle CHECK_ARGUMENTS_NOT_NULL;

static {
try {
Expand All @@ -45,6 +50,16 @@ private MethodHandleUtils() {
"throwReturnValue", Object.class));
TRIM_ARRAY_TO_SIZE = createMethodHandle(ArrayUtils.class.getDeclaredMethod(
"trimArrayToSize", Object[].class, int.class));
CHECK_INSTANCE_HAS_TYPE = createMethodHandle(
InvokerValidationUtils.class.getDeclaredMethod("instanceHasType", Method.class, Class.class, Object.class));
CHECK_INSTANCE_NOT_NULL = createMethodHandle(
InvokerValidationUtils.class.getDeclaredMethod("instanceNotNull", Method.class, Object.class));
CHECK_ARG_COUNT_AT_LEAST = createMethodHandle(
InvokerValidationUtils.class.getDeclaredMethod("argCountAtLeast", Method.class, int.class, Object[].class));
CHECK_ARGUMENTS_HAVE_CORRECT_TYPE = createMethodHandle(InvokerValidationUtils.class
.getDeclaredMethod("argumentsHaveCorrectType", Method.class, Class[].class, Object[].class));
CHECK_ARGUMENTS_NOT_NULL = createMethodHandle(InvokerValidationUtils.class.getDeclaredMethod("argumentsNotNull",
Method.class, Class[].class, Object[].class));
} catch (NoSuchMethodException e) {
// should never happen
throw new IllegalStateException("Unable to locate Weld internal helper method", e);
Expand Down
19 changes: 19 additions & 0 deletions impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.jboss.logging.Logger;
import org.jboss.logging.annotations.Cause;
import org.jboss.logging.annotations.Message;
import org.jboss.logging.annotations.Message.Format;
import org.jboss.logging.annotations.MessageLogger;
import org.jboss.weld.exceptions.DeploymentException;
import org.jboss.weld.exceptions.IllegalArgumentException;
Expand Down Expand Up @@ -65,4 +66,22 @@ public interface InvokerLogger extends WeldLogger {

@Message(id = 2014, value = "Invocation wrapper has unexpected parameters: {0} \nExpected param types are: {1}, Object[], Invoker.class", format = Message.Format.MESSAGE_FORMAT)
DeploymentException wrapperUnexpectedParams(Object transformerMetadata, Object clazz);

@Message(id = 2015, value = "Cannot invoke {0} because the instance passed to the Invoker was null", format = Format.MESSAGE_FORMAT)
NullPointerException nullInstance(Object method);

@Message(id = 2016, value = "Cannot invoke {0} because the instance passed to the Invoker has type {1} which cannot be cast to {2}", format = Format.MESSAGE_FORMAT)
ClassCastException wrongInstanceType(Object method, Class<?> actualType, Class<?> expectedType);

@Message(id = 2017, value = "Cannot invoke {0} because {1} arguments were expected but only {2} were provided", format = Format.MESSAGE_FORMAT)
IllegalArgumentException notEnoughArguments(Object method, int expectedCount, int actualCount);

@Message(id = 2018, value = "Cannot invoke {0} because argument {1} has type {2} which cannot be cast to {3}", format = Format.MESSAGE_FORMAT)
ClassCastException wrongArgumentType(Object method, int pos, Class<?> actualType, Class<?> expectedType);

@Message(id = 2019, value = "Cannot invoke {0} because parameter {1} is a primitive type but the argument is null", format = Format.MESSAGE_FORMAT)
NullPointerException nullPrimitiveArgument(Object method, int pos);

@Message(id = 2020, value = "Cannot invoke {0} because the args parameter is null and arguments are required", format = Format.MESSAGE_FORMAT)
NullPointerException nullArgumentArray(Object method);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.jboss.weld.tests.invokable.exceptions;

import jakarta.enterprise.context.Dependent;

@Dependent
public class ExceptionTestBean {

public String ping(String s, int i) {
return s + i;
}

public static String staticPing(String s, int i) {
return s + i;
}

public void voidPing(String s, int i) {
String result = s + i;
}

public String noargPing() {
return "42";
}
}
Loading
Loading