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}