是什么?
JMS(Java Message Service)即Java消息服务应用程序接口,是Java平台面向消息中间件的一套规范的Java API接口,用于在两个应用程序之间或者分布式系统中发送消息,进行异步通信。
这套规范由SUN提出,目前主要使用的版本有两个:一个是2002年发布的1.1版;一个是2013年发布的1.2版本。
JMS并不是消息队列协议的一种,更不是消息队列产品,它是与具体平台无关的API,目前市面上的绝大多数中间件厂商都支持JMS接口规范。
你可以使用JMS API来连接支持AMQP、STOMP等协议的消息中间件产品(比如ActiveMQ、RabbitMQ等),在这一点上它与Java中的JDBC的作用很像,我们可以使用JDBC API来访问具体的数据库产品(比如Oracle、MySQL等)。
2.0版本相对1.1版本来说,主要是API的简化和在消息使用场景中新增了一些特性,其核心模型并没有特别大的变动。
体系架构
在JMS之前,大部分消息队列产品都支持点对点和发布/订阅两种方式来传递消息。基于此,JMS将这两种消息模型抽象成两类规范,它们相互独立,由JMS的提供商(即消息队列产品的具体厂商)自己选择实现其中的一种还是两种模型。
JMS的作用是提供通用接口保证基于JMS API编写的程序适用于任何一种模型,使得在更换消息队列提供商的情况下应用程序相关代码也不需要做太大的改动。
(1)点对点模型(Point to Point)
在点对点模型中,应用程序由队列(Queue)、发送者(Sender)和接收者(Receiver)组成。每条消息都被发送到一个特定的队列中,接收者从队列中获取消息,队列中一直保留着消息,直到它们被接收或超时。点对点模型的特点如下:
- 每条消息只有一个接收者,消息一旦被接收就不再保留在消息队列中了。
- 发送者和接收者之间在时间上没有依赖。也就是说,当消息被发送之后,不管接收者有没有在运行,都不会影响消息被发送到队列中。
- 每条消息仅会被传送给一个接收者。也就是说,一个队列中可能会有多个接收者在监听,但是消息只能被队列中的一个接收者接收。
- 消息存在先后顺序。一个队列会按照消息服务器将消息放入队列中的顺序把它们传送给接收者。当消息已经被接收时就会从队列头部将它们删除(除非使用了消息优先级)
- 当接收者收到消息时,会发送确认收到通知。
所以,一般情况下,如果希望所发送的每条消息都能被成功处理,则需要使用点对点模型。
(2)发布/订阅模型
在发布/订阅(Pub/Sub)模型中,应用程序由主题(Topic)、发布者(Publisher)和订阅者(
Subscriber)组成。发布者发布一条消息,该消息通过主题传递给所有的订阅者。在这种模型中,发布者和订阅者彼此不知道对方,它们是匿名的并且可以动态发布和订阅主题。主题用于保存和传递消息,并且会一直保存消息指导消息被传递给订阅者。
发布/订阅模型的特点如下: - 每条消息可以有多个订阅者
- 发布者和订阅者之间有时间上的依赖。一般情况下,某个主题的订阅者需要在创建了订阅之后才能接收到消息,而且为了接收消息订阅者必须保持运行的状态。
- JMS允许订阅者创建一个可持久化的订阅,这样即使订阅者没有运行也能接收到所订阅的消息。
- 每条消息都会传送给该主题下的所有订阅者。
- 通常发布者不会知道也意识不到哪一个订阅者正在接收消息。
所以,如果希望所发送的消息不被做任何处理或者被一个或多个订阅者处理,则可以使用发布/订阅模型。
基本概念
按照JMS规范中所说的,一个JMS应用由如下几个部分组成。
- JMS客户端(JMS Client):指发送和接收消息的Java程序。
- 非JMS客户端(Non-JMS Client):指使用消息系统原生的客户端API代替JMS的客户端。如果应用程序在JMS规范前就已经存在,则它可能同时包含JMS客户端和非JMS客户端。
- 消息(Message):每个应用都定义了一组消息,用于多个客户端之间的消息通信。
- JMS提供商(JMS Provider):指实现了JMS API的实际消息系统。
- 受管对象(Administered Object):指由管理员创建,并预先配置好给客户端使用的JMS对象。JMS中的受管对象分为两种,即ConnectionFactory(客户端使用这个对象来创建到提供者的连接)和Destination(客户端使用这个对象来指定发送或接收消息的目的地)
而具体到JMS应用程序,则主要涉及到以下基本概念。
- 生产者(Producer):创建并发送消息的JMS客户端,在点对点模型中就是发送者,在发布/订阅模型中就是发布者。
- 消费者(Consumer):接收消息的JMS客户端,在点对点模型中就是接收者,在发布/订阅模型中就是订阅者。
- 客户端(Client):生产或消费消息的基于Java的应用程序或对象。
- 队列(Queue):一个容纳被发送的等待阅读的消息的区域。它是点对点模型中的队列。
- 主题(Topic):一种支持发送消息给多个订阅者的机制。它是发布/订阅模型中的主题。
- 消息(Message):在JMS客户端之间传递的数据对象。JMS消息包括消息头、属性和消息体三部分。消息头是指所有消息都支持的相同的头字段集,它包含了客户端和JMS提供商都要使用的用于标识和路由消息的值。属性是指除标准的头字段外,消息接口还包含了一种支持属性值的内建机制。实际上,这也是为消息提供了一种添加可选的消息头字段的机制。消息属性包括应用专有属性、标准属性、提供商专有属性。JMS定义了集中消息体类型,这些类型覆盖了当前使用的大部分消息风格。消息体就是指实际的消息内容。
编程接口
- ConnectionFactory接口(连接工厂)
ConnectionFactory是创建Connection对象的工厂,根据不同的消息类型用户可选择用队列连接工厂或者主题连接工厂,分别对应QueueConnectionFactory 和 TopicConnectionFactory。可以通过JNDI来查找ConnectionFactory对象。 - Connection接口(连接)
Connection表示在客户端和JMS系统之间建立的连接(实际上是对TCP/IP Socket的包装)。Connection可以产生一个或多个Session,跟ConnectionFactory一样,Connection也有两种类型:QueueConnection 和 TopicConnection。 - Session接口(会话)
Session是实际操作消息的接口,表示一个单线程的上下文,用于发送和接收消息。因为会话是单线程的,所以消息是按照发送的顺序一个个接收的。可以通过Session创建生产者、消费者、消息等。在规范中Session还提供了事务的功能。Session也分为两种类型:QueueSession和TopicSession。 - Destination接口(目的地)
Destination是一个包装了消息目的地标识符的受管对象。消息目的地是指消息发布和接收的地点,消息目的地要么是队列要么是主题。对于消息生产者来说,它的Destination是某个队列或某个主题;对于消息消费者来说,它的Destination也是某个队列或主题。所以Destination实际上就是两种类型的对象:Queue和Topic,可以通过JNDI来查找Destination。 - MessageProducer 接口(消息生产者)
消息生产者由Session创建并用于将消息发送到Destination。消费者可以同步(阻塞模式)或异步(非阻塞模式)接收队列和主题类型的消息。消息生产者有两种类型:QueueSender和TopicPublisher。 - MessageConsumer接口(消息消费者)
消息消费者由Session创建,用于接收被发送到Destination的消息。消息消费者有两种类型:QueueReceiver和TopicSubscriber。 - Message接口(消息)
消息是在消费者和生产者之间传送的对象,即将消息从一个应用程序发送到另一个应用程序。 - MessageListener(消息监听器)
如果注册了消息监听器,那么当消息到达时将自动调用监听器的onMessage方法。
JMS 1.1 示例
ActiveMQ中基于点对点模型使用JMS1.1 API的一个例子。
(1)引入依赖
首先,在工程中需要引入ActiveMQ包的依赖。
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
(2)消息生产者
消息生产者的代码如下:
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* @author wangmin
* @description 消息生产者
* created at 2020/5/28 5:15 下午
*/
public class QueueProducer {
//默认用户名
public static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
//默认密码
public static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
//默认连接地址
public static final String BROKER_URL = ActiveMQConnection.DEFAULT_BROKER_URL;
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKER_URL);
try {
//创建连接
Connection connection = connectionFactory.createConnection();
//启动连接
connection.start();
//创建会话
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
//创建队列,需要指定队列名称,消息生产者和消费者根据它来发送、接收对应的消息
Queue myTestQueue = session.createQueue("activemq-queue-testl");
//消息生产者
MessageProducer producer = session.createProducer(myTestQueue);
//创建一个消息对象
TextMessage message = session.createTextMessage("测试点对点的一条消息");
//发送一条消息
producer.send(message);
//提交事务
session.commit();
//关闭资源
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
基本步骤就是首先通过设置消息队列厂商提供的参数创建连接工厂,接下来创建连接、创建会话、创建队列、创建消息对象,然后将消息发送到相应的队列对象中,最后关闭资源连接。
(3)消息消费者
消息消费者的代码如下:
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* @author wangmin
* @description 消息消费者
* created at 2020/5/28 5:34 下午
*/
public class QueueConsumer {
//默认用户名
public static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
//默认密码
public static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
//默认连接地址
public static final String BROKER_URL = ActiveMQConnection.DEFAULT_BROKER_URL;
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKER_URL);
try {
//创建连接
Connection connection = connectionFactory.createConnection();
//启动连接
connection.start();
//创建会话
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
//创建队列,作为消费者消费消息的目的地
Queue myTestQueue = session.createQueue("activemq-queue-test1");
//消息消费者
MessageConsumer consumer = session.createConsumer(myTestQueue);
//消费者实现监听接口消费消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
try {
session.commit();
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//让主线程休眠100秒,使消息消费者对象能继续存活一段时间,从而能监听到消息
Thread.sleep(100 * 1000);
//关闭资源
session.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
各种组建的创建步骤同上面的消息生产者,不同的是在消费消息时注册了消息监听器,通过这种方式来消费消息。
这样,一个简单的基于点对点模型的代码编写工作就完成了。在这个例子中,可以看到在ActiveMQ中时如何通过JMS的API来完成消息的发送和接收功能的。由于上面代码是基于JMS规范的,所以若换成使用其他消息中间件产品,只要该产品支持JMS,那么只需要做很少的改动就可以继续使用该段代码(即修改连接工厂初始化的代码,因为该代码和具体的消息产品提供商有关)。这也是JMS的优势之一。
JMS2.0 概述
在JMS 2.0中主要进行了易用性方面的改进,这样可以帮助开发者减少代码的编写量。新的API被称作简化的API(Simplified API),其比JMS 1.1 API更简单易用;后者被称作经典API(Classic API)
简化的API由三个新接口构成:JMSContext、JMSProducer、JMSConsumer。
- JMSContext:用于替换经典API中单独的Connection 和 Session。
- JMSProducer:用于替换经典API中的MessageProducer,其支持以链式操作(方法链)方式配置消息传递选项、消息头和消息属性。
- JMSConcumer:用于替换经典API中的MessageConsumer,其使用方式与JMSProducer类似。
简化的API不仅提供了经典API的所有特性,还增加了一些其他特性。经典API并没有被弃用,而是作为JMS的一部分被保留下来。下面通过发送文本消息的例子来看一下两者之间的区别。
在经典API中,一般需要经过下面几个步骤来发送文本消息。
/**
* 使用JMS 1.1 发送消息
* @param connectionFactory
* @param queue
* @param text
*/
public void sendMessageJMS11(ConnectionFactory connectionFactory, Queue queue, String text) {
try {
Connection connection = connectionFactory.createConnection();
try {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer messageProducer = session.createProducer(queue);
TextMessage textMessage= session.createTextMessage(text);
messageProducer.send(textMessage);
} finally {
connection.close();
}
} catch (JMSException ex) {
//handle exception
}
}
而用简化的API实现这个功能的代码如下:
/**
* 使用JMS 2.0 发送消息
* @param connectionFactory
* @param queue
* @param text
*/
public void sendMessageJMS20(ConnectionFactory connectionFactory, Queue queue, String text) {
try {
JMSContext jmsContext = connectionFactory.createContext();
jmsContext.createProducer().send(queue, text);
} catch (JMSRuntimeException ex) {
//handle exception
}
}
可以看到,要编写的代码量减少了很多,具体包括:
- 只需要创建一个JMSContext对象,而不是创建单独的Connection和Session对象。
- 在JMS 1.1 中,使用Connection后需要用一个finally语句块来关闭Connection对象。而在JMS 2.0 中,JMSContext对象也有一个需要在使用后调用的close方法,但不需要在代码中显示调用该方法。因为JMSContext实现了Java7的java.lang.AutoCloseable接口,所以如果在try-with-resources语句块中创建了JMSContext,则会在语句块的结尾处自动调用close方法,而无须在代码中显示调用。
- 在JMS 1.1 中,创建Session对象时需要传入参数(false和Session.AUTO_ACKNOWLEDGE),指明希望创建一个非事务性会话,在该会话中收到的所有消息都将被自动确认。而在JMS 2.0 中,这些都是默认设置的,无须指定任何参数。如果希望指定其他会话模式(本地事务、CLIENT_ACKNOWLEDGE或DUPS_OK_ACKNOWLEDGE),只需传入一个参数即可,而不是两个参数。
- 无须创建一个TextMessage对象并将其设置为指定字符串,只需将字符串传入send方法即可,由JMS提供商自动创建一个TextMessage对象并将其设置为所提供的字符串。
- 在JMS 1.1 中,几乎所有方法都会抛出JMSException,由于该异常是已检查异常,所以必须调用方法捕获它,或者自己抛出该异常。而在 JMS 2.0 中,抛出的异常是JMSRuntimeException,该异常是运行时异常,所以无须通过调用方法来显示捕获它,也不必在其throws字句中声明。
在JMS 2.0 中对其他API的简化包括直接从消息提取正文的新方法、直接接收消息征文的方法、创建会话的新方法、通过多种方式简化资源配置、设置传递延迟、异步发送消息、对于同一个主题订阅允许有多个使用者等。
详细的可参考:https://www.oracle.com/technetwork/cn/articles/java/jms20-1947669-zhs.html
https://www.oracle.com/technetwork/cn/articles/java/jms2messaging-1954190-zhs.html