16. 微服务综合案例4 exception rabbit sql 注解 测试 (2刷)

1. common项目

fastjosn

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.31</version>
        </dependency>

注解

注解的定义

@Target(ElementType.METHOD) //用在方法上
@Retention(RetentionPolicy.RUNTIME) //运行的时候
@Documented //有文档
public @interface SysLogger {
    String value() default "";
}

注解的逻辑和使用

  • 在user-service代码
@Aspect // 进行切割
@Component //给spring管理

		//切的位置
		@Pointcut("@annotation(com.forezp.annotation.SysLogger)")

		//之前进行操作
		@Before("loggerPointCut()")

        //得到 反射的自然
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //得到 放射的方法
        Method method = signature.getMethod();

        String methodName = signature.getName();
        
         //从注解上,得到 SysLogger
        SysLogger sysLogger = method.getAnnotation(SysLogger.class);
		//得到描述
		sysLogger.value()
        
        //请求的方法名 类的路径
        String className = joinPoint.getTarget().getClass().getName();

@Aspect // 进行切割
@Component //给spring管理
public class SysLoggerAspect {
    @Autowired
    private LoggerService loggerService;
	
    //切的位置
    @Pointcut("@annotation(com.forezp.annotation.SysLogger)")
    public void loggerPointCut() {

    }
	
    //之前进行操作
    @Before("loggerPointCut()")
    public void saveSysLog(JoinPoint joinPoint) {
        //得到 反射的自然
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //得到 放射的方法
        Method method = signature.getMethod();
		//创建log
        SysLog sysLog = new SysLog();
        //从注解上,得到 SysLogger
        SysLogger sysLogger = method.getAnnotation(SysLogger.class);
        if(sysLogger != null){
            //得到:注解上的描述
            sysLog.setOperation(sysLogger.value());
        }
        //请求的方法名 类的路径
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        //拼接成 完整的方法
        sysLog.setMethod(className + "." + methodName + "()");
        //请求的参数
        Object[] args = joinPoint.getArgs();
        String params="";
        for(Object o:args){
            //对象转成 json,拼接
            params+=JSON.toJSONString(o);
        }
        //如果 参数不为空
        if(!StringUtils.isEmpty(params)) {
            //设置上参数
            sysLog.setParams(params);
        }
        //设置IP地址
        sysLog.setIp(HttpUtils.getIpAddress());
        //用户名
        String username = UserUtils.getCurrentPrinciple();
        if(!StringUtils.isEmpty(username)) {
            //设置上用户名
            sysLog.setUsername(username);
        }
        //日志的创建时间
        sysLog.setCreateDate(new Date());
        //保存系统日志
        loggerService.log(sysLog);
    }

}
@SysLogger("registry")

dto 和 exception

public class RespDTO<T> implements Serializable{


    public int code = 0;
    public String error = "";
    public T data;

    public static RespDTO onSuc(Object data) {
        RespDTO resp = new RespDTO();
        resp.data = data;
        return resp;
    }
}

exception

异常的定义

public class CommonException extends RuntimeException {

    private ErrorCode errorCode;

    public CommonException(ErrorCode errorCode) {
        super(errorCode.getMsg());
        this.errorCode = errorCode;
    }

    public CommonException(ErrorCode errorCode, String msg) {
        super(msg);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() 

    public int getCode()
    public String getMsg()
    return errorCode.XXX(如:getMsg())

}

异常的 枚举

public enum ErrorCode {

    OK(0, ""),
    FAIL(-1, "操作失败"),
    RPC_ERROR(-2,"远程调度失败"),
    USER_NOT_FOUND(1000,"用户不存在"),
    USER_PASSWORD_ERROR(1001,"密码错误"),
    GET_TOKEN_FAIL(1002,"获取token失败"),
    TOKEN_IS_NOT_MATCH_USER(1003,"请使用自己的token进行接口请求"),

    BLOG_IS_NOT_EXIST(2001,"该博客不存在")
    ;
    private int code;
    private String msg;


    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    
    public int getCode()
    public String getMsg()

     
     //根据 code,获取 到,这个枚举
    public static ErrorCode codeOf(int code) {
        for (ErrorCode state : values()) {
            if (state.getCode() == code) {
                return state;
            }
        }
        return null;
    }
    
}

异常的逻辑处理 和 使用

@ControllerAdvice //想controller 返回
@ResponseBody
public class CommonExceptionHandler {

    @ExceptionHandler(CommonException.class) //切这个异常
    public ResponseEntity<RespDTO> handleException(Exception e) {
        RespDTO resp = new RespDTO();
        //强转
        CommonException taiChiException = (CommonException) e;
        resp.code = taiChiException.getCode();
        resp.error = e.getMessage();
		//返回
        return new ResponseEntity(resp, HttpStatus.OK);
    }

}
        if(null==jwt){
            throw new CommonException(ErrorCode.GET_TOKEN_FAIL);
        }

2. blog-service

pom 和 yaml 和 main

<dependencies>

		<dependency>
			<groupId>com.forezp</groupId>
			<artifactId>common</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>

starter-netflix-eureka-client

starter-config

starter-web
    
starter-openfeign

starter-actuator
    
starter-netflix-hystrix-dashboard

starter-netflix-hystrix
    
starter-sleuth
    
starter-zipkin
    
springfox-swagger2
    
springfox-swagger-ui.0
    
mysql-connector-java
    
starter-data-jpa

		<!--security-->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-jwt</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security.oauth</groupId>
			<artifactId>spring-security-oauth2</artifactId>
		</dependency>


starter-amqp
	</dependencies>
@SpringBootApplication
@EnableEurekaClient

@EnableFeignClients

@EnableHystrixDashboard

@EnableHystrix
spring:
  application:
    name: blog-service
  cloud:
    config:
      uri: http://localhost:8769
      fail-fast: true
  profiles:
    active: pro
#  zipkin:
#    base-url: http://localhost:9411
#
#  datasource:
#    driver-class-name: com.mysql.jdbc.Driver
#    url: jdbc:mysql://localhost:3306/sys_blog?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
#    username: root
#    password: 123456
#  jpa:
#    hibernate:
#      ddl-auto: update
#    show-sql: true
#
#  rabbitmq:
#    host: localhost
#    port: 5672
#    username: guest
#    password: guest
#    publisher-confirms: true
#    virtual-host: /
  • public.cert 省略

其他包说明

  • aop 一样
  • config包下的:
    • GlobalMethodSecurityConfiguration
    • JwtConfiguration
    • RabbitConfig
    • ResourceServerConfiguration
    • SwaggerConfig

client 包,就是feign包

@FeignClient(value = "user-service",fallback = UserServiceHystrix.class )
public interface UserServiceClient {

    @PostMapping(value = "/user/{username}")
    RespDTO<User> getUser(@RequestHeader(value = "Authorization") String token, @PathVariable("username") String username);
}
@Component
public class UserServiceHystrix implements UserServiceClient {

    @Override
    public RespDTO<User> getUser(String token, String username) {
        System.out.println(token);
        System.out.println(username);
        return null;
    }
}

dao


public interface BlogDao extends JpaRepository<Blog, Long> {

    List<Blog> findByUsername(String username);

}

service

@Service
public class LoggerService {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void log(SysLog sysLog){
        rabbitTemplate.convertAndSend(RabbitConfig.queueName, JSON.toJSONString(sysLog));
    }
}
@Service 
public class BlogService {

    @Autowired
    BlogDao blogDao;

    @Autowired
    UserServiceClient userServiceClient;

    //保存
    public Blog postBlog(Blog blog) {
        return blogDao.save(blog);
    }
	
    //查找
    public List<Blog> findBlogs(String username) {
        return blogDao.findByUsername(username);
    }

	//根据ID查找
    public BlogDetailDTO findBlogDetail(Long id) {
        //查询 blog
        Optional<Blog> blogOptional = blogDao.findById(id);
        Blog blog=null;
        //if(blogOptional!=null){ 这是错误的代码,永远为true
            //如果 博客存在,就得到它
            //blog=blogOptional.get(); //会报错
        //}
        
        if(blogOptional.isPresent()){
            blog=blogOptional.get();
        }
        
        if (null == blog) {
            throw new CommonException(ErrorCode.BLOG_IS_NOT_EXIST);
        }
        RespDTO<User> respDTO = userServiceClient.getUser(UserUtils.getCurrentToken(), blog.getUsername());
        if (respDTO==null) {
            throw new CommonException(ErrorCode.RPC_ERROR);
        }
        BlogDetailDTO blogDetailDTO = new BlogDetailDTO();
        blogDetailDTO.setBlog(blog);
        blogDetailDTO.setUser(respDTO.data);
        return blogDetailDTO;
    }

}

Entity

public class BlogDetailDTO {
    //get set
    private Blog blog;
    private User user;
}
@Entity
public class Blog implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column
    private String title;

    @Column
    private String suject;
    
    }
public class SysLog {

    private Long id;
    //用户名
    private String username;
    //用户操作
    private String operation;
    //请求方法
    private String method;
    //请求参数
    private String params;
    //IP地址
    private String ip;
    //创建时间
    private Date createDate;
    }
public class User {

    private Long id;
    private String username;
    private String password;
    
    }
  • util 不变

web

@RestController
@RequestMapping("/blog") 
public class BlogController {

    @Autowired
    BlogService blogService;

    @ApiOperation(value = "发布博客", notes = "发布博客")
    @PreAuthorize("hasRole('USER')") //user权限,发布
    @PostMapping("")
    @SysLogger("postBlog")
    public RespDTO postBlog(@RequestBody Blog blog){
        //字段判读省略
       Blog blog1= blogService.postBlog(blog);
       return RespDTO.onSuc(blog1);
    }

    @ApiOperation(value = "根据用户id获取所有的blog", notes = "根据用户id获取所有的blog")
    @PreAuthorize("hasAuthority('ROLE_USER')") //必须User权限的 另一种写法
    @GetMapping("/{username}")
    @SysLogger("getBlogs")
    public RespDTO getBlogs(@PathVariable String  username){
        //字段判读省略
        if(UserUtils.isMyself(username)) {
            List<Blog> blogs = blogService.findBlogs(username);
            return RespDTO.onSuc(blogs);
        }else {
            throw new CommonException(ErrorCode.TOKEN_IS_NOT_MATCH_USER);
        }
    }

    @ApiOperation(value = "获取博文的详细信息", notes = "获取博文的详细信息")
    @PreAuthorize("hasAuthority('ROLE_USER')") //获得细节
    @GetMapping("/{id}/detail")
    @SysLogger("getBlogDetail")
    public RespDTO getBlogDetail(@PathVariable Long id){
        return RespDTO.onSuc(blogService.findBlogDetail(id));
    }
}


public class RespDTO<T> implements Serializable{


    public int code = 0;
    public String error = "";
    public T data;

    public static RespDTO onSuc(Object data) {
        RespDTO resp = new RespDTO();
        resp.data = data;
        return resp;
    }
    
}
    public BlogDetailDTO findBlogDetail(Long id){
    
    BlogDetailDTO blogDetailDTO = new BlogDetailDTO();
        blogDetailDTO.setBlog(blog);
        blogDetailDTO.setUser(respDTO.data);
        return blogDetailDTO;
        
}

public class BlogDetailDTO {
    private Blog blog;
    private User user;
}

3. log-service

pom 和 yaml 和 main

common 自己写的

starter-config
starter-actuator
starter-netflix-eureka-client
starter-web
spring-security-jwt
spring-security-oauth2
mysql-connector-java
starter-data-jpa
starter-amqp
spring:
  application:
    name: logger-service
  cloud:
    config:
      uri: http://localhost:8769
      fail-fast: true
  profiles:
    active: pro
  • public.cert 不变
@EnableEurekaClient
@EnableDiscoveryClient

其他包 config

  • GlobalMethodSecurityConfiguration
  • JwtConfiguration
  • RabbitConfig 增加了东西
  • ResourceServerConfiguration
public interface SysLogDAO extends JpaRepository<SysLog, Long> {
}
@Entity
public class SysLog implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //用户名
    @Column
    private String username;
    //用户操作
    @Column
    private String operation;
    //请求方法
    @Column
    private String method;
    //请求参数
    @Column
    private String params;
    //IP地址
    @Column
    private String ip;
    //创建时间
    @Column
    private Date createDate;
    
    }

rabbit 消息监听者 实现

//每收到一个消息,就会 走这个方法
@Component
public class Receiver {

    private CountDownLatch latch = new CountDownLatch(1);

    @Autowired
    SysLogService sysLogService;
    public void receiveMessage(String message) {
        System.out.println("Received <" + message + ">");
        //收到的消息,转成 syslog
        SysLog  sysLog=  JSON.parseObject(message,SysLog.class);
        //保存日志
        sysLogService.saveLogger(sysLog);
        latch.countDown(); //释放信号量
    }

}

CountDownLatch 信号量的作用,只有其他的线程 完成了一系列的操作,释放信号后,其他被阻塞的线程 获取到 信号才能被唤醒
    
@Component
public class Sender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
        String context = "hello " + new Date();
        System.out.println("Sender : " + context);
        //发送消息
        rabbitTemplate.convertAndSend(RabbitConfig.queueName, "Hello from RabbitMQ!");
    }

}
public class User {
    private String username;
    private String password;


    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

service

@Service
public class SysLogService {

    @Autowired
    SysLogDAO sysLogDAO;

    public void saveLogger(SysLog sysLog){
        sysLogDAO.save(sysLog);
    }
}

rabbitMq config

@Configuration
public class RabbitConfig {


  public   final static String queueName = "spring-boot";

    @Bean
    Queue queue() {
        return new Queue(queueName, false);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange("spring-boot-exchange");
    }

    @Bean
    Binding binding(Queue queue, TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(queueName);
    }

    @Bean
    SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                             MessageListenerAdapter listenerAdapter) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(queueName);
        container.setMessageListener(listenerAdapter);
        return container;
    }

    @Bean //把receiveMessage类,传递到 消息监听者 中
    MessageListenerAdapter listenerAdapter(Receiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }


}

4. SQL

sys-blog.sql




CREATE DATABASE  `sys-blog` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

use `sys-blog`;

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for blog
-- ----------------------------
DROP TABLE IF EXISTS `blog`;
CREATE TABLE `blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `suject` varchar(255) DEFAULT NULL,
  `title` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

INSERT INTO `blog` VALUES ('5', '今天天气真好', '一起出去玩啊', 'fzp');

sys-log.sql


CREATE DATABASE  `sys-log` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

use `sys-log`;

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `create_date` datetime DEFAULT NULL,
  `ip` varchar(255) DEFAULT NULL,
  `method` varchar(255) DEFAULT NULL,
  `operation` varchar(255) DEFAULT NULL,
  `params` varchar(255) DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=267 DEFAULT CHARSET=utf8;


sys-user.sql


CREATE DATABASE  `sys-user` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

use `sys-user`;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),
  KEY `FK859n2jvi8ivhui0rl0esws6o` (`user_id`),
  CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `user` VALUES ('1', '$2a$10$rlM./Q4dh5qXYmxFxUqkRetMPf6JewV/Hj/s4qBg/6U1.mzcue2oK', 'fzp');


INSERT INTO `role` VALUES ('1', 'ROLE_USER');
INSERT INTO `role` VALUES ('2', 'ROLE_ADMIN');

INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('1', '2');


5. 测试

注册

http://localhost:5000/userapi/user/registry
post json请求

{
    "password":"123456",
    "username":"miya2"
}

返回:
{
    "id": 15,
    "username": "miya2",
    "password": "$2a$10$I/mvNH15Axgv/M6e6ml8WuFt2kEjzY4K9PXNlFfDr.50FSq563Aca"
}

登录

http://localhost:5000/userapi/user/login

post请求, form-data请求,加两个参数:
username  miya2
password  123456

返回:
{
    "code": 0,
    "error": "",
    "data": {
        "user": {
            "id": 15,
            "username": "miya2",
            "password": "$2a$10$I/mvNH15Axgv/M6e6ml8WuFt2kEjzY4K9PXNlFfDr.50FSq563Aca"
        },
        "token": "eyJhbGciOiJSU9YQ4bKw"
    }
}

获取用户的API

post
http://localhost:5000/userapi/user/miya
hearder
Authorization 值为:Bearer eyJhbGcixxxtoken


{
    "error": "access_denied",
    "error_description": "不允许访问"
}
  • 在库里增加权限,id=15,权限为1
  • 重新登录,用新的 token,再次访问,即可访问成功
{
    "code": 0,
    "error": "",
    "data": {
        "id": 13,
        "username": "miya",
        "password": "$2a$10$FM3gA3uFuYwDXH8BIfKm9egtWQRTDyM4z885rY5UMXnflcVVgbYie"
    }
}

发布博客

http://localhost:5000/blogapi/blog   post请求:
Authorization 注意header
{
    "id":"20",
    "username":"miya",
    "title":"标题1",
    "suject":"主题1"
}
返回这样的内容:
{
    "code": 0,
    "error": "",
    "data": {
        "id": 6,
        "username": "miya",
        "title": "标题1",
        "suject": "主题1"
    }
}

查看博客

http://localhost:5000/blogapi/blog/6/detail 获得博客。
上面的ID没用

{
    "code": 0,
    "error": "",
    "data": {
        "blog": {
            "id": 6,
            "username": "miya",
            "title": "标题1",
            "suject": "主题1"
        },
        "user": {
            "id": 13,
            "username": "miya",
            "password": "$2a$10$FM3gA3uFuYwDXH8BIfKm9egtWQRTDyM4z885rY5UMXnflcVVgbYie"
        }
    }

其他测试

config-server 测试

  • 引用了 starter-bus-amqp,用于事实更新,所以配置Mq
1. Queue springCloudBus.anonymous.Z8qPhVppRkuH0RA-v0jCXg
启动后,mq服务器上 有此队列,绑定到:springCloudBus

2. 访问:http://localhost:8769/uaa-service-pro.yml,即可得到此项目配置
访问:http://localhost:8769/admin-service-pro.yml 有一个默认 8761的 eureka配置,不知原因。共两个eureka配置
service-url:
      defaultZone: http://localhost:8761/eureka/

3. 阿波罗会在本地这样的缓存,C:\opt\data\account-system\config-cache 。cloud config好像没有

jolokia

http://localhost:8769/actuator/jolokia

http://localhost:8769/actuator

{
    "request": {
        "type": "version"
    },
    "value": {
        "agent": "1.6.0",
        "protocol": "7.2",
        "config": {
            "listenForHttpService": "true",
            "authIgnoreCerts": "false",
            "agentId": "192.168.44.1-14152-4def42c3-servlet",
            "debug": "false",
            "agentType": "servlet",
            "policyLocation": "classpath:/jolokia-access.xml",
        },
        "info": {
            "product": "tomcat",
            "vendor": "Apache",
            "version": "9.0.12"
        }
    },
    "timestamp": 1629773086,
    "status": 200
}

zipkin

  • 启动访问 http://localhost:9411/zipkin/
zk也是注册到 eureka 上的。哎,不能用。

项目整理

admin-server:9998
blog-service:8763
gateway-service:5000
logger-service:9997
monitor-service:8766
uaa-service:9999
user-service:8762
zipkin-server:9411

共计11个项目,配置中心,和 common 和 eureka 不算。还有8个。
另外 zipkin server需要 自己建立

user blog log都配置了 Mq,队列:spring-boot 交换器:spring-boot-exchange 。加上 配置中心,mq一共4个客户端。

监控相关admin和 dashboard

登录admin server:http://localhost:9998/#/applications
输入用户名密码:admin

看 web下 mappings, 可以看到所有的 请求路径 mappings

1. 访问:monitor-service 的 dashboard:
http://localhost:8766/hystrix

2. turbine.stream访问:
在dashboard填入:http://localhost:8766/turbine.stream 看配置 自动聚合了:blog-service,user-service 的 hystrix


http://localhost:8766/actuator/hystrix.stream 监控项目的hystrix无用的。


3. 访问下面的接口后:,dashboard里面将会有内容: 
http://localhost:5000/userapi/user/miya 
http://localhost:5000/blogapi/blog/miya2

(重要的是下面登录接口)
http://localhost:5000/blogapi/blog/6/detail  会访问:/user-service/user/{username}

http://localhost:5000/userapi/user/login?username=miya2&password=123456
//会访问:/uaa-service/oauth/token

访问成功后,即可看到 dashboard
上一篇:Rabbit——RabbitMQ的高级特性


下一篇:RabbitMQ-进阶