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