当前位置: 首页 > news >正文

gRPC之proto数据验证

1、proto数据验证

本篇将介绍grpc_validator,它可以对gRPC数据的输入和输出进行验证。

这里复用上一篇文章的代码。

1.1 创建proto文件,添加验证规则

这里使用第三方插件go-proto-validators 自动生成验证规则。

地址:https://github.com/mwitkow/go-proto-validators

$ go get github.com/mwitkow/go-proto-validators
1.1.1 新建simple.proto文件
syntax = "proto3";
package proto;
option go_package = "./proto;proto";// validator.proto是github.com/mwitkow/go-proto-validators/validator.proto
import "validator.proto";message InnerMessage {// some_integer can only be in range (1, 100).int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}];// some_float can only be in range (0;1).double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}];
}message OuterMessage {// important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}];// proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.InnerMessage inner = 2 [(validator.field) = {msg_exists: true}];
}service Simple {rpc Route (InnerMessage) returns (OuterMessage) {};
}

validator.proto文件内容:

// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.// Protocol Buffers extensions for defining auto-generateable validators for messages.// TODO(mwitkow): Add example.syntax = "proto2";
package validator;import "google/protobuf/descriptor.proto";option go_package = "github.com/mwitkow/go-proto-validators;validator";// TODO(mwitkow): Email protobuf-global-extension-registry@google.com to get an extension ID.extend google.protobuf.FieldOptions {optional FieldValidator field = 65020;
}extend google.protobuf.OneofOptions {optional OneofValidator oneof = 65021;
}message FieldValidator {// Uses a Golang RE2-syntax regex to match the field contents.optional string regex = 1;// Field value of integer strictly greater than this value.optional int64 int_gt = 2;// Field value of integer strictly smaller than this value.optional int64 int_lt = 3;// Used for nested message types, requires that the message type exists.optional bool msg_exists = 4;// Human error specifies a user-customizable error that is visible to the user.optional string human_error = 5;// Field value of double strictly greater than this value.// Note that this value can only take on a valid floating point// value. Use together with float_epsilon if you need something more specific.optional double float_gt = 6;// Field value of double strictly smaller than this value.// Note that this value can only take on a valid floating point// value. Use together with float_epsilon if you need something more specific.optional double float_lt = 7;// Field value of double describing the epsilon within which// any comparison should be considered to be true. For example,// when using float_gt = 0.35, using a float_epsilon of 0.05// would mean that any value above 0.30 is acceptable. It can be// thought of as a {float_value_condition} +- {float_epsilon}.// If unset, no correction for floating point inaccuracies in// comparisons will be attempted.optional double float_epsilon = 8;// Floating-point value compared to which the field content should be greater or equal.optional double float_gte = 9;// Floating-point value compared to which the field content should be smaller or equal.optional double float_lte = 10;// Used for string fields, requires the string to be not empty (i.e different from "").optional bool string_not_empty = 11;// Repeated field with at least this number of elements.optional int64 repeated_count_min = 12;// Repeated field with at most this number of elements.optional int64 repeated_count_max = 13;// Field value of length greater than this value.optional int64 length_gt = 14;// Field value of length smaller than this value.optional int64 length_lt = 15;// Field value of length strictly equal to this value.optional int64 length_eq = 16;// Requires that the value is in the enum.optional bool is_in_enum = 17;// Ensures that a string value is in UUID format.// uuid_ver specifies the valid UUID versions. Valid values are: 0-5.// If uuid_ver is 0 all UUID versions are accepted.optional int32 uuid_ver = 18;
}message OneofValidator {// Require that one of the oneof fields is set.optional bool required = 1;
}
1.1.2 编译simple.proto文件
$ go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
$ go install github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
$ protoc --govalidators_out=. --go_out=plugins=grpc:. simple.proto

编译完成后,自动生成simple.pb.gosimple.validator.pb.go文件,simple.pb.go文件不再介绍,我们

看下simple.validator.pb.go文件。

// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: simple.protopackage protoimport (fmt "fmt"math "math"proto "github.com/golang/protobuf/proto"_ "github.com/mwitkow/go-proto-validators"regexp "regexp"github_com_mwitkow_go_proto_validators "github.com/mwitkow/go-proto-validators"
)// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inffunc (this *InnerMessage) Validate() error {if !(this.SomeInteger > 0) {return github_com_mwitkow_go_proto_validators.FieldError("SomeInteger", fmt.Errorf(`value '%v' must be greater than '0'`, this.SomeInteger))}if !(this.SomeInteger < 100) {return github_com_mwitkow_go_proto_validators.FieldError("SomeInteger", fmt.Errorf(`value '%v' must be less than '100'`, this.SomeInteger))}if !(this.SomeFloat >= 0) {return github_com_mwitkow_go_proto_validators.FieldError("SomeFloat", fmt.Errorf(`value '%v' must be greater than or equal to '0'`, this.SomeFloat))}if !(this.SomeFloat <= 1) {return github_com_mwitkow_go_proto_validators.FieldError("SomeFloat", fmt.Errorf(`value '%v' must be lower than or equal to '1'`, this.SomeFloat))}return nil
}var _regex_OuterMessage_ImportantString = regexp.MustCompile(`^[a-z]{2,5}$`)func (this *OuterMessage) Validate() error {if !_regex_OuterMessage_ImportantString.MatchString(this.ImportantString) {return github_com_mwitkow_go_proto_validators.FieldError("ImportantString", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z]{2,5}$"`, this.ImportantString))}if nil == this.Inner {return github_com_mwitkow_go_proto_validators.FieldError("Inner", fmt.Errorf("message must exist"))}if this.Inner != nil {if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(this.Inner); err != nil {return github_com_mwitkow_go_proto_validators.FieldError("Inner", err)}}return nil
}

里面自动生成了message中属性的验证规则。

1.2 把grpc_validator验证拦截器添加到服务端

package mainimport ("context"grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"pb "middleware/proto""middleware/server/middleware/auth""middleware/server/middleware/cred""middleware/server/middleware/recovery""middleware/server/middleware/zap""google.golang.org/grpc""log""net"
)// SimpleService 定义我们的服务
type SimpleService struct{}// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.InnerMessage) (*pb.OuterMessage, error) {res := pb.OuterMessage{ImportantString: "hello grpc validator",Inner:           req,}return &res, nil
}const (// Address 监听地址Address string = ":8000"// Network 网络通信协议Network string = "tcp"
)func main() {// 监听本地端口listener, err := net.Listen(Network, Address)if err != nil {log.Fatalf("net.Listen err: %v", err)}// 新建gRPC服务器实例grpcServer := grpc.NewServer(// TLS+Token认证cred.TLSInterceptor(),grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(// 参数验证grpc_validator.StreamServerInterceptor(),// grpc_zap日志记录grpc_zap.StreamServerInterceptor(zap.ZapInterceptor()),// grpc_auth认证grpc_auth.StreamServerInterceptor(auth.AuthInterceptor),// grpc_recovery恢复grpc_recovery.StreamServerInterceptor(recovery.RecoveryInterceptor()),)),grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(// 参数验证grpc_validator.UnaryServerInterceptor(),// grpc_zap日志记录grpc_zap.UnaryServerInterceptor(zap.ZapInterceptor()),// grpc_auth认证grpc_auth.UnaryServerInterceptor(auth.AuthInterceptor),// grpc_recovery恢复grpc_recovery.UnaryServerInterceptor(recovery.RecoveryInterceptor()),)),)// 在gRPC服务器注册我们的服务pb.RegisterSimpleServer(grpcServer, &SimpleService{})log.Println(Address + " net.Listing with TLS and token...")//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用err = grpcServer.Serve(listener)if err != nil {log.Fatalf("grpcServer.Serve err: %v", err)}
}
[root@zsx middleware]# go run server.go
2023/02/11 21:42:01 :8000 net.Listing with TLS and token...

1.3 客户端

package mainimport ("context""middleware/token"pb "middleware/proto""google.golang.org/grpc""google.golang.org/grpc/credentials""log"
)// Address 连接地址
const Address string = ":8000"var grpcClient pb.SimpleClientfunc main() {//从输入的证书文件中为客户端构造TLS凭证creds, err := credentials.NewClientTLSFromFile("./cert/server/server.pem", "test.example.com")if err != nil {log.Fatalf("Failed to create TLS credentials %v", err)}//构建Tokentoken := auth.Token{Value: "bearer grpc.auth.token",}// 连接服务器conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&token))if err != nil {log.Fatalf("net.Connect err: %v", err)}defer conn.Close()// 建立gRPC连接grpcClient = pb.NewSimpleClient(conn)route()
}// route 调用服务端Route方法
func route() {// 创建发送结构体req := pb.InnerMessage{SomeInteger: 99,SomeFloat:   1,}// 调用我们的服务(Route方法)// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPCres, err := grpcClient.Route(context.Background(), &req)if err != nil {log.Fatalf("Call Route err: %v", err)}// 打印返回值log.Println(res)
}
[root@zsx middleware]# go run client.go
2023/02/11 21:44:11 important_string:"hello grpc validator" inner:{some_integer:99 some_float:1}

运行后,当输入不符合要求的数据:

// 创建发送结构体
req := pb.InnerMessage{SomeInteger: 199,SomeFloat:   1,
}

验证失败后,会有以下错误返回:

[root@zsx middleware]# go run client.go
2023/02/11 21:45:25 Call Route err: rpc error: code = InvalidArgument desc = invalid field SomeInteger: value '199' must be less than '100'
exit status 1
# 项目结构
$ tree middleware/
middleware/
├── cert
│   ├── ca.crt
│   ├── ca.csr
│   ├── ca.key
│   ├── ca.srl
│   ├── client
│   │   ├── client.csr
│   │   ├── client.key
│   │   └── client.pem
│   ├── openssl.cnf
│   └── server
│       ├── server.csr
│       ├── server.key
│       └── server.pem
├── client.go
├── go.mod
├── go.sum
├── log
│   └── debug.log
├── proto
│   ├── simple.pb.go
│   └── simple.validator.pb.go
├── server
│   └── middleware
│       ├── auth
│       │   └── auth.go
│       ├── cred
│       │   └── cred.go
│       ├── recovery
│       │   └── recovery.go
│       └── zap
│           └── zap.go
├── server.go
├── simple.proto
├── token
│   └── token.go
└── validator.proto12 directories, 25 files

1.3 其他类型验证规则设置

enum验证:

syntax = "proto3";
package proto;
option go_package = "./proto;enum";import "validator.proto";message SomeMsg {Action do = 1 [(validator.field) = {is_in_enum: true}];
}
enum Action {ALLOW = 0;DENY = 1;CHILL = 2;
}

UUID验证:

syntax = "proto3";
package proto;
option go_package = "./proto;uuid";import "validator.proto";message UUIDMsg {// user_id must be a valid version 4 UUID.string user_id = 1 [(validator.field) = {uuid_ver: 4, string_not_empty: true}];
}

1.4 总结

go-grpc-middlewaregrpc_validator集成go-proto-validators,我们只需要在编写proto时设好验证规

则,并把grpc_validator添加到gRPC服务端,就能完成gRPC的数据验证,很简单也很方便。

相关文章:

gRPC之proto数据验证

1、proto数据验证 本篇将介绍grpc_validator&#xff0c;它可以对gRPC数据的输入和输出进行验证。 这里复用上一篇文章的代码。 1.1 创建proto文件&#xff0c;添加验证规则 这里使用第三方插件go-proto-validators 自动生成验证规则。 地址&#xff1a;https://github.co…...

计算机竞赛 题目: 基于深度学习的疲劳驾驶检测 深度学习

文章目录 0 前言1 课题背景2 实现目标3 当前市面上疲劳驾驶检测的方法4 相关数据集5 基于头部姿态的驾驶疲劳检测5.1 如何确定疲劳状态5.2 算法步骤5.3 打瞌睡判断 6 基于CNN与SVM的疲劳检测方法6.1 网络结构6.2 疲劳图像分类训练6.3 训练结果 7 最后 0 前言 &#x1f525; 优…...

css--踩坑

1. 子元素的宽高不生效问题 设置flex布局后&#xff0c;子元素的宽高不生效问题。 如果希望子元素的宽高生效&#xff0c;解决方法&#xff0c;给子元素添加如下属性&#xff1a; flex-shrink: 0; flex-shrink: 0;2. 横向滚动&#xff08;子元素宽度不固定&#xff09; /* tab…...

C超市商品信息查询系统

一、系统界面介绍 1. 超市商品信息查询系统 1、显示商品信息&#xff0c;包括&#xff1a;商品名称、商品种类&#xff08;休闲食品、奶品水饮、生鲜水果&#xff09;、商品价格、商品保质期、商品生产日期&#xff1b; 2、从文件中导入数据、显示、排序、查询的功能。&…...

黑马JVM总结(二十七)

&#xff08;1&#xff09;synchronized代码块 synchronized代码块的底层原理&#xff0c;它是给一个对象进行一个加锁操作&#xff0c;它是如何保证如果你出现了synchronized代码块中出现了问题&#xff0c;它需要给这个对象有一个正确的解锁操作呢&#xff0c;加锁解锁是成对…...

软件测试/测试开发丨Python异常处理 学习笔记

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/27722 异常处理 编写程序时&#xff0c;即使语句或表达式使用了正确的语法&#xff0c;执行时仍可能触发错误。执行时检测到的错误称为异常&#xff0c;大…...

026 - STM32学习笔记 - 液晶屏控制(三) - DMA2D快速绘制矩形、直线

026- STM32学习笔记 - 液晶屏控制&#xff08;三&#xff09; - DMA2D快速绘制矩形、直线等 上节直接操作LTDC在先视频上直接显示&#xff0c;我们直接操作显存地址空间中的内容&#xff0c;用来显示图形&#xff0c;但是相对来说&#xff0c;这种方法费时费力&#xff0c;这节…...

【牛客网】OR59 字符串中找出连续最长的数字串

题目 思路 创建两个字符串 temp 和 ret 创建指针i用来遍历字符串通过i遍历字符串,如果遇到数字则将这个数组加到字符串temp中 i,如果遇到字母,则判断temp字符串的长度和ret字符串的长度,如果temp<ret则说明这个字符串不是要的字符串,如果temp>ret则说明此时temp字符串是…...

云原生监控系统Prometheus:基于Prometheus构建智能化监控告警系统

目录 一、理论 1.Promethues简介 2.监控告警系统设计思路 3.Prometheus监控体系 4.Prometheus时间序列数据 5.Prometheus的生态组件 6.Prometheus工作原理 7.Prometheus监控内容 8.部署Prometheus 9.部署Exporters 10.部署Grafana进行展示 二、实验 1.部署Prometh…...

C++ 学习系列 -- std::list

一 std::list 介绍 list 是 c 中的序列式容器&#xff0c;其实现是双向链表&#xff0c;每个元素都有两个指针&#xff0c;分别指向前一个节点与后一个节点 链表与数组都是计算机常用的内存数据结构&#xff0c;与数组连续内存空间不一样的地方在于&#xff0c;链表的空间是不…...

YOLOv8血细胞检测(6):多维协作注意模块MCA | 原创独家创新首发

💡💡💡本文改进:多维协作注意模块MCA,效果秒杀ECA、SRM、CBAM,创新性十足,可直接作为创新点使用。 MCA | 亲测在血细胞检测项目中涨点,map@0.5 从原始0.895提升至0.910 收录专栏: 💡💡💡YOLO医学影像检测:http://t.csdnimg.cn/N4zBP ✨✨✨实战医学影…...

FFmpeg横竖版视频互换背景模糊一键生成

视频处理是现代多媒体应用中常见的需求。其中横竖版视频互换和背景模糊是视频编辑中常见的操作。FFmpeg是一个功能强大的工具,适用于这些任务。 本文将详细介绍如何使用FFmpeg进行横竖版视频互换和背景模糊。 文章目录 操作命令与命令说明横版转竖版竖版转横版背景模糊处理横…...

Java 华为真题-小朋友分班

需求&#xff1a; 题目描述 幼儿园两个班的小朋友在排队时混在了一起&#xff0c;每位小朋友都知道自己是否与前面一位小朋友同班&#xff0c;请你帮忙把同班的小朋友找出来小朋友的编号是整数&#xff0c;与前一位小朋友同班用Y表示&#xff0c;不同班用N表示学生序号范围(0&…...

机器学习必修课 - 编码分类变量 encoding categorical variables

1. 数据预处理和数据集分割 import pandas as pd from sklearn.model_selection import train_test_split导入所需的Python库 !git clone https://github.com/JeffereyWu/Housing-prices-data.git下载数据集 # Read the data X pd.read_csv(/content/Housing-prices-data/t…...

ClickHouse进阶(二十二):clickhouse管理与运维-服务监控

进入正文前,感谢宝子们订阅专题、点赞、评论、收藏!关注IT贫道,获取高质量博客内容! 🏡个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Kerberos安全认证-CSDN博客 📌订阅:拥抱独家专题,你的订阅将点燃我的创作热情! 👍点赞:赞同优秀创作,你的点赞是对我创…...

Hadoop使用hdfs指令查看hdfs目录的根目录显示被拒

背景 分布式部署hadoop,服务机只有namenode节点,主机包含其他所有节点 主机关机后,没有停止所有节点,导致服务机namenode继续保存 再次开启主机hadoop,使用hdfs查看hdfs根目录的时候显示访问被拒 解决方案 1.主机再次开启hadoop并继续执行关闭 2.服务器再次开启hadoop并继…...

[Mac] 安装paddle-pipelines出现 ERROR: Failed building wheel for lmdb

今天在mac换了新系统&#xff0c;然后重新安装paddle-piplines的时候出现了下面的问题&#xff1a; xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrunerror: comma…...

LED灯亮灭

.text .global _start _start: 设置GPIO寄存器的时钟使能  RCC_MP_AHB4ENSETR[4]->1 0x50000a28LDR R0,0x50000A28LDR R1,[R0] 从R0为起始地址的&#xff14;个字节数据取出放入R1中ORR R1,R1,#(0x1<<4) 第四位设置为1STR R1,[R0] 写回LDR R0,0x5000…...

Acwing.143 最大异或对(trie树)

题目 在给定的N个整数A1&#xff0c;A2 . …Ax中选出两个进行xor(异或)运算&#xff0c;得到的结果最大是多少? 输入格式 第一行输入一个整数N。 第二行输入N个整数A1~AN。 输出格式 输出一个整数表示答案。 数据范围 1 ≤N ≤105,0≤A<231 输入样例: 3 1 2 3输出样…...

day10.8ubentu流水灯

流水灯 .text .global _start _start: 1.设置GPIOE寄存器的时钟使能 RCC_MP_AHB4ENSETR[4]->1 0x50000a28LDR R0,0X50000A28LDR R1,[R0] 从r0为起始地址的4字节数据取出放在R1ORR R1,R1,#(0x1<<4) 第4位设置为1STR R1,[R0] 写回2.设置PE10管脚为输出模式 G…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

QMC5883L的驱动

简介 本篇文章的代码已经上传到了github上面&#xff0c;开源代码 作为一个电子罗盘模块&#xff0c;我们可以通过I2C从中获取偏航角yaw&#xff0c;相对于六轴陀螺仪的yaw&#xff0c;qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

前端导出带有合并单元格的列表

// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

C# 类和继承(抽象类)

抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

css3笔记 (1) 自用

outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size&#xff1a;0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格&#xff…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...

JS设计模式(4):观察者模式

JS设计模式(4):观察者模式 一、引入 在开发中&#xff0c;我们经常会遇到这样的场景&#xff1a;一个对象的状态变化需要自动通知其他对象&#xff0c;比如&#xff1a; 电商平台中&#xff0c;商品库存变化时需要通知所有订阅该商品的用户&#xff1b;新闻网站中&#xff0…...