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.RemoteInvocationFailureException;
039: import org.springframework.remoting.RemoteLookupFailureException;
040: import org.springframework.remoting.support.RemoteInvocationBasedAccessor;
041: import 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</code>
050: * or throw <code>java.rmi.RemoteException</code>. 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: */
070: public 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: * Set whether to look up the RMI stub on startup. Default is "true".
087: * <p>Can be turned off to allow for late start of the RMI server.
088: * In this case, the RMI stub will be fetched on first access.
089: * @see #setCacheStub
090: */
091: public void setLookupStubOnStartup(boolean lookupStubOnStartup) {
092: this .lookupStubOnStartup = lookupStubOnStartup;
093: }
094:
095: /**
096: * Set whether to cache the RMI stub once it has been located.
097: * Default is "true".
098: * <p>Can be turned off to allow for hot restart of the RMI server.
099: * In this case, the RMI stub will be fetched for each invocation.
100: * @see #setLookupStubOnStartup
101: */
102: public void setCacheStub(boolean cacheStub) {
103: this .cacheStub = cacheStub;
104: }
105:
106: /**
107: * Set whether to refresh the RMI stub on connect failure.
108: * Default is "false".
109: * <p>Can be turned on to allow for hot restart of the RMI server.
110: * If a cached RMI stub throws an RMI exception that indicates a
111: * remote connect failure, a fresh proxy will be fetched and the
112: * invocation will be retried.
113: * @see java.rmi.ConnectException
114: * @see java.rmi.ConnectIOException
115: * @see java.rmi.NoSuchObjectException
116: */
117: public void setRefreshStubOnConnectFailure(
118: 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(
128: RMIClientSocketFactory registryClientSocketFactory) {
129: this .registryClientSocketFactory = registryClientSocketFactory;
130: }
131:
132: public void afterPropertiesSet() {
133: super .afterPropertiesSet();
134: prepare();
135: }
136:
137: /**
138: * Fetches RMI stub on startup, if necessary.
139: * @throws RemoteLookupFailureException if RMI stub creation failed
140: * @see #setLookupStubOnStartup
141: * @see #lookupStub
142: */
143: public void prepare() throws RemoteLookupFailureException {
144: // Cache RMI stub on initialization?
145: if (this .lookupStubOnStartup) {
146: Remote remoteObj = lookupStub();
147: if (logger.isDebugEnabled()) {
148: if (remoteObj instanceof RmiInvocationHandler) {
149: logger.debug("RMI stub [" + getServiceUrl()
150: + "] is an RMI invoker");
151: } else if (getServiceInterface() != null) {
152: boolean isImpl = getServiceInterface().isInstance(
153: remoteObj);
154: logger.debug("Using service interface ["
155: + getServiceInterface().getName()
156: + "] for RMI stub [" + getServiceUrl()
157: + "] - " + (!isImpl ? "not " : "")
158: + "directly implemented");
159: }
160: }
161: if (this .cacheStub) {
162: this .cachedStub = remoteObj;
163: }
164: }
165: }
166:
167: /**
168: * Create the RMI stub, typically by looking it up.
169: * <p>Called on interceptor initialization if "cacheStub" is "true";
170: * else called for each invocation by {@link #getStub()}.
171: * <p>The default implementation looks up the service URL via
172: * <code>java.rmi.Naming</code>. This can be overridden in subclasses.
173: * @return the RMI stub to store in this interceptor
174: * @throws RemoteLookupFailureException if RMI stub creation failed
175: * @see #setCacheStub
176: * @see java.rmi.Naming#lookup
177: */
178: protected Remote lookupStub() throws RemoteLookupFailureException {
179: try {
180: Remote stub = null;
181: if (this .registryClientSocketFactory != null) {
182: // RMIClientSocketFactory specified for registry access.
183: // Unfortunately, due to RMI API limitations, this means
184: // that we need to parse the RMI URL ourselves and perform
185: // straight LocateRegistry.getRegistry/Registry.lookup calls.
186: URL url = new URL(null, getServiceUrl(),
187: new DummyURLStreamHandler());
188: String protocol = url.getProtocol();
189: if (protocol != null && !"rmi".equals(protocol)) {
190: throw new MalformedURLException(
191: "Invalid URL scheme '" + protocol + "'");
192: }
193: String host = url.getHost();
194: int port = url.getPort();
195: String name = url.getPath();
196: if (name != null && name.startsWith("/")) {
197: name = name.substring(1);
198: }
199: Registry registry = LocateRegistry.getRegistry(host,
200: port, this .registryClientSocketFactory);
201: stub = registry.lookup(name);
202: } else {
203: // Can proceed with standard RMI lookup API...
204: stub = Naming.lookup(getServiceUrl());
205: }
206: if (logger.isDebugEnabled()) {
207: logger.debug("Located RMI stub with URL ["
208: + getServiceUrl() + "]");
209: }
210: return stub;
211: } catch (MalformedURLException ex) {
212: throw new RemoteLookupFailureException("Service URL ["
213: + getServiceUrl() + "] is invalid", ex);
214: } catch (NotBoundException ex) {
215: throw new RemoteLookupFailureException(
216: "Could not find RMI service [" + getServiceUrl()
217: + "] in RMI registry", ex);
218: } catch (RemoteException ex) {
219: throw new RemoteLookupFailureException(
220: "Lookup of RMI stub failed", ex);
221: }
222: }
223:
224: /**
225: * Return the RMI stub to use. Called for each invocation.
226: * <p>The default implementation returns the stub created on initialization,
227: * if any. Else, it invokes {@link #lookupStub} to get a new stub for
228: * each invocation. This can be overridden in subclasses, for example in
229: * order to cache a stub for a given amount of time before recreating it,
230: * or to test the stub whether it is still alive.
231: * @return the RMI stub to use for an invocation
232: * @throws RemoteLookupFailureException if RMI stub creation failed
233: * @see #lookupStub
234: */
235: protected Remote getStub() throws RemoteLookupFailureException {
236: if (!this .cacheStub
237: || (this .lookupStubOnStartup && !this .refreshStubOnConnectFailure)) {
238: return (this .cachedStub != null ? this .cachedStub
239: : lookupStub());
240: } else {
241: synchronized (this .stubMonitor) {
242: if (this .cachedStub == null) {
243: this .cachedStub = lookupStub();
244: }
245: return this .cachedStub;
246: }
247: }
248: }
249:
250: /**
251: * Fetches an RMI stub and delegates to <code>doInvoke</code>.
252: * If configured to refresh on connect failure, it will call
253: * {@link #refreshAndRetry} on corresponding RMI exceptions.
254: * @see #getStub
255: * @see #doInvoke(MethodInvocation, Remote)
256: * @see #refreshAndRetry
257: * @see java.rmi.ConnectException
258: * @see java.rmi.ConnectIOException
259: * @see java.rmi.NoSuchObjectException
260: */
261: public Object invoke(MethodInvocation invocation) throws Throwable {
262: Remote stub = getStub();
263: try {
264: return doInvoke(invocation, stub);
265: } catch (RemoteConnectFailureException ex) {
266: return handleRemoteConnectFailure(invocation, ex);
267: } catch (RemoteException ex) {
268: if (isConnectFailure(ex)) {
269: return handleRemoteConnectFailure(invocation, ex);
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, if failed too.
295: */
296: private Object handleRemoteConnectFailure(
297: MethodInvocation invocation, Exception ex) throws Throwable {
298: if (this .refreshStubOnConnectFailure) {
299: if (logger.isDebugEnabled()) {
300: logger.debug("Could not connect to RMI service ["
301: + getServiceUrl() + "] - retrying", ex);
302: } else if (logger.isWarnEnabled()) {
303: logger.warn("Could not connect to RMI service ["
304: + getServiceUrl() + "] - retrying");
305: }
306: return refreshAndRetry(invocation);
307: } else {
308: throw ex;
309: }
310: }
311:
312: /**
313: * Refresh the RMI stub and retry the given invocation.
314: * Called by invoke on connect failure.
315: * @param invocation the AOP method invocation
316: * @return the invocation result, if any
317: * @throws Throwable in case of invocation failure
318: * @see #invoke
319: */
320: protected Object refreshAndRetry(MethodInvocation invocation)
321: throws Throwable {
322: Remote freshStub = null;
323: synchronized (this .stubMonitor) {
324: this .cachedStub = null;
325: freshStub = lookupStub();
326: if (this .cacheStub) {
327: this .cachedStub = freshStub;
328: }
329: }
330: return doInvoke(invocation, freshStub);
331: }
332:
333: /**
334: * Perform the given invocation on the given RMI stub.
335: * @param invocation the AOP method invocation
336: * @param stub the RMI stub to invoke
337: * @return the invocation result, if any
338: * @throws Throwable in case of invocation failure
339: */
340: protected Object doInvoke(MethodInvocation invocation, Remote stub)
341: throws Throwable {
342: if (stub instanceof RmiInvocationHandler) {
343: // RMI invoker
344: try {
345: return doInvoke(invocation, (RmiInvocationHandler) stub);
346: } catch (RemoteException ex) {
347: throw RmiClientInterceptorUtils
348: .convertRmiAccessException(invocation
349: .getMethod(), ex, isConnectFailure(ex),
350: getServiceUrl());
351: } catch (InvocationTargetException ex) {
352: Throwable exToThrow = ex.getTargetException();
353: RemoteInvocationUtils
354: .fillInClientStackTraceIfPossible(exToThrow);
355: throw exToThrow;
356: } catch (Throwable ex) {
357: throw new RemoteInvocationFailureException(
358: "Invocation of method ["
359: + invocation.getMethod()
360: + "] failed in RMI service ["
361: + getServiceUrl() + "]", ex);
362: }
363: } else {
364: // traditional RMI stub
365: try {
366: return RmiClientInterceptorUtils.doInvoke(invocation,
367: stub);
368: } catch (InvocationTargetException ex) {
369: Throwable targetEx = ex.getTargetException();
370: if (targetEx instanceof RemoteException) {
371: RemoteException rex = (RemoteException) targetEx;
372: throw RmiClientInterceptorUtils
373: .convertRmiAccessException(invocation
374: .getMethod(), rex,
375: isConnectFailure(rex),
376: getServiceUrl());
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: protected Object doInvoke(MethodInvocation methodInvocation,
397: RmiInvocationHandler invocationHandler)
398: throws RemoteException, NoSuchMethodException,
399: IllegalAccessException, InvocationTargetException {
400:
401: if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
402: return "RMI invoker proxy for service URL ["
403: + getServiceUrl() + "]";
404: }
405:
406: return invocationHandler
407: .invoke(createRemoteInvocation(methodInvocation));
408: }
409:
410: /**
411: * Dummy URLStreamHandler that's just specified to suppress the standard
412: * <code>java.net.URL</code> URLStreamHandler lookup, to be able to
413: * use the standard URL class for parsing "rmi:..." URLs.
414: */
415: private static class DummyURLStreamHandler extends URLStreamHandler {
416:
417: protected URLConnection openConnection(URL url)
418: throws IOException {
419: throw new UnsupportedOperationException();
420: }
421: }
422:
423: }
|