1 /*
2 * Copyright 2002-2008 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.jms.listener;
18
19 import javax.jms.Connection;
20 import javax.jms.Destination;
21 import javax.jms.ExceptionListener;
22 import javax.jms.JMSException;
23 import javax.jms.Message;
24 import javax.jms.MessageListener;
25 import javax.jms.Queue;
26 import javax.jms.Session;
27 import javax.jms.Topic;
28
29 import org.springframework.jms.support.JmsUtils;
30 import org.springframework.util.Assert;
31
32 /**
33 * Abstract base class for message listener containers. Can either host
34 * a standard JMS {@link javax.jms.MessageListener} or a Spring-specific
35 * {@link SessionAwareMessageListener}.
36 *
37 * <p>Usually holds a single JMS {@link Connection} that all listeners are
38 * supposed to be registered on, which is the standard JMS way of managing
39 * listeners. Can alternatively also be used with a fresh Connection per
40 * listener, for J2EE-style XA-aware JMS messaging. The actual registration
41 * process is up to concrete subclasses.
42 *
43 * <p><b>NOTE:</b> The default behavior of this message listener container
44 * is to <b>never</b> propagate an exception thrown by a message listener up to
45 * the JMS provider. Instead, it will log any such exception at the error level.
46 * This means that from the perspective of the attendant JMS provider no such
47 * listener will ever fail.
48 *
49 * <p>The listener container offers the following message acknowledgment options:
50 * <ul>
51 * <li>"sessionAcknowledgeMode" set to "AUTO_ACKNOWLEDGE" (default):
52 * Automatic message acknowledgment <i>before</i> listener execution;
53 * no redelivery in case of exception thrown.
54 * <li>"sessionAcknowledgeMode" set to "CLIENT_ACKNOWLEDGE":
55 * Automatic message acknowledgment <i>after</i> successful listener execution;
56 * no redelivery in case of exception thrown.
57 * <li>"sessionAcknowledgeMode" set to "DUPS_OK_ACKNOWLEDGE":
58 * <i>Lazy</i> message acknowledgment during or after listener execution;
59 * <i>potential redelivery</i> in case of exception thrown.
60 * <li>"sessionTransacted" set to "true":
61 * Transactional acknowledgment after successful listener execution;
62 * <i>guaranteed redelivery</i> in case of exception thrown.
63 * </ul>
64 * The exact behavior might vary according to the concrete listener container
65 * and JMS provider used.
66 *
67 * <p>There are two solutions to the duplicate processing problem:
68 * <ul>
69 * <li>Either add <i>duplicate message detection</i> to your listener, in the
70 * form of a business entity existence check or a protocol table check. This
71 * usually just needs to be done in case of the JMSRedelivered flag being
72 * set on the incoming message (else just process straightforwardly).
73 * <li>Or wrap the <i>entire processing with an XA transaction</i>, covering the
74 * reception of the message as well as the execution of the message listener.
75 * This is only supported by {@link DefaultMessageListenerContainer}, through
76 * specifying a "transactionManager" (typically a
77 * {@link org.springframework.transaction.jta.JtaTransactionManager}, with
78 * a corresponding XA-aware JMS {@link javax.jms.ConnectionFactory} passed in as
79 * "connectionFactory").
80 * </ul>
81 * Note that XA transaction coordination adds significant runtime overhead,
82 * so it might be feasible to avoid it unless absolutely necessary.
83 *
84 * <p><b>Recommendations:</b>
85 * <ul>
86 * <li>The general recommendation is to set "sessionTransacted" to "true",
87 * typically in combination with local database transactions triggered by the
88 * listener implementation, through Spring's standard transaction facilities.
89 * This will work nicely in Tomcat or in a standalone environment, often
90 * combined with custom duplicate message detection (if it is unacceptable
91 * to ever process the same message twice).
92 * <li>Alternatively, specify a
93 * {@link org.springframework.transaction.jta.JtaTransactionManager} as
94 * "transactionManager" for a fully XA-aware JMS provider - typically when
95 * running on a J2EE server, but also for other environments with a JTA
96 * transaction manager present. This will give full "exactly-once" guarantees
97 * without custom duplicate message checks, at the price of additional
98 * runtime processing overhead.
99 * </ul>
100 *
101 * <p>Note that it is also possible to specify a
102 * {@link org.springframework.jms.connection.JmsTransactionManager} as external
103 * "transactionManager", providing fully synchronized Spring transactions based
104 * on local JMS transactions. The effect is similar to "sessionTransacted" set
105 * to "true", the difference being that this external transaction management
106 * will also affect independent JMS access code within the service layer
107 * (e.g. based on {@link org.springframework.jms.core.JmsTemplate} or
108 * {@link org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy}),
109 * not just direct JMS Session usage in a {@link SessionAwareMessageListener}.
110 *
111 * @author Juergen Hoeller
112 * @since 2.0
113 * @see #setMessageListener
114 * @see javax.jms.MessageListener
115 * @see SessionAwareMessageListener
116 * @see #handleListenerException
117 * @see DefaultMessageListenerContainer
118 * @see SimpleMessageListenerContainer
119 * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager
120 */
121 public abstract class AbstractMessageListenerContainer extends AbstractJmsListeningContainer {
122
123 private volatile Object destination;
124
125 private volatile String messageSelector;
126
127 private volatile Object messageListener;
128
129 private boolean subscriptionDurable = false;
130
131 private String durableSubscriptionName;
132
133 private ExceptionListener exceptionListener;
134
135 private boolean exposeListenerSession = true;
136
137 private boolean acceptMessagesWhileStopping = false;
138
139
140 /**
141 * Set the destination to receive messages from.
142 * <p>Alternatively, specify a "destinationName", to be dynamically
143 * resolved via the {@link org.springframework.jms.support.destination.DestinationResolver}.
144 * <p>Note: The destination may be replaced at runtime, with the listener
145 * container picking up the new destination immediately (works e.g. with
146 * DefaultMessageListenerContainer, as long as the cache level is less than
147 * CACHE_CONSUMER). However, this is considered advanced usage; use it with care!
148 * @see #setDestinationName(String)
149 */
150 public void setDestination(Destination destination) {
151 Assert.notNull(destination, "'destination' must not be null");
152 this.destination = destination;
153 if (destination instanceof Topic && !(destination instanceof Queue)) {
154 // Clearly a Topic: let's set the "pubSubDomain" flag accordingly.
155 setPubSubDomain(true);
156 }
157 }
158
159 /**
160 * Return the destination to receive messages from. Will be <code>null</code>
161 * if the configured destination is not an actual {@link Destination} type;
162 * c.f. {@link #setDestinationName(String) when the destination is a String}.
163 */
164 public Destination getDestination() {
165 return (this.destination instanceof Destination ? (Destination) this.destination : null);
166 }
167
168 /**
169 * Set the name of the destination to receive messages from.
170 * <p>The specified name will be dynamically resolved via the configured
171 * {@link #setDestinationResolver destination resolver}.
172 * <p>Alternatively, specify a JMS {@link Destination} object as "destination".
173 * <p>Note: The destination may be replaced at runtime, with the listener
174 * container picking up the new destination immediately (works e.g. with
175 * DefaultMessageListenerContainer, as long as the cache level is less than
176 * CACHE_CONSUMER). However, this is considered advanced usage; use it with care!
177 * @param destinationName the desired destination (can be <code>null</code>)
178 * @see #setDestination(javax.jms.Destination)
179 */
180 public void setDestinationName(String destinationName) {
181 Assert.notNull(destinationName, "'destinationName' must not be null");
182 this.destination = destinationName;
183 }
184
185 /**
186 * Return the name of the destination to receive messages from.
187 * Will be <code>null</code> if the configured destination is not a
188 * {@link String} type; c.f. {@link #setDestination(Destination) when
189 * it is an actual Destination}.
190 */
191 public String getDestinationName() {
192 return (this.destination instanceof String ? (String) this.destination : null);
193 }
194
195 /**
196 * Return a descriptive String for this container's JMS destination
197 * (never <code>null</code>).
198 */
199 protected String getDestinationDescription() {
200 return this.destination.toString();
201 }
202
203 /**
204 * Set the JMS message selector expression (or <code>null</code> if none).
205 * Default is none.
206 * <p>See the JMS specification for a detailed definition of selector expressions.
207 * <p>Note: The message selector may be replaced at runtime, with the listener
208 * container picking up the new selector value immediately (works e.g. with
209 * DefaultMessageListenerContainer, as long as the cache level is less than
210 * CACHE_CONSUMER). However, this is considered advanced usage; use it with care!
211 */
212 public void setMessageSelector(String messageSelector) {
213 this.messageSelector = messageSelector;
214 }
215
216 /**
217 * Return the JMS message selector expression (or <code>null</code> if none).
218 */
219 public String getMessageSelector() {
220 return this.messageSelector;
221 }
222
223
224 /**
225 * Set the message listener implementation to register.
226 * This can be either a standard JMS {@link MessageListener} object
227 * or a Spring {@link SessionAwareMessageListener} object.
228 * <p>Note: The message listener may be replaced at runtime, with the listener
229 * container picking up the new listener object immediately (works e.g. with
230 * DefaultMessageListenerContainer, as long as the cache level is less than
231 * CACHE_CONSUMER). However, this is considered advanced usage; use it with care!
232 * @throws IllegalArgumentException if the supplied listener is not a
233 * {@link MessageListener} or a {@link SessionAwareMessageListener}
234 * @see javax.jms.MessageListener
235 * @see SessionAwareMessageListener
236 */
237 public void setMessageListener(Object messageListener) {
238 checkMessageListener(messageListener);
239 this.messageListener = messageListener;
240 if (this.durableSubscriptionName == null) {
241 this.durableSubscriptionName = getDefaultSubscriptionName(messageListener);
242 }
243 }
244
245 /**
246 * Return the message listener object to register.
247 */
248 public Object getMessageListener() {
249 return this.messageListener;
250 }
251
252 /**
253 * Check the given message listener, throwing an exception
254 * if it does not correspond to a supported listener type.
255 * <p>By default, only a standard JMS {@link MessageListener} object or a
256 * Spring {@link SessionAwareMessageListener} object will be accepted.
257 * @param messageListener the message listener object to check
258 * @throws IllegalArgumentException if the supplied listener is not a
259 * {@link MessageListener} or a {@link SessionAwareMessageListener}
260 * @see javax.jms.MessageListener
261 * @see SessionAwareMessageListener
262 */
263 protected void checkMessageListener(Object messageListener) {
264 if (!(messageListener instanceof MessageListener ||
265 messageListener instanceof SessionAwareMessageListener)) {
266 throw new IllegalArgumentException(
267 "Message listener needs to be of type [" + MessageListener.class.getName() +
268 "] or [" + SessionAwareMessageListener.class.getName() + "]");
269 }
270 }
271
272 /**
273 * Determine the default subscription name for the given message listener.
274 * @param messageListener the message listener object to check
275 * @return the default subscription name
276 * @see SubscriptionNameProvider
277 */
278 protected String getDefaultSubscriptionName(Object messageListener) {
279 if (messageListener instanceof SubscriptionNameProvider) {
280 return ((SubscriptionNameProvider) messageListener).getSubscriptionName();
281 }
282 else {
283 return messageListener.getClass().getName();
284 }
285 }
286
287 /**
288 * Set whether to make the subscription durable. The durable subscription name
289 * to be used can be specified through the "durableSubscriptionName" property.
290 * <p>Default is "false". Set this to "true" to register a durable subscription,
291 * typically in combination with a "durableSubscriptionName" value (unless
292 * your message listener class name is good enough as subscription name).
293 * <p>Only makes sense when listening to a topic (pub-sub domain).
294 * @see #setDurableSubscriptionName
295 */
296 public void setSubscriptionDurable(boolean subscriptionDurable) {
297 this.subscriptionDurable = subscriptionDurable;
298 }
299
300 /**
301 * Return whether to make the subscription durable.
302 */
303 public boolean isSubscriptionDurable() {
304 return this.subscriptionDurable;
305 }
306
307 /**
308 * Set the name of a durable subscription to create. To be applied in case
309 * of a topic (pub-sub domain) with subscription durability activated.
310 * <p>The durable subscription name needs to be unique within this client's
311 * JMS client id. Default is the class name of the specified message listener.
312 * <p>Note: Only 1 concurrent consumer (which is the default of this
313 * message listener container) is allowed for each durable subscription.
314 * @see #setSubscriptionDurable
315 * @see #setClientId
316 * @see #setMessageListener
317 */
318 public void setDurableSubscriptionName(String durableSubscriptionName) {
319 this.durableSubscriptionName = durableSubscriptionName;
320 }
321
322 /**
323 * Return the name of a durable subscription to create, if any.
324 */
325 public String getDurableSubscriptionName() {
326 return this.durableSubscriptionName;
327 }
328
329 /**
330 * Set the JMS ExceptionListener to notify in case of a JMSException thrown
331 * by the registered message listener or the invocation infrastructure.
332 */
333 public void setExceptionListener(ExceptionListener exceptionListener) {
334 this.exceptionListener = exceptionListener;
335 }
336
337 /**
338 * Return the JMS ExceptionListener to notify in case of a JMSException thrown
339 * by the registered message listener or the invocation infrastructure, if any.
340 */
341 public ExceptionListener getExceptionListener() {
342 return this.exceptionListener;
343 }
344
345 /**
346 * Set whether to expose the listener JMS Session to a registered
347 * {@link SessionAwareMessageListener} as well as to
348 * {@link org.springframework.jms.core.JmsTemplate} calls.
349 * <p>Default is "true", reusing the listener's {@link Session}.
350 * Turn this off to expose a fresh JMS Session fetched from the same
351 * underlying JMS {@link Connection} instead, which might be necessary
352 * on some JMS providers.
353 * <p>Note that Sessions managed by an external transaction manager will
354 * always get exposed to {@link org.springframework.jms.core.JmsTemplate}
355 * calls. So in terms of JmsTemplate exposure, this setting only affects
356 * locally transacted Sessions.
357 * @see SessionAwareMessageListener
358 */
359 public void setExposeListenerSession(boolean exposeListenerSession) {
360 this.exposeListenerSession = exposeListenerSession;
361 }
362
363 /**
364 * Return whether to expose the listener JMS {@link Session} to a
365 * registered {@link SessionAwareMessageListener}.
366 */
367 public boolean isExposeListenerSession() {
368 return this.exposeListenerSession;
369 }
370
371 /**
372 * Set whether to accept received messages while the listener container
373 * in the process of stopping.
374 * <p>Default is "false", rejecting such messages through aborting the
375 * receive attempt. Switch this flag on to fully process such messages
376 * even in the stopping phase, with the drawback that even newly sent
377 * messages might still get processed (if coming in before all receive
378 * timeouts have expired).
379 * <p><b>NOTE:</b> Aborting receive attempts for such incoming messages
380 * might lead to the provider's retry count decreasing for the affected
381 * messages. If you have a high number of concurrent consumers, make sure
382 * that the number of retries is higher than the number of consumers,
383 * to be on the safe side for all potential stopping scenarios.
384 */
385 public void setAcceptMessagesWhileStopping(boolean acceptMessagesWhileStopping) {
386 this.acceptMessagesWhileStopping = acceptMessagesWhileStopping;
387 }
388
389 /**
390 * Return whether to accept received messages while the listener container
391 * in the process of stopping.
392 */
393 public boolean isAcceptMessagesWhileStopping() {
394 return this.acceptMessagesWhileStopping;
395 }
396
397 protected void validateConfiguration() {
398 if (this.destination == null) {
399 throw new IllegalArgumentException("Property 'destination' or 'destinationName' is required");
400 }
401 if (isSubscriptionDurable() && !isPubSubDomain()) {
402 throw new IllegalArgumentException("A durable subscription requires a topic (pub-sub domain)");
403 }
404 }
405
406
407 //-------------------------------------------------------------------------
408 // Template methods for listener execution
409 //-------------------------------------------------------------------------
410
411 /**
412 * Execute the specified listener,
413 * committing or rolling back the transaction afterwards (if necessary).
414 * @param session the JMS Session to operate on
415 * @param message the received JMS Message
416 * @see #invokeListener
417 * @see #commitIfNecessary
418 * @see #rollbackOnExceptionIfNecessary
419 * @see #handleListenerException
420 */
421 protected void executeListener(Session session, Message message) {
422 try {
423 doExecuteListener(session, message);
424 }
425 catch (Throwable ex) {
426 handleListenerException(ex);
427 }
428 }
429
430 /**
431 * Execute the specified listener,
432 * committing or rolling back the transaction afterwards (if necessary).
433 * @param session the JMS Session to operate on
434 * @param message the received JMS Message
435 * @throws JMSException if thrown by JMS API methods
436 * @see #invokeListener
437 * @see #commitIfNecessary
438 * @see #rollbackOnExceptionIfNecessary
439 * @see #convertJmsAccessException
440 */
441 protected void doExecuteListener(Session session, Message message) throws JMSException {
442 if (!isAcceptMessagesWhileStopping() && !isRunning()) {
443 if (logger.isWarnEnabled()) {
444 logger.warn("Rejecting received message because of the listener container " +
445 "having been stopped in the meantime: " + message);
446 }
447 rollbackIfNecessary(session);
448 throw new MessageRejectedWhileStoppingException();
449 }
450 try {
451 invokeListener(session, message);
452 }
453 catch (JMSException ex) {
454 rollbackOnExceptionIfNecessary(session, ex);
455 throw ex;
456 }
457 catch (RuntimeException ex) {
458 rollbackOnExceptionIfNecessary(session, ex);
459 throw ex;
460 }
461 catch (Error err) {
462 rollbackOnExceptionIfNecessary(session, err);
463 throw err;
464 }
465 commitIfNecessary(session, message);
466 }
467
468 /**
469 * Invoke the specified listener: either as standard JMS MessageListener
470 * or (preferably) as Spring SessionAwareMessageListener.
471 * @param session the JMS Session to operate on
472 * @param message the received JMS Message
473 * @throws JMSException if thrown by JMS API methods
474 * @see #setMessageListener
475 */
476 protected void invokeListener(Session session, Message message) throws JMSException {
477 Object listener = getMessageListener();
478 if (listener instanceof SessionAwareMessageListener) {
479 doInvokeListener((SessionAwareMessageListener) listener, session, message);
480 }
481 else if (listener instanceof MessageListener) {
482 doInvokeListener((MessageListener) listener, message);
483 }
484 else if (listener != null) {
485 throw new IllegalArgumentException(
486 "Only MessageListener and SessionAwareMessageListener supported: " + listener);
487 }
488 else {
489 throw new IllegalStateException("No message listener specified - see property 'messageListener'");
490 }
491 }
492
493 /**
494 * Invoke the specified listener as Spring SessionAwareMessageListener,
495 * exposing a new JMS Session (potentially with its own transaction)
496 * to the listener if demanded.
497 * @param listener the Spring SessionAwareMessageListener to invoke
498 * @param session the JMS Session to operate on
499 * @param message the received JMS Message
500 * @throws JMSException if thrown by JMS API methods
501 * @see SessionAwareMessageListener
502 * @see #setExposeListenerSession
503 */
504 protected void doInvokeListener(SessionAwareMessageListener listener, Session session, Message message)
505 throws JMSException {
506
507 Connection conToClose = null;
508 Session sessionToClose = null;
509 try {
510 Session sessionToUse = session;
511 if (!isExposeListenerSession()) {
512 // We need to expose a separate Session.
513 conToClose = createConnection();
514 sessionToClose = createSession(conToClose);
515 sessionToUse = sessionToClose;
516 }
517 // Actually invoke the message listener...
518 listener.onMessage(message, sessionToUse);
519 // Clean up specially exposed Session, if any.
520 if (sessionToUse != session) {
521 if (sessionToUse.getTransacted() && isSessionLocallyTransacted(sessionToUse)) {
522 // Transacted session created by this container -> commit.
523 JmsUtils.commitIfNecessary(sessionToUse);
524 }
525 }
526 }
527 finally {
528 JmsUtils.closeSession(sessionToClose);
529 JmsUtils.closeConnection(conToClose);
530 }
531 }
532
533 /**
534 * Invoke the specified listener as standard JMS MessageListener.
535 * <p>Default implementation performs a plain invocation of the
536 * <code>onMessage</code> method.
537 * @param listener the JMS MessageListener to invoke
538 * @param message the received JMS Message
539 * @throws JMSException if thrown by JMS API methods
540 * @see javax.jms.MessageListener#onMessage
541 */
542 protected void doInvokeListener(MessageListener listener, Message message) throws JMSException {
543 listener.onMessage(message);
544 }
545
546 /**
547 * Perform a commit or message acknowledgement, as appropriate.
548 * @param session the JMS Session to commit
549 * @param message the Message to acknowledge
550 * @throws javax.jms.JMSException in case of commit failure
551 */
552 protected void commitIfNecessary(Session session, Message message) throws JMSException {
553 // Commit session or acknowledge message.
554 if (session.getTransacted()) {
555 // Commit necessary - but avoid commit call within a JTA transaction.
556 if (isSessionLocallyTransacted(session)) {
557 // Transacted session created by this container -> commit.
558 JmsUtils.commitIfNecessary(session);
559 }
560 }
561 else if (isClientAcknowledge(session)) {
562 message.acknowledge();
563 }
564 }
565
566 /**
567 * Perform a rollback, if appropriate.
568 * @param session the JMS Session to rollback
569 * @throws javax.jms.JMSException in case of a rollback error
570 */
571 protected void rollbackIfNecessary(Session session) throws JMSException {
572 if (session.getTransacted() && isSessionLocallyTransacted(session)) {
573 // Transacted session created by this container -> rollback.
574 JmsUtils.rollbackIfNecessary(session);
575 }
576 }
577
578 /**
579 * Perform a rollback, handling rollback exceptions properly.
580 * @param session the JMS Session to rollback
581 * @param ex the thrown application exception or error
582 * @throws javax.jms.JMSException in case of a rollback error
583 */
584 protected void rollbackOnExceptionIfNecessary(Session session, Throwable ex) throws JMSException {
585 try {
586 if (session.getTransacted() && isSessionLocallyTransacted(session)) {
587 // Transacted session created by this container -> rollback.
588 if (logger.isDebugEnabled()) {
589 logger.debug("Initiating transaction rollback on application exception", ex);
590 }
591 JmsUtils.rollbackIfNecessary(session);
592 }
593 }
594 catch (IllegalStateException ex2) {
595 logger.debug("Could not roll back because Session already closed", ex2);
596 }
597 catch (JMSException ex2) {
598 logger.error("Application exception overridden by rollback exception", ex);
599 throw ex2;
600 }
601 catch (RuntimeException ex2) {
602 logger.error("Application exception overridden by rollback exception", ex);
603 throw ex2;
604 }
605 catch (Error err) {
606 logger.error("Application exception overridden by rollback error", ex);
607 throw err;
608 }
609 }
610
611 /**
612 * Check whether the given Session is locally transacted, that is, whether
613 * its transaction is managed by this listener container's Session handling
614 * and not by an external transaction coordinator.
615 * <p>Note: The Session's own transacted flag will already have been checked
616 * before. This method is about finding out whether the Session's transaction
617 * is local or externally coordinated.
618 * @param session the Session to check
619 * @return whether the given Session is locally transacted
620 * @see #isSessionTransacted()
621 * @see org.springframework.jms.connection.ConnectionFactoryUtils#isSessionTransactional
622 */
623 protected boolean isSessionLocallyTransacted(Session session) {
624 return isSessionTransacted();
625 }
626
627 /**
628 * Handle the given exception that arose during listener execution.
629 * <p>The default implementation logs the exception at error level,
630 * not propagating it to the JMS provider - assuming that all handling of
631 * acknowledgement and/or transactions is done by this listener container.
632 * This can be overridden in subclasses.
633 * @param ex the exception to handle
634 */
635 protected void handleListenerException(Throwable ex) {
636 if (ex instanceof MessageRejectedWhileStoppingException) {
637 // Internal exception - has been handled before.
638 return;
639 }
640 if (ex instanceof JMSException) {
641 invokeExceptionListener((JMSException) ex);
642 }
643 if (isActive()) {
644 // Regular case: failed while active.
645 // Log at error level.
646 logger.warn("Execution of JMS message listener failed", ex);
647 }
648 else {
649 // Rare case: listener thread failed after container shutdown.
650 // Log at debug level, to avoid spamming the shutdown log.
651 logger.debug("Listener exception after container shutdown", ex);
652 }
653 }
654
655 /**
656 * Invoke the registered JMS ExceptionListener, if any.
657 * @param ex the exception that arose during JMS processing
658 * @see #setExceptionListener
659 */
660 protected void invokeExceptionListener(JMSException ex) {
661 ExceptionListener exceptionListener = getExceptionListener();
662 if (exceptionListener != null) {
663 exceptionListener.onException(ex);
664 }
665 }
666
667
668 /**
669 * Internal exception class that indicates a rejected message on shutdown.
670 * Used to trigger a rollback for an external transaction manager in that case.
671 */
672 private static class MessageRejectedWhileStoppingException extends RuntimeException {
673
674 }
675
676 }