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