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