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