Open Source Repository

Home /spring/spring-context-3.0.5 | Repository Home



org/springframework/remoting/rmi/JndiRmiClientInterceptor.java
/*
 * Copyright 2002-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.remoting.rmi;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;

import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.omg.CORBA.OBJECT_NOT_EXIST;
import org.omg.CORBA.SystemException;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jndi.JndiObjectLocator;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.remoting.RemoteConnectFailureException;
import org.springframework.remoting.RemoteInvocationFailureException;
import org.springframework.remoting.RemoteLookupFailureException;
import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
import org.springframework.remoting.support.RemoteInvocation;
import org.springframework.remoting.support.RemoteInvocationFactory;
import org.springframework.util.ReflectionUtils;

/**
 {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI services from JNDI.
 * Typically used for RMI-IIOP (CORBA), but can also be used for EJB home objects
 * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup,
 * this accessor also performs narrowing through PortableRemoteObject.
 *
 <p>With conventional RMI services, this invoker is typically used with the RMI
 * service interface. Alternatively, this invoker can also proxy a remote RMI service
 * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI
 * service methods but does not declare RemoteExceptions. In the latter case,
 * RemoteExceptions thrown by the RMI stub will automatically get converted to
 * Spring's unchecked RemoteAccessException.
 *
 <p>The JNDI environment can be specified as "jndiEnvironment" property,
 * or be configured in a <code>jndi.properties</code> file or as system properties.
 * For example:
 *
 <pre class="code">&lt;property name="jndiEnvironment"&gt;
 *    &lt;props>
 *     &lt;prop key="java.naming.factory.initial"&gt;com.sun.jndi.cosnaming.CNCtxFactory&lt;/prop&gt;
 *     &lt;prop key="java.naming.provider.url"&gt;iiop://localhost:1050&lt;/prop&gt;
 *   &lt;/props&gt;
 * &lt;/property&gt;</pre>
 *
 @author Juergen Hoeller
 @since 1.1
 @see #setJndiTemplate
 @see #setJndiEnvironment
 @see #setJndiName
 @see JndiRmiServiceExporter
 @see JndiRmiProxyFactoryBean
 @see org.springframework.remoting.RemoteAccessException
 @see java.rmi.RemoteException
 @see java.rmi.Remote
 @see javax.rmi.PortableRemoteObject#narrow
 */
public class JndiRmiClientInterceptor extends JndiObjectLocator implements MethodInterceptor, InitializingBean {

  private Class serviceInterface;

  private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory();

  private boolean lookupStubOnStartup = true;

  private boolean cacheStub = true;

  private boolean refreshStubOnConnectFailure = false;

  private Object cachedStub;

  private final Object stubMonitor = new Object();


  /**
   * Set the interface of the service to access.
   * The interface must be suitable for the particular service and remoting tool.
   <p>Typically required to be able to create a suitable service proxy,
   * but can also be optional if the lookup returns a typed stub.
   */
  public void setServiceInterface(Class serviceInterface) {
    if (serviceInterface != null && !serviceInterface.isInterface()) {
      throw new IllegalArgumentException("'serviceInterface' must be an interface");
    }
    this.serviceInterface = serviceInterface;
  }

  /**
   * Return the interface of the service to access.
   */
  public Class getServiceInterface() {
    return this.serviceInterface;
  }

  /**
   * Set the RemoteInvocationFactory to use for this accessor.
   * Default is a {@link DefaultRemoteInvocationFactory}.
   <p>A custom invocation factory can add further context information
   * to the invocation, for example user credentials.
   */
  public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
    this.remoteInvocationFactory = remoteInvocationFactory;
  }

  /**
   * Return the RemoteInvocationFactory used by this accessor.
   */
  public RemoteInvocationFactory getRemoteInvocationFactory() {
    return this.remoteInvocationFactory;
  }

  /**
   * Set whether to look up the RMI stub on startup. Default is "true".
   <p>Can be turned off to allow for late start of the RMI server.
   * In this case, the RMI stub will be fetched on first access.
   @see #setCacheStub
   */
  public void setLookupStubOnStartup(boolean lookupStubOnStartup) {
    this.lookupStubOnStartup = lookupStubOnStartup;
  }

  /**
   * Set whether to cache the RMI stub once it has been located.
   * Default is "true".
   <p>Can be turned off to allow for hot restart of the RMI server.
   * In this case, the RMI stub will be fetched for each invocation.
   @see #setLookupStubOnStartup
   */
  public void setCacheStub(boolean cacheStub) {
    this.cacheStub = cacheStub;
  }

  /**
   * Set whether to refresh the RMI stub on connect failure.
   * Default is "false".
   <p>Can be turned on to allow for hot restart of the RMI server.
   * If a cached RMI stub throws an RMI exception that indicates a
   * remote connect failure, a fresh proxy will be fetched and the
   * invocation will be retried.
   @see java.rmi.ConnectException
   @see java.rmi.ConnectIOException
   @see java.rmi.NoSuchObjectException
   */
  public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) {
    this.refreshStubOnConnectFailure = refreshStubOnConnectFailure;
  }


  @Override
  public void afterPropertiesSet() throws NamingException {
    super.afterPropertiesSet();
    prepare();
  }

  /**
   * Fetches the RMI stub on startup, if necessary.
   @throws RemoteLookupFailureException if RMI stub creation failed
   @see #setLookupStubOnStartup
   @see #lookupStub
   */
  public void prepare() throws RemoteLookupFailureException {
    // Cache RMI stub on initialization?
    if (this.lookupStubOnStartup) {
      Object remoteObj = lookupStub();
      if (logger.isDebugEnabled()) {
        if (remoteObj instanceof RmiInvocationHandler) {
          logger.debug("JNDI RMI object [" + getJndiName() "] is an RMI invoker");
        }
        else if (getServiceInterface() != null) {
          boolean isImpl = getServiceInterface().isInstance(remoteObj);
          logger.debug("Using service interface [" + getServiceInterface().getName() +
              "] for JNDI RMI object [" + getJndiName() "] - " +
              (!isImpl ? "not " """directly implemented");
        }
      }
      if (this.cacheStub) {
        this.cachedStub = remoteObj;
      }
    }
  }

  /**
   * Create the RMI stub, typically by looking it up.
   <p>Called on interceptor initialization if "cacheStub" is "true";
   * else called for each invocation by {@link #getStub()}.
   <p>The default implementation retrieves the service from the
   * JNDI environment. This can be overridden in subclasses.
   @return the RMI stub to store in this interceptor
   @throws RemoteLookupFailureException if RMI stub creation failed
   @see #setCacheStub
   @see #lookup
   */
  protected Object lookupStub() throws RemoteLookupFailureException {
    try {
      Object stub = lookup();
      if (getServiceInterface() != null && !(stub instanceof RmiInvocationHandler)) {
        try {
          stub = PortableRemoteObject.narrow(stub, getServiceInterface());
        }
        catch (ClassCastException ex) {
          throw new RemoteLookupFailureException(
              "Could not narrow RMI stub to service interface [" + getServiceInterface().getName() "]", ex);
        }
      }
      return stub;
    }
    catch (NamingException ex) {
      throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() "] failed", ex);
    }
  }

  /**
   * Return the RMI stub to use. Called for each invocation.
   <p>The default implementation returns the stub created on initialization,
   * if any. Else, it invokes {@link #lookupStub} to get a new stub for
   * each invocation. This can be overridden in subclasses, for example in
   * order to cache a stub for a given amount of time before recreating it,
   * or to test the stub whether it is still alive.
   @return the RMI stub to use for an invocation
   @throws NamingException if stub creation failed
   @throws RemoteLookupFailureException if RMI stub creation failed
   */
  protected Object getStub() throws NamingException, RemoteLookupFailureException {
    if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) {
      return (this.cachedStub != null this.cachedStub : lookupStub());
    }
    else {
      synchronized (this.stubMonitor) {
        if (this.cachedStub == null) {
          this.cachedStub = lookupStub();
        }
        return this.cachedStub;
      }
    }
  }


  /**
   * Fetches an RMI stub and delegates to {@link #doInvoke}.
   * If configured to refresh on connect failure, it will call
   {@link #refreshAndRetry} on corresponding RMI exceptions.
   @see #getStub
   @see #doInvoke
   @see #refreshAndRetry
   @see java.rmi.ConnectException
   @see java.rmi.ConnectIOException
   @see java.rmi.NoSuchObjectException
   */
  public Object invoke(MethodInvocation invocationthrows Throwable {
    Object stub = null;
    try {
      stub = getStub();
    }
    catch (NamingException ex) {
      throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() "] failed", ex);
    }
    try {
      return doInvoke(invocation, stub);
    }
    catch (RemoteConnectFailureException ex) {
      return handleRemoteConnectFailure(invocation, ex);
    }
    catch (RemoteException ex) {
      if (isConnectFailure(ex)) {
        return handleRemoteConnectFailure(invocation, ex);
      }
      else {
        throw ex;
      }
    }
    catch (SystemException ex) {
      if (isConnectFailure(ex)) {
        return handleRemoteConnectFailure(invocation, ex);
      }
      else {
        throw ex;
      }
    }
  }

  /**
   * Determine whether the given RMI exception indicates a connect failure.
   <p>The default implementation delegates to
   {@link RmiClientInterceptorUtils#isConnectFailure}.
   @param ex the RMI exception to check
   @return whether the exception should be treated as connect failure
   */
  protected boolean isConnectFailure(RemoteException ex) {
    return RmiClientInterceptorUtils.isConnectFailure(ex);
  }

  /**
   * Determine whether the given CORBA exception indicates a connect failure.
   <p>The default implementation checks for CORBA's
   {@link org.omg.CORBA.OBJECT_NOT_EXIST} exception.
   @param ex the RMI exception to check
   @return whether the exception should be treated as connect failure
   */
  protected boolean isConnectFailure(SystemException ex) {
    return (ex instanceof OBJECT_NOT_EXIST);
  }

  /**
   * Refresh the stub and retry the remote invocation if necessary.
   <p>If not configured to refresh on connect failure, this method
   * simply rethrows the original exception.
   @param invocation the invocation that failed
   @param ex the exception raised on remote invocation
   @return the result value of the new invocation, if succeeded
   @throws Throwable an exception raised by the new invocation, if failed too.
   */
  private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception exthrows Throwable {
    if (this.refreshStubOnConnectFailure) {
      if (logger.isDebugEnabled()) {
        logger.debug("Could not connect to RMI service [" + getJndiName() "] - retrying", ex);
      }
      else if (logger.isWarnEnabled()) {
        logger.warn("Could not connect to RMI service [" + getJndiName() "] - retrying");
      }
      return refreshAndRetry(invocation);
    }
    else {
      throw ex;
    }
  }

  /**
   * Refresh the RMI stub and retry the given invocation.
   * Called by invoke on connect failure.
   @param invocation the AOP method invocation
   @return the invocation result, if any
   @throws Throwable in case of invocation failure
   @see #invoke
   */
  protected Object refreshAndRetry(MethodInvocation invocationthrows Throwable {
    Object freshStub = null;
    synchronized (this.stubMonitor) {
      this.cachedStub = null;
      freshStub = lookupStub();
      if (this.cacheStub) {
        this.cachedStub = freshStub;
      }
    }
    return doInvoke(invocation, freshStub);
  }


  /**
   * Perform the given invocation on the given RMI stub.
   @param invocation the AOP method invocation
   @param stub the RMI stub to invoke
   @return the invocation result, if any
   @throws Throwable in case of invocation failure
   */
  protected Object doInvoke(MethodInvocation invocation, Object stubthrows Throwable {
    if (stub instanceof RmiInvocationHandler) {
      // RMI invoker
      try {
        return doInvoke(invocation, (RmiInvocationHandlerstub);
      }
      catch (RemoteException ex) {
        throw convertRmiAccessException(ex, invocation.getMethod());
      }
      catch (SystemException ex) {
        throw convertCorbaAccessException(ex, invocation.getMethod());
      }
      catch (InvocationTargetException ex) {
        throw ex.getTargetException();
      }
      catch (Throwable ex) {
        throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() +
            "] failed in RMI service [" + getJndiName() "]", ex);
      }
    }
    else {
      // traditional RMI stub
      try {
        return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub);
      }
      catch (InvocationTargetException ex) {
        Throwable targetEx = ex.getTargetException();
        if (targetEx instanceof RemoteException) {
          throw convertRmiAccessException((RemoteExceptiontargetEx, invocation.getMethod());
        }
        else if (targetEx instanceof SystemException) {
          throw convertCorbaAccessException((SystemExceptiontargetEx, invocation.getMethod());
        }
        else {
          throw targetEx;
        }
      }
    }
  }

  /**
   * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}.
   <p>The default implementation delegates to {@link #createRemoteInvocation}.
   @param methodInvocation the current AOP method invocation
   @param invocationHandler the RmiInvocationHandler to apply the invocation to
   @return the invocation result
   @throws RemoteException in case of communication errors
   @throws NoSuchMethodException if the method name could not be resolved
   @throws IllegalAccessException if the method could not be accessed
   @throws InvocationTargetException if the method invocation resulted in an exception
   @see org.springframework.remoting.support.RemoteInvocation
   */
  protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler)
      throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {

    if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
      return "RMI invoker proxy for service URL [" + getJndiName() "]";
    }

    return invocationHandler.invoke(createRemoteInvocation(methodInvocation));
  }

  /**
   * Create a new RemoteInvocation object for the given AOP method invocation.
   <p>The default implementation delegates to the configured
   {@link #setRemoteInvocationFactory RemoteInvocationFactory}.
   * This can be overridden in subclasses in order to provide custom RemoteInvocation
   * subclasses, containing additional invocation parameters (e.g. user credentials).
   <p>Note that it is preferable to build a custom RemoteInvocationFactory
   * as a reusable strategy, instead of overriding this method.
   @param methodInvocation the current AOP method invocation
   @return the RemoteInvocation object
   @see RemoteInvocationFactory#createRemoteInvocation
   */
  protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
    return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation);
  }

  /**
   * Convert the given RMI RemoteException that happened during remote access
   * to Spring's RemoteAccessException if the method signature does not declare
   * RemoteException. Else, return the original RemoteException.
   @param method the invoked method
   @param ex the RemoteException that happened
   @return the exception to be thrown to the caller
   */
  private Exception convertRmiAccessException(RemoteException ex, Method method) {
    return RmiClientInterceptorUtils.convertRmiAccessException(method, ex, isConnectFailure(ex), getJndiName());
  }

  /**
   * Convert the given CORBA SystemException that happened during remote access
   * to Spring's RemoteAccessException if the method signature does not declare
   * RemoteException. Else, return the SystemException wrapped in a RemoteException.
   @param method the invoked method
   @param ex the RemoteException that happened
   @return the exception to be thrown to the caller
   */
  private Exception convertCorbaAccessException(SystemException ex, Method method) {
    if (ReflectionUtils.declaresException(method, RemoteException.class)) {
      // A traditional RMI service: wrap CORBA exceptions in standard RemoteExceptions.
      return new RemoteException("Failed to access CORBA service [" + getJndiName() "]", ex);
    }
    else {
      if (isConnectFailure(ex)) {
        return new RemoteConnectFailureException("Could not connect to CORBA service [" + getJndiName() "]", ex);
      }
      else {
        return new RemoteAccessException("Could not access CORBA service [" + getJndiName() "]", ex);
      }
    }
  }

}