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}