1 /*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License").
5 * You may not use this file except in compliance with the License.
6 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15
16 package software.amazon.awssdk.utils;
17
18 import java.lang.reflect.InvocationTargetException;
19 import java.lang.reflect.Method;
20 import software.amazon.awssdk.annotations.SdkProtectedApi;
21
22 /**
23 * This class acts as a proxy to invoke a specific method on objects of a specific class. It will use the JDK's
24 * reflection library to find and invoke the method.
25 * <p>
26 * The relatively expensive call to find the correct method on the class is lazy and will not be performed until the
27 * first invocation or until the invoker is explicitly initialized. Once found, the method is cached so repeated
28 * calls to initialize() or invoke() will not incur the reflection cost of searching for the method on the class.
29 * <p>
30 * Example:
31 * {@code
32 * ReflectionMethodInvoker<String, Integer> invoker =
33 * new ReflectionMethodInvoker<String, Integer>(String.class, Integer.class, "indexOf", String.class, int.class);
34 * invoker.invoke("ababab", "ab", 1); // This is equivalent to calling "ababab".indexOf("ab", 1);
35 * }
36 * @param <T> The class type that has the method to be invoked.
37 * @param <R> The expected return type of the method invocation.
38 */
39 @SdkProtectedApi
40 public class ReflectionMethodInvoker<T, R> {
41 private final Class<T> clazz;
42 private final String methodName;
43 private final Class<R> returnType;
44 private final Class<?>[] parameterTypes;
45
46 private Method targetMethod;
47
48 /**
49 * Construct an instance of {@code ReflectionMethodInvoker}.
50 * <p>
51 * This constructor will not make any reflection calls as part of initialization; i.e. no validation of the
52 * existence of the given method signature will occur.
53 * @param clazz The class that has the method to be invoked.
54 * @param returnType The expected return class of the method invocation. The object returned by the invocation
55 * will be cast to this class.
56 * @param methodName The name of the method to invoke.
57 * @param parameterTypes The classes of the parameters of the method to invoke.
58 */
59 public ReflectionMethodInvoker(Class<T> clazz,
60 Class<R> returnType,
61 String methodName,
62 Class<?>... parameterTypes) {
63 this.clazz = clazz;
64 this.methodName = methodName;
65 this.returnType = returnType;
66 this.parameterTypes = parameterTypes;
67 }
68
69 /**
70 * Attempt to invoke the method this proxy targets for on the given object with the given arguments. If the
71 * invoker has not yet been initialized, an attempt to initialize the invoker will be made first. If the call
72 * succeeds the invoker will be in an initialized state after this call and future calls to the same method will
73 * not incur an initialization cost. If the call fails because the target method could not be found, the invoker
74 * will remain in an uninitialized state after the exception is thrown.
75 * @param obj The object to invoke the method on.
76 * @param args The arguments to pass to the method. These arguments must match the signature of the method.
77 * @return The returned value of the method cast to the 'returnType' class that this proxy was initialized with.
78 * @throws NoSuchMethodException if the JVM could not find a method matching the signature specified in the
79 * initialization of this proxy.
80 * @throws RuntimeException if any other exception is thrown when attempting to invoke the method or by the
81 * method itself. The cause of this exception will be the exception that was actually thrown.
82 */
83 public R invoke(T obj, Object... args) throws NoSuchMethodException {
84 Method targetMethod = getTargetMethod();
85
86 try {
87 Object rawResult = targetMethod.invoke(obj, args);
88 return returnType.cast(rawResult);
89 } catch (IllegalAccessException | InvocationTargetException e) {
90 throw new RuntimeException(createInvocationErrorMessage(), e);
91 }
92 }
93
94 /**
95 * Initializes the method invoker by finding and caching the target method from the target class that will be used
96 * for subsequent invoke operations. If the invoker is already initialized this call will do nothing.
97 * @throws NoSuchMethodException if the JVM could not find a method matching the signature specified in the
98 * initialization of this proxy.
99 */
100 public void initialize() throws NoSuchMethodException {
101 getTargetMethod();
102 }
103
104 /**
105 * Gets the initialization state of the invoker.
106 * @return true if the invoker has been initialized and the method has been cached for invocation; otherwise false.
107 */
108 public boolean isInitialized() {
109 return targetMethod != null;
110 }
111
112 private Method getTargetMethod() throws NoSuchMethodException {
113 if (targetMethod != null) {
114 return targetMethod;
115 }
116
117 try {
118 targetMethod = clazz.getMethod(methodName, parameterTypes);
119 return targetMethod;
120 } catch (NullPointerException e) {
121 throw new RuntimeException(createInvocationErrorMessage(), e);
122 }
123 }
124
125 private String createInvocationErrorMessage() {
126 return String.format("Failed to reflectively invoke method %s on %s", methodName, clazz.getName());
127 }
128 }
129