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.jms.remoting;
018
019import javax.jms.Connection;
020import javax.jms.ConnectionFactory;
021import javax.jms.JMSException;
022import javax.jms.Message;
023import javax.jms.MessageConsumer;
024import javax.jms.MessageFormatException;
025import javax.jms.MessageProducer;
026import javax.jms.Queue;
027import javax.jms.Session;
028import javax.jms.TemporaryQueue;
029
030import org.aopalliance.intercept.MethodInterceptor;
031import org.aopalliance.intercept.MethodInvocation;
032
033import org.springframework.aop.support.AopUtils;
034import org.springframework.beans.factory.InitializingBean;
035import org.springframework.jms.connection.ConnectionFactoryUtils;
036import org.springframework.jms.support.JmsUtils;
037import org.springframework.jms.support.converter.MessageConverter;
038import org.springframework.jms.support.converter.SimpleMessageConverter;
039import org.springframework.jms.support.destination.DestinationResolver;
040import org.springframework.jms.support.destination.DynamicDestinationResolver;
041import org.springframework.lang.Nullable;
042import org.springframework.remoting.RemoteAccessException;
043import org.springframework.remoting.RemoteInvocationFailureException;
044import org.springframework.remoting.RemoteTimeoutException;
045import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
046import org.springframework.remoting.support.RemoteInvocation;
047import org.springframework.remoting.support.RemoteInvocationFactory;
048import org.springframework.remoting.support.RemoteInvocationResult;
049import org.springframework.util.Assert;
050
051/**
052 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing a
053 * JMS-based remote service.
054 *
055 * <p>Serializes remote invocation objects and deserializes remote invocation
056 * result objects. Uses Java serialization just like RMI, but with the JMS
057 * provider as communication infrastructure.
058 *
059 * <p>To be configured with a {@link javax.jms.QueueConnectionFactory} and a
060 * target queue (either as {@link javax.jms.Queue} reference or as queue name).
061 *
062 * <p>Thanks to James Strachan for the original prototype that this
063 * JMS invoker mechanism was inspired by!
064 *
065 * @author Juergen Hoeller
066 * @author James Strachan
067 * @author Stephane Nicoll
068 * @since 2.0
069 * @see #setConnectionFactory
070 * @see #setQueue
071 * @see #setQueueName
072 * @see org.springframework.jms.remoting.JmsInvokerServiceExporter
073 * @see org.springframework.jms.remoting.JmsInvokerProxyFactoryBean
074 */
075public class JmsInvokerClientInterceptor implements MethodInterceptor, InitializingBean {
076
077        @Nullable
078        private ConnectionFactory connectionFactory;
079
080        @Nullable
081        private Object queue;
082
083        private DestinationResolver destinationResolver = new DynamicDestinationResolver();
084
085        private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory();
086
087        private MessageConverter messageConverter = new SimpleMessageConverter();
088
089        private long receiveTimeout = 0;
090
091
092        /**
093         * Set the QueueConnectionFactory to use for obtaining JMS QueueConnections.
094         */
095        public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) {
096                this.connectionFactory = connectionFactory;
097        }
098
099        /**
100         * Return the QueueConnectionFactory to use for obtaining JMS QueueConnections.
101         */
102        @Nullable
103        protected ConnectionFactory getConnectionFactory() {
104                return this.connectionFactory;
105        }
106
107        /**
108         * Set the target Queue to send invoker requests to.
109         */
110        public void setQueue(Queue queue) {
111                this.queue = queue;
112        }
113
114        /**
115         * Set the name of target queue to send invoker requests to.
116         * <p>The specified name will be dynamically resolved via the
117         * {@link #setDestinationResolver DestinationResolver}.
118         */
119        public void setQueueName(String queueName) {
120                this.queue = queueName;
121        }
122
123        /**
124         * Set the DestinationResolver that is to be used to resolve Queue
125         * references for this accessor.
126         * <p>The default resolver is a {@code DynamicDestinationResolver}. Specify a
127         * {@code JndiDestinationResolver} for resolving destination names as JNDI locations.
128         * @see org.springframework.jms.support.destination.DynamicDestinationResolver
129         * @see org.springframework.jms.support.destination.JndiDestinationResolver
130         */
131        public void setDestinationResolver(@Nullable DestinationResolver destinationResolver) {
132                this.destinationResolver =
133                                (destinationResolver != null ? destinationResolver : new DynamicDestinationResolver());
134        }
135
136        /**
137         * Set the {@link RemoteInvocationFactory} to use for this accessor.
138         * <p>Default is a {@link DefaultRemoteInvocationFactory}.
139         * <p>A custom invocation factory can add further context information
140         * to the invocation, for example user credentials.
141         */
142        public void setRemoteInvocationFactory(@Nullable RemoteInvocationFactory remoteInvocationFactory) {
143                this.remoteInvocationFactory =
144                                (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory());
145        }
146
147        /**
148         * Specify the {@link MessageConverter} to use for turning
149         * {@link org.springframework.remoting.support.RemoteInvocation}
150         * objects into request messages, as well as response messages into
151         * {@link org.springframework.remoting.support.RemoteInvocationResult} objects.
152         * <p>Default is a {@link SimpleMessageConverter}, using a standard JMS
153         * {@link javax.jms.ObjectMessage} for each invocation / invocation result
154         * object.
155         * <p>Custom implementations may generally adapt {@link java.io.Serializable}
156         * objects into special kinds of messages, or might be specifically tailored for
157         * translating {@code RemoteInvocation(Result)s} into specific kinds of messages.
158         */
159        public void setMessageConverter(@Nullable MessageConverter messageConverter) {
160                this.messageConverter = (messageConverter != null ? messageConverter : new SimpleMessageConverter());
161        }
162
163        /**
164         * Set the timeout to use for receiving the response message for a request
165         * (in milliseconds).
166         * <p>The default is 0, which indicates a blocking receive without timeout.
167         * @see javax.jms.MessageConsumer#receive(long)
168         * @see javax.jms.MessageConsumer#receive()
169         */
170        public void setReceiveTimeout(long receiveTimeout) {
171                this.receiveTimeout = receiveTimeout;
172        }
173
174        /**
175         * Return the timeout to use for receiving the response message for a request
176         * (in milliseconds).
177         */
178        protected long getReceiveTimeout() {
179                return this.receiveTimeout;
180        }
181
182
183        @Override
184        public void afterPropertiesSet() {
185                if (getConnectionFactory() == null) {
186                        throw new IllegalArgumentException("Property 'connectionFactory' is required");
187                }
188                if (this.queue == null) {
189                        throw new IllegalArgumentException("'queue' or 'queueName' is required");
190                }
191        }
192
193
194        @Override
195        @Nullable
196        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
197                if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
198                        return "JMS invoker proxy for queue [" + this.queue + "]";
199                }
200
201                RemoteInvocation invocation = createRemoteInvocation(methodInvocation);
202                RemoteInvocationResult result;
203                try {
204                        result = executeRequest(invocation);
205                }
206                catch (JMSException ex) {
207                        throw convertJmsInvokerAccessException(ex);
208                }
209                try {
210                        return recreateRemoteInvocationResult(result);
211                }
212                catch (Throwable ex) {
213                        if (result.hasInvocationTargetException()) {
214                                throw ex;
215                        }
216                        else {
217                                throw new RemoteInvocationFailureException("Invocation of method [" + methodInvocation.getMethod() +
218                                                "] failed in JMS invoker remote service at queue [" + this.queue + "]", ex);
219                        }
220                }
221        }
222
223        /**
224         * Create a new {@code RemoteInvocation} object for the given AOP method invocation.
225         * <p>The default implementation delegates to the {@link RemoteInvocationFactory}.
226         * <p>Can be overridden in subclasses to provide custom {@code RemoteInvocation}
227         * subclasses, containing additional invocation parameters like user credentials.
228         * Note that it is preferable to use a custom {@code RemoteInvocationFactory} which
229         * is a reusable strategy.
230         * @param methodInvocation the current AOP method invocation
231         * @return the RemoteInvocation object
232         * @see RemoteInvocationFactory#createRemoteInvocation
233         */
234        protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
235                return this.remoteInvocationFactory.createRemoteInvocation(methodInvocation);
236        }
237
238        /**
239         * Execute the given remote invocation, sending an invoker request message
240         * to this accessor's target queue and waiting for a corresponding response.
241         * @param invocation the RemoteInvocation to execute
242         * @return the RemoteInvocationResult object
243         * @throws JMSException in case of JMS failure
244         * @see #doExecuteRequest
245         */
246        protected RemoteInvocationResult executeRequest(RemoteInvocation invocation) throws JMSException {
247                Connection con = createConnection();
248                Session session = null;
249                try {
250                        session = createSession(con);
251                        Queue queueToUse = resolveQueue(session);
252                        Message requestMessage = createRequestMessage(session, invocation);
253                        con.start();
254                        Message responseMessage = doExecuteRequest(session, queueToUse, requestMessage);
255                        if (responseMessage != null) {
256                                return extractInvocationResult(responseMessage);
257                        }
258                        else {
259                                return onReceiveTimeout(invocation);
260                        }
261                }
262                finally {
263                        JmsUtils.closeSession(session);
264                        ConnectionFactoryUtils.releaseConnection(con, getConnectionFactory(), true);
265                }
266        }
267
268        /**
269         * Create a new JMS Connection for this JMS invoker.
270         */
271        protected Connection createConnection() throws JMSException {
272                ConnectionFactory connectionFactory = getConnectionFactory();
273                Assert.state(connectionFactory != null, "No ConnectionFactory set");
274                return connectionFactory.createConnection();
275        }
276
277        /**
278         * Create a new JMS Session for this JMS invoker.
279         */
280        protected Session createSession(Connection con) throws JMSException {
281                return con.createSession(false, Session.AUTO_ACKNOWLEDGE);
282        }
283
284        /**
285         * Resolve this accessor's target queue.
286         * @param session the current JMS Session
287         * @return the resolved target Queue
288         * @throws JMSException if resolution failed
289         */
290        protected Queue resolveQueue(Session session) throws JMSException {
291                if (this.queue instanceof Queue) {
292                        return (Queue) this.queue;
293                }
294                else if (this.queue instanceof String) {
295                        return resolveQueueName(session, (String) this.queue);
296                }
297                else {
298                        throw new javax.jms.IllegalStateException(
299                                        "Queue object [" + this.queue + "] is neither a [javax.jms.Queue] nor a queue name String");
300                }
301        }
302
303        /**
304         * Resolve the given queue name into a JMS {@link javax.jms.Queue},
305         * via this accessor's {@link DestinationResolver}.
306         * @param session the current JMS Session
307         * @param queueName the name of the queue
308         * @return the located Queue
309         * @throws JMSException if resolution failed
310         * @see #setDestinationResolver
311         */
312        protected Queue resolveQueueName(Session session, String queueName) throws JMSException {
313                return (Queue) this.destinationResolver.resolveDestinationName(session, queueName, false);
314        }
315
316        /**
317         * Create the invoker request message.
318         * <p>The default implementation creates a JMS {@link javax.jms.ObjectMessage}
319         * for the given RemoteInvocation object.
320         * @param session the current JMS Session
321         * @param invocation the remote invocation to send
322         * @return the JMS Message to send
323         * @throws JMSException if the message could not be created
324         */
325        protected Message createRequestMessage(Session session, RemoteInvocation invocation) throws JMSException {
326                return this.messageConverter.toMessage(invocation, session);
327        }
328
329        /**
330         * Actually execute the given request, sending the invoker request message
331         * to the specified target queue and waiting for a corresponding response.
332         * <p>The default implementation is based on standard JMS send/receive,
333         * using a {@link javax.jms.TemporaryQueue} for receiving the response.
334         * @param session the JMS Session to use
335         * @param queue the resolved target Queue to send to
336         * @param requestMessage the JMS Message to send
337         * @return the RemoteInvocationResult object
338         * @throws JMSException in case of JMS failure
339         */
340        @Nullable
341        protected Message doExecuteRequest(Session session, Queue queue, Message requestMessage) throws JMSException {
342                TemporaryQueue responseQueue = null;
343                MessageProducer producer = null;
344                MessageConsumer consumer = null;
345                try {
346                        responseQueue = session.createTemporaryQueue();
347                        producer = session.createProducer(queue);
348                        consumer = session.createConsumer(responseQueue);
349                        requestMessage.setJMSReplyTo(responseQueue);
350                        producer.send(requestMessage);
351                        long timeout = getReceiveTimeout();
352                        return (timeout > 0 ? consumer.receive(timeout) : consumer.receive());
353                }
354                finally {
355                        JmsUtils.closeMessageConsumer(consumer);
356                        JmsUtils.closeMessageProducer(producer);
357                        if (responseQueue != null) {
358                                responseQueue.delete();
359                        }
360                }
361        }
362
363        /**
364         * Extract the invocation result from the response message.
365         * <p>The default implementation expects a JMS {@link javax.jms.ObjectMessage}
366         * carrying a {@link RemoteInvocationResult} object. If an invalid response
367         * message is encountered, the {@code onInvalidResponse} callback gets invoked.
368         * @param responseMessage the response message
369         * @return the invocation result
370         * @throws JMSException is thrown if a JMS exception occurs
371         * @see #onInvalidResponse
372         */
373        protected RemoteInvocationResult extractInvocationResult(Message responseMessage) throws JMSException {
374                Object content = this.messageConverter.fromMessage(responseMessage);
375                if (content instanceof RemoteInvocationResult) {
376                        return (RemoteInvocationResult) content;
377                }
378                return onInvalidResponse(responseMessage);
379        }
380
381        /**
382         * Callback that is invoked by {@link #executeRequest} when the receive
383         * timeout has expired for the specified {@link RemoteInvocation}.
384         * <p>By default, an {@link RemoteTimeoutException} is thrown. Sub-classes
385         * can choose to either throw a more dedicated exception or even return
386         * a default {@link RemoteInvocationResult} as a fallback.
387         * @param invocation the invocation
388         * @return a default result when the receive timeout has expired
389         */
390        protected RemoteInvocationResult onReceiveTimeout(RemoteInvocation invocation) {
391                throw new RemoteTimeoutException("Receive timeout after " + this.receiveTimeout + " ms for " + invocation);
392        }
393
394        /**
395         * Callback that is invoked by {@link #extractInvocationResult} when
396         * it encounters an invalid response message.
397         * <p>The default implementation throws a {@link MessageFormatException}.
398         * @param responseMessage the invalid response message
399         * @return an alternative invocation result that should be returned to
400         * the caller (if desired)
401         * @throws JMSException if the invalid response should lead to an
402         * infrastructure exception propagated to the caller
403         * @see #extractInvocationResult
404         */
405        protected RemoteInvocationResult onInvalidResponse(Message responseMessage) throws JMSException {
406                throw new MessageFormatException("Invalid response message: " + responseMessage);
407        }
408
409        /**
410         * Recreate the invocation result contained in the given {@link RemoteInvocationResult}
411         * object.
412         * <p>The default implementation calls the default {@code recreate()} method.
413         * <p>Can be overridden in subclasses to provide custom recreation, potentially
414         * processing the returned result object.
415         * @param result the RemoteInvocationResult to recreate
416         * @return a return value if the invocation result is a successful return
417         * @throws Throwable if the invocation result is an exception
418         * @see org.springframework.remoting.support.RemoteInvocationResult#recreate()
419         */
420        @Nullable
421        protected Object recreateRemoteInvocationResult(RemoteInvocationResult result) throws Throwable {
422                return result.recreate();
423        }
424
425        /**
426         * Convert the given JMS invoker access exception to an appropriate
427         * Spring {@link RemoteAccessException}.
428         * @param ex the exception to convert
429         * @return the RemoteAccessException to throw
430         */
431        protected RemoteAccessException convertJmsInvokerAccessException(JMSException ex) {
432                return new RemoteAccessException("Could not access JMS invoker queue [" + this.queue + "]", ex);
433        }
434
435}