diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java
index 6fa666ed0a059..23ff1bda6ba40 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java
@@ -8,12 +8,14 @@
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
+import java.lang.reflect.WildcardType;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
@@ -71,6 +73,9 @@
import io.quarkus.runtime.StartupTask;
import io.quarkus.runtime.annotations.IgnoreProperty;
import io.quarkus.runtime.annotations.RelaxedValidation;
+import io.quarkus.runtime.types.GenericArrayTypeImpl;
+import io.quarkus.runtime.types.ParameterizedTypeImpl;
+import io.quarkus.runtime.types.WildcardTypeImpl;
/**
* A class that can be used to record invocations to bytecode so they can be replayed later. This is done through the
@@ -769,6 +774,68 @@ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle ar
}
};
}
+ } else if (param instanceof ParameterizedType parameterized) {
+ DeferredParameter raw = loadObjectInstance(parameterized.getRawType(), existing,
+ java.lang.reflect.Type.class, relaxedValidation);
+ DeferredParameter args = loadObjectInstance(parameterized.getActualTypeArguments(), existing,
+ java.lang.reflect.Type[].class, relaxedValidation);
+ DeferredParameter owner = loadObjectInstance(parameterized.getOwnerType(), existing,
+ java.lang.reflect.Type.class, relaxedValidation);
+ return new DeferredParameter() {
+ @Override
+ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
+ return method.newInstance(ofConstructor(ParameterizedTypeImpl.class, java.lang.reflect.Type.class,
+ java.lang.reflect.Type[].class, java.lang.reflect.Type.class),
+ context.loadDeferred(raw), context.loadDeferred(args), context.loadDeferred(owner));
+ }
+ };
+ } else if (param instanceof GenericArrayType array) {
+ DeferredParameter res = loadObjectInstance(array.getGenericComponentType(), existing,
+ java.lang.reflect.Type.class, relaxedValidation);
+ return new DeferredParameter() {
+ @Override
+ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
+ return method.newInstance(ofConstructor(GenericArrayTypeImpl.class, java.lang.reflect.Type.class),
+ context.loadDeferred(res));
+ }
+ };
+ } else if (param instanceof WildcardType wildcard) {
+ java.lang.reflect.Type[] upperBound = wildcard.getUpperBounds();
+ java.lang.reflect.Type[] lowerBound = wildcard.getLowerBounds();
+ if (lowerBound.length == 0 && upperBound.length == 1 && Object.class.equals(upperBound[0])) {
+ // unbounded
+ return new DeferredParameter() {
+ @Override
+ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
+ return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "defaultInstance",
+ WildcardType.class));
+ }
+ };
+ } else if (lowerBound.length == 0 && upperBound.length == 1) {
+ // upper bound
+ DeferredParameter res = loadObjectInstance(upperBound[0], existing,
+ java.lang.reflect.Type.class, relaxedValidation);
+ return new DeferredParameter() {
+ @Override
+ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
+ return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withUpperBound",
+ WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res));
+ }
+ };
+ } else if (lowerBound.length == 1) {
+ // lower bound
+ DeferredParameter res = loadObjectInstance(lowerBound[0], existing,
+ java.lang.reflect.Type.class, relaxedValidation);
+ return new DeferredParameter() {
+ @Override
+ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
+ return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withLowerBound",
+ WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res));
+ }
+ };
+ } else {
+ throw new UnsupportedOperationException("Unsupported wildcard type: " + wildcard);
+ }
} else if (expectedType == boolean.class || expectedType == Boolean.class || param instanceof Boolean) {
return new DeferredParameter() {
@Override
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java b/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java
new file mode 100644
index 0000000000000..3e79d19e6f38d
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java
@@ -0,0 +1,233 @@
+package io.quarkus.deployment.types;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import io.quarkus.runtime.types.GenericArrayTypeImpl;
+import io.quarkus.runtime.types.ParameterizedTypeImpl;
+import io.quarkus.runtime.types.WildcardTypeImpl;
+
+/**
+ * Creates a {@link Type} by parsing the given string according to the following grammar:
+ *
+ *
+ * Type -> VoidType | PrimitiveType | ReferenceType
+ * VoidType -> 'void'
+ * PrimitiveType -> 'boolean' | 'byte' | 'short' | 'int'
+ * | 'long' | 'float' | 'double' | 'char'
+ * ReferenceType -> PrimitiveType ('[' ']')+
+ * | ClassType ('<' TypeArgument (',' TypeArgument)* '>')? ('[' ']')*
+ * ClassType -> FULLY_QUALIFIED_NAME
+ * TypeArgument -> ReferenceType | WildcardType
+ * WildcardType -> '?' | '?' ('extends' | 'super') ReferenceType
+ *
+ *
+ * Notice that the resulting type never contains type variables, only "proper" types.
+ * Also notice that the grammar above does not support all kinds of nested types;
+ * it should be possible to add that later, if there's an actual need.
+ *
+ * Types produced by this parser can be transferred from build time to runtime
+ * via the recorder mechanism.
+ */
+public class TypeParser {
+ public static Type parse(String str) {
+ return new TypeParser(str).parse();
+ }
+
+ private final String str;
+
+ private int pos = 0;
+
+ private TypeParser(String str) {
+ this.str = Objects.requireNonNull(str);
+ }
+
+ private Type parse() {
+ Type result;
+
+ String token = nextToken();
+ if (token.isEmpty()) {
+ throw unexpected(token);
+ } else if (token.equals("void")) {
+ result = void.class;
+ } else if (isPrimitiveType(token) && peekToken().isEmpty()) {
+ result = parsePrimitiveType(token);
+ } else {
+ result = parseReferenceType(token);
+ }
+
+ expect("");
+ return result;
+ }
+
+ private Type parseReferenceType(String token) {
+ if (isPrimitiveType(token)) {
+ Type primitive = parsePrimitiveType(token);
+ return parseArrayType(primitive);
+ } else if (isClassType(token)) {
+ Type result = parseClassType(token);
+ if (peekToken().equals("<")) {
+ expect("<");
+ List typeArguments = new ArrayList<>();
+ typeArguments.add(parseTypeArgument());
+ while (peekToken().equals(",")) {
+ expect(",");
+ typeArguments.add(parseTypeArgument());
+ }
+ expect(">");
+ result = new ParameterizedTypeImpl(result, typeArguments.toArray(Type[]::new));
+ }
+ if (peekToken().equals("[")) {
+ return parseArrayType(result);
+ }
+ return result;
+ } else {
+ throw unexpected(token);
+ }
+ }
+
+ private Type parseArrayType(Type elementType) {
+ expect("[");
+ expect("]");
+ int dimensions = 1;
+ while (peekToken().equals("[")) {
+ expect("[");
+ expect("]");
+ dimensions++;
+ }
+
+ if (elementType instanceof Class> clazz) {
+ return parseClassType("[".repeat(dimensions)
+ + (clazz.isPrimitive() ? clazz.descriptorString() : "L" + clazz.getName() + ";"));
+ } else {
+ Type result = elementType;
+ for (int i = 0; i < dimensions; i++) {
+ result = new GenericArrayTypeImpl(result);
+ }
+ return result;
+ }
+ }
+
+ private Type parseTypeArgument() {
+ String token = nextToken();
+ if (token.equals("?")) {
+ if (peekToken().equals("extends")) {
+ expect("extends");
+ Type bound = parseReferenceType(nextToken());
+ return WildcardTypeImpl.withUpperBound(bound);
+ } else if (peekToken().equals("super")) {
+ expect("super");
+ Type bound = parseReferenceType(nextToken());
+ return WildcardTypeImpl.withLowerBound(bound);
+ } else {
+ return WildcardTypeImpl.defaultInstance();
+ }
+ } else {
+ return parseReferenceType(token);
+ }
+ }
+
+ private boolean isPrimitiveType(String token) {
+ return token.equals("boolean")
+ || token.equals("byte")
+ || token.equals("short")
+ || token.equals("int")
+ || token.equals("long")
+ || token.equals("float")
+ || token.equals("double")
+ || token.equals("char");
+ }
+
+ private Type parsePrimitiveType(String token) {
+ return switch (token) {
+ case "boolean" -> boolean.class;
+ case "byte" -> byte.class;
+ case "short" -> short.class;
+ case "int" -> int.class;
+ case "long" -> long.class;
+ case "float" -> float.class;
+ case "double" -> double.class;
+ case "char" -> char.class;
+ default -> throw unexpected(token);
+ };
+ }
+
+ private boolean isClassType(String token) {
+ return !token.isEmpty() && Character.isJavaIdentifierStart(token.charAt(0));
+ }
+
+ private Type parseClassType(String token) {
+ try {
+ return Class.forName(token, true, Thread.currentThread().getContextClassLoader());
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unknown class: " + token, e);
+ }
+ }
+
+ // ---
+
+ private void expect(String expected) {
+ String token = nextToken();
+ if (!expected.equals(token)) {
+ throw unexpected(token);
+ }
+ }
+
+ private IllegalArgumentException unexpected(String token) {
+ if (token.isEmpty()) {
+ throw new IllegalArgumentException("Unexpected end of input: " + str);
+ }
+ return new IllegalArgumentException("Unexpected token '" + token + "' at position " + (pos - token.length())
+ + ": " + str);
+ }
+
+ private String peekToken() {
+ // skip whitespace
+ while (pos < str.length() && Character.isWhitespace(str.charAt(pos))) {
+ pos++;
+ }
+
+ // end of input
+ if (pos == str.length()) {
+ return "";
+ }
+
+ int pos = this.pos;
+
+ // current char is a token on its own
+ if (isSpecial(str.charAt(pos))) {
+ return str.substring(pos, pos + 1);
+ }
+
+ // token is a keyword or fully qualified name
+ int begin = pos;
+ while (pos < str.length() && Character.isJavaIdentifierStart(str.charAt(pos))) {
+ do {
+ pos++;
+ } while (pos < str.length() && Character.isJavaIdentifierPart(str.charAt(pos)));
+
+ if (pos < str.length() && str.charAt(pos) == '.') {
+ pos++;
+ } else {
+ return str.substring(begin, pos);
+ }
+ }
+
+ if (pos == str.length()) {
+ throw new IllegalArgumentException("Unexpected end of input: " + str);
+ }
+ throw new IllegalArgumentException("Unexpected character '" + str.charAt(pos) + "' at position " + pos + ": " + str);
+ }
+
+ private String nextToken() {
+ String result = peekToken();
+ pos += result.length();
+ return result;
+ }
+
+ private boolean isSpecial(char c) {
+ return c == ',' || c == '?' || c == '<' || c == '>' || c == '[' || c == ']';
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java b/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java
new file mode 100644
index 0000000000000..b335c674d8889
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java
@@ -0,0 +1,157 @@
+package io.quarkus.deployment.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.enterprise.util.TypeLiteral;
+
+import org.junit.jupiter.api.Test;
+
+public class TypeParserTest {
+ @Test
+ public void testVoid() {
+ assertCorrect("void", void.class);
+ assertCorrect(" void", void.class);
+ assertCorrect("void ", void.class);
+ assertCorrect(" void ", void.class);
+ }
+
+ @Test
+ public void testPrimitive() {
+ assertCorrect("boolean", boolean.class);
+ assertCorrect(" byte", byte.class);
+ assertCorrect("short ", short.class);
+ assertCorrect(" int ", int.class);
+ assertCorrect("\tlong", long.class);
+ assertCorrect("float\t", float.class);
+ assertCorrect("\tdouble\t", double.class);
+ assertCorrect(" \n char \n ", char.class);
+ }
+
+ @Test
+ public void testPrimitiveArray() {
+ assertCorrect("boolean[]", boolean[].class);
+ assertCorrect("byte [][]", byte[][].class);
+ assertCorrect("short [] [] []", short[][][].class);
+ assertCorrect("int [ ] [ ] [ ] [ ]", int[][][][].class);
+ assertCorrect("long [][][]", long[][][].class);
+ assertCorrect(" float[][]", float[][].class);
+ assertCorrect(" double [] ", double[].class);
+ assertCorrect(" char [ ][ ] ", char[][].class);
+ }
+
+ @Test
+ public void testClass() {
+ assertCorrect("java.lang.Object", Object.class);
+ assertCorrect("java.lang.String", String.class);
+
+ assertCorrect(" java.lang.Boolean", Boolean.class);
+ assertCorrect("java.lang.Byte ", Byte.class);
+ assertCorrect(" java.lang.Short ", Short.class);
+ assertCorrect("\tjava.lang.Integer", Integer.class);
+ assertCorrect("java.lang.Long\t", Long.class);
+ assertCorrect("\tjava.lang.Float\t", Float.class);
+ assertCorrect(" java.lang.Double", Double.class);
+ assertCorrect("java.lang.Character ", Character.class);
+ }
+
+ @Test
+ public void testClassArray() {
+ assertCorrect("java.lang.Object[]", Object[].class);
+ assertCorrect("java.lang.String[][]", String[][].class);
+
+ assertCorrect("java.lang.Boolean[][][]", Boolean[][][].class);
+ assertCorrect("java.lang.Byte[][][][]", Byte[][][][].class);
+ assertCorrect("java.lang.Short[][][]", Short[][][].class);
+ assertCorrect("java.lang.Integer[][]", Integer[][].class);
+ assertCorrect("java.lang.Long[]", Long[].class);
+ assertCorrect("java.lang.Float[][]", Float[][].class);
+ assertCorrect("java.lang.Double[][][]", Double[][][].class);
+ assertCorrect("java.lang.Character[][][][]", Character[][][][].class);
+ }
+
+ @Test
+ public void testParameterizedType() {
+ assertCorrect("java.util.List", new TypeLiteral>() {
+ }.getType());
+ assertCorrect("java.util.Map", new TypeLiteral