From a52c27bec092fdaee9201c08793f249173e1da6c Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 29 Jan 2024 17:22:54 +0100 Subject: [PATCH 1/2] Initial specification text for invoker lookups --- .../enterprise/invoke/InvokerBuilder.java | 343 ++---------------- spec/src/main/asciidoc/core/invokers.asciidoc | 43 ++- .../core/packagingdeployment.asciidoc | 2 +- .../core/packagingdeployment_full.asciidoc | 2 +- 4 files changed, 70 insertions(+), 320 deletions(-) diff --git a/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java b/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java index 2212e521..dd950f79 100644 --- a/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java +++ b/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java @@ -11,353 +11,68 @@ package jakarta.enterprise.invoke; /** - * Builder of {@link Invoker}s that allows configuring input lookups, input and output - * transformations, and invoker wrapping. The method for which the invoker is built is - * called the target method. If a lookup is configured, the corresponding input - * of the invoker is ignored and an instance is looked up from the CDI container before - * the target method is invoked. If a transformation is configured, the corresponding input - * or output of the invoker is modified in certain way before or after the target method - * is invoked. If a wrapper is configured, the invoker is passed to custom code for getting - * invoked. As a result, the built {@code Invoker} instance may have more complex behavior - * than just directly calling the target method. - *

- * Transformations and wrapping are expressed by ordinary methods that must have - * a pre-defined signature, as described below. Such methods are called - * transformers and wrappers. - *

- * Invokers may only be built during deployment. It is not possible to build new invokers - * at application runtime. + * Builder of {@link Invoker}s. Allows configuring additional behaviors on top of a plain + * method invocation. * - *

Example

+ *

Lookups

* - * Before describing in detail how lookups, transformers and wrappers work, let's take - * a look at an example. Say we have the following bean with a method: + * For the target bean instance ({@link #withInstanceLookup()}) and for each target method + * parameter ({@link #withArgumentLookup(int)}), it is possible to specify that the corresponding + * value passed to {@code Invoker.invoke()} shall be ignored and a value shall be looked up + * from the CDI container instead. + *

+ * For example, assume the following managed bean exists: * *

- * class MyService {
- *     String hello(String name) {
+ * @Dependent
+ * public class MyService {
+ *     public String hello(String name) {
  *         return "Hello " + name + "!";
  *     }
  * }
  * 
* - * And we want to build an invoker that looks up {@code MyService} from the CDI container, - * always passes the argument to {@code hello()} as all upper-case, and repeats the return - * value twice. To transform the argument, we can use the zero-parameter method - * {@code String.toUpperCase()}, and to transform the return value, we write a transformer - * as a simple {@code static} method: - * - *
- * class Transformations {
- *     static String repeatTwice(String str) {
- *         return str + " " + str;
- *     }
- * }
- * 
- * - * Then, assuming we have obtained the {@code InvokerBuilder} for {@code MyService.hello()}, - * we can set up the lookup and transformations and build an invoker like so: + * A CDI-based framework may want to build an invoker for the {@code hello()} method that + * automatically looks up {@code MyService} from the CDI container, instead of having to + * obtain a contextual reference manually. + *

+ * Assuming that {@code builder} is an {@code InvokerBuilder} for {@code MyService.hello()}, + * such invoker can be built: * *

- * builder.setInstanceLookup()
- *         .setArgumentTransformer(0, String.class, "toUpperCase")
- *         .setReturnValueTransformer(Transformations.class, "repeatTwice")
- *         .build();
+ * builder.withInstanceLookup().build();
  * 
* - * The resulting invoker will be equivalent to the following class: + * Later, to invoke the {@code hello()} method, a framework could pass {@code null} as the instance: * *
- * class TheInvoker implements Invoker<MyService, String> {
- *     String invoke(MyService ignored, Object[] arguments) {
- *         MyService instance = CDI.current().select(MyService.class).get();
- *         String argument = (String) arguments[0];
- *         String transformedArgument = argument.toUpperCase();
- *         String result = instance.hello(transformedArgument);
- *         String transformedResult = Transformations.repeatTwice(result);
- *         return transformedResult;
- *     }
- * }
+ * invoker.invoke(null, new Object[] { "world" })
  * 
* - * The caller of this invoker may pass {@code null} as the target instance, because - * the invoker will lookup the target instance on its own. Therefore, calling - * {@code invoker.invoke(null, new Object[] {"world"})} will return - * {@code "Hello WORLD! Hello WORLD!"}. - * - *

General requirements

- * - * To refer to a transformer or a wrapper, all methods in this builder accept: - * 1. the {@code Class} that that declares the method, and 2. the {@code String} name - * of the method. - *

- * Transformers may be {@code static}, in which case they must be declared directly - * on the given class, or they may be instance methods, in which case they may be declared - * on the given class or inherited from any of its supertypes. - *

- * It is possible to register only one transformer of each kind, or for each argument - * position in case of argument transformers. Attempting to register a second transformer - * of the same kind, or for the same argument position, leads to an exception. - *

- * Wrappers must be {@code static} and must be declared directly on the given class. - * It is possible to register only one wrapper. Attempting to register a second wrapper - * leads to an exception. - *

- * It is a deployment problem if no method with given name and valid signature is found, - * or if multiple methods with given name and different valid signatures are found. It is - * a deployment problem if a registered transformer or wrapper is not {@code public}. - *

- * Transformers and wrappers may declare the {@code throws} clause. The declared exception - * types are ignored when searching for the method. - *

- * For the purpose of the specification of transformers and wrappers below, the term - * any-type is recursively defined as: the {@code java.lang.Object} class type, - * or a type variable that has no bound, or a type variable whose first bound is - * any-type. - * - *

Input lookups

- * - * For the target instance and for each argument, it is possible to specify that the value - * passed to {@code Invoker.invoke()} should be ignored and a value should be looked up - * from the CDI container instead. - *

- * For the target instance, a CDI lookup is performed with the required type equal to the bean - * class of the bean to which the target method belongs, and required qualifiers equal to the set - * of all qualifier annotations present on the bean class of the bean to which the target method - * belongs. When the target method is {@code static}, the target instance lookup is skipped. - *

- * For an argument, a CDI lookup is performed with the required type equal to the type of - * the corresponding parameter of the target method, and required qualifiers equal to the set - * of all qualifier annotations present on the corresponding parameter of the target method. - *

- * Implementations are required to resolve all lookups during deployment. It is a deployment - * problem if the lookup ends up unresolved or ambiguous. - *

- * If the looked up bean is {@code @Dependent}, it is guaranteed that the instance will be - * destroyed after the target method is invoked but before the the invoker returns. The order - * in which the looked up {@code @Dependent} beans are destroyed is not specified. - *

- * The order in which input lookups are performed in not specified and must not be relied upon. - * - *

Input transformations

- * - * The target method has 2 kinds of inputs: the target instance (unless the target method is - * {@code static}, in which case the target instance is ignored and should be {@code null} - * by convention) and arguments. These inputs correspond to the parameters of - * {@link Invoker#invoke(Object, Object[]) Invoker.invoke()}. - *

- * Each input can be transformed by a transformer that has one of the following signatures, - * where {@code X} and {@code Y} are types: - * - *

- * - * An input transformer must produce a type that can be consumed by the target method. - * Specifically: when {@code X} is any-type, it is not type checked during deployment. - * Otherwise, it is a deployment problem if {@code X} is not assignable to the corresponding type - * in the declaration of the target method (that is the bean class in case of target instance - * transformers, or the corresponding parameter type in case of argument transformers). {@code Y} - * is not type checked during deployment, so that input transformers may consume arbitrary types. - * TODO this paragraph refers to "assignability", which needs to be defined somewhere! - *

- * When a transformer is registered for given input, it is called before the target method is - * invoked, and the outcome of the transformer is used in the invocation instead of the original - * value passed to the invoker by its caller. - *

- * If the transformer declares the {@code Consumer} parameter, and the execution - * of the transformer calls {@code Consumer.accept()} with some {@code Runnable}, it is - * guaranteed that the {@code Runnable} will be called after the target method is invoked but - * before the invoker returns. These {@code Runnable}s are called cleanup tasks. - * The order of cleanup task execution is not specified. Passing a {@code null} cleanup task - * to the {@code Consumer} is permitted, but has no effect. - *

- * If an input transformation is configured for an input for which a lookup is also configured, - * the lookup is performed first and the transformation is applied to the looked up value. - * If the looked up bean for some input is {@code @Dependent}, it is guaranteed that all - * cleanup tasks registered by a transformer for that input are called before that looked up - * {@code @Dependent} bean is destroyed. - *

- * The order in which input transformations are performed in not specified and must not - * be relied upon. - * - *

Output transformations

- * - * The target method has 2 kinds of outputs: the return value and the thrown exception. These - * outputs correspond to the return value of {@link Invoker#invoke(Object, Object[]) Invoker.invoke()} - * or its thrown exception, respectively. - *

- * Each output can be transformed by a transformer that has one of the following signatures, - * where {@code X} and {@code Y} are types: - * - *

- * - * An output transformer must consume a type that can be produced by the target method. - * Specifically: when {@code Y} is any-type, it is not type checked during deployment. - * Otherwise, it is a deployment problem if {@code Y} is not assignable from the return type of - * the target method in case of return value transformers, or from {@code java.lang.Throwable} - * in case of exception transformers. {@code X} is not type checked during deployment, so that - * output transformers may produce arbitrary types. - * TODO this paragraph refers to "assignability", which needs to be defined somewhere! - *

- * When a transformer is registered for given output, it is called after the target method - * is invoked, and the outcome of the transformer is passed back to the caller of the invoker - * instead of the original output produced by the target method. - *

- * If the target method returns normally, any registered exception transformer is ignored; only - * the return value transformer is called. The return value transformer may throw, in which case - * the invoker will rethrow the exception. If the invoker is supposed to return normally, - * the return value transformer must return normally. - *

- * Similarly, if the target method throws, any registered return value transformer is ignored; - * only the exception transformer is called. The exception transformer may return normally, - * in which case the invoker will return the return value of the exception transformer. If - * the invoker is supposed to throw an exception, the exception transformer must throw. - * TODO this requires that implementations catch java.lang.Throwable, which is perhaps a bit too much? - * maybe stick with java.lang.Exception? - * - *

Invoker wrapping

- * - * An invoker, possibly utilizing input lookups and input/output transformations, may be wrapped - * by a custom piece of code for maximum flexibility. A wrapper must have the following signature, - * where {@code X}, {@code Y} and {@code Z} are types: - * - * - * - * A wrapper must operate on a matching instance type. Specifically: when {@code X} is - * any-type, it is not type checked during deployment. Otherwise, it is a deployment - * problem if {@code X} is not assignable from the class type of the bean class to which - * the target method belongs. {@code Y} and {@code Z} are not type checked during deployment. - *

- * When a wrapper is registered, 2 invokers for the same method are created. The inner - * invoker applies all lookups and transformations, as described in previous sections, and - * invokes the target method. The outer invoker calls the wrapper with the passed - * instance and arguments and an instance of the inner invoker. The outer invoker is returned - * by this invoker builder. - *

- * In other words, the outer invoker is equivalent to the following class: - * - *

- * class InvokerWrapper implements Invoker<X, Z> {
- *     Z invoke(X instance, Object[] arguments) {
- *         // obtain the invoker as if no wrapper existed
- *         Invoker<X, Y> invoker = obtainInvoker();
- *         return SomeClass.wrap(instance, arguments, invoker);
- *     }
- * }
- * 
- * - * If the wrapper returns normally, the outer invoker returns its return value, unless the wrapper - * is declared {@code void}, in which case the outer invoker returns {@code null}. If the wrapper - * throws an exception, the outer invoker rethrows it directly. - *

- * The wrapper is supposed to call the invoker it is passed, but does not necessarily have to. - * The wrapper may call the invoker multiple times. The wrapper must not use the invoker - * in any other way; specifically, it is forbidden to store the invoker instance anywhere - * or pass it to other methods that do not follow these rules. Doing so leads to non-portable - * behavior. - * - *

Type checking

- * - * An invoker created by this builder has relaxed type checking rules, when compared to - * the description in {@link Invoker#invoke(Object, Object[]) Invoker.invoke()}, depending - * on configured lookups, transformers and wrapper. Some types are checked during - * deployment, as described in previous sections. Other types are checked during invocation, - * at the very least due to the type checks performed implicitly by the JVM. The lookups, - * transformers and the wrapper must arrange the inputs and outputs so that when the method - * is eventually invoked, the rules described in - * {@link Invoker#invoke(Object, Object[]) Invoker.invoke()} all hold. - *

- * TODO specify what happens when a transformer/wrapper declares a parameter of a primitive type - * but the actual value passed to the invoker is `null` (the transformer should get a zero value?) - * TODO specify what happens when a transformer/wrapper declares a parameter of some type - * but the actual value passed to the invoker is not assignable to it (CCE?) + * The invoker would look up the instance of the target bean automatically, so the method would be + * invoked correctly and the return value would be {@code "Hello world!"}. * * @param type of outcome of this builder; always represents an {@code Invoker}, * but does not necessarily have to be an {@code Invoker} instance directly * @since 4.1 */ -// TODO more kinds of transformations could be defined, expecially for argument handling -// TODO it would be possible to specify a sequence of transformations for each input/output, instead of just one public interface InvokerBuilder { /** - * Enables lookup of the target instance. + * Enables lookup of the target bean instance. * * @return this builder */ - InvokerBuilder setInstanceLookup(); + InvokerBuilder withInstanceLookup(); /** * Enables lookup of the argument on given {@code position}. * - * @param position zero-based argument position for which lookup is enabled - * @return this builder - * @throws IllegalArgumentException if {@code position} is greather than or equal to - * the number of parameters declared by the target method - */ - InvokerBuilder setArgumentLookup(int position); - - /** - * Configures an input transformer for the target instance. - * - * @param clazz class that declares the transformer - * @param methodName transformer method name - * @return this builder - * @throws IllegalStateException if this method is called more than once - */ - InvokerBuilder setInstanceTransformer(Class clazz, String methodName); - - /** - * Configures an input transformer for the argument on given {@code position}. - * - * @param position zero-based argument position for which the input transformer is configured - * @param clazz class that declares the transformer - * @param methodName transformer method name - * @return this builder - * @throws IllegalArgumentException if {@code position} is greather than or equal to - * the number of parameters declared by the target method - * @throws IllegalStateException if this method is called more than once with the same {@code position} - */ - InvokerBuilder setArgumentTransformer(int position, Class clazz, String methodName); - - /** - * Configures an output transformer for the return value. - * - * @param clazz class that declares the transformer - * @param methodName transformer method name - * @return this builder - * @throws IllegalStateException if this method is called more than once - */ - InvokerBuilder setReturnValueTransformer(Class clazz, String methodName); - - /** - * Configures an output transformer for the thrown exception. - * - * @param clazz class that declares the transformer - * @param methodName transformer method name - * @return this builder - * @throws IllegalStateException if this method is called more than once - */ - InvokerBuilder setExceptionTransformer(Class clazz, String methodName); - - /** - * Configures an invoker wrapper. - * - * @param clazz class that declares the invoker wrapper - * @param methodName invoker wrapper method name + * @param position zero-based position of the target method parameter for which lookup should be enabled * @return this builder - * @throws IllegalStateException if this method is called more than once + * @throws IllegalArgumentException if {@code position} is less than 0 or greater than + * or equal to the number of parameters declared by the target method */ - InvokerBuilder setInvocationWrapper(Class clazz, String methodName); + InvokerBuilder withArgumentLookup(int position); /** * Returns the built {@link Invoker} or some representation of it. Implementations are allowed diff --git a/spec/src/main/asciidoc/core/invokers.asciidoc b/spec/src/main/asciidoc/core/invokers.asciidoc index 351531bd..4d957d30 100644 --- a/spec/src/main/asciidoc/core/invokers.asciidoc +++ b/spec/src/main/asciidoc/core/invokers.asciidoc @@ -98,8 +98,6 @@ Otherwise, runtime failures are likely to occur. If the target method returns normally, its return value is returned (after boxing conversion if the target method's return type is a primitive type), unless the target method is declared `void`, in which case `null` is returned. If the target method throws an exception, it is rethrown directly. -// TODO when the `InvokerBuilder` applies transformations, some of the requirements above are no longer strictly necessary, we should reflect that in this text somehow - ==== Example To illustrate how method invokers work, let's take a look at an example. @@ -141,7 +139,7 @@ invoker.invoke(myService, new Object[] {"world"}) The return value is `"Hello world!"`. -An implementation of the invoker above is equivalent to the following class: +Internally, the container will create an implementation of the invoker, equivalent to the following class: [source,java] ---- @@ -180,4 +178,41 @@ public interface InvokerBuilder { Calling `InvokerBuilder.build()` produces an opaque token (`InvokerInfo`) that can be passed as a parameter to a `SyntheticBeanBuilder` or `SyntheticObserverBuilder` and materializes as an `Invoker` at application runtime. -// TODO lookups, transformers, wrappers +[[invoker_lookups]] +==== Configuring invoker lookups + +The `InvokerBuilder` allows configuring that the `instance` or any of the `arguments` passed to `Invoker.invoke()` should be ignored and a value should be looked up from the CDI container instead. + +[source,java] +---- +public interface InvokerBuilder { + InvokerBuilder withInstanceLookup(); + InvokerBuilder withArgumentLookup(int position); +} +---- + +When `withInstanceLookup()` is called on an invoker builder and the target method is not `static`, the `invoke()` method of the built invoker shall ignore the `instance` argument and instead obtain and use a contextual reference for the target bean and the bean type that declares the target method. +Calling `withInstanceLookup()` on an invoker builder for a `static` target method has no effect. + +When `withArgumentLookup()` is called on an invoker builder, the `invoke()` method of the built invoker shall ignore the given element of the `arguments` array and instead: + +1. identify a bean according to the rules of typesafe resolution, as defined in <>, where the required type is the declared type of the corresponding parameter of the target method and the required qualifiers are all qualifiers present on the parameter, resolving ambiguities according to <>; +2. obtain and use a contextual reference for the identified bean and the declared type of the parameter. + +Calling `withArgumentLookup()` with `position` less than 0 or greater than or equal to the number of parameters of the target method leads to an `IllegalArgumentException`. + +In the following paragraphs, the beans whose instances shall be obtained by `Invoker.invoke()` as a result of calling `withInstanceLookup()` and `withArgumentLookup()` are called _looked up beans_. + +During deployment validation, implementations are required to identify all looked up beans for all built invokers, as described above. +It is a deployment problem if an attempt to identify a looked up bean results in an unsatisfied dependency or an ambiguous dependency that is not resolvable. +Implementations are permitted to remember the identified beans and not repeat the resolution process for each invocation of `Invoker.invoke()`. + +All instances of `@Dependent` looked up beans obtained during `Invoker.invoke()` are destroyed before the `invoke()` method returns or throws. +The order in which the instances of `@Dependent` looked up beans are destroyed is not specified. + +The order in which instances of looked up beans are obtained during `Invoker.invoke()` in not specified. + +If an exception is thrown when creating an instance of a looked up bean during `Invoker.invoke()`, the exception is rethrown. + +NOTE: Destroying an instance is not permitted to throw an exception. +See <> for more information. diff --git a/spec/src/main/asciidoc/core/packagingdeployment.asciidoc b/spec/src/main/asciidoc/core/packagingdeployment.asciidoc index eab2469a..029f41fc 100644 --- a/spec/src/main/asciidoc/core/packagingdeployment.asciidoc +++ b/spec/src/main/asciidoc/core/packagingdeployment.asciidoc @@ -81,7 +81,7 @@ At deployment time, the container performs the following steps: As part of that, the container must execute the <> and <> phases of build compatible extensions. * Next, the container must perform bean discovery, as defined in <>. As part of that, the container must execute the <> and <> phases of build compatible extensions. -* Finally, the container must detect deployment problems by validating bean dependencies. +* Finally, the container must detect deployment problems by validating bean dependencies and <>. As part of that, the container must execute the <> phase of build compatible extensions. At any step, the container must abort deployment if any definition errors or deployment problems exist, as defined in <>. diff --git a/spec/src/main/asciidoc/core/packagingdeployment_full.asciidoc b/spec/src/main/asciidoc/core/packagingdeployment_full.asciidoc index fa7108a2..00cad567 100644 --- a/spec/src/main/asciidoc/core/packagingdeployment_full.asciidoc +++ b/spec/src/main/asciidoc/core/packagingdeployment_full.asciidoc @@ -86,7 +86,7 @@ When an application is started, the container performs the following steps: * Next, the container must fire an event of type `AfterTypeDiscovery`, as defined in <>. * Next, the container must perform bean discovery, as defined in <>. * Next, the container must fire an event of type `AfterBeanDiscovery`, as defined in <>, and abort initialization of the application if any observer registers a definition error. -* Next, the container must detect deployment problems by validating bean dependencies and specialization and abort initialization of the application if any deployment problems exist, as defined in <>. +* Next, the container must detect deployment problems by validating bean dependencies, specialization and <> and abort initialization of the application if any deployment problems exist, as defined in <>. * Next, the container must fire an event of type `AfterDeploymentValidation`, as defined in <>, and abort initialization of the application if any observer registers a deployment problem. * Finally, the container begins directing requests to the application. From 9c41e87fb46456b4354158d7c093b745eafd46ad Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 13 Feb 2024 17:09:36 +0100 Subject: [PATCH 2/2] add explicit note that invoker lookups don't relax any rules; to be squashed --- spec/src/main/asciidoc/core/invokers.asciidoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/src/main/asciidoc/core/invokers.asciidoc b/spec/src/main/asciidoc/core/invokers.asciidoc index 4d957d30..b8566a71 100644 --- a/spec/src/main/asciidoc/core/invokers.asciidoc +++ b/spec/src/main/asciidoc/core/invokers.asciidoc @@ -67,6 +67,7 @@ Whether concurrent invocations of the target method are safe depends on the impl Whenever a direct invocation of a method on an object is a business method invocation, an indirect invocation of that method on that object through an invoker is also a business method invocation. +[[invoke_behavior]] ==== Behavior of `invoke()` If the target method is `static`, the `instance` is ignored; by convention, it should be `null`. @@ -201,6 +202,10 @@ When `withArgumentLookup()` is called on an invoker builder, the `invoke()` meth Calling `withArgumentLookup()` with `position` less than 0 or greater than or equal to the number of parameters of the target method leads to an `IllegalArgumentException`. +Configuring a lookup using `withInstanceLookup()` or `withArgumentLookup()` does not relax the requirements defined in <>. +Notably, the `arguments` array must still have an element for each argument, regardless of whether a lookup was configured for it. +This means that for a target method with N parameters, the `arguments` array must always have at least N elements. + In the following paragraphs, the beans whose instances shall be obtained by `Invoker.invoke()` as a result of calling `withInstanceLookup()` and `withArgumentLookup()` are called _looked up beans_. During deployment validation, implementations are required to identify all looked up beans for all built invokers, as described above.