Phần trước đo được rằng convertAndSend mặc định gửi Java serialization: 82 byte cho một record đáng lẽ 29 byte JSON, kèm tên class nằm trong thân. Bài này đổi sang JSON và đo xem việc đó gỡ được bao nhiêu phần của sự ràng buộc.

Jackson không có sẵn

Chi tiết đầu tiên, và nó làm tôi mất mấy phút: spring-boot-starter-amqp không kéo Jackson theo. Đếm số jar Jackson trong classpath trước khi khai thêm: 0. Thiếu nó thì new Jackson2JsonMessageConverter() ném NoClassDefFoundError lúc chạy, không phải lúc biên dịch.

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>

JSON gửi ra gì

@Bean
MessageConverter jsonConverter() { return new Jackson2JsonMessageConverter(); }
contentType : application/json
độ dài thân : 29 byte   (Java serialization ở phần 24: 82 byte)
thân        : {"ma":"DH-001","tien":250000}
headers     :
  __TypeId__ = lab.donhang.DonHang

Thân gọn hơn 2,8 lần và đọc được bằng mắt. Nhưng nhìn dòng cuối.

__TypeId__ là sợi dây vẫn còn nguyên

Jackson converter ghi tên class đầy đủ vào header để bên nhận biết dựng lại thành gì. Nghe hợp lý, cho tới khi bên nhận là một dịch vụ khác. Gửi một thông điệp với __TypeId__ trỏ tới class mà bên nhận không có:

LỖI: ClassNotFoundException
     com.doitac.donhang.DonHang
Đổi từ Java serialization sang JSON không tự gỡ được sự ràng buộc — nó chỉ chuyển chỗ. Thân thông điệp giờ ai cũng đọc được, nhưng header vẫn đòi bên nhận có đúng class đó, đúng package. Đổi tên package ở bên gửi là bên nhận hỏng, y như cũ.

Cách gỡ: đặt tên logic

DefaultJackson2JavaTypeMapper cho phép ánh xạ một tên do bạn đặt sang class ở mỗi phía:

// bên gửi
var mapper = new DefaultJackson2JavaTypeMapper();
mapper.setIdClassMapping(Map.of("don-hang", lab.donhang.DonHang.class));

// bên nhận — class khác package hoàn toàn
var mapper = new DefaultJackson2JavaTypeMapper();
mapper.setIdClassMapping(Map.of("don-hang", lab.ketoan.DonHang.class));
__TypeId__ gửi đi : don-hang
bên nhận đọc được : DonHang[ma=DH-003, tien=7]  (kiểu lab.ketoan.DonHang)

Giờ trên dây chỉ còn một chuỗi don-hang — một hợp đồng, không phải một đường dẫn class. Hai dịch vụ đổi tên package thoải mái, miễn giữ nguyên tên logic đó.

Mapper tự khai phải khai luôn setTrustedPackages

Cái bẫy đi kèm: DefaultJackson2JavaTypeMapper vừa tạo ra chỉ tin java.utiljava.lang.

mapper mới tạo, đọc một kiểu ngoài java.*:
  LỖI: The class 'lab.donhang.DonHang' is not in the trusted packages: [java.util, java.lang]

sau khi setTrustedPackages("lab.donhang"): đọc được lab.donhang.DonHang

Converter mặc định (không đặt mapper riêng) thì tin tất cả — nên mọi thứ chạy cho tới lúc bạn động vào mapper để làm đúng, và lúc đó nó vỡ. Đây là bảo vệ chống deserialize class tuỳ ý, nên đừng đặt "*" cho xong; hãy liệt kê đúng package của bạn.

Java serialization nhanh hơn, JSON nhẹ hơn

20 000 thông điệp, cùng dữ liệu, chỉ đổi converter:

Converter Thông lượng Kích thước thân
SimpleMessageConverter (Java) 234 982 msg/s 82 byte
Jackson2JsonMessageConverter 190 909 msg/s 29 byte

JSON chậm hơn 19% trên đường gửi vì phải tuần tự hoá sang văn bản, nhưng thân nhẹ hơn 2,8 lần. Với thông điệp nhỏ và mạng nội bộ, chênh lệch CPU đó không đáng gì so với việc bên nhận không cần là Java. Với luồng cực lớn thì hãy đo lại bằng dữ liệu thật của bạn — và cân nhắc định dạng nhị phân thay vì quay về Java serialization.

Nên đặt gì

@Bean
MessageConverter jsonConverter() {
    var mapper = new DefaultJackson2JavaTypeMapper();
    mapper.setTrustedPackages("com.congty.hopdong");
    mapper.setIdClassMapping(Map.of(
            "don-hang.tao", DonHangTao.class,
            "don-hang.huy", DonHangHuy.class));
    var conv = new Jackson2JsonMessageConverter();
    conv.setJavaTypeMapper(mapper);
    return conv;
}

Tên logic ở bên trái là hợp đồng giữa các dịch vụ — đặt nó cùng chỗ và cùng cẩn thận như bạn đặt routing key ở phần 10.

Bài sau: khai báo topology bằng bean Spring, và vì sao ứng dụng vẫn khởi động thành công khi broker đã tắt.

Thử ba mươi giây

curl -su guest:guest -X POST 'localhost:15672/api/queues/%2F/<ten>/get' \
  -H 'Content-Type: application/json' \
  -d '{"count":1,"ackmode":"ack_requeue_true","encoding":"auto"}' \
  | jq '{content_type: .[0].properties.content_type,
         typeId: .[0].properties.headers.__TypeId__}'

__TypeId__ ra một tên class đầy đủ nghĩa là hợp đồng giữa các dịch vụ của bạn đang là đường dẫn package — thứ sẽ gãy trong lần tái cấu trúc tiếp theo.