001/* 002 * Copyright 2006-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.batch.item.adapter; 018 019import java.lang.reflect.InvocationTargetException; 020import java.lang.reflect.Method; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.List; 024 025import org.springframework.beans.factory.InitializingBean; 026import org.springframework.util.Assert; 027import org.springframework.util.ClassUtils; 028import org.springframework.util.MethodInvoker; 029 030/** 031 * Superclass for delegating classes which dynamically call a custom method of 032 * injected object. Provides convenient API for dynamic method invocation 033 * shielding subclasses from low-level details and exception handling. 034 * 035 * {@link Exception}s thrown by a successfully invoked delegate method are 036 * re-thrown without wrapping. In case the delegate method throws a 037 * {@link Throwable} that doesn't subclass {@link Exception} it will be wrapped 038 * by {@link InvocationTargetThrowableWrapper}. 039 * 040 * @author Robert Kasanicky 041 * @author Mahmoud Ben Hassine 042 */ 043public abstract class AbstractMethodInvokingDelegator<T> implements InitializingBean { 044 045 private Object targetObject; 046 047 private String targetMethod; 048 049 private Object[] arguments; 050 051 /** 052 * Invoker the target method with arguments set by 053 * {@link #setArguments(Object[])}. 054 * 055 * @return object returned by invoked method 056 * 057 * @throws Exception exception thrown when executing the delegate method. 058 */ 059 protected T invokeDelegateMethod() throws Exception { 060 MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod); 061 invoker.setArguments(arguments); 062 return doInvoke(invoker); 063 } 064 065 /** 066 * Invokes the target method with given argument. 067 * 068 * @param object argument for the target method 069 * @return object returned by target method 070 * 071 * @throws Exception exception thrown when executing the delegate method. 072 */ 073 protected T invokeDelegateMethodWithArgument(Object object) throws Exception { 074 MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod); 075 invoker.setArguments(new Object[] { object }); 076 return doInvoke(invoker); 077 } 078 079 /** 080 * Invokes the target method with given arguments. 081 * 082 * @param args arguments for the invoked method 083 * @return object returned by invoked method 084 * 085 * @throws Exception exception thrown when executing the delegate method. 086 */ 087 protected T invokeDelegateMethodWithArguments(Object[] args) throws Exception { 088 MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod); 089 invoker.setArguments(args); 090 return doInvoke(invoker); 091 } 092 093 /** 094 * Create a new configured instance of {@link MethodInvoker}. 095 */ 096 private MethodInvoker createMethodInvoker(Object targetObject, String targetMethod) { 097 HippyMethodInvoker invoker = new HippyMethodInvoker(); 098 invoker.setTargetObject(targetObject); 099 invoker.setTargetMethod(targetMethod); 100 invoker.setArguments(arguments); 101 return invoker; 102 } 103 104 /** 105 * Prepare and invoke the invoker, rethrow checked exceptions as unchecked. 106 * @param invoker configured invoker 107 * @return return value of the invoked method 108 */ 109 @SuppressWarnings("unchecked") 110 private T doInvoke(MethodInvoker invoker) throws Exception { 111 try { 112 invoker.prepare(); 113 } 114 catch (ClassNotFoundException | NoSuchMethodException e) { 115 throw new DynamicMethodInvocationException(e); 116 } 117 118 try { 119 return (T) invoker.invoke(); 120 } 121 catch (InvocationTargetException e) { 122 if (e.getCause() instanceof Exception) { 123 throw (Exception) e.getCause(); 124 } 125 else { 126 throw new InvocationTargetThrowableWrapper(e.getCause()); 127 } 128 } 129 catch (IllegalAccessException e) { 130 throw new DynamicMethodInvocationException(e); 131 } 132 } 133 134 @Override 135 public void afterPropertiesSet() throws Exception { 136 Assert.notNull(targetObject, "targetObject must not be null"); 137 Assert.hasLength(targetMethod, "targetMethod must not be empty"); 138 Assert.state(targetClassDeclaresTargetMethod(), 139 "target class must declare a method with matching name and parameter types"); 140 } 141 142 /** 143 * @return true if target class declares a method matching target method 144 * name with given number of arguments of appropriate type. 145 */ 146 private boolean targetClassDeclaresTargetMethod() { 147 MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod); 148 149 Method[] memberMethods = invoker.getTargetClass().getMethods(); 150 Method[] declaredMethods = invoker.getTargetClass().getDeclaredMethods(); 151 152 List<Method> allMethods = new ArrayList<>(); 153 allMethods.addAll(Arrays.asList(memberMethods)); 154 allMethods.addAll(Arrays.asList(declaredMethods)); 155 156 String targetMethodName = invoker.getTargetMethod(); 157 158 for (Method method : allMethods) { 159 if (method.getName().equals(targetMethodName)) { 160 Class<?>[] params = method.getParameterTypes(); 161 if (arguments == null) { 162 // don't check signature, assume arguments will be supplied 163 // correctly at runtime 164 return true; 165 } 166 if (arguments.length == params.length) { 167 boolean argumentsMatchParameters = true; 168 for (int j = 0; j < params.length; j++) { 169 if (arguments[j] == null) { 170 continue; 171 } 172 if (!(ClassUtils.isAssignableValue(params[j], arguments[j]))) { 173 argumentsMatchParameters = false; 174 } 175 } 176 if (argumentsMatchParameters) { 177 return true; 178 } 179 } 180 } 181 } 182 183 return false; 184 } 185 186 /** 187 * @param targetObject the delegate - bean id can be used to set this value 188 * in Spring configuration 189 */ 190 public void setTargetObject(Object targetObject) { 191 this.targetObject = targetObject; 192 } 193 194 /** 195 * @param targetMethod name of the method to be invoked on 196 * {@link #setTargetObject(Object)}. 197 */ 198 public void setTargetMethod(String targetMethod) { 199 this.targetMethod = targetMethod; 200 } 201 202 /** 203 * @param arguments arguments values for the { 204 * {@link #setTargetMethod(String)}. These will be used only when the 205 * subclass tries to invoke the target method without providing explicit 206 * argument values. 207 * 208 * If arguments are set to not-null value {@link #afterPropertiesSet()} will 209 * check the values are compatible with target method's signature. In case 210 * arguments are null (not set) method signature will not be checked and it 211 * is assumed correct values will be supplied at runtime. 212 */ 213 public void setArguments(Object[] arguments) { 214 this.arguments = arguments == null ? null : Arrays.asList(arguments).toArray(); 215 } 216 217 /** 218 * Return arguments. 219 * @return arguments 220 */ 221 protected Object[] getArguments() { 222 return arguments; 223 } 224 225 /** 226 * Used to wrap a {@link Throwable} (not an {@link Exception}) thrown by a 227 * reflectively-invoked delegate. 228 * 229 * @author Robert Kasanicky 230 */ 231 @SuppressWarnings("serial") 232 public static class InvocationTargetThrowableWrapper extends RuntimeException { 233 234 public InvocationTargetThrowableWrapper(Throwable cause) { 235 super(cause); 236 } 237 238 } 239}