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