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:
024: import javax.naming.NamingException;
025: import javax.rmi.PortableRemoteObject;
026:
027: import org.aopalliance.intercept.MethodInterceptor;
028: import org.aopalliance.intercept.MethodInvocation;
029: import org.omg.CORBA.OBJECT_NOT_EXIST;
030: import org.omg.CORBA.SystemException;
031:
032: import org.springframework.aop.support.AopUtils;
033: import org.springframework.beans.factory.InitializingBean;
034: import org.springframework.jndi.JndiObjectLocator;
035: import org.springframework.remoting.RemoteAccessException;
036: import org.springframework.remoting.RemoteConnectFailureException;
037: import org.springframework.remoting.RemoteInvocationFailureException;
038: import org.springframework.remoting.RemoteLookupFailureException;
039: import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
040: import org.springframework.remoting.support.RemoteInvocation;
041: import org.springframework.remoting.support.RemoteInvocationFactory;
042: import org.springframework.util.ReflectionUtils;
043:
044: /**
045: * {@link org.aopalliance.intercept.MethodInterceptor} 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: * @throws RemoteLookupFailureException if RMI stub creation failed
181: * @see #setLookupStubOnStartup
182: * @see #lookupStub
183: */
184: public void prepare() throws RemoteLookupFailureException {
185: // Cache RMI stub on initialization?
186: if (this .lookupStubOnStartup) {
187: Remote remoteObj = lookupStub();
188: if (logger.isDebugEnabled()) {
189: if (remoteObj instanceof RmiInvocationHandler) {
190: logger.debug("JNDI RMI object [" + getJndiName()
191: + "] is an RMI invoker");
192: } else if (getServiceInterface() != null) {
193: boolean isImpl = getServiceInterface().isInstance(
194: remoteObj);
195: logger.debug("Using service interface ["
196: + getServiceInterface().getName()
197: + "] for JNDI RMI object [" + getJndiName()
198: + "] - " + (!isImpl ? "not " : "")
199: + "directly implemented");
200: }
201: }
202: if (this .cacheStub) {
203: this .cachedStub = remoteObj;
204: }
205: }
206: }
207:
208: /**
209: * Create the RMI stub, typically by looking it up.
210: * <p>Called on interceptor initialization if "cacheStub" is "true";
211: * else called for each invocation by {@link #getStub()}.
212: * <p>The default implementation retrieves the service from the
213: * JNDI environment. This can be overridden in subclasses.
214: * @return the RMI stub to store in this interceptor
215: * @throws RemoteLookupFailureException if RMI stub creation failed
216: * @see #setCacheStub
217: * @see #lookup
218: */
219: protected Remote lookupStub() throws RemoteLookupFailureException {
220: try {
221: Object stub = lookup();
222: if (getServiceInterface() != null
223: && Remote.class
224: .isAssignableFrom(getServiceInterface())) {
225: try {
226: stub = PortableRemoteObject.narrow(stub,
227: getServiceInterface());
228: } catch (ClassCastException ex) {
229: throw new RemoteLookupFailureException(
230: "Could not narrow RMI stub to service interface ["
231: + getServiceInterface().getName()
232: + "]", ex);
233: }
234: }
235: if (!(stub instanceof Remote)) {
236: throw new RemoteLookupFailureException(
237: "Located RMI stub of class ["
238: + stub.getClass().getName()
239: + "], with JNDI name ["
240: + getJndiName()
241: + "], does not implement interface [java.rmi.Remote]");
242: }
243: return (Remote) stub;
244: } catch (NamingException ex) {
245: throw new RemoteLookupFailureException(
246: "JNDI lookup for RMI service [" + getJndiName()
247: + "] failed", ex);
248: }
249: }
250:
251: /**
252: * Return the RMI stub to use. Called for each invocation.
253: * <p>The default implementation returns the stub created on initialization,
254: * if any. Else, it invokes {@link #lookupStub} to get a new stub for
255: * each invocation. This can be overridden in subclasses, for example in
256: * order to cache a stub for a given amount of time before recreating it,
257: * or to test the stub whether it is still alive.
258: * @return the RMI stub to use for an invocation
259: * @throws NamingException if stub creation failed
260: * @throws RemoteLookupFailureException if RMI stub creation failed
261: */
262: protected Remote getStub() throws NamingException,
263: RemoteLookupFailureException {
264: if (!this .cacheStub
265: || (this .lookupStubOnStartup && !this .refreshStubOnConnectFailure)) {
266: return (this .cachedStub != null ? this .cachedStub
267: : lookupStub());
268: } else {
269: synchronized (this .stubMonitor) {
270: if (this .cachedStub == null) {
271: this .cachedStub = lookupStub();
272: }
273: return this .cachedStub;
274: }
275: }
276: }
277:
278: /**
279: * Fetches an RMI stub and delegates to {@link #doInvoke}.
280: * If configured to refresh on connect failure, it will call
281: * {@link #refreshAndRetry} on corresponding RMI exceptions.
282: * @see #getStub
283: * @see #doInvoke
284: * @see #refreshAndRetry
285: * @see java.rmi.ConnectException
286: * @see java.rmi.ConnectIOException
287: * @see java.rmi.NoSuchObjectException
288: */
289: public Object invoke(MethodInvocation invocation) throws Throwable {
290: Remote stub = null;
291: try {
292: stub = getStub();
293: } catch (NamingException ex) {
294: throw new RemoteLookupFailureException(
295: "JNDI lookup for RMI service [" + getJndiName()
296: + "] failed", ex);
297: }
298: try {
299: return doInvoke(invocation, stub);
300: } catch (RemoteConnectFailureException ex) {
301: return handleRemoteConnectFailure(invocation, ex);
302: } catch (RemoteException ex) {
303: if (isConnectFailure(ex)) {
304: return handleRemoteConnectFailure(invocation, ex);
305: } else {
306: throw ex;
307: }
308: } catch (SystemException ex) {
309: if (isConnectFailure(ex)) {
310: return handleRemoteConnectFailure(invocation, ex);
311: } else {
312: throw ex;
313: }
314: }
315: }
316:
317: /**
318: * Determine whether the given RMI exception indicates a connect failure.
319: * <p>The default implementation delegates to
320: * {@link RmiClientInterceptorUtils#isConnectFailure}.
321: * @param ex the RMI exception to check
322: * @return whether the exception should be treated as connect failure
323: */
324: protected boolean isConnectFailure(RemoteException ex) {
325: return RmiClientInterceptorUtils.isConnectFailure(ex);
326: }
327:
328: /**
329: * Determine whether the given CORBA exception indicates a connect failure.
330: * <p>The default implementation checks for CORBA's
331: * {@link org.omg.CORBA.OBJECT_NOT_EXIST} exception.
332: * @param ex the RMI exception to check
333: * @return whether the exception should be treated as connect failure
334: */
335: protected boolean isConnectFailure(SystemException ex) {
336: return (ex instanceof OBJECT_NOT_EXIST);
337: }
338:
339: /**
340: * Refresh the stub and retry the remote invocation if necessary.
341: * <p>If not configured to refresh on connect failure, this method
342: * simply rethrows the original exception.
343: * @param invocation the invocation that failed
344: * @param ex the exception raised on remote invocation
345: * @return the result value of the new invocation, if succeeded
346: * @throws Throwable an exception raised by the new invocation, if failed too.
347: */
348: private Object handleRemoteConnectFailure(
349: MethodInvocation invocation, Exception ex) throws Throwable {
350: if (this .refreshStubOnConnectFailure) {
351: if (logger.isDebugEnabled()) {
352: logger.debug("Could not connect to RMI service ["
353: + getJndiName() + "] - retrying", ex);
354: } else if (logger.isWarnEnabled()) {
355: logger.warn("Could not connect to RMI service ["
356: + getJndiName() + "] - retrying");
357: }
358: return refreshAndRetry(invocation);
359: } else {
360: throw ex;
361: }
362: }
363:
364: /**
365: * Refresh the RMI stub and retry the given invocation.
366: * Called by invoke on connect failure.
367: * @param invocation the AOP method invocation
368: * @return the invocation result, if any
369: * @throws Throwable in case of invocation failure
370: * @see #invoke
371: */
372: protected Object refreshAndRetry(MethodInvocation invocation)
373: throws Throwable {
374: Remote freshStub = null;
375: synchronized (this .stubMonitor) {
376: this .cachedStub = null;
377: freshStub = lookupStub();
378: if (this .cacheStub) {
379: this .cachedStub = freshStub;
380: }
381: }
382: return doInvoke(invocation, freshStub);
383: }
384:
385: /**
386: * Perform the given invocation on the given RMI stub.
387: * @param invocation the AOP method invocation
388: * @param stub the RMI stub to invoke
389: * @return the invocation result, if any
390: * @throws Throwable in case of invocation failure
391: */
392: protected Object doInvoke(MethodInvocation invocation, Remote stub)
393: throws Throwable {
394: if (stub instanceof RmiInvocationHandler) {
395: // RMI invoker
396: try {
397: return doInvoke(invocation, (RmiInvocationHandler) stub);
398: } catch (RemoteException ex) {
399: throw convertRmiAccessException(ex, invocation
400: .getMethod());
401: } catch (SystemException ex) {
402: throw convertCorbaAccessException(ex, invocation
403: .getMethod());
404: } catch (InvocationTargetException ex) {
405: throw ex.getTargetException();
406: } catch (Throwable ex) {
407: throw new RemoteInvocationFailureException(
408: "Invocation of method ["
409: + invocation.getMethod()
410: + "] failed in RMI service ["
411: + getJndiName() + "]", ex);
412: }
413: } else {
414: // traditional RMI stub
415: try {
416: return RmiClientInterceptorUtils.doInvoke(invocation,
417: stub);
418: } catch (InvocationTargetException ex) {
419: Throwable targetEx = ex.getTargetException();
420: if (targetEx instanceof RemoteException) {
421: throw convertRmiAccessException(
422: (RemoteException) targetEx, invocation
423: .getMethod());
424: } else if (targetEx instanceof SystemException) {
425: throw convertCorbaAccessException(
426: (SystemException) targetEx, invocation
427: .getMethod());
428: } else {
429: throw targetEx;
430: }
431: }
432: }
433: }
434:
435: /**
436: * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}.
437: * <p>The default implementation delegates to {@link #createRemoteInvocation}.
438: * @param methodInvocation the current AOP method invocation
439: * @param invocationHandler the RmiInvocationHandler to apply the invocation to
440: * @return the invocation result
441: * @throws RemoteException in case of communication errors
442: * @throws NoSuchMethodException if the method name could not be resolved
443: * @throws IllegalAccessException if the method could not be accessed
444: * @throws InvocationTargetException if the method invocation resulted in an exception
445: * @see org.springframework.remoting.support.RemoteInvocation
446: */
447: protected Object doInvoke(MethodInvocation methodInvocation,
448: RmiInvocationHandler invocationHandler)
449: throws RemoteException, NoSuchMethodException,
450: IllegalAccessException, InvocationTargetException {
451:
452: if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
453: return "RMI invoker proxy for service URL ["
454: + getJndiName() + "]";
455: }
456:
457: return invocationHandler
458: .invoke(createRemoteInvocation(methodInvocation));
459: }
460:
461: /**
462: * Create a new RemoteInvocation object for the given AOP method invocation.
463: * <p>The default implementation delegates to the configured
464: * {@link #setRemoteInvocationFactory RemoteInvocationFactory}.
465: * This can be overridden in subclasses in order to provide custom RemoteInvocation
466: * subclasses, containing additional invocation parameters (e.g. user credentials).
467: * <p>Note that it is preferable to build a custom RemoteInvocationFactory
468: * as a reusable strategy, instead of overriding this method.
469: * @param methodInvocation the current AOP method invocation
470: * @return the RemoteInvocation object
471: * @see RemoteInvocationFactory#createRemoteInvocation
472: */
473: protected RemoteInvocation createRemoteInvocation(
474: MethodInvocation methodInvocation) {
475: return getRemoteInvocationFactory().createRemoteInvocation(
476: methodInvocation);
477: }
478:
479: /**
480: * Convert the given RMI RemoteException that happened during remote access
481: * to Spring's RemoteAccessException if the method signature does not declare
482: * RemoteException. Else, return the original RemoteException.
483: * @param method the invoked method
484: * @param ex the RemoteException that happened
485: * @return the exception to be thrown to the caller
486: */
487: private Exception convertRmiAccessException(RemoteException ex,
488: Method method) {
489: return RmiClientInterceptorUtils.convertRmiAccessException(
490: method, ex, isConnectFailure(ex), getJndiName());
491: }
492:
493: /**
494: * Convert the given CORBA SystemException that happened during remote access
495: * to Spring's RemoteAccessException if the method signature does not declare
496: * RemoteException. Else, return the SystemException wrapped in a RemoteException.
497: * @param method the invoked method
498: * @param ex the RemoteException that happened
499: * @return the exception to be thrown to the caller
500: */
501: private Exception convertCorbaAccessException(SystemException ex,
502: Method method) {
503: if (ReflectionUtils.declaresException(method,
504: RemoteException.class)) {
505: // A traditional RMI service: wrap CORBA exceptions in standard RemoteExceptions.
506: return new RemoteException(
507: "Failed to access CORBA service [" + getJndiName()
508: + "]", 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: }
|