使用 Apache Camel 实现消费 RabbitMQ 消息并通过 SMPP 协议发送短消息

Apache Camel 是一个开源集成框架,使您能够快速轻松地集成各种消费或生产数据的系统。^1^

这里使用 Apache Camel 来实现消费 RabbitMQ 的消息,并将其通过 SMPP 协议发送短信息的功能。第一次使用,了解不多,但总体感觉确实集成的很彻底,使用起来很方便。

主要功能的代码如下:

java 复制代码
package me.liujiajia.sample.samplesmpp;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import static org.apache.camel.component.smpp.SmppConstants.DEST_ADDR;
import static org.apache.camel.component.smpp.SmppConstants.SOURCE_ADDR;

@Component
class Mq2SmsRoute extends RouteBuilder {

    @Autowired
    private ShortMsgFormatService shortMsgFormatService;

    @Autowired
    private SMSProcessor smsProcessor;

    @Override
    public void configure() {

        from("rabbitmq:sms" +
                "?queue=sms" +
                // 也可以在这里配置 RabbitMQ 服务器的地址、用户和密码
                // "&addresses=localhost:5672&username=sms&password=123456" +
                "&autoDelete=false&autoAck=false" +
                // 如果配置了死信队列,发送失败的消息会自动转发到配置的死信队列
                "&deadLetterExchange=sms-failed&deadLetterQueue=sms-failed")
                .log("mq >>> ${body}")
                // 使用 Jackson 反序列化
                .unmarshal()
                .json(JsonLibrary.Jackson, ShortMsgEntity.class)
                .log("jp >>> ${body.id}")
                .log("jp >>> ${body.name}")
                .process(exchange -> {
                    ShortMsgEntity shortMsg = exchange.getIn().getBody(ShortMsgEntity.class);
                    // 调用 service 示例
                    shortMsgFormatService.format(shortMsg);
                    // 可以通过 setHeader 方法指定 Header
                    // exchange.getIn().setHeader(SERVICE_TYPE, "CMT");
                    // exchange.getIn().setHeader(SOURCE_ADDR_TON, TypeOfNumber.ALPHANUMERIC.value());
                    // exchange.getIn().setHeader(SOURCE_ADDR_NPI, NumberingPlanIndicator.UNKNOWN.value());
                    exchange.getIn().setHeader(SOURCE_ADDR, shortMsg.getFrom());
                    // exchange.getIn().setHeader(DEST_ADDR_TON, TypeOfNumber.ALPHANUMERIC.value());
                    // exchange.getIn().setHeader(DEST_ADDR_NPI, NumberingPlanIndicator.UNKNOWN.value());
                    exchange.getIn().setHeader(DEST_ADDR, shortMsg.getTo());
                    // 设置消息内容
                    exchange.getIn().setBody(shortMsg.getContent());
                })
                .log("to >>> ${body}")
                // 默认配置下,如果和 SMPP 服务器直接的链接断了,会自动尝试重连,直至成功为止(默认的最大重连次数很大)。
                // 断连过程中接收的消息会发送失败。
                .to("smpp://sms@localhost:2775" +
                                "?password=123456" +
                                "&enquireLinkTimer=3000" +
                                "&transactionTimer=5000" +
                                "&systemType=producer"
                )
                // processor 示例
                // 运行到这里的时候,已经可以从 Header 中获取 msgid(在 messages.log 文件中为 queue-msgid)
                // exchange.getIn().getHeader(SmppConstants.ID, String.class)
                // 如果发送处理中发生异常(比如连接不上 SMPP 服务器),代码不进行到这里的 processor
                .process(smsProcessor)
                // 这里的 body 仍然和 to 之前一样,没有变化
                .log("ed >>> ${body}");
    }
}

从上面的代码可以看到,除了 from()to() 之外,最重要的就是 process() 方法,它接收一个 Processor 参数。

java 复制代码
package org.apache.camel;

@FunctionalInterface
public interface Processor {
    void process(Exchange exchange) throws Exception;
}

Processor 仅包含一个 process() 方法,方法中可以通过 exchange.getIn() 获取当前的消息,之后就可以对其进行设置了。

本示例中使用了 3 个组件:RabbitMQ ^2^、JSON Jackson ^3^ 和 SMPP ^4^ 。

每个组件的文档基本都包含如下几个部分:

  • URI FORMAT | URI 格式

    这部分是描述资源路径的格式,和网页地址比较类似。 如下是 RabbitMQ 组件的 URI 格式。

    javascript 复制代码
    rabbitmq:exchangeName?[options]

    其中 [options] 的具体参数见下面的 ENDPOINT OPTIONS 部分。

  • COMPONENT OPTIONS | 组件选项

    组件的选项一般和下面的端点选项(ENDPOINT OPTIONS)一般都是一样的,如果两个都配置的话,端点选项的优先级较高。

    组件选项示例:

    yaml 复制代码
    camel:
      component:
        rabbitmq:
          addresses: localhost:5672
          username: sms
          password: 123456
        smpp:
          enquire-link-timer: 3000
  • ENDPOINT OPTIONS | 端点选项

    用来指定单个端点的选项,拼在 URI 中 ? 的后面即可。

  • MESSAGE HEADERS | 消息头

    当前组件消息支持的 Header,可以通过 exchange.getIn().setHeader() 方法进行设置。

  • SAMPLES | 示例

    这里是组件的写法示例。

虽然是第一次用,但是组件的文档描述的都很详细,选项也比较容易理解。不过组件的选项大都比较多,使用前最好还是花点时间仔细看一下。

关于组件的版本,虽然现在最新的是 4.1.0 ,但由于部分组件还没有更新到这个版本,为避免版本不一致可能导致的问题,这里选用了 3.21.2 版。

另外官方文档建议使用 Java 的 LTS 版本(11 或 17)^5^。

pom.xml 文件内容如下。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>me.liujiajia.sample</groupId>
    <artifactId>sample-smpp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sample-smpp</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
        <!--<camel.version>4.1.0</camel.version>-->
        <camel.version>3.21.2</camel.version>
    </properties>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.camel.springboot/camel-spring-boot-starter -->
        <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-spring-boot-starter</artifactId>
            <version>${camel.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.camel.springboot/camel-smpp-starter -->
        <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-smpp-starter</artifactId>
            <version>${camel.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.camel.springboot/camel-jackson-starter -->
        <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-jackson-starter</artifactId>
            <version>${camel.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.camel.springboot/camel-rabbitmq-starter -->
        <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-rabbitmq-starter</artifactId>
            <version>${camel.version}</version>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

附 1. 相关代码

Mq2SmsRoute 中使用的几个自定义的类都比较简单,仅作为示例展示。为方便理解,也一并贴一下。

ShortMsgFormatService

java 复制代码
package me.liujiajia.sample.samplesmpp;

public interface ShortMsgFormatService {
    void format(ShortMsgEntity bodyIn);
}

ShortMsgFormatServiceImpl

java 复制代码
package me.liujiajia.sample.samplesmpp;

import org.springframework.stereotype.Service;

@Service
public class ShortMsgFormatServiceImpl implements ShortMsgFormatService {

    @Override
    public void format(ShortMsgEntity msg) {
        msg.setContent("%s, %s.".formatted(msg.getContent(), msg.getName()));
    }
}

SMSProcessor

java 复制代码
package me.liujiajia.sample.samplesmpp;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.component.smpp.SmppConstants;
import org.springframework.stereotype.Component;

@Component
public class SMSProcessor implements Processor {

    @Override
    public void process(Exchange exchange) throws Exception {
        Message m = exchange.getIn();
        System.out.println(m.getBody());
        System.out.println(m.getHeader(SmppConstants.ID, String.class));
        System.out.println(m.getHeaders());
    }
}

ShortMsgEntity

java 复制代码
package me.liujiajia.sample.samplesmpp;

public class ShortMsgEntity {

    private Integer id;
    private String name;
    private String from;
    private String to;
    private String content;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getFrom() {
        return from;
    }
    public void setFrom(String from) {
        this.from = from;
    }
    public String getTo() {
        return to;
    }
    public void setTo(String to) {
        this.to = to;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
}

application.yml

yaml 复制代码
camel:
  springboot:
    name: SMS Service
  component:
    rabbitmq:
      addresses: localhost:5672
      username: sms
      password: 123456
#    smpp:
#      enquire-link-timer: 3000

附 2. 本地开发用的 docker-compose 文件

RabbitMQ

yaml 复制代码
version: '3.0'
services:
  rabbitmq:
    image: rabbitmq:3-management
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
      - RABBITMQ_VM_MEMORY_HIGH_WATERMARK_RELATIVE=0.8
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - ./src/rabbitmq/20-mem.conf:/etc/rabbitmq/conf.d/20-mem.conf

SMPP Server

为了在本地尝试搭建 SMPP 服务器,找了好几种方式,最终在本地能运行起来的只有 Jasmin ^6^ 。

yaml 复制代码
version: "3.10"

services:
  redis:
    image: redis:alpine
    restart: unless-stopped
    healthcheck:
      test: redis-cli ping | grep PONG
    deploy:
      resources:
        limits:
          cpus: '0.2'
          memory: 128M
    security_opt:
      - no-new-privileges:true

  rabbit-mq:
    image: rabbitmq:3.10-management-alpine
    restart: unless-stopped
    healthcheck:
      test: rabbitmq-diagnostics -q ping
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 525M
    security_opt:
      - no-new-privileges:true

  jasmin:
    image: jookies/jasmin:latest
    restart: unless-stopped
    ports:
      - 2775:2775
      - 8990:8990
      - 1401:1401
    depends_on:
      redis:
        condition: service_healthy
      rabbit-mq:
        condition: service_healthy
    environment:
      REDIS_CLIENT_HOST: redis
      AMQP_BROKER_HOST: rabbit-mq
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
    security_opt:
      - no-new-privileges:true

启动后配置用户

需要通过 telnet 链接 jcli 工具来创建用户 ^7^ 。

bash 复制代码
telnet 127.0.0.1 8990

Windows 用户的需要启用 Telnet 客户端功能:

  1. WIN + R 快捷键,输入 appwiz.cpl 打开 程序和功能 页面;
  2. 点击左侧的 启用或关闭 Windows 功能,勾选 Telnet 客户端,然后确定。

Windows 下的 Telnet 虽然能用,但是每次回车后都需要再任一输入一个字符才能看到响应,不知道是不是只有这个 jcli 才会这样,使用起来很不方便。

总的命令汇总如下,全部复制然后直接粘贴就可以了。

bash 复制代码
smppccm -a
cid DEMO_CONNECTOR
host 127.0.0.1
port 2775
username sms
password 123456
submit_throughput 110
ok

smppccm -1 DEMO_CONNECTOR
smppccm --list

mtrouter -a
type defaultroute
connector smppc(DEMO_CONNECTOR)
rate 0.00
ok

group -a
gid smsgroup
ok

user -a
username sms
password 123456
gid smsgroup
uid sms
ok

注意: Jasmin 的 Pod 每次重启后用户信息都会丢失,需要重新创建。

发送消息

创建用户后可以通过点击如下链接发送测试消息:

http://127.0.0.1:1401/send?username=sms&password=123456&to=06222172&content=hello

能看到类似 Success "0ae1613a-7fbf-409f-b3d2-efd24fc49ad7" 的响应则说明用户创建成功了。

通过 HTTP 发送的消息会记录在日志文件 /var/log/jasmin/http-api.log 中,如果是通过代码发送的消息,则可以查看 /var/log/jasmin/messages.log 日志文件。

可以在进入 Pod 后执行如下命令实时查看 messages.log 的内容。

bash 复制代码
tail -fn100 /var/log/jasmin/messages.log

Footnotes

  1. camel.apache.org/

  2. camel.apache.org/components/...

  3. camel.apache.org/components/...

  4. camel.apache.org/components/...

  5. camel.apache.org/camel-core/...

  6. docs.jasminsms.com/en/latest/i...

  7. docs.jasminsms.com/en/latest/i...

相关推荐
石榴树下8 分钟前
00. 马里奥的 OAuth 2 和 OIDC 历险记
后端
uhakadotcom9 分钟前
开源:subdomainpy快速高效的 Python 子域名检测工具
前端·后端·面试
似水流年流不尽思念25 分钟前
容器化技术了解吗?主要解决什么问题?原理是什么?
后端
Java水解27 分钟前
Java中的四种引用类型详解:强引用、软引用、弱引用和虚引用
java·后端
i听风逝夜27 分钟前
看好了,第二遍,SpringBoot单体应用真正的零停机无缝更新代码
后端
柏油1 小时前
可视化 MySQL binlog 监听方案
数据库·后端·mysql
舒一笑2 小时前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端
M1A12 小时前
Java Enum 类:优雅的常量定义与管理方式(深度解析)
后端
AAA修煤气灶刘哥3 小时前
别再懵了!Spring、Spring Boot、Spring MVC 的区别,一篇讲透
后端·面试
柏油3 小时前
MySQL 字符集 utf8 与 utf8mb4
数据库·后端·mysql