Spring Boot--RestTemplate、Kafka 、ActiveMQ

一、RESTful 风格 Controller

(1)@PathVariable @RequestParam @RequestBody 三种参数注解

@RestController 注解继承自 Spring MVC 中的 @Controller 注解,顾名思义就是一个基于 RESTful 风格的 HTTP 端点,并且会自动使用 JSON 实现 HTTP 请求和响应的序列化/反序列化方式。

Spring Boot 提供了一系列简单有用的注解来简化对请求输入的控制过程,常用的包括 @PathVariable、@RequestParam 和 @RequestBody。

其中 @PathVariable 注解用于获取路径参数,即从类似 url/{id} 这种形式的路径中获取 {id} 参数的值。

使用 @PathVariable 注解时,我们只需要指定一个参数的名称即可。我们可以再看一个示例,如下代码所示:

@GetMapping(value = "/{accountName}")
public Account getAccountByAccountName(@PathVariable("accountName") String accountName) {
 
    Account account = accountService.getAccountByAccountName(accountName);
    return account;
}

@RequestParam 注解的作用与 @PathVariable 注解类似,也是用于获取请求中的参数,但是它面向类似 url?id=XXX 这种路径形式。

在 HTTP 协议中,content-type 属性用来指定所传输的内容类型,我们可以通过 @RequestMapping 注解中的 produces 属性来设置这个属性。

在设置这个属性时,我们通常会将其设置为“application/json”,如下代码所示:

@RestController
@RequestMapping(value = "accounts", produces="application/json")
public class AccountController {

@RequestBody 注解用来处理 content-type 为 application/json 类型时的编码内容,通过 @RequestBody 注解可以将请求体中的 JSON 字符串绑定到相应的 JavaBean 上。

如下代码所示就是一个使用 @RequestBody 注解来控制输入的场景。

@PutMapping(value = "/")
public void updateAccount(@RequestBody Account account) {

如果使用 @RequestBody 注解,我们可以在 Postman 中输入一个 JSON 字符串来构建输入对象,如下代码所示:

(2)输入规则

关于请求输入的规则,关键在于按照 RESTful 风格的设计原则设计 HTTP 端点,对于这点业界也存在一些约定。

  • 以 Account 这个领域实体为例,如果我们把它视为一种资源,那么 HTTP 端点的根节点命名上通常采用复数形式,即“/accounts”,正如前面的示例代码所示。
  • 在设计 RESTful API 时,我们需要基于 HTTP 语义设计对外暴露的端点的详细路径。针对常见的 CRUD 操作,我们展示了 RESTful API 与非 RESTful API 的一些区别。
  • HTTP 语义即get post put delete的作用,get用来获取信息,post用来新增信息,put用来更新信息,delete用来删除。

 

二、使用 RestTemplate 调用RESTful 风格的服务

分为如下4步

(1)创建 RestTemplate

如果我们想创建一个 RestTemplate 对象,最简单且最常见的方法是直接 new 一个该类的实例,如下代码所示:

@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

这里我们创建了一个 RestTemplate 实例,并通过 @Bean 注解将其注入 Spring 容器中。

(2)RestTemplate构造函数

下面我们查看下 RestTemplate 的无参构造函数,看看创建它的实例时,RestTemplate 都做了哪些事情,如下代码所示:

public RestTemplate() {
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter(false));
        this.messageConverters.add(new SourceHttpMessageConverter<>());
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
 
        //省略其他添加 HttpMessageConverter 的代码
}

可以看到 RestTemplate 的无参构造函数只做了一件事情,添加了一批用于实现消息转换的 HttpMessageConverter 对象。

我们知道通过 RestTemplate 发送的请求和获取的响应都是以 JSON 作为序列化方式,而我们调用后续将要介绍的 getForObject、exchange 等方法时所传入的参数及获取的结果都是普通的 Java 对象,我们就是通过使用 RestTemplate 中的 HttpMessageConverter 自动帮我们做了这一层转换操作。

这里请注意,其实 RestTemplate 还有另外一个更强大的有参构造函数,如下代码所示:

public RestTemplate(ClientHttpRequestFactory requestFactory) {
        this();
        setRequestFactory(requestFactory);
}

从以上代码中,我们可以看到这个构造函数一方面调用了前面的无参构造函数,另一方面可以设置一个 ClientHttpRequestFactory 接口。而基于这个 ClientHttpRequestFactory 接口的各种实现类,我们可以对 RestTemplate 中的行为进行精细化控制。

这方面典型的应用场景是设置 HTTP 请求的超时时间等属性,如下代码所示:

@Bean
public RestTemplate customRestTemplate(){
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        httpRequestFactory.setConnectionRequestTimeout(3000);
        httpRequestFactory.setConnectTimeout(3000);
        httpRequestFactory.setReadTimeout(3000);
 
        return new RestTemplate(httpRequestFactory);
}

这里我们创建了一个 HttpComponentsClientHttpRequestFactory 工厂类,它是 ClientHttpRequestFactory 接口的一个实现类。通过设置连接请求超时时间 ConnectionRequestTimeout、连接超时时间 ConnectTimeout 等属性,我们对 RestTemplate 的默认行为进行了定制化处理。

(3)RestTemplate常用方法

        下面讲解getForObject、postForObject、exchange方法。

getForObject 方法组中的三个方法如下代码所示:

public <T> T getForObject(URI url, Class<T> responseType)
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables){}
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)

从以上方法定义上,我们不难看出它们之间的区别只是传入参数的处理方式不同。

这里,我们注意到第一个 getForObject 方法只有两个参数(后面的两个 getForObject 方法分别支持不定参数以及一个 Map 对象),如果我们想在访问路径上添加一个参数,则需要我们构建一个独立的 URI 对象,示例如下代码所示:

String url = "http://localhost:8080/hello?name=" + URLEncoder.encode(name, "UTF-8");
URI uri = URI.create(url);

假设 AccountController如下代码所示:

@RestController
@RequestMapping(value = "accounts")
public class AccountController {
 
    @GetMapping(value = "/{accountId}")
    public Account getAccountById(@PathVariable("accountId") Long accountId) {
    …
    }
}

对于上述端点,我们可以通过 getForObject 方法构建一个 HTTP 请求来获取目标 Account 对象,实现代码如下所示:

Account result = restTemplate.getForObject("http://localhost:8082/accounts/{accountId}", Account.class, accountId);

当然,我们也可以使用 getForEntity 方法实现同样的效果,但在写法上会有所区别,如下代码所示:

ResponseEntity<Account> result = restTemplate.getForEntity("http://localhost:8082/accounts/{accountId}", Account.class, accountId);
Account account = result.getBody();

在以上代码中,我们可以看到 getForEntity 方法的返回值是一个 ResponseEntity 对象,在这个对象中还包含了 HTTP 消息头等信息,而 getForObject 方法返回的只是业务对象本身。这是这两个方法组的主要区别,你可以根据个人需要自行选择。

与 GET 请求相比,RestTemplate 中的 POST 请求除提供了 postForObject 和 postForEntity 方法组以外,还额外提供了一组 postForLocation 方法。

假设我们有如下所示的 OrderController ,它暴露了一个用于添加 Order 的端点。

@RestController
@RequestMapping(value="orders")
public class OrderController {
 
    @PostMapping(value = "")
    public Order addOrder(@RequestBody Order order) {
        Order result = orderService.addOrder(order);
         return result;
    }
}

那么,通过 postForEntity 发送 POST 请求的示例如下代码所示:

Order order = new Order();
order.setOrderNumber("Order0001");
order.setDeliveryAddress("DemoAddress");
ResponseEntity<Order> responseEntity = restTemplate.postForEntity("http://localhost:8082/orders", order, Order.class);
return responseEntity.getBody();

从以上代码中可以看到,这里我们构建了一个 Order 实体,通过 postForEntity 传递给了 OrderController 所暴露的端点,并获取了该端点的返回值。(特殊说明:postForObject 的操作方式也与此类似。)

掌握了 get 和 post 方法组后,理解 put 方法组和 delete 方法组就会非常容易了。其中 put 方法组与 post 方法组相比只是操作语义上的差别,而 delete 方法组的使用过程也和 get 方法组类似。这里我们就不再一一展开,你可以自己尝试做一些练习。

最后,我们还有必要介绍下 exchange 方法组

对于 RestTemplate 而言,exchange 是一个通用且统一的方法,它既能发送 GET 和 POST 请求,也能用于发送其他各种类型的请求。

我们来看下 exchange 方法组中的其中一个方法签名,如下代码所示:

public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException

请注意,这里的 requestEntity 变量是一个 HttpEntity 对象,它封装了请求头和请求体,而 responseType 用于指定返回数据类型。 假如前面的 OrderController 中存在一个根据订单编号 OrderNumber 获取 Order 信息的端点,那么我们使用 exchange 方法发起请求的代码就变成这样了,如下代码所示。

ResponseEntity<Order> result = restTemplate.exchange("http://localhost:8082/orders/{orderNumber}", HttpMethod.GET, null, Order.class, orderNumber);

而比较复杂的一种使用方式是分别设置 HTTP 请求头及访问参数,并发起远程调用,示例代码如下所示:

//设置 HTTP Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
 
//设置访问参数
HashMap<String, Object> params = new HashMap<>();
params.put("orderNumber", orderNumber);
 
//设置请求 Entity
HttpEntity entity = new HttpEntity<>(params, headers);
ResponseEntity<Order> result = restTemplate.exchange(url, HttpMethod.GET, entity, Order.class);

(4)一些高级设置

除了实现常规的 HTTP 请求,RestTemplate 还有一些高级用法可供我们进行使用,如指定消息转换器、设置拦截器和处理异常等。

指定消息转换器

在 RestTemplate 中,实际上还存在第三个构造函数,如下代码所示:

public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
        Assert.notEmpty(messageConverters, "At least one HttpMessageConverter required");
        this.messageConverters.addAll(messageConverters);
}

从以上代码中不难看出,我们可以通过传入一组 HttpMessageConverter 来初始化 RestTemplate,这也为消息转换器的定制提供了途径。

假如,我们希望把支持 Gson 的 GsonHttpMessageConverter 加载到 RestTemplate 中,就可以使用如下所示的代码。

@Bean
public RestTemplate restTemplate() {
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new GsonHttpMessageConverter());
        RestTemplate restTemplate = new RestTemplate(messageConverters);
        return restTemplate;
}

原则上,我们可以根据需要实现各种自定义的 HttpMessageConverter ,并通过以上方法完成对 RestTemplate 的初始化。

设置拦截器

如果我们想对请求做一些通用拦截设置,那么我们可以使用拦截器。不过,使用拦截器之前,首先我们需要实现 ClientHttpRequestInterceptor 接口。

这方面最典型的应用场景是在 Spring Cloud 中通过 @LoadBalanced 注解为 RestTemplate 添加负载均衡机制。我们可以在 LoadBalanceAutoConfiguration 自动配置类中找到如下代码。

@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return restTemplate -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
        };
}

在上面代码中,我们可以看到这里出现了一个 LoadBalancerInterceptor 类,该类实现了 ClientHttpRequestInterceptor 接口,然后我们通过调用 setInterceptors 方法将这个自定义的 LoadBalancerInterceptor 注入 RestTemplate 的拦截器列表中。

处理异常

请求状态码不是返回 200 时,RestTemplate 在默认情况下会抛出异常,并中断接下来的操作,如果我们不想采用这个处理过程,那么就需要覆盖默认的 ResponseErrorHandler。示例代码结构如下所示:

RestTemplate restTemplate = new RestTemplate();
 
ResponseErrorHandler responseErrorHandler = new ResponseErrorHandler() {
        @Override
        public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
            return true;
        }
 
        @Override
        public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
            //添加定制化的异常处理代码
        }
};
restTemplate.setErrorHandler(responseErrorHandler);

在上述的 handleError 方法中,我们可以实现任何自己想控制的异常处理代码。


三、使用Spring Boot实现不同项目间的消息通信

项目间的消息通信,用来在不同项目之间进行数据的交换。

(1)Kafka消息通信

KafkaTemplate 是 Spring 中提供的基于 Kafka 完成消息通信的模板工具类,而要想使用这个模板工具类,我们必须在消息的发送者和消费者应用程序中都添加如下 Maven 依赖:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

使用 KafkaTemplate 发送消息
KafkaTemplate 提供了一系列 send 方法用来发送消息,典型的 send 方法定义如下代码所示:

@Override
public ListenableFuture<SendResult<K, V>> send(String topic, @Nullable V data) {
}

在上述方法实际传入了两个参数,一个是消息对应的 Topic,另一个是消息体的内容。通过该方法,我们就能完成最基本的消息发送过程。事实上,我们在调用 KafkaTemplate 的 send 方法时,如果 Kafka 中不存在该方法中指定的 Topic,它就会自动创建一个新的 Topic。

另一方面,KafkaTemplate 也提供了一组 sendDefault 方法,它通过使用默认的 Topic 来发送消息,如下代码所示:

@Override
public ListenableFuture<SendResult<K, V>> sendDefault(V data) {
    return send(this.defaultTopic, data);
}

从代码中我们可以看到,在上述 sendDefault 方法内部中也是使用了 send 方法完成消息的发送过程。

那么,如何指定这里的 defaultTopic 呢?在 Spring Boot 中,我们可以使用如下配置项完成这个工作。

spring:
  kafka:
    bootstrap-servers:
    - localhost:9092
    template:
      default-topic: demo.topic

现在,我们已经了解了通过 KafkaTemplate 发送消息的实现方式,KafkaTemplate 高度抽象了消息的发送过程,整个过程非常简单。

使用 @KafkaListener 注解消费消息
首先需要强调一点,通过翻阅 KafkaTemplate 提供的类定义,我们并未找到有关接收消息的任何方法,这实际上与 Kafka 的设计思想有很大关系。

这点也与本课程后续要介绍的 JmsTemplate 和 RabbitTemplate 存在很大区别,因为它们都提供了明确的 receive 方法来接收消息。

从前面提供的 Kafka 架构图中我们可以看出,在 Kafka 中消息通过服务器推送给各个消费者,而 Kafka 的消费者在消费消息时,需要提供一个监听器(Listener)对某个 Topic 实现监听,从而获取消息,这也是 Kafka 消费消息的唯一方式。

在 Spring 中提供了一个 @KafkaListener 注解实现监听器

使用 @KafkaListener 注解时,我们把它直接添加在处理消息的方法上即可,如下代码所示:

@KafkaListener(topics = "demo.topic")
public void handlerEvent(DemoEvent event) {
    //TODO:添加消息处理逻辑
}

当然,我们还需要在消费者的配置文件中指定用于消息消费的配置项,如下代码所示:

spring:      
  kafka:
    bootstrap-servers:
    - localhost:9092
    template:
      default-topic: demo.topic
    consumer:
      group-id: demo.group

可以看到,这里除了指定 template.default-topic 配置项之外,还指定了 consumer. group-id 配置项来指定消费者分组信息。

(2)JMS 规范与 ActiveMQ

自结步骤:

  • pom文件引入artemis依赖
  • 定义生产者生产消息。通过使用Spring boot 自带的JmsTemplate的send方法。
  • 定义消费者消费消息。通过使用Spring boot 自带的JmsTemplate的recive或者注解订阅地址的方法。
  • 注意修改配置文件的配置项。

上面介绍了基于 Kafka 和 KafkaTemplate 实现消息发送和消费。今天,我们继续介绍 ActiveMQ,并基于 JmsTemplate 模板工具类为 SpringCSS 案例添加对应的消息通信机制。

JMS 规范与 ActiveMQ

JMS(Java Messaging Service)是一种 Java 消息服务,它基于消息传递语义,提供了一整套经过抽象的公共 API。目前,业界也存在一批 JMS 规范的实现框架,其中具备代表性的是 ActiveMQ。

JMS 规范

JMS 规范提供了一批核心接口供开发人员使用,而这些接口构成了客户端的 API 体系,如下图所示:

上图中可以看到,我们可以通过 ConnectionFactory 创建 Connection,作为客户端的 MessageProducer 和 MessageConsumer 通过 Connection 提供的会话(Session)与服务器进行交互,而交互的媒介就是各种经过封装、包含目标地址(Destination)的消息。

JMS 的消息由两大部分组成,即消息头(Header)和消息体(Payload)。

消息体只包含具体的业务数据,而消息头包含了 JMS 规范定义的通用属性,比如消息的唯一标识 MessageId、目标地址 Destination、接收消息的时间 Timestamp、有效期 Expiration、优先级 Priority、持久化模式 DeliveryMode 等都是常见的通用属性,这些通用属性构成了消息通信的基础元数据(Meta Data),由消息通信系统默认设置。

JMS 规范中的点对点模型表现为队列(Queue),队列为消息通信提供了一对一顺序发送和消费的机制。点对点模型 API 在通用 API 基础上,专门区分生产者 QueueSender 和消费者 QueueReceiver。

而 Topic 是 JMS 规范中对发布-订阅模型的抽象,JMS 同样提供了专门的 TopicPublisher 和 TopicSubscriber。

对于 Topic 而言,因多个消费者存在同时消费一条消息的情况,所以消息有副本的概念。相较点对点模型,发布-订阅模型通常用于更新、事件、通知等非响应式请求场景。在这些场景中,消费者和生产者之间是透明的,消费者可以通过配置文件进行静态管理,也可以在运行过程中动态被创建,同时还支持取消订阅操作。

ActiveMQ 

JMS 规范存在 ActiveMQ、WMQ、TIBCO 等多种第三方实现方式,其中较主流的是 ActiveMQ。

针对 ActiveMQ,目前有两个实现项目可供选择,一个是经典的 5.x 版本,另一个是下一代的 Artemis,关于这两者之间的关系,我们可以简单地认为 Artemis 是 ActiveMQ 的未来版本,代表 ActiveMQ 的发展趋势。因此,本课程我们将使用 Artemis 演示消息通信机制。

启动Atremis

如果我们想启动 Artemis 服务,首先需要通过如下所示的命名创建一个服务实例。ps.直接官网下载zip版本,然后解压缩后,在windows上cmd运行如下命令即可。记得在d盘下新建指定的文件夹存放新建的artemis示例。

artemis.cmd create D:\artemis --user springcss --password springcss_password

然后,执行如下命令,我们就可以正常启动这个 Artemis 服务实例了。

D:\artemis \bin\artemis run

Spring 提供了对 JMS 规范及各种实现的友好集成,通过直接配置 Queue 或 Topic,我们就可以使用 JmsTemplate 提供的各种方法简化对 Artemis 的操作了。

使用 JmsTemplate 集成 ActiveMQ

如果我们想基于 Artemis 使用 JmsTemplate,首先需要在 Spring Boot 应用程序中添加对 spring-boot-starter-artemis 的依赖,如下代码所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-artemis</artifactId>
</dependency>

在讨论如何使用 JmsTemplate 实现消息发送和消费之前,我们先来分析消息生产者和消费者的工作模式。

通常,生产者行为模式单一,而消费者根据消费方式的不同有一些特定的分类,比如常见的有推送型消费者(Push Consumer)和拉取型消费者(Pull Consumer)。

推送型方式指的是应用系统向消费者对象注册一个 Listener 接口并通过回调 Listener 接口方法实现消息消费,而在拉取方式下应用系统通常主动调用消费者的拉取消息方法消费消息,主动权由应用系统控制。

在消息通信的两种基本模型中,发布-订阅模型支持生产者/消费者之间的一对多关系,属于一种典型的推送消费者实现机制;而点对点模型中有且仅有一个消费者,他们主要通过基于间隔性拉取的轮询(Polling)方式进行消息消费。

我们提到 Kafka 中消费消息的方式是一种典型的推送型消费者,所以 KafkaTemplate 只提供了发送消息的方法而没有提供实现消费消息的方法。而 JmsTemplate 则不同,它同时支持推送型消费和拉取型消费,接下来我们一起看下如何使用JmsTemplate 发送消息。

使用 JmsTemplate 发送消息

JmsTemplate 中存在一批 send 方法用来实现消息发送,如下代码所示:

@Override
public void send(MessageCreator messageCreator) throws JmsException {
}
 
@Override
public void send(final Destination destination, final MessageCreator messageCreator) throws JmsException {
}
 
@Override
public void send(final String destinationName, final MessageCreator messageCreator) throws JmsException {
}

这些方法一方面指定了目标 Destination,另一方面提供了一个用于创建消息对象的 MessageCreator 接口,如下代码所示:

public interface MessageCreator {

    Message createMessage(Session session) throws JMSException;
}

通过 send 方法发送消息的典型实现方式如下代码所示:

public void sendDemoObject(DemoObject demoObject) { 
    jmsTemplate.send("demo.queue", new MessageCreator() { 
        @Override 
        public Message createMessage(Session session) 
        throws JMSException { 
            return session.createObjectMessage(demoObject); 
        } 
}

与 KakfaTemplate 不同,JmsTemplate 还提供了一组更为简便的方法实现消息发送,即 convertAndSend 方法,如下代码所示:

public void convertAndSend(Destination destination, final Object message) throws JmsException {
}

通过 convertAndSend 方法,我们可以直接传入任意业务对象,且该方法能自动将业务对象转换为消息对象并进行消息发送,具体的示例代码如下所示:

public void sendDemoObject(DemoObject demoObject) { 
    jmsTemplate.convertAndSend("demo.queue", demoObject); 
}

在以上代码中,我们注意到 convertAndSend 方法还存在一批重载方法,它包含了消息后处理功能,如下代码所示:

@Override
public void convertAndSend( Destination destination, final Object message, final MessagePostProcessor postProcessor)throws JmsException {
}

上述方法中的 MessagePostProcessor 就是一种消息后处理器,它用来在构建消息过程中添加自定义的消息属性,它的一种典型的使用方法如下代码所示:

public void sendDemoObject(DemoObject demoObject) { 
    jmsTemplate.convertAndSend("demo.queue", demoObject, new                 MessagePostProcessor() { 
        @Override 
        public Message postProcessMessage(Message message) throws       JMSException { 
            //针对 Message 的处理
            return message;
        } 
});

配置项

使用 JmsTemplate 的最后一步就是在配置文件中添加配置项,如下代码所示:


spring:
  artemis:
    host: localhost
    port: 61616
    user: springcss
    password: springcss_password

这里我们指定了 artemis 服务器的地址、端口、用户名和密码等信息。同时,我们也可以在配置文件中指定 Destination 信息,具体配置方式如下代码所示:

spring:
  jms:
    template:
      default-destination: springcss.account.queue


使用 JmsTemplate 消费消息

基于前面的讨论,我们知道 JmsTemplate 同时支持推送型消费和拉取型消费两种消费类型。我们先来看一下如何实现拉取型消费模式。

在 JmsTemplate 中提供了一批 receive 方法供我们从 artemis 中拉取消息,如下代码所示:

public Message receive() throws JmsException {
}
 
public Message receive(Destination destination) throws JmsException {
}
 
public Message receive(String destinationName) throws JmsException {
}

到这一步我们需要注意一点:调用上述方法时,当前线程会发生阻塞,直到一条新消息的到来。针对阻塞场景,这时 receive 方法的使用方式如下代码所示:

@JmsListener(queues = “demo.queue”)
public void handlerEvent(DemoEvent event) {
    //TODO:添加消息处理逻辑
}
public DemoEvent receiveEvent() {
    Message message = jmsTemplate.receive(“demo.queue”); 
    return (DemoEvent) messageConverter.fromMessage(message);
}

这里我们使用了一个 messageConverter 对象将消息转化为领域对象。

在使用 JmsTemplate 时,我们可以使用 Spring 提供的 MappingJackson2MessageConverter、MarshallingMessageConverter、MessagingMessageConverter,以及 SimpleMessageConverter 实现消息转换,一般系统默认使用 SimpleMessageConverter。而在日常开发过程中,我们通常会使用 MappingJackson2MessageConverter 来完成 JSON 字符串与对象之间的转换。

同时,JmsTemplate 还提供了一组更为高阶的 receiveAndConvert 方法完成消息的接收和转换,如下代码所示:

public Object receiveAndConvert(Destination destination) throws JmsException {

}

顾名思义,receiveAndConvert 方法能在接收消息后完成对消息对象的自动转换,使得接收消息的代码更为简单,如下代码所示:

public DemoEvent receiveEvent() { 
    return (DemoEvent)jmsTemplate.receiveAndConvert("demo.queue"); 
}

当然,在消费者端,我们同样需要指定与发送者端完全一致的 MessageConverter 和 Destination 来分别实现消息转换和设置消息目的地。

介绍完拉模式,接下来我们介绍推模式下的消息消费方法,实现方法也很简单,如下代码所示:

@JmsListener(queues = “demo.queue”)
public void handlerEvent(DemoEvent event) {
    //TODO:添加消息处理逻辑
}

在推模式下,开发人员只需要在 @JmsListener 注解中指定目标队列,就能自动接收来自该队列的消息。

(3)使用 RabbitTemplate 集成 RabbitMQ

自结步骤

  1. pom引入
  2. 使用RabbitTemplate的send或convertAndSend 发送消息
  3. 使用RabbitTemplate的recive方法或者注解消费消息

我们介绍了基于 ActiveMQ 和 JmsTemplate 实现消息发送和消费,此小节,我们将介绍另一款主流的消息中间件 RabbitMQ,并介绍RabbitTemplate 模板工具类。

AMQP 规范与 RabbitMQ

AMQP(Advanced Message Queuing Protocol)是一个提供统一消息服务的应用层标准高级消息队列规范。和 JMS 规范一样,AMQP 描述了一套模块化的组件及组件之间进行连接的标准规则,用于明确客户端与服务器交互的语义。而业界也存在一批实现 AMQP 规范的框架,其中极具代表性的是 RabbitMQ。

AMQP 规范

在 AMQP 规范中存在三个核心组件,分别是交换器(Exchange)、消息队列(Queue)和绑定(Binding)。其中交换器用于接收应用程序发送的消息,并根据一定的规则将这些消息路由发送到消息队列中;消息队列用于存储消息,直到这些消息被消费者安全处理完毕;而绑定定义了交换器和消息队列之间的关联,为它们提供了路由规则。

在 AMQP 规范中并没有明确指明类似 JMS 中一对一的点对点模型和一对多的发布-订阅模型,不过通过控制 Exchange 与 Queue 之间的路由规则,我们可以很容易地模拟 Topic 这种典型消息中间件的概念。

如果存在多个 Queue,Exchange 如何知道把消息发送到哪个 Queue 中呢?

通过 Binding 规则设置路由信息即可。在与多个 Queue 关联之后,Exchange 中会存在一个路由表,这个表中维护着每个 Queue 存储消息的限制条件。

消息中包含一个路由键(Routing Key),它由消息发送者产生,并提供给 Exchange 路由这条消息的标准。而 Exchange 会检查 Routing Key,并结合路由算法决定将消息路由发送到哪个 Queue 中。

通过下面 Exchange 与 Queue 之间的路由关系图,我们可以看到一条来自生产者的消息通过 Exchange 中的路由算法可以发送给一个或多个 Queue,从而实现点对点和发布订阅功能。

AMQP 路由关系图

上图中,不同的路由算法存在不同的 Exchange 类型,而 AMQP 规范中指定了直接式交换器(Direct Exchange)、广播式交换器(Fanout Exchange)、主题式交换器(Topic Exchange)和消息头式交换器(Header Exchange)这几种 Exchange 类型,不过这一讲我们将重点介绍直接式交换器。

通过精确匹配消息的 Routing Key,直接式交换器可以将消息路由发送到零个或多个队列中,如下图所示:

Direct Exchange 示意图

RabbitMQ 基本架构

RabbitMQ 使用 Erlang 语言开发的 AMQP 规范标准实现框架,而 ConnectionFactory、Connection、Channel 是 RabbitMQ 对外提供的 API 中最基本的对象,都需要遵循 AMQP 规范的建议。其中,Channel 是应用程序与 RabbitMQ 交互过程中最重要的一个接口,因为我们大部分的业务操作需要通过 Channel 接口完成,如定义 Queue、定义 Exchange、绑定 Queue 与 Exchange、发布消息等。

如果想启动 RabbitMQ,我们只需要运行 rabbitmq-server.sh 文件即可。不过,因为 RabbitMQ 依赖于 Erlang,所以首先我们需要确保安装上 Erlang 环境。

接下来,我们一起看下如何使用 Spring 框架所提供的 RabbitTemplate 模板工具类集成 RabbitMQ。

使用 RabbitTemplate 集成 RabbitMQ

如果想使用 RabbitTemplate 集成 RabbitMQ,首先我们需要在 Spring Boot 应用程序中添加对 spring-boot-starter-amqp 的依赖,如下代码所示:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

使用 RabbitTemplate 发送消息

和其他模板类一样,RabbitTemplate 也提供了一批 send 方法用来发送消息,如下代码所示:

@Override

public void send(Message message) throws AmqpException {

    send(this.exchange, this.routingKey, message);

}

@Override

public void send(String routingKey, Message message) throws AmqpException {

    send(this.exchange, routingKey, message);

}

@Override

public void send(final String exchange, final String routingKey, final Message message) throws AmqpException {

    send(exchange, routingKey, message, null);

}

在这里可以看到,我们指定了消息发送的 Exchange 及用于消息路由的路由键 RoutingKey。因为这些 send 方法发送的是原生消息对象,所以在与业务代码进行集成时,我们需要将业务对象转换为 Message 对象,示例代码如下所示:

public void sendDemoObject(DemoObject demoObject) {

    MessageConverter converter = rabbitTemplate.getMessageConverter();

    MessageProperties props = new MessageProperties();

    Message message = converter.toMessage(demoObject, props);

    rabbitTemplate.send("demo.queue", message);

}

如果我们不想在业务代码中嵌入 Message 等原生消息对象,还可以使用 RabbitTemplate 的 convertAndSend 方法组进行实现,如下代码所示:

@Override

public void convertAndSend(Object object) throws AmqpException {

        convertAndSend(this.exchange, this.routingKey, object, (CorrelationData) null);

}

@Override

public void correlationConvertAndSend(Object object, CorrelationData correlationData) throws AmqpException {

        convertAndSend(this.exchange, this.routingKey, object, correlationData);

}

@Override

public void convertAndSend(String routingKey, final Object object) throws AmqpException {

        convertAndSend(this.exchange, routingKey, object, (CorrelationData) null);

}

 
@Override

public void convertAndSend(String routingKey, final Object object, CorrelationData correlationData)

            throws AmqpException {

        convertAndSend(this.exchange, routingKey, object, correlationData);

}


@Override

public void convertAndSend(String exchange, String routingKey, final Object object) throws AmqpException {

        convertAndSend(exchange, routingKey, object, (CorrelationData) null);

}

上述 convertAndSend 方法组在内部就完成了业务对象向原生消息对象的自动转换过程,因此,我们可以使用如下所示的代码来简化消息发送过程。

public void sendDemoObject(DemoObject demoObject) {

    rabbitTemplate.convertAndSend("demo.queue", demoObject);

}

当然,有时候我们需要在消息发送的过程中为消息添加一些属性,这就不可避免需要操作原生 Message 对象,而 RabbitTemplate 也提供了一组 convertAndSend 重载方法应对这种场景,如下代码所示:

@Override

public void convertAndSend(String exchange, String routingKey, final Object message,

            final MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException {

        Message messageToSend = convertMessageIfNecessary(message);

        messageToSend = messagePostProcessor.postProcessMessage(messageToSend, correlationData);

        send(exchange, routingKey, messageToSend, correlationData);

}

注意这里,我们使用了一个 MessagePostProcessor 类对所生成的消息进行后处理,MessagePostProcessor 的使用方式如下代码所示:

rabbitTemplate.convertAndSend(“demo.queue”, event, new MessagePostProcessor() {

     @Override

     public Message postProcessMessage(Message message) throws AmqpException {

          //针对 Message 的处理

          return message;

     }

});

使用 RabbitTemplate 的最后一步是在配置文件中添加配置项,在配置时我们需要指定 RabbitMQ 服务器的地址、端口、用户名和密码等信息,如下代码所示:

spring:

  rabbitmq:

    host: 127.0.0.1

    port: 5672

    username: guest

    password: guest

    virtual-host: DemoHost

注意,出于对多租户和安全因素的考虑,AMQP 还提出了虚拟主机(Virtual Host)概念,因此这里出现了一个 virtual-host 配置项。

Virtual Host 类似于权限控制组,内部可以包含若干个 Exchange 和 Queue。多个不同用户使用同一个 RabbitMQ 服务器提供的服务时,我们可以将其划分为多个 Virtual Host,并在自己的 Virtual Host 中创建相应组件,如下图所示:

使用 RabbitTemplate 消费消息

和 JmsTemplate 一样,使用 RabbitTemplate 消费消息时,我们也可以使用推模式和拉模式。

在拉模式下,使用 RabbitTemplate 的典型示例如下代码所示:

public DemoEvent receiveEvent() {

        return (DemoEvent) rabbitTemplate.receiveAndConvert(“demo.queue”);

}

这里,我们使用了 RabbitTemplate 中的 receiveAndConvert 方法,该方法可以从一个指定的 Queue 中拉取消息,如下代码所示:

@Override

public Object receiveAndConvert(String queueName) throws AmqpException {

        return receiveAndConvert(queueName, this.receiveTimeout);

}

这里请注意,内部的 receiveAndConvert 方法中出现了第二个参数 receiveTimeout,这个参数的默认值是 0,意味着即使调用 receiveAndConvert 时队列中没有消息,该方法也会立即返回一个空对象,而不会等待下一个消息的到来,这点与 15 讲介绍的 JmsTemplate 存在本质性的区别。

如果我们想实现与 JmsTemplate 一样的阻塞等待,设置好 receiveTimeout 参数即可,如下代码所示:

public DemoEvent receiveEvent() {

return (DemoEvent)rabbitTemplate.receiveAndConvert("demo.queue", 2000ms);

}

如果不想每次方法调用都指定 receiveTimeout,我们可以在配置文件中通过添加配置项的方式设置 RabbitTemplate 级别的时间,如下代码所示:

spring:

  rabbitmq:

    template:

      receive-timeout: 2000

当然,RabbitTemplate 也提供了一组支持接收原生消息的 receive 方法,但我们还是建议使用 receiveAndConvert 方法实现拉模式下的消息消费。

介绍完拉模式,接下来我们介绍推模式,它的实现方法也很简单,如下代码所示:

@RabbitListener(queues = “demo.queue”)

public void handlerEvent(DemoEvent event) {

    //TODO:添加消息处理逻辑

}

开发人员在 @RabbitListener 中指定目标队列即可自动接收来自该队列的消息,这种实现方式与 15 讲中介绍的 @JmsListener 完全一致。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fang·up·ad

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值