001/*
002 * Copyright 2002-2018 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.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.rmi.RemoteException;
022
023import javax.naming.Context;
024import javax.naming.NamingException;
025
026import org.aopalliance.intercept.MethodInterceptor;
027import org.aopalliance.intercept.MethodInvocation;
028
029import org.springframework.aop.support.AopUtils;
030import org.springframework.beans.factory.InitializingBean;
031import org.springframework.jndi.JndiObjectLocator;
032import org.springframework.lang.Nullable;
033import org.springframework.remoting.RemoteConnectFailureException;
034import org.springframework.remoting.RemoteInvocationFailureException;
035import org.springframework.remoting.RemoteLookupFailureException;
036import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
037import org.springframework.remoting.support.RemoteInvocation;
038import org.springframework.remoting.support.RemoteInvocationFactory;
039import org.springframework.util.Assert;
040
041/**
042 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI services
043 * from JNDI. Typically used for RMI-IIOP but can also be used for EJB home objects
044 * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup,
045 * this accessor also performs narrowing through PortableRemoteObject.
046 *
047 * <p>With conventional RMI services, this invoker is typically used with the RMI
048 * service interface. Alternatively, this invoker can also proxy a remote RMI service
049 * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI
050 * service methods but does not declare RemoteExceptions. In the latter case,
051 * RemoteExceptions thrown by the RMI stub will automatically get converted to
052 * Spring's unchecked RemoteAccessException.
053 *
054 * <p>The JNDI environment can be specified as "jndiEnvironment" property,
055 * or be configured in a {@code jndi.properties} file or as system properties.
056 * For example:
057 *
058 * <pre class="code">&lt;property name="jndiEnvironment"&gt;
059 *       &lt;props>
060 *               &lt;prop key="java.naming.factory.initial"&gt;com.sun.jndi.cosnaming.CNCtxFactory&lt;/prop&gt;
061 *               &lt;prop key="java.naming.provider.url"&gt;iiop://localhost:1050&lt;/prop&gt;
062 *       &lt;/props&gt;
063 * &lt;/property&gt;</pre>
064 *
065 * @author Juergen Hoeller
066 * @since 1.1
067 * @see #setJndiTemplate
068 * @see #setJndiEnvironment
069 * @see #setJndiName
070 * @see JndiRmiServiceExporter
071 * @see JndiRmiProxyFactoryBean
072 * @see org.springframework.remoting.RemoteAccessException
073 * @see java.rmi.RemoteException
074 * @see java.rmi.Remote
075 */
076public class JndiRmiClientInterceptor extends JndiObjectLocator implements MethodInterceptor, InitializingBean {
077
078        private Class<?> serviceInterface;
079
080        private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory();
081
082        private boolean lookupStubOnStartup = true;
083
084        private boolean cacheStub = true;
085
086        private boolean refreshStubOnConnectFailure = false;
087
088        private boolean exposeAccessContext = false;
089
090        private Object cachedStub;
091
092        private final Object stubMonitor = new Object();
093
094
095        /**
096         * Set the interface of the service to access.
097         * The interface must be suitable for the particular service and remoting tool.
098         * <p>Typically required to be able to create a suitable service proxy,
099         * but can also be optional if the lookup returns a typed stub.
100         */
101        public void setServiceInterface(Class<?> serviceInterface) {
102                Assert.notNull(serviceInterface, "'serviceInterface' must not be null");
103                Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface");
104                this.serviceInterface = serviceInterface;
105        }
106
107        /**
108         * Return the interface of the service to access.
109         */
110        public Class<?> getServiceInterface() {
111                return this.serviceInterface;
112        }
113
114        /**
115         * Set the RemoteInvocationFactory to use for this accessor.
116         * Default is a {@link DefaultRemoteInvocationFactory}.
117         * <p>A custom invocation factory can add further context information
118         * to the invocation, for example user credentials.
119         */
120        public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
121                this.remoteInvocationFactory = remoteInvocationFactory;
122        }
123
124        /**
125         * Return the RemoteInvocationFactory used by this accessor.
126         */
127        public RemoteInvocationFactory getRemoteInvocationFactory() {
128                return this.remoteInvocationFactory;
129        }
130
131        /**
132         * Set whether to look up the RMI stub on startup. Default is "true".
133         * <p>Can be turned off to allow for late start of the RMI server.
134         * In this case, the RMI stub will be fetched on first access.
135         * @see #setCacheStub
136         */
137        public void setLookupStubOnStartup(boolean lookupStubOnStartup) {
138                this.lookupStubOnStartup = lookupStubOnStartup;
139        }
140
141        /**
142         * Set whether to cache the RMI stub once it has been located.
143         * Default is "true".
144         * <p>Can be turned off to allow for hot restart of the RMI server.
145         * In this case, the RMI stub will be fetched for each invocation.
146         * @see #setLookupStubOnStartup
147         */
148        public void setCacheStub(boolean cacheStub) {
149                this.cacheStub = cacheStub;
150        }
151
152        /**
153         * Set whether to refresh the RMI stub on connect failure.
154         * Default is "false".
155         * <p>Can be turned on to allow for hot restart of the RMI server.
156         * If a cached RMI stub throws an RMI exception that indicates a
157         * remote connect failure, a fresh proxy will be fetched and the
158         * invocation will be retried.
159         * @see java.rmi.ConnectException
160         * @see java.rmi.ConnectIOException
161         * @see java.rmi.NoSuchObjectException
162         */
163        public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) {
164                this.refreshStubOnConnectFailure = refreshStubOnConnectFailure;
165        }
166
167        /**
168         * Set whether to expose the JNDI environment context for all access to the target
169         * RMI stub, i.e. for all method invocations on the exposed object reference.
170         * <p>Default is "false", i.e. to only expose the JNDI context for object lookup.
171         * Switch this flag to "true" in order to expose the JNDI environment (including
172         * the authorization context) for each RMI invocation, as needed by WebLogic
173         * for RMI stubs with authorization requirements.
174         */
175        public void setExposeAccessContext(boolean exposeAccessContext) {
176                this.exposeAccessContext = exposeAccessContext;
177        }
178
179
180        @Override
181        public void afterPropertiesSet() throws NamingException {
182                super.afterPropertiesSet();
183                prepare();
184        }
185
186        /**
187         * Fetches the RMI stub on startup, if necessary.
188         * @throws RemoteLookupFailureException if RMI stub creation failed
189         * @see #setLookupStubOnStartup
190         * @see #lookupStub
191         */
192        public void prepare() throws RemoteLookupFailureException {
193                // Cache RMI stub on initialization?
194                if (this.lookupStubOnStartup) {
195                        Object remoteObj = lookupStub();
196                        if (logger.isDebugEnabled()) {
197                                if (remoteObj instanceof RmiInvocationHandler) {
198                                        logger.debug("JNDI RMI object [" + getJndiName() + "] is an RMI invoker");
199                                }
200                                else if (getServiceInterface() != null) {
201                                        boolean isImpl = getServiceInterface().isInstance(remoteObj);
202                                        logger.debug("Using service interface [" + getServiceInterface().getName() +
203                                                        "] for JNDI RMI object [" + getJndiName() + "] - " +
204                                                        (!isImpl ? "not " : "") + "directly implemented");
205                                }
206                        }
207                        if (this.cacheStub) {
208                                this.cachedStub = remoteObj;
209                        }
210                }
211        }
212
213        /**
214         * Create the RMI stub, typically by looking it up.
215         * <p>Called on interceptor initialization if "cacheStub" is "true";
216         * else called for each invocation by {@link #getStub()}.
217         * <p>The default implementation retrieves the service from the
218         * JNDI environment. This can be overridden in subclasses.
219         * @return the RMI stub to store in this interceptor
220         * @throws RemoteLookupFailureException if RMI stub creation failed
221         * @see #setCacheStub
222         * @see #lookup
223         */
224        protected Object lookupStub() throws RemoteLookupFailureException {
225                try {
226                        return lookup();
227                }
228                catch (NamingException ex) {
229                        throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex);
230                }
231        }
232
233        /**
234         * Return the RMI stub to use. Called for each invocation.
235         * <p>The default implementation returns the stub created on initialization,
236         * if any. Else, it invokes {@link #lookupStub} to get a new stub for
237         * each invocation. This can be overridden in subclasses, for example in
238         * order to cache a stub for a given amount of time before recreating it,
239         * or to test the stub whether it is still alive.
240         * @return the RMI stub to use for an invocation
241         * @throws NamingException if stub creation failed
242         * @throws RemoteLookupFailureException if RMI stub creation failed
243         */
244        protected Object getStub() throws NamingException, RemoteLookupFailureException {
245                if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) {
246                        return (this.cachedStub != null ? this.cachedStub : lookupStub());
247                }
248                else {
249                        synchronized (this.stubMonitor) {
250                                if (this.cachedStub == null) {
251                                        this.cachedStub = lookupStub();
252                                }
253                                return this.cachedStub;
254                        }
255                }
256        }
257
258
259        /**
260         * Fetches an RMI stub and delegates to {@link #doInvoke}.
261         * If configured to refresh on connect failure, it will call
262         * {@link #refreshAndRetry} on corresponding RMI exceptions.
263         * @see #getStub
264         * @see #doInvoke
265         * @see #refreshAndRetry
266         * @see java.rmi.ConnectException
267         * @see java.rmi.ConnectIOException
268         * @see java.rmi.NoSuchObjectException
269         */
270        @Override
271        public Object invoke(MethodInvocation invocation) throws Throwable {
272                Object stub;
273                try {
274                        stub = getStub();
275                }
276                catch (NamingException ex) {
277                        throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex);
278                }
279
280                Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null);
281                try {
282                        return doInvoke(invocation, stub);
283                }
284                catch (RemoteConnectFailureException ex) {
285                        return handleRemoteConnectFailure(invocation, ex);
286                }
287                catch (RemoteException ex) {
288                        if (isConnectFailure(ex)) {
289                                return handleRemoteConnectFailure(invocation, ex);
290                        }
291                        else {
292                                throw ex;
293                        }
294                }
295                finally {
296                        getJndiTemplate().releaseContext(ctx);
297                }
298        }
299
300        /**
301         * Determine whether the given RMI exception indicates a connect failure.
302         * <p>The default implementation delegates to
303         * {@link RmiClientInterceptorUtils#isConnectFailure}.
304         * @param ex the RMI exception to check
305         * @return whether the exception should be treated as connect failure
306         */
307        protected boolean isConnectFailure(RemoteException ex) {
308                return RmiClientInterceptorUtils.isConnectFailure(ex);
309        }
310
311        /**
312         * Refresh the stub and retry the remote invocation if necessary.
313         * <p>If not configured to refresh on connect failure, this method
314         * simply rethrows the original exception.
315         * @param invocation the invocation that failed
316         * @param ex the exception raised on remote invocation
317         * @return the result value of the new invocation, if succeeded
318         * @throws Throwable an exception raised by the new invocation, if failed too.
319         */
320        private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable {
321                if (this.refreshStubOnConnectFailure) {
322                        if (logger.isDebugEnabled()) {
323                                logger.debug("Could not connect to RMI service [" + getJndiName() + "] - retrying", ex);
324                        }
325                        else if (logger.isInfoEnabled()) {
326                                logger.info("Could not connect to RMI service [" + getJndiName() + "] - retrying");
327                        }
328                        return refreshAndRetry(invocation);
329                }
330                else {
331                        throw ex;
332                }
333        }
334
335        /**
336         * Refresh the RMI stub and retry the given invocation.
337         * Called by invoke on connect failure.
338         * @param invocation the AOP method invocation
339         * @return the invocation result, if any
340         * @throws Throwable in case of invocation failure
341         * @see #invoke
342         */
343        @Nullable
344        protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable {
345                Object freshStub;
346                synchronized (this.stubMonitor) {
347                        this.cachedStub = null;
348                        freshStub = lookupStub();
349                        if (this.cacheStub) {
350                                this.cachedStub = freshStub;
351                        }
352                }
353                return doInvoke(invocation, freshStub);
354        }
355
356
357        /**
358         * Perform the given invocation on the given RMI stub.
359         * @param invocation the AOP method invocation
360         * @param stub the RMI stub to invoke
361         * @return the invocation result, if any
362         * @throws Throwable in case of invocation failure
363         */
364        @Nullable
365        protected Object doInvoke(MethodInvocation invocation, Object stub) throws Throwable {
366                if (stub instanceof RmiInvocationHandler) {
367                        // RMI invoker
368                        try {
369                                return doInvoke(invocation, (RmiInvocationHandler) stub);
370                        }
371                        catch (RemoteException ex) {
372                                throw convertRmiAccessException(ex, invocation.getMethod());
373                        }
374                        catch (InvocationTargetException ex) {
375                                throw ex.getTargetException();
376                        }
377                        catch (Throwable ex) {
378                                throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() +
379                                                "] failed in RMI service [" + getJndiName() + "]", ex);
380                        }
381                }
382                else {
383                        // traditional RMI stub
384                        try {
385                                return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub);
386                        }
387                        catch (InvocationTargetException ex) {
388                                Throwable targetEx = ex.getTargetException();
389                                if (targetEx instanceof RemoteException) {
390                                        throw convertRmiAccessException((RemoteException) targetEx, invocation.getMethod());
391                                }
392                                else {
393                                        throw targetEx;
394                                }
395                        }
396                }
397        }
398
399        /**
400         * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}.
401         * <p>The default implementation delegates to {@link #createRemoteInvocation}.
402         * @param methodInvocation the current AOP method invocation
403         * @param invocationHandler the RmiInvocationHandler to apply the invocation to
404         * @return the invocation result
405         * @throws RemoteException in case of communication errors
406         * @throws NoSuchMethodException if the method name could not be resolved
407         * @throws IllegalAccessException if the method could not be accessed
408         * @throws InvocationTargetException if the method invocation resulted in an exception
409         * @see org.springframework.remoting.support.RemoteInvocation
410         */
411        protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler)
412                        throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
413
414                if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
415                        return "RMI invoker proxy for service URL [" + getJndiName() + "]";
416                }
417
418                return invocationHandler.invoke(createRemoteInvocation(methodInvocation));
419        }
420
421        /**
422         * Create a new RemoteInvocation object for the given AOP method invocation.
423         * <p>The default implementation delegates to the configured
424         * {@link #setRemoteInvocationFactory RemoteInvocationFactory}.
425         * This can be overridden in subclasses in order to provide custom RemoteInvocation
426         * subclasses, containing additional invocation parameters (e.g. user credentials).
427         * <p>Note that it is preferable to build a custom RemoteInvocationFactory
428         * as a reusable strategy, instead of overriding this method.
429         * @param methodInvocation the current AOP method invocation
430         * @return the RemoteInvocation object
431         * @see RemoteInvocationFactory#createRemoteInvocation
432         */
433        protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
434                return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation);
435        }
436
437        /**
438         * Convert the given RMI RemoteException that happened during remote access
439         * to Spring's RemoteAccessException if the method signature does not declare
440         * RemoteException. Else, return the original RemoteException.
441         * @param method the invoked method
442         * @param ex the RemoteException that happened
443         * @return the exception to be thrown to the caller
444         */
445        private Exception convertRmiAccessException(RemoteException ex, Method method) {
446                return RmiClientInterceptorUtils.convertRmiAccessException(method, ex, isConnectFailure(ex), getJndiName());
447        }
448
449}