RabbitMQ (五)实现类似Dubbo的RPC调用

springboot对rabbitMQ的接口做了封装,要实现 request/reponse 模式的调用,只需要调用 rabbitTemplate.convertSendAndReceive 方法即可,队列和交换器的设置使用topic模式即可。

  Object res = rabbitTemplate.convertSendAndReceive(exchangeName, routingKey, reqJson,message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);// 持久化消息
            // messageProperties.setContentType("application/json");
            String correlationId = UUID.randomUUID().toString().replaceAll("-", "");
            messageProperties.setCorrelationId(correlationId);
​
            return message;
        }, null);

下面通过spring aop、反射、rabbitmq实现一个类似dubbo的rpc调用系统:

  1. rabbitmq 使用topic工作模式

  2. springboot 创建client和server两个应用

  3. 通过注解 @Service 声明远程调用

  4. 客户端通过 RPCAspect 切面拦截本地服务调用,然后通过rabbitMQ发起远程调用,调用服务端的service并返回结果

这样就实现了一个简单的rpc过程,这里因为使用反射,并且序列化直接使用json,所以性能上可能弱鸡,后面有时间再跟dubbo的方式做个压测对比看看。

1 项目结构

项目结构图如下:

2 pom依赖

这里把项目用到的依赖全部直接贴出来

    
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>

3 common模块

主要定义了接口和接口实现,以及自定义注解,很简单,直接贴代码

1 IService接口

package com.fmi110.rabbitmq.service;
​
public interface IService {
    public Object sayHello(String reqObjStr);
}

2 IService实现类

这里需要注意 @Service 是自定义的注解类 com.fmi110.rpc.Service !!!

package com.fmi110.rabbitmq.service;
​
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import com.fmi110.rpc.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
​
/**
 * @author fmi110
 * @description 远程服务实现类
 * @date 2021/7/3 20:51
 */
@Slf4j
@Component
@Service // 这个是自定义的注解类 com.fmi110.rpc.Service!!!
public class ServiceImpl implements IService {
    @Override
    public Object sayHello(String reqObjStr) {
​
        // 模拟耗时 50 ms
        try { Thread.sleep(50);} catch (InterruptedException e) {}
​
        String now = DateUtil.now();
        String uuid   = UUID.fastUUID().toString();
        String result = now + ":" + uuid;
        log.info(">>>>> rpcService  >>>>>");
        log.info(result);
        return result;
    }
}

3 自定义注解

package com.fmi110.rpc;
​
import java.lang.annotation.*;
​
/**
 * @author fmi110
 * @description rpc服务注解
 * @date 2021/7/3 21:09
 */
​
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
    String value() default "";
}

4 服务端

服务端主要执行如下内容:

  1. 监听消息队列内容

  2. 获取消息内容,反序列化请求数据,通过反射从spring上下文中获取service对象,并调用对应的方法,返回结果

1 application.properties

rabbitMQ配置:

  1. 每次只消费一条消息

  2. 使用自动ack机制,消息投递给消费者后自动从消息队列中移除

  3. 开启spring提供的消费失败自动重试机制,每条消息最多消费3次

# 应用名称
spring.application.name=rabbitmq
server.port=9089
server.servlet.context-path=/
​
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# 指定连接的虚拟主机,可以在rabbitMQ控制台查看对应的虚拟主机的名字
spring.rabbitmq.virtual-host=my_vhost
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
​
# 每次只消费一条消息
spring.rabbitmq.listener.simple.prefetch=1
# 开启消费者应答 ack 机制
#spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 开启spring提供的retry
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=3
spring.rabbitmq.listener.simple.retry.initial-interval=3000

2 RabbitConfigRPC

配置消息队列和交换器,这里使用了 topic 模式

package com.fmi110.rabbitmq.config;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @author fmi110
 * @description 配置交换器、队列
 * @date 2021/7/3 9:58
 */
@Slf4j
@Configuration
public class RabbitConfigRPC {
​
    String exchangeName = "rpc-exchange";
    String queueName    = "rpc-request-queue";
​
    @Bean
    public TopicExchange exchange() {
        boolean durable    = true; // 持久化
        boolean autoDelete = false; // 消费者全部解绑时不自动删除
        return new TopicExchange(exchangeName, durable, autoDelete);
    }
​
    /**
     * 持久化队列
     * @return
     */
    @Bean
    public Queue queue() {
        return new Queue(queueName, true, false, false);
    }
​
    @Bean
    public Binding binding(Queue queue,TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("rabbit.rpc");
    }
}
​

3 RabbitConsumer

消息消费者,主要执行:

  1. 从消息中提取反射调用需要的 class method arg 等信息

  2. applicationContext 获取服务对象,确定调用的 method 对象,通过反射调用方法并返回结果

package com.fmi110.rabbitmq;
​
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
​
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
​
​
/**
 * @author fmi110
 * @description 消息消费者
 * @date 2021/7/1 16:08
 */
@Component
@Slf4j
public class RabbitConsumer {
​
    @Autowired
    private ApplicationContext applicationContext;
​
    @RabbitListener(queues = "rpc-request-queue")
    public Object consumeRPC(String data, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        return this.doInvoke(data);
    }
​
    /**
     * 反射调用
     *
     * @param data
     * @return
     * @throws ClassNotFoundException
     */
    private Object doInvoke(String data) throws ClassNotFoundException, NoSuchMethodException {
        Map       map            = JSONUtil.toBean(data, Map.class);
        String    className      = (String) map.get("class");
        String    methodName     = (String) map.get("method");
        JSONArray args           = (JSONArray) map.get("args");
        JSONArray parameterTypes = (JSONArray) map.get("parameterTypes");
        log.info(">>>>  RPC请求: {}", data);
        Class<?>   clazz   = Class.forName(className);
        Object     service = applicationContext.getBean(clazz);
        Class<?>[] clzArray   = new Class[parameterTypes.size()];
​
        for (int i = 0; i < parameterTypes.size(); i++) {
            String   clz    = (String) parameterTypes.get(i);
            Class<?> class_ = Class.forName(clz.replace("class ", ""));
            clzArray[i] = class_;
​
            // Object arg = args.get(i);
        }
        Method method = clazz.getMethod(methodName, clzArray);
        return ReflectionUtils.invokeMethod(method, service, args.stream().toArray());
    }
}
​

5 客户端

客户端需要做的事情:

  1. 定义aop切面,拦截远程服务的方法

  2. 获取服务调用的类、方法、参数等信息,封装成rpc调用的参数

  3. 通过rabbitMQ发起rpc调用,并获取返回结果

1 application.properties

主要配置rabbitMQ的链接信息

# 应用名称
spring.application.name=rabbitmq
server.port=8080
server.servlet.context-path=/
​
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# 指定连接的虚拟主机,可以在rabbitMQ控制台查看对应的虚拟主机的名字
spring.rabbitmq.virtual-host=my_vhost
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
​
spring.rabbitmq.listener.simple.prefetch=1
​
# 开启 publish-comfirm 机制和消息路由匹配失败退回机制
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirm-type=correlated
# 开启消费者应答 ack 机制
#spring.rabbitmq.listener.simple.acknowledge-mode=manual

2 RabbitConfigRPC

package com.fmi110.rabbitmq.config;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @author fmi110
 * @description 配置交换器、队列
 * @date 2021/7/3 9:58
 */
@Slf4j
@Configuration
public class RabbitConfigRPC {
​
    String exchangeName = "rpc-exchange";
    String queueName    = "rpc-request-queue";
​
    @Bean
    public TopicExchange exchange() {
        boolean durable    = true; // 持久化
        boolean autoDelete = false; // 消费者全部解绑时不自动删除
        return new TopicExchange(exchangeName, durable, autoDelete);
    }
​
    /**
     * 持久化、队列
     * @return
     */
    @Bean
    public Queue queue() {
        return new Queue(queueName, true, false, false);
    }
​
    @Bean
    public Binding binding(Queue queue,TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("rabbit.rpc");
    }
}
​

3 RPCAspect切面

这是实现远程调用的核心,拦截请求,使用 rabbitmq 实现远程调用

package com.fmi110.rabbitmq.aspect;
​
import cn.hutool.json.JSONUtil;
import com.fmi110.rabbitmq.RabbitProducer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
​
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
​
/**
 * @author fmi110
 * @description RPC服务 aop切面
 * @date 2021/7/3 20:54
 */
@Component
@Slf4j
@Aspect
public class RPCAspect {
​
    @Autowired
    RabbitProducer rabbitProducer;
​
    @Pointcut("execution(public * com.fmi110.rabbitmq.service.ServiceImpl.*(..))")
    public void rpcPointCut() {
    }
​
​
    @Around("rpcPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
​
        Method          method    = signature.getMethod();
​
        // 类名
        Class target    =  point.getTarget().getClass();
​
        String className = target.getName();
        // 方法名
        String methodName = method.getName();
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        Class<?>[] parameterTypes = method.getParameterTypes();
        //请求的参数
        Object[] args = point.getArgs();
        String   argsStr    = JSONUtil.toJsonStr(args);
        log.info("class:{}",className);
        log.info("method:{}",methodName);
        log.info("args:{}",argsStr);
        HashMap<String, Object> map = new HashMap<>();
        map.put("class", className);
        map.put("method", methodName);
        map.put("parameterTypes", parameterTypes);
        map.put("args", args);
        String reqJson = JSONUtil.toJsonStr(map);
        // Object proceed = point.proceed();
        Object proceed = rabbitProducer.sendRPC(reqJson); // rabbitMQ rpc调用
        return proceed;
    }
}
​

4 RabbitProducer

rabbitMQ发送消息的逻辑

package com.fmi110.rabbitmq;
​
import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.AMQP;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
​
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmConstraints;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
​
/**
 * @author fmi110
 * @description 消息生产者
 * @date 2021/7/1 15:08
 */
@Component
@Slf4j
public class RabbitProducer {
    @Autowired
    RabbitTemplate rabbitTemplate;
​
    /**
     * 1 设置 confirm 回调,消息发送到 exchange 时回调
     * 2 设置 return callback ,当路由规则无法匹配到消息队列时,回调
     * <p>
     * correlationData:消息发送时,传递的参数,里边只有一个id属性,标识消息用
     */
    @PostConstruct
    public void enableConfirmCallback() {
        // #1
        /**
         * 连接不上 exchange或exchange不存在时回调
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                log.error("消息发送失败");
                // TODO 记录日志,发送通知等逻辑
            }
        });
​
        // #2
        /**
         * 消息投递到队列失败时,才会回调该方法
         * message:发送的消息
         * exchange:消息发往的交换器的名称
         * routingKey:消息携带的路由关键字信息
         */
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            log.error("{},exchange={},routingKey={}",replyText,exchange,routingKey);
            // TODO 路由失败后续处理逻辑
        });
    }
​
    public Object sendRPC(String reqJson) {
​
        String exchangeName = "rpc-exchange";
        String routingKey = "rabbit.rpc";
        Object res = rabbitTemplate.convertSendAndReceive(exchangeName, routingKey, reqJson,message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);// 持久化消息
            messageProperties.setContentType("application/json");
            String correlationId = UUID.randomUUID().toString().replaceAll("-", "");
            messageProperties.setCorrelationId(correlationId);
​
            return message;
        }, null);
        log.info(">>>>>服务端返回的响应:");
        log.info(JSONUtil.toJsonStr(res));
        return res;
    }
}
​

5 Controller

触发远程调用使用的

package com.fmi110.rabbitmq.controller;
@Slf4j
@RestController
public class TestController {
   
    @Resource(name = "serviceImpl")
    IService service;
​
    @GetMapping("/rpcCall")
    public Object rpcCall(String rpc) {
        Object result =service.sayHello("aaa");
        return result;
    }
}

6 运行效果

客户端截图:

服务端截图:

7 使用场景

在网络可以进行双向通信的场景下,实现远程调用的方式有很多,比如dubbo、EJB等,但是在某些特殊的场景下,比如某些政务系统网络,对网络通信的方向有严格的限制,有些限制外网区不能主动向内网区发起通信,只能内网通外网,这个时候如果要实现外网调用内网的服务,使用dubbo,EJB就无法实现了,这个时候就可以借助消息队列来绕过这个问题,消息服务部署在外网区,消息消费者在内网,消费者就可以连接上外网的服务,进而实现双向通信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值