001/*
002 * Copyright 2002-2017 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.ejb.access;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.rmi.RemoteException;
022
023import javax.ejb.EJBHome;
024import javax.ejb.EJBObject;
025import javax.naming.NamingException;
026
027import org.aopalliance.intercept.MethodInvocation;
028
029import org.springframework.lang.Nullable;
030import org.springframework.remoting.RemoteConnectFailureException;
031import org.springframework.remoting.RemoteLookupFailureException;
032import org.springframework.remoting.rmi.RmiClientInterceptorUtils;
033
034/**
035 * Base class for interceptors proxying remote Stateless Session Beans.
036 * Designed for EJB 2.x, but works for EJB 3 Session Beans as well.
037 *
038 * <p>Such an interceptor must be the last interceptor in the advice chain.
039 * In this case, there is no target object.
040 *
041 * @author Rod Johnson
042 * @author Juergen Hoeller
043 */
044public abstract class AbstractRemoteSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor {
045
046        private boolean refreshHomeOnConnectFailure = false;
047
048        private volatile boolean homeAsComponent = false;
049
050
051
052        /**
053         * Set whether to refresh the EJB home on connect failure.
054         * Default is "false".
055         * <p>Can be turned on to allow for hot restart of the EJB server.
056         * If a cached EJB home throws an RMI exception that indicates a
057         * remote connect failure, a fresh home will be fetched and the
058         * invocation will be retried.
059         * @see java.rmi.ConnectException
060         * @see java.rmi.ConnectIOException
061         * @see java.rmi.NoSuchObjectException
062         */
063        public void setRefreshHomeOnConnectFailure(boolean refreshHomeOnConnectFailure) {
064                this.refreshHomeOnConnectFailure = refreshHomeOnConnectFailure;
065        }
066
067        @Override
068        protected boolean isHomeRefreshable() {
069                return this.refreshHomeOnConnectFailure;
070        }
071
072
073        /**
074         * Check for EJB3-style home object that serves as EJB component directly.
075         */
076        @Override
077        protected Method getCreateMethod(Object home) throws EjbAccessException {
078                if (this.homeAsComponent) {
079                        return null;
080                }
081                if (!(home instanceof EJBHome)) {
082                        // An EJB3 Session Bean...
083                        this.homeAsComponent = true;
084                        return null;
085                }
086                return super.getCreateMethod(home);
087        }
088
089
090        /**
091         * Fetches an EJB home object and delegates to {@code doInvoke}.
092         * <p>If configured to refresh on connect failure, it will call
093         * {@link #refreshAndRetry} on corresponding RMI exceptions.
094         * @see #getHome
095         * @see #doInvoke
096         * @see #refreshAndRetry
097         */
098        @Override
099        @Nullable
100        public Object invokeInContext(MethodInvocation invocation) throws Throwable {
101                try {
102                        return doInvoke(invocation);
103                }
104                catch (RemoteConnectFailureException ex) {
105                        return handleRemoteConnectFailure(invocation, ex);
106                }
107                catch (RemoteException ex) {
108                        if (isConnectFailure(ex)) {
109                                return handleRemoteConnectFailure(invocation, ex);
110                        }
111                        else {
112                                throw ex;
113                        }
114                }
115        }
116
117        /**
118         * Determine whether the given RMI exception indicates a connect failure.
119         * <p>The default implementation delegates to RmiClientInterceptorUtils.
120         * @param ex the RMI exception to check
121         * @return whether the exception should be treated as connect failure
122         * @see org.springframework.remoting.rmi.RmiClientInterceptorUtils#isConnectFailure
123         */
124        protected boolean isConnectFailure(RemoteException ex) {
125                return RmiClientInterceptorUtils.isConnectFailure(ex);
126        }
127
128        @Nullable
129        private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable {
130                if (this.refreshHomeOnConnectFailure) {
131                        if (logger.isDebugEnabled()) {
132                                logger.debug("Could not connect to remote EJB [" + getJndiName() + "] - retrying", ex);
133                        }
134                        else if (logger.isWarnEnabled()) {
135                                logger.warn("Could not connect to remote EJB [" + getJndiName() + "] - retrying");
136                        }
137                        return refreshAndRetry(invocation);
138                }
139                else {
140                        throw ex;
141                }
142        }
143
144        /**
145         * Refresh the EJB home object and retry the given invocation.
146         * Called by invoke on connect failure.
147         * @param invocation the AOP method invocation
148         * @return the invocation result, if any
149         * @throws Throwable in case of invocation failure
150         * @see #invoke
151         */
152        @Nullable
153        protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable {
154                try {
155                        refreshHome();
156                }
157                catch (NamingException ex) {
158                        throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex);
159                }
160                return doInvoke(invocation);
161        }
162
163
164        /**
165         * Perform the given invocation on the current EJB home.
166         * Template method to be implemented by subclasses.
167         * @param invocation the AOP method invocation
168         * @return the invocation result, if any
169         * @throws Throwable in case of invocation failure
170         * @see #getHome
171         * @see #newSessionBeanInstance
172         */
173        @Nullable
174        protected abstract Object doInvoke(MethodInvocation invocation) throws Throwable;
175
176
177        /**
178         * Return a new instance of the stateless session bean.
179         * To be invoked by concrete remote SLSB invoker subclasses.
180         * <p>Can be overridden to change the algorithm.
181         * @throws NamingException if thrown by JNDI
182         * @throws InvocationTargetException if thrown by the create method
183         * @see #create
184         */
185        protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException {
186                if (logger.isDebugEnabled()) {
187                        logger.debug("Trying to create reference to remote EJB");
188                }
189                Object ejbInstance = create();
190                if (logger.isDebugEnabled()) {
191                        logger.debug("Obtained reference to remote EJB: " + ejbInstance);
192                }
193                return ejbInstance;
194        }
195
196        /**
197         * Remove the given EJB instance.
198         * To be invoked by concrete remote SLSB invoker subclasses.
199         * @param ejb the EJB instance to remove
200         * @see javax.ejb.EJBObject#remove
201         */
202        protected void removeSessionBeanInstance(@Nullable EJBObject ejb) {
203                if (ejb != null && !this.homeAsComponent) {
204                        try {
205                                ejb.remove();
206                        }
207                        catch (Throwable ex) {
208                                logger.warn("Could not invoke 'remove' on remote EJB proxy", ex);
209                        }
210                }
211        }
212
213}