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}