9.1.2 Gửi tin nhắn với JmsTemplate
Với một dependency starter JMS (dù là Artemis hay ActiveMQ) đã được thêm vào build của bạn, Spring Boot sẽ tự động cấu hình một JmsTemplate (cùng với một số thành phần khác) mà bạn có thể inject và sử dụng để gửi và nhận tin nhắn.
JmsTemplate là trung tâm trong hỗ trợ tích hợp JMS của Spring. Tương tự như các thành phần dựa trên mẫu (template) khác của Spring, JmsTemplate loại bỏ rất nhiều mã lặp lại (boilerplate) mà bình thường bạn phải viết để làm việc với JMS. Nếu không có JmsTemplate, bạn sẽ phải viết mã để tạo kết nối và phiên làm việc (session) với message broker và xử lý các ngoại lệ có thể phát sinh khi gửi tin nhắn. JmsTemplate tập trung vào điều mà bạn thực sự muốn làm: gửi tin nhắn.
JmsTemplate cung cấp một số phương thức hữu ích để gửi tin nhắn, bao gồm:
// Send raw messages
void send(MessageCreator messageCreator) throws JmsException;
void send(Destination destination, MessageCreator messageCreator)
throws JmsException;
void send(String destinationName, MessageCreator messageCreator)
throws JmsException;
// Send messages converted from objects
void convertAndSend(Object message) throws JmsException;
void convertAndSend(Destination destination, Object message)
throws JmsException;
void convertAndSend(String destinationName, Object message)
throws JmsException;
// Send messages converted from objects with post-processing
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;Như bạn thấy, thực tế chỉ có hai phương thức chính là send() và convertAndSend(), mỗi phương thức được overload để hỗ trợ các kiểu tham số khác nhau. Nếu bạn phân tích kỹ hơn, bạn sẽ nhận ra các dạng khác nhau của convertAndSend() có thể được chia thành hai loại phụ. Để hiểu rõ các phương thức này làm gì, hãy xem phân tích sau:
- Ba phương thức
send()yêu cầu mộtMessageCreatorđể tạo ra một đối tượngMessage. - Ba phương thức
convertAndSend()nhận vào mộtObjectvà tự động chuyển đổi đối tượng đó thành mộtMessagetrong nội bộ. - Ba phương thức
convertAndSend()khác cũng tự động chuyểnObjectthànhMessagenhưng cho phép thêm mộtMessagePostProcessorđể tùy biếnMessagetrước khi gửi.
Hơn nữa, mỗi nhóm ba phương thức kể trên đều có ba dạng overload dựa trên cách chỉ định đích đến (destination) của JMS như sau:
- Một phương thức không có tham số
destinationvà gửi đến đích mặc định. - Một phương thức nhận một đối tượng
Destinationchỉ định nơi gửi tin nhắn. - Một phương thức nhận một chuỗi
Stringchỉ định tên của đích gửi tin nhắn.
Để áp dụng các phương thức này, hãy xem ví dụ JmsOrderMessagingService trong đoạn mã dưới đây, sử dụng dạng cơ bản nhất của phương thức send().
Danh sách 9.1 Gửi một đơn hàng với .send() đến đích mặc định
package tacos.messaging;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Service;
@Service
public class JmsOrderMessagingService implements OrderMessagingService {
private JmsTemplate jms;
@Autowired
public JmsOrderMessagingService(JmsTemplate jms) {
this.jms = jms;
}
@Override
public void sendOrder(TacoOrder order) {
jms.send(new MessageCreator() {
@Override
public Message createMessage(Session session)
throws JMSException {
return session.createObjectMessage(order);
}
}
);
}
}Phương thức sendOrder() gọi jms.send(), truyền vào một implementation ẩn danh của MessageCreator. Implementation này override phương thức createMessage() để tạo một ObjectMessage mới từ đối tượng TacoOrder được cung cấp.
Vì lớp JmsOrderMessagingService (đặc thù JMS) hiện thực giao diện OrderMessagingService (tổng quát hơn), nên ta có thể sử dụng nó bằng cách inject vào OrderApiController và gọi sendOrder() khi một đơn hàng được tạo, như trong ví dụ sau:
@RestController
@RequestMapping(path="/api/orders",
produces="application/json")
@CrossOrigin(origins="*")
public class OrderApiController {
private OrderRepository repo;
private OrderMessagingService messageService;
public OrderApiController(
OrderRepository repo,
OrderMessagingService messageService) {
this.repo = repo;
this.messageService = messageService;
}
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public TacoOrder postOrder(@RequestBody TacoOrder order) {
messageService.sendOrder(order);
return repo.save(order);
}
...
}Bây giờ, khi bạn tạo một đơn hàng qua website Taco Cloud, một tin nhắn sẽ được gửi tới message broker để chuyển tiếp đến một ứng dụng khác sẽ nhận đơn hàng. Hiện tại ta chưa có thành phần nào để nhận tin nhắn đó. Tuy vậy, bạn có thể sử dụng bảng điều khiển của Artemis để xem nội dung trong hàng đợi (queue). Xem hướng dẫn tại: https://activemq.apache.org/components/artemis/documentation/latest/management-console.html để biết thêm chi tiết.
Tôi không biết bạn thế nào, nhưng tôi nghĩ rằng đoạn mã trong danh sách 9.1, dù đơn giản, vẫn hơi cồng kềnh. Việc phải khai báo một lớp ẩn danh khiến lời gọi phương thức vốn đơn giản trở nên phức tạp. Vì MessageCreator là một functional interface, bạn có thể làm cho phương thức sendOrder() gọn hơn bằng lambda, như ví dụ sau:
@Override
public void sendOrder(TacoOrder order) {
jms.send(session -> session.createObjectMessage(order));
}Nhưng hãy lưu ý rằng lời gọi jms.send() không chỉ định đích đến. Để điều này hoạt động, bạn cần chỉ định một tên đích mặc định bằng thuộc tính spring.jms.template.default-destination. Ví dụ, bạn có thể thiết lập thuộc tính này trong file application.yml như sau:
spring:
jms:
template:
default-destination: tacocloud.order.queueTrong nhiều trường hợp, việc sử dụng một đích mặc định là lựa chọn dễ dàng nhất. Điều đó cho phép bạn chỉ định tên đích một lần, giúp mã chỉ tập trung vào việc gửi tin nhắn mà không cần quan tâm đến việc chúng được gửi đến đâu. Nhưng nếu bạn cần gửi tin nhắn đến một đích khác ngoài đích mặc định, bạn cần chỉ định đích đó như một tham số trong lời gọi send().
Một cách để làm điều này là truyền một đối tượng Destination như là tham số đầu tiên cho send(). Cách dễ nhất để làm điều đó là khai báo một bean Destination và inject nó vào bean sẽ thực hiện việc gửi tin nhắn. Ví dụ, bean dưới đây khai báo một đích là hàng đợi đơn hàng Taco Cloud:
@Bean
public Destination orderQueue() {
return new ActiveMQQueue("tacocloud.order.queue");
}Phương thức bean này có thể được thêm vào bất kỳ lớp cấu hình nào trong ứng dụng dùng để gửi hoặc nhận tin nhắn thông qua JMS. Để tổ chức tốt hơn, bạn nên thêm nó vào một lớp cấu hình dành riêng cho cấu hình messaging, ví dụ MessagingConfig.
Điều quan trọng cần lưu ý là ActiveMQQueue được sử dụng ở đây thực chất là từ Artemis (thuộc package org.apache.activemq.artemis.jms.client). Nếu bạn đang dùng ActiveMQ (không phải Artemis), cũng có một lớp tên là ActiveMQQueue (trong package org.apache.activemq.command).
Nếu bean Destination này được inject vào JmsOrderMessagingService, bạn có thể sử dụng nó để chỉ định đích khi gọi send(), như sau:
private Destination orderQueue;
@Autowired
public JmsOrderMessagingService(JmsTemplate jms,
Destination orderQueue) {
this.jms = jms;
this.orderQueue = orderQueue;
}
...
@Override
public void sendOrder(TacoOrder order) {
jms.send(
orderQueue,
session -> session.createObjectMessage(order));
}Việc chỉ định đích với một đối tượng Destination như thế này cho phép bạn cấu hình đích với nhiều thông tin hơn chỉ là tên. Nhưng trên thực tế, bạn hiếm khi chỉ định nhiều hơn tên đích. Thường thì dễ dàng hơn khi chỉ gửi tên dưới dạng chuỗi như sau:
@Override
public void sendOrder(TacoOrder order) {
jms.send(
"tacocloud.order.queue",
session -> session.createObjectMessage(order));
}Dù phương thức send() không quá khó để sử dụng (đặc biệt là khi dùng MessageCreator dưới dạng lambda), nhưng nó vẫn thêm một chút phức tạp vì bạn cần cung cấp một MessageCreator. Sẽ đơn giản hơn nếu bạn chỉ cần chỉ định đối tượng muốn gửi (và tùy chọn thêm đích gửi). Điều này mô tả đúng cách mà convertAndSend() hoạt động. Hãy cùng tìm hiểu.
CHUYỂN ĐỔI TIN NHẮN TRƯỚC KHI GỬI
Phương thức convertAndSend() của JmsTemplate đơn giản hóa việc gửi tin nhắn bằng cách loại bỏ yêu cầu cung cấp MessageCreator. Thay vào đó, bạn chỉ cần truyền đối tượng muốn gửi trực tiếp vào convertAndSend(), và đối tượng đó sẽ được chuyển đổi thành Message trước khi gửi.
Ví dụ, phương thức sendOrder() dưới đây sử dụng convertAndSend() để gửi một TacoOrder đến một đích cụ thể:
@Override
public void sendOrder(TacoOrder order) {
jms.convertAndSend("tacocloud.order.queue", order);
}Cũng giống như phương thức send(), convertAndSend() có thể nhận một đối tượng Destination hoặc chuỗi String để chỉ định đích đến, hoặc bạn có thể bỏ qua đích để gửi đến đích mặc định.
Dù bạn chọn dạng nào của convertAndSend(), đối tượng TacoOrder được truyền vào sẽ được chuyển thành Message trước khi gửi. Trong nội bộ, điều này được thực hiện bởi một implementation của MessageConverter, thứ chịu trách nhiệm chuyển đổi các đối tượng miền ứng dụng thành các đối tượng Message.
CẤU HÌNH MỘT MESSAGE CONVERTER
MessageConverter là một interface được định nghĩa bởi Spring, chỉ có hai phương thức cần hiện thực:
public interface MessageConverter {
Message toMessage(Object object, Session session)
throws JMSException, MessageConversionException;
Object fromMessage(Message message)
}Mặc dù interface này đủ đơn giản để bạn có thể tự viết implementation, nhưng bạn thường không cần phải làm vậy. Spring đã cung cấp sẵn một số implementation, như được mô tả trong bảng 9.3.
Table 9.3 Các bộ chuyển đổi tin nhắn của Spring cho các nhiệm vụ chuyển đổi phổ biến (tất cả nằm trong package org.springframework.jms.support.converter)
| Message converter | What it does |
|---|---|
| MappingJackson2MessageConverter | Uses the Jackson 2 JSON library to convert messages toand from JSON |
| MarshallingMessageConverter | Uses JAXB to convert messages to and from XML |
| MessagingMessageConverter | Converts a Message from the messaging abstraction to and from a Message using an underlying MessageConverter for the payload and a JmsHeaderMapper to map the JMS headers to and from standard message headers |
| SimpleMessageConverter | Converts a String to and from a TextMessage, byte arrays to and from a BytesMessage, a Map to and from a MapMessage, and a Serializable to and from an ObjectMessage |
SimpleMessageConverter là mặc định, nhưng nó yêu cầu đối tượng gửi phải implement Serializable. Điều này có thể là hợp lý, nhưng bạn có thể thích dùng một trong những bộ chuyển đổi khác, chẳng hạn như MappingJackson2MessageConverter, để tránh hạn chế đó.
Để sử dụng một bộ chuyển đổi khác, bạn chỉ cần khai báo một instance của bộ chuyển đổi đó dưới dạng một bean. Ví dụ, đoạn khai báo bean dưới đây sẽ kích hoạt việc sử dụng MappingJackson2MessageConverter thay vì SimpleMessageConverter:
@Bean
public MappingJackson2MessageConverter messageConverter() {
MappingJackson2MessageConverter messageConverter =
new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
return messageConverter;
}Phương thức bean này có thể được đặt trong bất kỳ lớp cấu hình nào trong ứng dụng gửi và nhận tin nhắn với JMS, bao gồm cả cùng với bean Destination trong MessagingConfig.
Lưu ý rằng bạn đã gọi setTypeIdPropertyName() trên MappingJackson2MessageConverter trước khi trả về nó. Điều này rất quan trọng, vì nó cho phép phía nhận biết kiểu dữ liệu nào cần chuyển đổi từ tin nhắn đến. Theo mặc định, thuộc tính này sẽ chứa tên lớp đầy đủ (fully qualified classname) của kiểu dữ liệu được chuyển đổi. Tuy nhiên, phương pháp này hơi cứng nhắc, yêu cầu phía nhận cũng phải có cùng một kiểu dữ liệu với cùng tên lớp đầy đủ.
Để linh hoạt hơn, bạn có thể ánh xạ một tên kiểu giả định (synthetic type name) đến kiểu dữ liệu thực tế bằng cách gọi setTypeIdMappings() trên converter. Ví dụ, thay đổi sau đây trong phương thức bean sẽ ánh xạ ID kiểu TacoOrder giả định đến lớp TacoOrder:
@Bean
public MappingJackson2MessageConverter messageConverter() {
MappingJackson2MessageConverter messageConverter =
new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
Map<String, Class<?>> typeIdMappings = new HashMap<String, Class<?>>();
typeIdMappings.put("order", TacoOrder.class);
messageConverter.setTypeIdMappings(typeIdMappings);
return messageConverter;
}Thay vì gửi tên lớp đầy đủ trong thuộc tính _typeId của tin nhắn, giá trị TacoOrder sẽ được gửi. Trong ứng dụng nhận, một converter tương tự sẽ được cấu hình, ánh xạ TacoOrder đến kiểu định nghĩa của riêng nó cho một đơn hàng. Việc triển khai đơn hàng đó có thể nằm trong một package khác, có tên khác, thậm chí chỉ chứa một phần dữ liệu của TacoOrder bên phía gửi.
XỬ LÝ SAU KHI TẠO TIN NHẮN (POSTPROCESSING MESSAGES)
Giả sử rằng ngoài hoạt động kinh doanh trực tuyến sinh lợi, Taco Cloud đã quyết định mở thêm một vài cửa hàng taco truyền thống. Vì bất kỳ cửa hàng nào cũng có thể là trung tâm thực hiện đơn hàng từ website, họ cần một cách để truyền đạt nguồn gốc của đơn hàng đến nhà bếp tại các nhà hàng. Điều này sẽ giúp nhân viên bếp áp dụng quy trình khác nhau cho đơn hàng từ cửa hàng và đơn hàng từ web.
Một cách hợp lý là thêm một thuộc tính source mới vào đối tượng TacoOrder để mang thông tin này, với giá trị là WEB cho đơn hàng trực tuyến và STORE cho đơn hàng đặt tại cửa hàng. Nhưng cách này sẽ yêu cầu thay đổi cả lớp TacoOrder ở website và trong ứng dụng nhà bếp, trong khi thực tế, thông tin này chỉ cần cho người chế biến taco.
Giải pháp dễ hơn là thêm một header tùy chỉnh vào tin nhắn để mang thông tin nguồn đơn hàng. Nếu bạn đang dùng phương thức send() để gửi đơn hàng taco, điều này có thể thực hiện dễ dàng bằng cách gọi setStringProperty() trên đối tượng Message như sau:
jms.send("tacocloud.order.queue",
session -> {
Message message = session.createObjectMessage(order);
message.setStringProperty("X_ORDER_SOURCE", "WEB");
});Vấn đề là bạn không sử dụng send(). Bằng việc chọn convertAndSend(), đối tượng Message được tạo ngầm, và bạn không thể truy cập vào nó.
May mắn thay, bạn có một cách để điều chỉnh Message được tạo ngầm trước khi nó được gửi. Bằng cách truyền vào một MessagePostProcessor như tham số cuối cùng cho convertAndSend(), bạn có thể làm bất cứ điều gì với Message sau khi nó đã được tạo. Đoạn mã sau đây vẫn dùng convertAndSend(), nhưng cũng dùng MessagePostProcessor để thêm header X_ORDER_SOURCE trước khi gửi tin nhắn:
jms.convertAndSend("tacocloud.order.queue", order, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("X_ORDER_SOURCE", "WEB");
return message;
}
});Bạn có thể nhận thấy rằng MessagePostProcessor là một functional interface. Điều này có nghĩa là bạn có thể đơn giản hóa nó một chút bằng cách thay thế lớp ẩn danh bằng một biểu thức lambda như sau:
jms.convertAndSend("tacocloud.order.queue", order,
message -> {
message.setStringProperty("X_ORDER_SOURCE", "WEB");
return message;
});Mặc dù bạn chỉ cần MessagePostProcessor này cho một lần gọi convertAndSend() duy nhất, bạn có thể sẽ sử dụng cùng một MessagePostProcessor cho nhiều lần gọi convertAndSend() khác nhau. Trong những trường hợp đó, có thể sử dụng method reference như dưới đây sẽ là lựa chọn tốt hơn biểu thức lambda, tránh lặp mã không cần thiết:
@GetMapping("/convertAndSend/order")
public String convertAndSendOrder() {
TacoOrder order = buildOrder();
jms.convertAndSend("tacocloud.order.queue", order,
this::addOrderSource);
return "Convert and sent order";
}
private Message addOrderSource(Message message) throws JMSException {
message.setStringProperty("X_ORDER_SOURCE", "WEB");
return message;
}Giờ bạn đã thấy nhiều cách khác nhau để gửi tin nhắn. Nhưng gửi tin nhắn sẽ vô ích nếu không ai nhận được. Hãy cùng tìm hiểu cách bạn có thể nhận tin nhắn với Spring JMS.
