JMS
JMS是一个Java标准,定义了使用消息代理(message broker)的通用API,最早于2001年提出。长期以来,JMS一直是实现异步消息的首选方案。在JMS出现之前,每个消息代理都有私有的API,这就使得不同代理之间的消息代码很难通用。但是借助JMS,所有遵从规范的实现都使用通用的接口,这就好像JDBC为数据库操作提供了通用的接口一样。
Spring通过基于模板的抽象为JMS功能提供了支持,这个模板就是JmsTemplate。借助JmsTemplate,我们能够非常容易地在消息生产方发送队列和主题消息,在消费消息的那一方,也能够非常容易地接收这些消息。
Spring还提供了消息驱动POJO的理念:这是一个简单的Java对象,它能够以异步的方式响应队列或主题上到达的消息。
搭建JMS环境
安装ActiveMQ
可通过docker快速安装一个ActiveMQ的实验环境,具体过程请查阅资料。
添加依赖到项目中
初始化一个Spring Boot Web项目,并添加ActiveMQ的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
属性配置
spring:
activemq:
# 代理的URL
broker-url: tcp://localhost:61616
# 用来访问代理的用户
user: admin
# 用来访问代理的密码
password: admin
配置消息转换器
消息转换器用于在消息发送之前,将某个pojo对象转为Message。在底层,这是通过MessageConverter的实现类来完成的,它替我们做了将对象转换成Message的脏活累活。
MessageConverter是Spring定义的接口,只有两个需要实现的方法:
public interface MessageConverter {
Message toMessage(Object object, Session session)
throws JMSException, MessageConversionException;
Object fromMessage(Message message)
}
这个接口实现起来很简单,但我们通常并没有必要创建自定义的实现。Spring已经提供了多个实现:
- MappingJackson2MessageConverter:使用Jackson 2 JSON 库实现消息与JSON格式之间的互相转换。
- MarshallingMessageConverter:使用JAXB库实现消息与XML格式之间的相互转换。
- MessagingMessageConverter:使用底层的MessageConverter实现消息抽象的Message载荷与javax.jms.Message之间的转换,同时会使用JmsHeaderMapper实现JMS头信息与标准消息头信息之间的转换。
- SimpleMessageConverter:实现String与TextMessage之间的互相转换、字节数组与ByteMessage之间的相互转换,Map与MapMessage之间的相互转换以及Serializable对象与ObjectMessage之间的相互转换。
默认情况下,将会使用SimpleMessageConverter,但是它需要被发送的对象实现Serializable。这种办法可能也不错,但有时候我们可能想要使用其他的消息转换器来消除这种限制,比如MappingJackson2MessageConverter
@Configuration
public class ActiveMQConfig {
@Bean
public MessageConverter messageConverter(){
MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
Map<String, Class<?>> typeIdMappings = new HashMap<>();
typeIdMappings.put("order", Order.class);
typeIdMappings.put("user", User.class);
messageConverter.setTypeIdMappings(typeIdMappings);
return messageConverter;
}
}
使用 JmsTemplate 发送消息
引入ActiveMQ的依赖后,Spring Boot会自动配置一个JmsTemplate(以及其他内容),我们可以将它注入到其他bean中,并使用它来发送和接收消息。
JmsTemplate是Spring对JMS集成支持功能的核心。与Spring其他面向模板的组件类似,JmsTemplate消除了大量传统使用JMS时所需的样板代码。如果没有JmsTemplate的话,那么我们需要编写代码来创建到消息代理的连接和会话,还要编写更多的代码来处理发送消息过程中可能出现的异常。JmsTemplate能够让我们关注真正要做的事情:发送消息。
JmsTemplate有多个用来发送消息的方法,包括:
// 发送原始的消息
void send(MessageCreator messageCreator) throws JmsException;
void send(Destination destination, MessageCreator messageCreator)
throws JmsException;
void send(String destinationName, MessageCreator messageCreator)
throws JmsException;
// 发送根据对象转换而成的消息
void convertAndSend(Object message) throws JmsException;
void convertAndSend(Destination destination, Object message)
throws JmsException;
void convertAndSend(String destinationName, Object message)
throws JmsException;
// 发送根据对象转换而成的消息并且带有后期处理的功能
void convertAndSend(Object message,
MessagePostProcessor postProcessor) throws JmsException;
void convertAndSend(Destination destination, Object message,
MessagePostProcessor postProcessor) throws JmsException;
void convertAndSend(String destinationName, Object message,
MessagePostProcessor postProcessor) throws JmsException;
3个send()方法都需要MessageCreator来生成Message对象。
3个convertAndSend()方法会接受Object对象,并且会在幕后自动将Object转换为Message。
最后3个convertAndSend()会自动将Object转换为Message,但同时还能接受一个MessagePostProcessor对象,用来在发送之前对Message进行自定义。
示例代码
@Service
public class JmsOrderMessageService implements OrderMessageService {
@Autowired
private JmsTemplate jmsTemplate;
@Override
public void sendOrderMessage(Order order) {
//匿名内部类方式
/*jmsTemplate.send("boot.integrate.jms.order.queue", new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
Message message = session.createObjectMessage(order);
return message;
}
});*/
//使用 lambda 替换上面的写法
/*jmsTemplate.send("boot.integrate.jms.order.queue", session -> {
Message message = session.createObjectMessage(order);
return message;
});*/
//使用ConvertAndSend
//jmsTemplate.convertAndSend("boot.integrate.jms.order.queue",order);
//在消息发送之前使用MessagePostProcessor添加一个自定义的头信息
/*jmsTemplate.convertAndSend("boot.integrate.jms.order.queue", order, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("X_ORDER_SOURCE","WEB");
return message;
}
});*/
//使用方法引用替换匿名内部类的写法
jmsTemplate.convertAndSend("boot.integrate.jms.order.queue", order, this::addOrderSource);
}
@Override
public void sendUserMessage(User user) {
jmsTemplate.convertAndSend("boot.integrate.jms.user.queue", user);
}
private Message addOrderSource(Message message) throws JMSException {
message.setStringProperty("X_ORDER_SOURCE","WEB");
return message;
}
}
接收JMS消息
在消费消息的时候,我们可以选择拉取模式(pull model)和推送模式(push model),前者会在我们的代码中请求消息并一直等待直到消息到达为止,而后者则会在消息可用的时候自动在你的代码中执行。
JmsTemplate提供了多种方式来接收消息,但它们使用的都是拉取模式。我们可以调用其中的某个方法来请求消息,而线程会一直阻塞到一个消息抵达为止(这可能马上发生,也可能需要等待一会儿)。
另外,我们也可以使用推送模式,在这种情况下,我们会定义一个消息监听器,每当有消息可用时,它就会被调用。
这两种方案能够适用于各种用户场景。人们普遍觉得推送模式是更好的方案,因为它不会阻塞线程;但是,在某些场景下,如果消息抵达的速度太快,那么监听器可能会过载。而拉取模式允许消费者声明它们何时才为接收新消息做好准备。
拉取模式
@Slf4j
@Service
public class JmsOrderMessageService implements OrderMessageService {
private JmsTemplate jmsTemplate;
private MessageConverter messageConverter;
@Autowired
public JmsOrderMessageService(JmsTemplate jmsTemplate,MessageConverter messageConverter) {
this.jmsTemplate = jmsTemplate;
this.messageConverter = messageConverter;
}
@Override
public Order receive() throws JMSException {
Message message = jmsTemplate.receive("boot.integrate.jms.order.queue");
Order order = (Order) messageConverter.fromMessage(message);
return order;
}
@Override
public User receiveUser() throws JMSException {
Message message = jmsTemplate.receive("boot.integrate.jms.user.queue");
User user = (User) messageConverter.fromMessage(message);
return user;
}
}
推送模式
@Slf4j
@Component
public class OrderMessageListener {
@JmsListener(destination = "boot.integrate.jms.order.queue")
public void receiveOrder(Order order){
log.info("Order info={}", JSON.toJSONString(order));
}
}
@Slf4j
@Component
public class UserMessageListener {
@JmsListener(destination = "boot.integrate.jms.user.queue")
public void receiveOrder(User user){
log.info("User info={}", JSON.toJSONString(user));
}
}