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