You are on page 1of 38

Java Message Service

Dr. Stephan Fischli

Summer 2005

2
Goals

● Understand the messaging paradigm


● Know the different JMS messaging models
● Develop a sample application using JMS

Contents

● Introduction
● Programming Model
● Messages
● Basic Messaging
● Advanced Messaging

4
Introduction

Messaging Systems

Application Application

Messaging
System

Application Application

● Messaging systems allow two or more applications to exchange


information in the form of messages
● A message is a self-contained package of business data and network
routing headers
● Messaging systems usually provide fault tolerance, load balancing,
scalability, and transactional support

6
Messaging versus RMI

1: send 3: deliver
Producer Destination Consumer
2: return 4: return

● When a remote method is invoked, the caller is blocked until the


method completes
● The synchronized nature of RMI tightly couples the client to the
server and creates highly interdependent systems
● Messaging applications exchange messages through virtual channels
called destinations so that senders and receivers are not bound to
each other
● Messages are delivered asynchronously, i.e. the sender is not required
to wait for the message to be received by the recipient

Architecture of Messaging Systems

Client Client

Message
Server

Client Client

● A centralized architecture relies on a message server that is


responsible for delivering messages
● Some of the server functionality is embedded as a local part of the
clients
● In practice, the centralized message server may be a cluster of
distributed servers operating as a logical unit
● Decentralized architectures use IP multicast at the network level

8
Java Message Service

Application

Java Message Service

IBM Progress BEA


...
MQSeries SonicMQ WebLogic

● Messaging systems use different message formats and network


protocols (TCP/IP, HTTP, SSL, IP multicast), but the basic semantics
are the same
● The Java Message Service (JMS) is a standardized API for sending
and receiving messages that can be used with many different
messaging systems

Messaging Domains

Subscriber

Publisher Topic

Subscriber

● In the publish-and-subscribe domain, a producer can send a message


to many consumers through a virtual channel called topic
● Consumers can subscribe to a topic and receive a copy of each
message
● Messages are automatically broadcast to consumers (push-based
model)

10
Messaging Domains (cont.)

Receiver

Sender Queue

Receiver

● In the point-to-point domain, a producer can send a message to one


consumer through a virtual channel called queue
● A given queue may have multiple receivers, but only one receiver
may consume each message
● Messages are usually requested from the queue (pull-based model)

11

Guaranteed Delivery

Message
Producer Consumer
Server

Persistent
Store

● Messaging systems provide guaranteed delivery which ensures that


the intended consumers will eventually receive a message even if a
partial failure occurs
● Guaranteed delivery uses store-and-forward mechanism, i.e. the
message server writes the incoming messages to a persistent store and
then forwards them to the intended consumers

12
Application Scenarios

● Enterprise Application Integration


Messaging systems allow new applications and legacy systems to
exchange data while remaining physically independent
● Business-to-Business
Messaging systems allow organizations to cooperate without
requiring them to tightly integrate their business systems
● Geographic Dispersion
Messaging systems can ensure the safe and secure exchange of data
across a geographically distributed business
● Dynamic Systems
Messaging systems make it possible to add and remove dynamically
participants in an enterprise system

13

References

● Richard Monson-Haefel and David A. Chappell


Java Message Service
Creating Distributed Enterprise Applications
O'Reilly & Associates, 2001
● Scott Grant (Editor)
Professional JMS Programming
Wrox Press Inc, 2001
● Mark Hapner et.al.
Java Message Service API Tutorial and Reference
Addison Wesley Professional, 2002

14
15

16
Programming Model

17

Overview

*
JNDI Connection
Context Factory

*
Connection

Producer Session Consumer Listener

* *
Destination Message Destination

* These objects are thread-safe


18
Administered Objects and JNDI

● Connection factories and destinations are established and configured


by the system administrator
● A JMS client can obtain access to connection factories and
destinations by looking them up using JNDI
● JNDI is a standard Java extension that provides a uniform API for
directory and naming services
● Creating a connection to a JNDI naming service requires an initial
context be created with appropriate properties

Properties env = new Properties();


env.put(Context.INITIAL_CONTEXT_FACTORY,
"org.exolab.jms.jndi.InitialContextFactory");
env.put(Context.PROVIDER_URL, "tcp://localhost:3035/");
Context context = new InitialContext(env);

19

Connection Factories

● A connection factory is the object a client uses to create a connection


with a JMS provider
● A connection factory encapsulates a set of connection configuration
parameters (server address, port, protocol etc.)
● A JMS client usually performs a JNDI lookup of the connection
factory

ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");

20
Destinations

● A destination is the object a client uses as the target of messages it


produces and the source of messages it consumes
● A JMS application may use multiple queues and/or topics
● A JMS client usually performs a JNDI lookup of the destination

Destination destination =
(Destination)context.lookup("Topic");

21

Connections

● A connection encapsulates a virtual connection with a JMS provider


(e.g. a TCP/IP socket) and is used to create one or more sessions
● When an application completes any connections need to be closed,
otherwise resources may not to be released by the JMS provider
● Before an application can consume messages, the connection's start()
method must be called
● To stop message delivery temporarily without closing the connection,
the stop() method can be called

Connection connection = factory.createConnection();


connection.start();

22
Sessions

● A session is used to create message producers, message consumers


and messages
● A session may not be operated on by more than one thread at a time
(single-threaded context)
● A session provides a transactional context with which to group sends
and receives into an atomic unit of work
● A session defines the acknowledgment behavior of messages

Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE );

23

Message Producers

● A message producer is an object which is used for sending messages


to a destination
● With an unidentified producer, the destination of a message can be
specified when the message is sent

MessageProducer producer = session.createProducer(destination);


producer.send(message);

24
Message Consumers

● A message consumer is an object which is used for receiving


messages from a destination
● A message consumer allows a JMS client to register interest in a
destination, and the JMS provider manages the delivery of messages
to the registered consumers
● The receive() method is used to consume a message synchronously
● To consume messages asynchronously, a message listener is needed

MessageConsumer consumer = session.createConsumer(destination);


Message message = consumer.receive();

25

Message Listener

● A message listener is an object that acts as an asynchronous event


handler for messages
● A message listener implements the onMessage() method which
defines the actions to be taken when a message arrives
● The message listener is registered with a specific message consumer
● A message listener is not specific to a particular destination type,
however it usually expects a specific message type and format
● The session used to create the message consumer serializes the
execution of all message listeners

TextListener listener = new TextListener();


consumer.setMessageListener(listener);

26
27

28
Messages

29

Anatomy of a JMS Message

Headers

Properties

Body

● A JMS message carries application data and provides event


notification
● A JMS message has three parts:
– the message headers provide metadata and routing information
– the message properties are defined by the JMS client
– the message body carries the payload of the message
● When a message is delivered, the properties and the body of the
message are made read-only

30
Headers

● Every JMS message has a set of standard headers


● For each header there is a corresponding set and get method
● Most JMS headers are automatically assigned, i.e. their values are set
by the JMS provider depending on declarations made by the
developer
● Other headers must be set explicitly on the message before it is
delivered by the producer

31

Automatically Assigned Headers

JMSDestination
● The JMSDestination header identifies the destination of a message
with either a topic or a queue

JMSDeliveryMode
● A persistent message should be delivered once-and-only-once even if
the JMS provider fails
● A non-persistent message is delivered at-most-once, which means
that it can be lost if the JMS provider fails
● The delivery mode can be set on the message producer using the
setJMSDeliveryMode() method (default is persistent)

JMSMessageID
● The JMSMessageID is a string value that uniquely identifies a
message

32
Automatically Assigned Headers (cont.)

JMSTimestamp
● The JMSTimestamp header is set automatically by the message
producer when the message is sent

JMSExpiration
● A message's expiration date prevents the message from being
delivered to consumers after it has expired
● The expiration time can be set on the message producer using the
setTimeToLive() method (by default a message doesn't expire)

JMSRedelivered
● The JMSRedelivered header indicates that a message was redelivered
to the consumer
● A message may be marked redelivered if a consumer failed to
acknowledge previous delivery of the message

33

Automatically Assigned Headers (cont.)

JMSPriority
● The message server may use a message's priority to prioritize
delivery to consumers
● Levels 0 – 4 are gradations of normal priority, levels 5 – 9 are
gradations of expedited priority
● The priority can be set on the message producer using the
setPriority() method (default is 4)

34
Developer-Assigned Headers

JMSReplyTo
● The JMSReplyTo header contains a destination to which the
consumer of the message should reply to

JMSCorrelationID
● The JMSCorrelationID header is used for associating the current
message with some previous message, e.g. to tag a message as a reply
to a previous message

JMSType
● The JMSType header can be set by the message producer to identify
the message structure and type of payload

35

Properties

● Properties are like additional headers that can be assigned to a


message
● The value of a property can be a String, a primitive value or a
wrapper object thereof
● There are three categories of properties:
– Application-specific properties are defined and applied to a
message by the application developer
– JMS-defined properties act as optional JMS headers and are set
by the JMS provider when the message is sent
– Provider-specific properties are proprietary properties that are
defined by the JMS provider

36
Message Types

Message TextMessage

ObjectMessage

BytesMessage

StreamMessage

MapMessage

● The message types represent the kind of payload a message can have
● Some types were included to support legacy payloads, other types
were defined to facilitate emerging needs

37

Message Types (cont.)

Message
● The type Message serves as the base interface of the other message
types
● It contains only JMS headers and properties and is used for event
notification

TextMessage
● The type TextMessage carries a String as its payload
● It is useful for exchanging simple text messages and more complex
character data like XML documents

ObjectMessage
● The type ObjectMessage carries a serializable Java object as its
payload
● The producer and consumer must be Java programs, and the class
definition of the object has to be available to both of them

38
Message Types (cont.)

BytesMessage
● The ByteMessage type carries an array of bytes as its payload
● It is useful for exchanging data in an application's native format or
when the message payload is opaque to the JMS client

StreamMessage
● The StreamMessage type carries a stream of primitive Java types as
its payload
● It keeps track of the order and types of primitives written to the
message

MapMessage
● The MapMessage type carries a set of name-value pairs as its payload
● It is useful for delivering keyed data that may change from one
message to the next

39

40
Basic Messaging

41

Publish-and-Subscribe

subscribe
Subscriber
publish deliver
Publisher
Topic
subscribe
Subscriber
deliver

● A message producer (called publisher) can broadcast a message to


one or more consumers (called subscribers) through a virtual channel
called topic
● Messages are usually pushed to the consumers, i.e. the consumers are
delivered messages without having to request them
● Every client that subscribes to a topic receives its own copies of
messages published to that topic
● A topic retains messages only as long as it takes to distribute them to
the current subscribers
● There is a timing dependency between publishers and subscribers
42
Point-to-Point

send receive
Sender Queue Receiver
acknowledge

● A message producer (called sender) can send a message to one


consumer (called receiver) through a virtual channel called queue
● Usually messages are pulled by the consumers, i.e. the consumers
explicitly request messages from the queue
● Each message is delivered only to one receiver even if multiple
receivers are connected to the queue
● A queue delivers messages in the order they were placed in the queue
● A queue retains all messages until they are consumed or until they
expire
● There is no timing dependency between senders and receivers

43

Example: SimpleProducer

import java.io.*;
import javax.jms.*;
import javax.naming.*;

public class SimpleProducer {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and producer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(destination);

44
Example: SimpleProducer (cont.)

// send messages
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
while (true) {
String text = in.readLine();
if (text.equals("")) break;
TextMessage message = session.createTextMessage();
message.setStringProperty("user", args[1]);
message.setText(text);
producer.send(message);
}
connection.close();
System.exit(0);
}
}

45

Example: SimpleConsumer

import javax.jms.*;
import javax.naming.*;

public class SimpleConsumer {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(destination);

46
Example: SimpleConsumer (cont.)

// receive messages
connection.start();
while (true) {
TextMessage message = (TextMessage)consumer.receive(10000);
if (message == null) break;
String user = message.getStringProperty("user");
String text = message.getText();
System.out.println(user + ": " + text);
}
connection.close();
System.exit(0);
}
}

47

Example: AsynchronousConsumer

import javax.jms.*;
import javax.naming.*;

public class AsynchronousConsumer {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(destination);

// register listener
consumer.setMessageListener(new TextListener());
48
Example: AsynchronousConsumer (cont.)

// receive messages
connection.start();
System.in.read();
connection.close();
System.exit(0);
}
}

public class TextListener implements MessageListener {


public void onMessage(Message message) {
try {
String user = message.getStringProperty("user");
String text = ((TextMessage)message).getText();
System.out.println(user + ": " + text);
}
catch (Exception e) {}
}
}

49

Durable Subscriptions

Message Message Message Message

Subscription
create unsubscribe
Connection Connection
start close start close

● A durable subscription is one that outlasts a client's connection with a


message server
● While a durable subscriber is disconnected from the server, the server
stores the messages that the subscriber misses
● When the subscriber reconnects, the server sends it all the unexpired
messages that accumulated
● A durable subscription's uniqueness is defined by the client ID of the
connection and a subscription name
● A durable subscription is terminated by calling the unsubscribe()
method of the session that created it
50
Example: DurableSubscriber

import javax.jms.*;
import javax.naming.*;

public class DurableSubscriber {


public static void main(String[] args) throws Exception {

// lookup connection factory and topic


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Topic topic = (Topic)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer =
session.createDurableSubscriber(topic, args[1]);

51

Example: DurableSubscriber (cont.)

// register listener
consumer.setMessageListener(new TextListener());

// receive messages
connection.start();
System.in.read();
if (args.length == 3 && args[2].equals("unsubscribe")) {
consumer.close();
session.unsubscribe(args[1]);
}
connection.close();
System.exit(0);
}
}

52
Browsing a Queue

● A queue browser is a specialized object that allows to peek ahead at


pending messages on a queue without consuming them
● Queue browsing can be useful for monitoring the contents of a queue
from an administrative tool
● Messages obtained from a queue browser only provide a snapshot of
the queue's content

53

Example: SimpleBrowser

import java.util.*;
import javax.jms.*;
import javax.naming.*;

public class SimpleBrowser {


public static void main(String[] args) throws Exception {

// lookup connection factory and queue


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Queue queue = (Queue)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
QueueBrowser browser = session.createBrowser(queue);

54
Example: SimpleBrowser (cont.)

// browse messages
connection.start();
Enumeration enum = browser.getEnumeration();
while (enum.hasMoreElements()) {
TextMessage message = (TextMessage)enum.nextElement();
String user = message.getStringProperty("user");
String text = message.getText();
System.out.println(user + ": " + text);
}
connection.close();
System.exit(0);
}
}

55

56
Advanced Messaging

57

Message Selectors

● A message selector allows a JMS consumer to be more selective


about the messages it receives from a destination
● Message selectors use message properties and headers as criteria in
conditional expressions (based on a subset of the SQL92 syntax for
WHERE clauses)
● Message selectors are declared when the message consumer is
created
● Messages that are not selected by a consumer are not delivered to that
consumer but to other consumers

58
Example: SelectiveConsumer

import javax.jms.*;
import javax.naming.*;

public class SelectiveConsumer {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer =
session.createConsumer(destination, "user LIKE '" + args[1] + "'");

59

Example: SelectiveConsumer (cont.)

// receive messages
connection.start();
while (true) {
TextMessage message = (TextMessage)consumer.receive(10000);
if (message == null) break;
String user = message.getStringProperty("user");
String text = message.getText();
System.out.println(user + ": " + text);
}
connection.close();
System.exit(0);
}
}

60
Temporary Destinations

● A temporary destination is a destination that is dynamically created


by a JMS client and only lives as long as the client lives
● A temporary destination is unavailable to other clients unless its
identity is transferred in a JMSReplyTo message header
● While any client may send messages to a temporary destination, only
the client that created the destination may receive messages from it
● The JMSReplyTo message header and temporary destinations can be
used to create a synchronous request-reply conversation

61

Example: SimpleRequestor

import java.io.*;
import javax.jms.*;
import javax.naming.*;

public class SimpleRequestor {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and producer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(destination);

62
Example: SimpleRequestor (cont.)

// send request
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String text = in.readLine();
TextMessage request = session.createTextMessage();
Destination temp = session.createTemporaryQueue();
request.setJMSReplyTo(temp);
request.setText(text);
producer.send(request);

// receive reply
connection.start();
MessageConsumer consumer = session.createConsumer(temp);
TextMessage reply = (TextMessage)consumer.receive();
System.out.println(reply.getText());
connection.close();
System.exit(0);
}
}

63

Example: SimpleReplier

import javax.jms.*;
import javax.naming.*;

public class SimpleReplier {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(destination);
connection.start();

64
Example: SimpleReplier (cont.)

while (true) {
// receive request
TextMessage request = (TextMessage)consumer.receive(10000);
if (request == null) break;
String id = request.getJMSMessageID();
String text = request.getText();
System.out.println(id + ": " + text);

// send reply
Destination temp = request.getJMSReplyTo();
TextMessage reply = session.createTextMessage();
reply.setJMSCorrelationID(id);
reply.setText(text);
MessageProducer producer = session.createProducer(temp);
producer.send(reply);
}
connection.close();
System.exit(0);
}
}
65

Message Acknowledgment

1: send 4: deliver
Message
Producer Consumer
Server
3: ack 5: ack

2: persist 6: delete

Persistent
Store

● Message acknowledgment is part of the protocol between the client


runtime library of the JMS provider and the message server
● The server acknowledges the receipt of messages from producers,
consumers acknowledge the receipt of messages from the server
● The acknowledgment protocol allows the JMS provider to manage
the distribution of messages and guarantee their delivery

66
Failure and Message Redelivery

● If the message server crashes, it will deliver the persistent messages


to consumers as soon as it starts up again
● In the point-to-point domain messages are always guaranteed to be
delivered
● In the publish-and-subscribe domain messages are only guaranteed to
be delivered to durable subscribers
● If a consumer fails to acknowledge a message, the server considers
the message undelivered and will attempt to redeliver it (with the
JMSRedelivered flag set)

67

Acknowledgment Modes

JMS specifies three acknowledgment modes which are set when a


session is created:
● AUTO_ACKNOWLEDGE:
The session automatically acknowledges a client's receipt of a
message after a successful return from the receive() or the
onMessage() method
● CLIENT_ACKNOWLEGE:
The client acknowledges a message by calling the message's
acknowledge() method which gives the client a finer grained control
● DUPS_OK_ACKNOWLEDGE:
The session lazily acknowledges the delivery of messages which may
result in the delivery of duplicate messages if the JMS provider fails

68
Example: AcknowledgeConsumer

import javax.jms.*;
import javax.naming.*;

public class AcknowledgeConsumer {


public static void main(String[] args) throws Exception {

// lookup connection factory and queue


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Queue queue = (Queue)context.lookup(args[0]);

// create connection, session and consumer


Connection connection = factory.createConnection();
Session session =
connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(queue);

69

Example: AcknowledgeConsumer (cont.)

// receive messages
connection.start();
while (true) {
TextMessage message = (TextMessage)consumer.receive(10000);
if (message == null) break;
String text = message.getText();
if (message.getJMSRedelivered())
System.out.print("Redelivered: ");
System.out.println(text);
if (args.length == 2 && args[1].equals("acknowledge"))
message.acknowledge();
}
connection.close();
System.exit(0);
}
}

70
Transacted Messages

1: send 4: deliver
2: send Message 5: deliver
Producer Consumer
Server
3: commit/ 6: commit/
rollback rollback

● In a transacted session, a JMS client can group multiple send or


receive operations into an atomic unit of work
● With transactional sends, messages delivered to the server are not
forwarded to the consumers until the producer commits the
transaction, otherwise the messages are discarded
● With transactional receives, messages delivered to the consumer are
not deleted by the server until the consumer commits the transaction,
otherwise the server attempts to redeliver the messages
● If one transaction is committed or rolled back, the session
automatically starts the next transaction

71

Example: TransactedProducer

import java.io.*;
import javax.jms.*;
import javax.naming.*;

public class TransactedProducer {


public static void main(String[] args) throws Exception {

// lookup connection factory and destination


Context context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory)context.lookup("ConnectionFactory");
Destination destination = (Destination)context.lookup(args[0]);

// create connection, session and producer


Connection connection = factory.createConnection();
Session session = connection.createSession(true, 0);
MessageProducer producer = session.createProducer(destination);

72
Example: TransactedProducer (cont.)

// send messages
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
while (true) {
String text = in.readLine();
if (text.equals("")) break;
TextMessage message = session.createTextMessage();
message.setStringProperty("user", args[1]);
message.setText(text);
producer.send(message);
}

// commit or rollback
if (args.length == 3 && args[2].equals("commit"))
session.commit();
else session.rollback();
connection.close();
System.exit(0);
}
}
73

Distributed Transactions

Message 2: receive 3: update Database


4: send Client
Server Server

1: begin 5: commit

Transaction
Manager

● Distributed transactions involve work performed across several


different resources (e.g. JMS providers and databases)
● The two-phase commit protocol (2PC) is used by a transaction
manager to coordinate the interactions of resources
● A resource that supports the 2PC protocol usually implements the
XA interface defined by The Open Group
● The client defines the scope of a transaction using the Java
Transaction API

74
75

76

You might also like