001/* 002 * Copyright 2002-2012 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.io.IOException; 020import java.lang.reflect.InvocationTargetException; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.net.URLConnection; 024import java.net.URLStreamHandler; 025import java.rmi.Naming; 026import java.rmi.NotBoundException; 027import java.rmi.Remote; 028import java.rmi.RemoteException; 029import java.rmi.registry.LocateRegistry; 030import java.rmi.registry.Registry; 031import java.rmi.server.RMIClientSocketFactory; 032 033import org.aopalliance.intercept.MethodInterceptor; 034import org.aopalliance.intercept.MethodInvocation; 035 036import org.springframework.aop.support.AopUtils; 037import org.springframework.lang.Nullable; 038import org.springframework.remoting.RemoteConnectFailureException; 039import org.springframework.remoting.RemoteInvocationFailureException; 040import org.springframework.remoting.RemoteLookupFailureException; 041import org.springframework.remoting.support.RemoteInvocationBasedAccessor; 042import org.springframework.remoting.support.RemoteInvocationUtils; 043 044/** 045 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing conventional 046 * RMI services or RMI invokers. The service URL must be a valid RMI URL 047 * (e.g. "rmi://localhost:1099/myservice"). 048 * 049 * <p>RMI invokers work at the RmiInvocationHandler level, needing only one stub for 050 * any service. Service interfaces do not have to extend {@code java.rmi.Remote} 051 * or throw {@code java.rmi.RemoteException}. Spring's unchecked 052 * RemoteAccessException will be thrown on remote invocation failure. 053 * Of course, in and out parameters have to be serializable. 054 * 055 * <p>With conventional RMI services, this invoker is typically used with the RMI 056 * service interface. Alternatively, this invoker can also proxy a remote RMI service 057 * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI 058 * service methods but does not declare RemoteExceptions. In the latter case, 059 * RemoteExceptions thrown by the RMI stub will automatically get converted to 060 * Spring's unchecked RemoteAccessException. 061 * 062 * @author Juergen Hoeller 063 * @since 29.09.2003 064 * @see RmiServiceExporter 065 * @see RmiProxyFactoryBean 066 * @see RmiInvocationHandler 067 * @see org.springframework.remoting.RemoteAccessException 068 * @see java.rmi.RemoteException 069 * @see java.rmi.Remote 070 */ 071public class RmiClientInterceptor extends RemoteInvocationBasedAccessor 072 implements MethodInterceptor { 073 074 private boolean lookupStubOnStartup = true; 075 076 private boolean cacheStub = true; 077 078 private boolean refreshStubOnConnectFailure = false; 079 080 private RMIClientSocketFactory registryClientSocketFactory; 081 082 private Remote cachedStub; 083 084 private final Object stubMonitor = new Object(); 085 086 087 /** 088 * Set whether to look up the RMI stub on startup. Default is "true". 089 * <p>Can be turned off to allow for late start of the RMI server. 090 * In this case, the RMI stub will be fetched on first access. 091 * @see #setCacheStub 092 */ 093 public void setLookupStubOnStartup(boolean lookupStubOnStartup) { 094 this.lookupStubOnStartup = lookupStubOnStartup; 095 } 096 097 /** 098 * Set whether to cache the RMI stub once it has been located. 099 * Default is "true". 100 * <p>Can be turned off to allow for hot restart of the RMI server. 101 * In this case, the RMI stub will be fetched for each invocation. 102 * @see #setLookupStubOnStartup 103 */ 104 public void setCacheStub(boolean cacheStub) { 105 this.cacheStub = cacheStub; 106 } 107 108 /** 109 * Set whether to refresh the RMI stub on connect failure. 110 * Default is "false". 111 * <p>Can be turned on to allow for hot restart of the RMI server. 112 * If a cached RMI stub throws an RMI exception that indicates a 113 * remote connect failure, a fresh proxy will be fetched and the 114 * invocation will be retried. 115 * @see java.rmi.ConnectException 116 * @see java.rmi.ConnectIOException 117 * @see java.rmi.NoSuchObjectException 118 */ 119 public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { 120 this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; 121 } 122 123 /** 124 * Set a custom RMI client socket factory to use for accessing the RMI registry. 125 * @see java.rmi.server.RMIClientSocketFactory 126 * @see java.rmi.registry.LocateRegistry#getRegistry(String, int, RMIClientSocketFactory) 127 */ 128 public void setRegistryClientSocketFactory(RMIClientSocketFactory registryClientSocketFactory) { 129 this.registryClientSocketFactory = registryClientSocketFactory; 130 } 131 132 133 @Override 134 public void afterPropertiesSet() { 135 super.afterPropertiesSet(); 136 prepare(); 137 } 138 139 /** 140 * Fetches RMI stub on startup, if necessary. 141 * @throws RemoteLookupFailureException if RMI stub creation failed 142 * @see #setLookupStubOnStartup 143 * @see #lookupStub 144 */ 145 public void prepare() throws RemoteLookupFailureException { 146 // Cache RMI stub on initialization? 147 if (this.lookupStubOnStartup) { 148 Remote remoteObj = lookupStub(); 149 if (logger.isDebugEnabled()) { 150 if (remoteObj instanceof RmiInvocationHandler) { 151 logger.debug("RMI stub [" + getServiceUrl() + "] is an RMI invoker"); 152 } 153 else if (getServiceInterface() != null) { 154 boolean isImpl = getServiceInterface().isInstance(remoteObj); 155 logger.debug("Using service interface [" + getServiceInterface().getName() + 156 "] for RMI stub [" + getServiceUrl() + "] - " + 157 (!isImpl ? "not " : "") + "directly implemented"); 158 } 159 } 160 if (this.cacheStub) { 161 this.cachedStub = remoteObj; 162 } 163 } 164 } 165 166 /** 167 * Create the RMI stub, typically by looking it up. 168 * <p>Called on interceptor initialization if "cacheStub" is "true"; 169 * else called for each invocation by {@link #getStub()}. 170 * <p>The default implementation looks up the service URL via 171 * {@code java.rmi.Naming}. This can be overridden in subclasses. 172 * @return the RMI stub to store in this interceptor 173 * @throws RemoteLookupFailureException if RMI stub creation failed 174 * @see #setCacheStub 175 * @see java.rmi.Naming#lookup 176 */ 177 protected Remote lookupStub() throws RemoteLookupFailureException { 178 try { 179 Remote stub = null; 180 if (this.registryClientSocketFactory != null) { 181 // RMIClientSocketFactory specified for registry access. 182 // Unfortunately, due to RMI API limitations, this means 183 // that we need to parse the RMI URL ourselves and perform 184 // straight LocateRegistry.getRegistry/Registry.lookup calls. 185 URL url = new URL(null, getServiceUrl(), new DummyURLStreamHandler()); 186 String protocol = url.getProtocol(); 187 if (protocol != null && !"rmi".equals(protocol)) { 188 throw new MalformedURLException("Invalid URL scheme '" + protocol + "'"); 189 } 190 String host = url.getHost(); 191 int port = url.getPort(); 192 String name = url.getPath(); 193 if (name != null && name.startsWith("/")) { 194 name = name.substring(1); 195 } 196 Registry registry = LocateRegistry.getRegistry(host, port, this.registryClientSocketFactory); 197 stub = registry.lookup(name); 198 } 199 else { 200 // Can proceed with standard RMI lookup API... 201 stub = Naming.lookup(getServiceUrl()); 202 } 203 if (logger.isDebugEnabled()) { 204 logger.debug("Located RMI stub with URL [" + getServiceUrl() + "]"); 205 } 206 return stub; 207 } 208 catch (MalformedURLException ex) { 209 throw new RemoteLookupFailureException("Service URL [" + getServiceUrl() + "] is invalid", ex); 210 } 211 catch (NotBoundException ex) { 212 throw new RemoteLookupFailureException( 213 "Could not find RMI service [" + getServiceUrl() + "] in RMI registry", ex); 214 } 215 catch (RemoteException ex) { 216 throw new RemoteLookupFailureException("Lookup of RMI stub failed", ex); 217 } 218 } 219 220 /** 221 * Return the RMI stub to use. Called for each invocation. 222 * <p>The default implementation returns the stub created on initialization, 223 * if any. Else, it invokes {@link #lookupStub} to get a new stub for 224 * each invocation. This can be overridden in subclasses, for example in 225 * order to cache a stub for a given amount of time before recreating it, 226 * or to test the stub whether it is still alive. 227 * @return the RMI stub to use for an invocation 228 * @throws RemoteLookupFailureException if RMI stub creation failed 229 * @see #lookupStub 230 */ 231 protected Remote getStub() throws RemoteLookupFailureException { 232 if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { 233 return (this.cachedStub != null ? this.cachedStub : lookupStub()); 234 } 235 else { 236 synchronized (this.stubMonitor) { 237 if (this.cachedStub == null) { 238 this.cachedStub = lookupStub(); 239 } 240 return this.cachedStub; 241 } 242 } 243 } 244 245 246 /** 247 * Fetches an RMI stub and delegates to {@code doInvoke}. 248 * If configured to refresh on connect failure, it will call 249 * {@link #refreshAndRetry} on corresponding RMI exceptions. 250 * @see #getStub 251 * @see #doInvoke(MethodInvocation, Remote) 252 * @see #refreshAndRetry 253 * @see java.rmi.ConnectException 254 * @see java.rmi.ConnectIOException 255 * @see java.rmi.NoSuchObjectException 256 */ 257 @Override 258 public Object invoke(MethodInvocation invocation) throws Throwable { 259 Remote stub = getStub(); 260 try { 261 return doInvoke(invocation, stub); 262 } 263 catch (RemoteConnectFailureException ex) { 264 return handleRemoteConnectFailure(invocation, ex); 265 } 266 catch (RemoteException ex) { 267 if (isConnectFailure(ex)) { 268 return handleRemoteConnectFailure(invocation, ex); 269 } 270 else { 271 throw ex; 272 } 273 } 274 } 275 276 /** 277 * Determine whether the given RMI exception indicates a connect failure. 278 * <p>The default implementation delegates to 279 * {@link RmiClientInterceptorUtils#isConnectFailure}. 280 * @param ex the RMI exception to check 281 * @return whether the exception should be treated as connect failure 282 */ 283 protected boolean isConnectFailure(RemoteException ex) { 284 return RmiClientInterceptorUtils.isConnectFailure(ex); 285 } 286 287 /** 288 * Refresh the stub and retry the remote invocation if necessary. 289 * <p>If not configured to refresh on connect failure, this method 290 * simply rethrows the original exception. 291 * @param invocation the invocation that failed 292 * @param ex the exception raised on remote invocation 293 * @return the result value of the new invocation, if succeeded 294 * @throws Throwable an exception raised by the new invocation, 295 * if it failed as well 296 * @see #setRefreshStubOnConnectFailure 297 * @see #doInvoke 298 */ 299 @Nullable 300 private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { 301 if (this.refreshStubOnConnectFailure) { 302 String msg = "Could not connect to RMI service [" + getServiceUrl() + "] - retrying"; 303 if (logger.isDebugEnabled()) { 304 logger.warn(msg, ex); 305 } 306 else if (logger.isWarnEnabled()) { 307 logger.warn(msg); 308 } 309 return refreshAndRetry(invocation); 310 } 311 else { 312 throw ex; 313 } 314 } 315 316 /** 317 * Refresh the RMI stub and retry the given invocation. 318 * Called by invoke on connect failure. 319 * @param invocation the AOP method invocation 320 * @return the invocation result, if any 321 * @throws Throwable in case of invocation failure 322 * @see #invoke 323 */ 324 @Nullable 325 protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { 326 Remote freshStub = null; 327 synchronized (this.stubMonitor) { 328 this.cachedStub = null; 329 freshStub = lookupStub(); 330 if (this.cacheStub) { 331 this.cachedStub = freshStub; 332 } 333 } 334 return doInvoke(invocation, freshStub); 335 } 336 337 /** 338 * Perform the given invocation on the given RMI stub. 339 * @param invocation the AOP method invocation 340 * @param stub the RMI stub to invoke 341 * @return the invocation result, if any 342 * @throws Throwable in case of invocation failure 343 */ 344 @Nullable 345 protected Object doInvoke(MethodInvocation invocation, Remote stub) throws Throwable { 346 if (stub instanceof RmiInvocationHandler) { 347 // RMI invoker 348 try { 349 return doInvoke(invocation, (RmiInvocationHandler) stub); 350 } 351 catch (RemoteException ex) { 352 throw RmiClientInterceptorUtils.convertRmiAccessException( 353 invocation.getMethod(), ex, isConnectFailure(ex), getServiceUrl()); 354 } 355 catch (InvocationTargetException ex) { 356 Throwable exToThrow = ex.getTargetException(); 357 RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); 358 throw exToThrow; 359 } 360 catch (Throwable ex) { 361 throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + 362 "] failed in RMI service [" + getServiceUrl() + "]", ex); 363 } 364 } 365 else { 366 // traditional RMI stub 367 try { 368 return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); 369 } 370 catch (InvocationTargetException ex) { 371 Throwable targetEx = ex.getTargetException(); 372 if (targetEx instanceof RemoteException) { 373 RemoteException rex = (RemoteException) targetEx; 374 throw RmiClientInterceptorUtils.convertRmiAccessException( 375 invocation.getMethod(), rex, isConnectFailure(rex), getServiceUrl()); 376 } 377 else { 378 throw targetEx; 379 } 380 } 381 } 382 } 383 384 /** 385 * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. 386 * <p>The default implementation delegates to {@link #createRemoteInvocation}. 387 * @param methodInvocation the current AOP method invocation 388 * @param invocationHandler the RmiInvocationHandler to apply the invocation to 389 * @return the invocation result 390 * @throws RemoteException in case of communication errors 391 * @throws NoSuchMethodException if the method name could not be resolved 392 * @throws IllegalAccessException if the method could not be accessed 393 * @throws InvocationTargetException if the method invocation resulted in an exception 394 * @see org.springframework.remoting.support.RemoteInvocation 395 */ 396 @Nullable 397 protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) 398 throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { 399 400 if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { 401 return "RMI invoker proxy for service URL [" + getServiceUrl() + "]"; 402 } 403 404 return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); 405 } 406 407 408 /** 409 * Dummy URLStreamHandler that's just specified to suppress the standard 410 * {@code java.net.URL} URLStreamHandler lookup, to be able to 411 * use the standard URL class for parsing "rmi:..." URLs. 412 */ 413 private static class DummyURLStreamHandler extends URLStreamHandler { 414 415 @Override 416 protected URLConnection openConnection(URL url) throws IOException { 417 throw new UnsupportedOperationException(); 418 } 419 } 420 421}