001/* 002 * Copyright 2002-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.remoting.rmi; 018 019import java.lang.reflect.InvocationTargetException; 020import java.lang.reflect.Method; 021import java.rmi.RemoteException; 022 023import javax.naming.Context; 024import javax.naming.NamingException; 025 026import org.aopalliance.intercept.MethodInterceptor; 027import org.aopalliance.intercept.MethodInvocation; 028 029import org.springframework.aop.support.AopUtils; 030import org.springframework.beans.factory.InitializingBean; 031import org.springframework.jndi.JndiObjectLocator; 032import org.springframework.lang.Nullable; 033import org.springframework.remoting.RemoteConnectFailureException; 034import org.springframework.remoting.RemoteInvocationFailureException; 035import org.springframework.remoting.RemoteLookupFailureException; 036import org.springframework.remoting.support.DefaultRemoteInvocationFactory; 037import org.springframework.remoting.support.RemoteInvocation; 038import org.springframework.remoting.support.RemoteInvocationFactory; 039import org.springframework.util.Assert; 040 041/** 042 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI services 043 * from JNDI. Typically used for RMI-IIOP but can also be used for EJB home objects 044 * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup, 045 * this accessor also performs narrowing through PortableRemoteObject. 046 * 047 * <p>With conventional RMI services, this invoker is typically used with the RMI 048 * service interface. Alternatively, this invoker can also proxy a remote RMI service 049 * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI 050 * service methods but does not declare RemoteExceptions. In the latter case, 051 * RemoteExceptions thrown by the RMI stub will automatically get converted to 052 * Spring's unchecked RemoteAccessException. 053 * 054 * <p>The JNDI environment can be specified as "jndiEnvironment" property, 055 * or be configured in a {@code jndi.properties} file or as system properties. 056 * For example: 057 * 058 * <pre class="code"><property name="jndiEnvironment"> 059 * <props> 060 * <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop> 061 * <prop key="java.naming.provider.url">iiop://localhost:1050</prop> 062 * </props> 063 * </property></pre> 064 * 065 * @author Juergen Hoeller 066 * @since 1.1 067 * @see #setJndiTemplate 068 * @see #setJndiEnvironment 069 * @see #setJndiName 070 * @see JndiRmiServiceExporter 071 * @see JndiRmiProxyFactoryBean 072 * @see org.springframework.remoting.RemoteAccessException 073 * @see java.rmi.RemoteException 074 * @see java.rmi.Remote 075 */ 076public class JndiRmiClientInterceptor extends JndiObjectLocator implements MethodInterceptor, InitializingBean { 077 078 private Class<?> serviceInterface; 079 080 private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); 081 082 private boolean lookupStubOnStartup = true; 083 084 private boolean cacheStub = true; 085 086 private boolean refreshStubOnConnectFailure = false; 087 088 private boolean exposeAccessContext = false; 089 090 private Object cachedStub; 091 092 private final Object stubMonitor = new Object(); 093 094 095 /** 096 * Set the interface of the service to access. 097 * The interface must be suitable for the particular service and remoting tool. 098 * <p>Typically required to be able to create a suitable service proxy, 099 * but can also be optional if the lookup returns a typed stub. 100 */ 101 public void setServiceInterface(Class<?> serviceInterface) { 102 Assert.notNull(serviceInterface, "'serviceInterface' must not be null"); 103 Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface"); 104 this.serviceInterface = serviceInterface; 105 } 106 107 /** 108 * Return the interface of the service to access. 109 */ 110 public Class<?> getServiceInterface() { 111 return this.serviceInterface; 112 } 113 114 /** 115 * Set the RemoteInvocationFactory to use for this accessor. 116 * Default is a {@link DefaultRemoteInvocationFactory}. 117 * <p>A custom invocation factory can add further context information 118 * to the invocation, for example user credentials. 119 */ 120 public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { 121 this.remoteInvocationFactory = remoteInvocationFactory; 122 } 123 124 /** 125 * Return the RemoteInvocationFactory used by this accessor. 126 */ 127 public RemoteInvocationFactory getRemoteInvocationFactory() { 128 return this.remoteInvocationFactory; 129 } 130 131 /** 132 * Set whether to look up the RMI stub on startup. Default is "true". 133 * <p>Can be turned off to allow for late start of the RMI server. 134 * In this case, the RMI stub will be fetched on first access. 135 * @see #setCacheStub 136 */ 137 public void setLookupStubOnStartup(boolean lookupStubOnStartup) { 138 this.lookupStubOnStartup = lookupStubOnStartup; 139 } 140 141 /** 142 * Set whether to cache the RMI stub once it has been located. 143 * Default is "true". 144 * <p>Can be turned off to allow for hot restart of the RMI server. 145 * In this case, the RMI stub will be fetched for each invocation. 146 * @see #setLookupStubOnStartup 147 */ 148 public void setCacheStub(boolean cacheStub) { 149 this.cacheStub = cacheStub; 150 } 151 152 /** 153 * Set whether to refresh the RMI stub on connect failure. 154 * Default is "false". 155 * <p>Can be turned on to allow for hot restart of the RMI server. 156 * If a cached RMI stub throws an RMI exception that indicates a 157 * remote connect failure, a fresh proxy will be fetched and the 158 * invocation will be retried. 159 * @see java.rmi.ConnectException 160 * @see java.rmi.ConnectIOException 161 * @see java.rmi.NoSuchObjectException 162 */ 163 public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { 164 this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; 165 } 166 167 /** 168 * Set whether to expose the JNDI environment context for all access to the target 169 * RMI stub, i.e. for all method invocations on the exposed object reference. 170 * <p>Default is "false", i.e. to only expose the JNDI context for object lookup. 171 * Switch this flag to "true" in order to expose the JNDI environment (including 172 * the authorization context) for each RMI invocation, as needed by WebLogic 173 * for RMI stubs with authorization requirements. 174 */ 175 public void setExposeAccessContext(boolean exposeAccessContext) { 176 this.exposeAccessContext = exposeAccessContext; 177 } 178 179 180 @Override 181 public void afterPropertiesSet() throws NamingException { 182 super.afterPropertiesSet(); 183 prepare(); 184 } 185 186 /** 187 * Fetches the RMI stub on startup, if necessary. 188 * @throws RemoteLookupFailureException if RMI stub creation failed 189 * @see #setLookupStubOnStartup 190 * @see #lookupStub 191 */ 192 public void prepare() throws RemoteLookupFailureException { 193 // Cache RMI stub on initialization? 194 if (this.lookupStubOnStartup) { 195 Object remoteObj = lookupStub(); 196 if (logger.isDebugEnabled()) { 197 if (remoteObj instanceof RmiInvocationHandler) { 198 logger.debug("JNDI RMI object [" + getJndiName() + "] is an RMI invoker"); 199 } 200 else if (getServiceInterface() != null) { 201 boolean isImpl = getServiceInterface().isInstance(remoteObj); 202 logger.debug("Using service interface [" + getServiceInterface().getName() + 203 "] for JNDI RMI object [" + getJndiName() + "] - " + 204 (!isImpl ? "not " : "") + "directly implemented"); 205 } 206 } 207 if (this.cacheStub) { 208 this.cachedStub = remoteObj; 209 } 210 } 211 } 212 213 /** 214 * Create the RMI stub, typically by looking it up. 215 * <p>Called on interceptor initialization if "cacheStub" is "true"; 216 * else called for each invocation by {@link #getStub()}. 217 * <p>The default implementation retrieves the service from the 218 * JNDI environment. This can be overridden in subclasses. 219 * @return the RMI stub to store in this interceptor 220 * @throws RemoteLookupFailureException if RMI stub creation failed 221 * @see #setCacheStub 222 * @see #lookup 223 */ 224 protected Object lookupStub() throws RemoteLookupFailureException { 225 try { 226 return lookup(); 227 } 228 catch (NamingException ex) { 229 throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); 230 } 231 } 232 233 /** 234 * Return the RMI stub to use. Called for each invocation. 235 * <p>The default implementation returns the stub created on initialization, 236 * if any. Else, it invokes {@link #lookupStub} to get a new stub for 237 * each invocation. This can be overridden in subclasses, for example in 238 * order to cache a stub for a given amount of time before recreating it, 239 * or to test the stub whether it is still alive. 240 * @return the RMI stub to use for an invocation 241 * @throws NamingException if stub creation failed 242 * @throws RemoteLookupFailureException if RMI stub creation failed 243 */ 244 protected Object getStub() throws NamingException, RemoteLookupFailureException { 245 if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { 246 return (this.cachedStub != null ? this.cachedStub : lookupStub()); 247 } 248 else { 249 synchronized (this.stubMonitor) { 250 if (this.cachedStub == null) { 251 this.cachedStub = lookupStub(); 252 } 253 return this.cachedStub; 254 } 255 } 256 } 257 258 259 /** 260 * Fetches an RMI stub and delegates to {@link #doInvoke}. 261 * If configured to refresh on connect failure, it will call 262 * {@link #refreshAndRetry} on corresponding RMI exceptions. 263 * @see #getStub 264 * @see #doInvoke 265 * @see #refreshAndRetry 266 * @see java.rmi.ConnectException 267 * @see java.rmi.ConnectIOException 268 * @see java.rmi.NoSuchObjectException 269 */ 270 @Override 271 public Object invoke(MethodInvocation invocation) throws Throwable { 272 Object stub; 273 try { 274 stub = getStub(); 275 } 276 catch (NamingException ex) { 277 throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); 278 } 279 280 Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null); 281 try { 282 return doInvoke(invocation, stub); 283 } 284 catch (RemoteConnectFailureException ex) { 285 return handleRemoteConnectFailure(invocation, ex); 286 } 287 catch (RemoteException ex) { 288 if (isConnectFailure(ex)) { 289 return handleRemoteConnectFailure(invocation, ex); 290 } 291 else { 292 throw ex; 293 } 294 } 295 finally { 296 getJndiTemplate().releaseContext(ctx); 297 } 298 } 299 300 /** 301 * Determine whether the given RMI exception indicates a connect failure. 302 * <p>The default implementation delegates to 303 * {@link RmiClientInterceptorUtils#isConnectFailure}. 304 * @param ex the RMI exception to check 305 * @return whether the exception should be treated as connect failure 306 */ 307 protected boolean isConnectFailure(RemoteException ex) { 308 return RmiClientInterceptorUtils.isConnectFailure(ex); 309 } 310 311 /** 312 * Refresh the stub and retry the remote invocation if necessary. 313 * <p>If not configured to refresh on connect failure, this method 314 * simply rethrows the original exception. 315 * @param invocation the invocation that failed 316 * @param ex the exception raised on remote invocation 317 * @return the result value of the new invocation, if succeeded 318 * @throws Throwable an exception raised by the new invocation, if failed too. 319 */ 320 private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { 321 if (this.refreshStubOnConnectFailure) { 322 if (logger.isDebugEnabled()) { 323 logger.debug("Could not connect to RMI service [" + getJndiName() + "] - retrying", ex); 324 } 325 else if (logger.isInfoEnabled()) { 326 logger.info("Could not connect to RMI service [" + getJndiName() + "] - retrying"); 327 } 328 return refreshAndRetry(invocation); 329 } 330 else { 331 throw ex; 332 } 333 } 334 335 /** 336 * Refresh the RMI stub and retry the given invocation. 337 * Called by invoke on connect failure. 338 * @param invocation the AOP method invocation 339 * @return the invocation result, if any 340 * @throws Throwable in case of invocation failure 341 * @see #invoke 342 */ 343 @Nullable 344 protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { 345 Object freshStub; 346 synchronized (this.stubMonitor) { 347 this.cachedStub = null; 348 freshStub = lookupStub(); 349 if (this.cacheStub) { 350 this.cachedStub = freshStub; 351 } 352 } 353 return doInvoke(invocation, freshStub); 354 } 355 356 357 /** 358 * Perform the given invocation on the given RMI stub. 359 * @param invocation the AOP method invocation 360 * @param stub the RMI stub to invoke 361 * @return the invocation result, if any 362 * @throws Throwable in case of invocation failure 363 */ 364 @Nullable 365 protected Object doInvoke(MethodInvocation invocation, Object stub) throws Throwable { 366 if (stub instanceof RmiInvocationHandler) { 367 // RMI invoker 368 try { 369 return doInvoke(invocation, (RmiInvocationHandler) stub); 370 } 371 catch (RemoteException ex) { 372 throw convertRmiAccessException(ex, invocation.getMethod()); 373 } 374 catch (InvocationTargetException ex) { 375 throw ex.getTargetException(); 376 } 377 catch (Throwable ex) { 378 throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + 379 "] failed in RMI service [" + getJndiName() + "]", ex); 380 } 381 } 382 else { 383 // traditional RMI stub 384 try { 385 return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); 386 } 387 catch (InvocationTargetException ex) { 388 Throwable targetEx = ex.getTargetException(); 389 if (targetEx instanceof RemoteException) { 390 throw convertRmiAccessException((RemoteException) targetEx, invocation.getMethod()); 391 } 392 else { 393 throw targetEx; 394 } 395 } 396 } 397 } 398 399 /** 400 * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. 401 * <p>The default implementation delegates to {@link #createRemoteInvocation}. 402 * @param methodInvocation the current AOP method invocation 403 * @param invocationHandler the RmiInvocationHandler to apply the invocation to 404 * @return the invocation result 405 * @throws RemoteException in case of communication errors 406 * @throws NoSuchMethodException if the method name could not be resolved 407 * @throws IllegalAccessException if the method could not be accessed 408 * @throws InvocationTargetException if the method invocation resulted in an exception 409 * @see org.springframework.remoting.support.RemoteInvocation 410 */ 411 protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) 412 throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { 413 414 if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { 415 return "RMI invoker proxy for service URL [" + getJndiName() + "]"; 416 } 417 418 return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); 419 } 420 421 /** 422 * Create a new RemoteInvocation object for the given AOP method invocation. 423 * <p>The default implementation delegates to the configured 424 * {@link #setRemoteInvocationFactory RemoteInvocationFactory}. 425 * This can be overridden in subclasses in order to provide custom RemoteInvocation 426 * subclasses, containing additional invocation parameters (e.g. user credentials). 427 * <p>Note that it is preferable to build a custom RemoteInvocationFactory 428 * as a reusable strategy, instead of overriding this method. 429 * @param methodInvocation the current AOP method invocation 430 * @return the RemoteInvocation object 431 * @see RemoteInvocationFactory#createRemoteInvocation 432 */ 433 protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { 434 return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation); 435 } 436 437 /** 438 * Convert the given RMI RemoteException that happened during remote access 439 * to Spring's RemoteAccessException if the method signature does not declare 440 * RemoteException. Else, return the original RemoteException. 441 * @param method the invoked method 442 * @param ex the RemoteException that happened 443 * @return the exception to be thrown to the caller 444 */ 445 private Exception convertRmiAccessException(RemoteException ex, Method method) { 446 return RmiClientInterceptorUtils.convertRmiAccessException(method, ex, isConnectFailure(ex), getJndiName()); 447 } 448 449}