特性
启动时检查
1、特性说明
(1)Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"
(2)可以通过 check="false" 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动
(3)另外,如果你的 Spring 容器是懒加载的,或者通过 API 编程延迟引用服务,请关闭 check,否则服务临时不可用时,会抛出异常,拿到 null 引用,如果 check="false",总是会返回引用,当服务恢复时,能自动连上
2、使用场景
(1)单向依赖:有依赖关系(建议默认设置)和无依赖关系(可以设置 check=false)
(2)相互依赖:即循环依赖(不建议设置 check=false)
(3)延迟加载处理
(4)check 只用来启动时检查,运行时没有相应的依赖仍然会报错
3、使用方式
(1)通过 Spring 配置文件
<!-- 关闭某个服务的启动时检查(没有提供者时报错) -->
<dubbo:reference interface="com.foo.BarService" check="false" />
<!-- 关闭所有服务的启动时检查(没有提供者时报错) -->
<dubbo:consumer check="false" />
<!-- 关闭注册中心启动时检查(注册订阅失败时报错) -->
<dubbo:registry check="false" />
(2)dubbo.properties
dubbo.reference.com.foo.BarService.check=false
dubbo.consumer.check=false
dubbo.registry.check=false
(3)-D 参数
java -Ddubbo.reference.com.foo.BarService.check=false
java -Ddubbo.consumer.check=false
java -Ddubbo.registry.check=false
4、含义
(1)dubbo.reference.com.foo.BarService.check,覆盖 com.foo.BarService的 reference 的 check 值,就算配置中有声明,也会被覆盖
(2)dubbo.consumer.check=false,是设置 reference 的 check 的默认值,如果配置中有显式的声明,如:<dubbo:reference check="true"/>,不会受影响
(3)dubbo.registry.check=false,前面两个都是指订阅成功,但提供者列表是否为空是否报错,如果注册订阅失败时,也允许启动,需使用此选项,将在后台定时重试
超时
属性 | 对应 URL 参数 | 类型 | 是否必填 | 默认值 | 作用 | 描述 | 兼容性 | |
service | timeout | timeout | int | 可选 | 1000 | 性能调优 | 远程服务调用超时时间(毫秒) | 2.0.0 以上版本 |
reference | timeout | timeout | long | 可选 | 缺省使用 <dubbo:consumer> 的 timeout | 性能调优 | 服务方法调用超时时间(毫秒) | 1.0.5 以上版本 |
registry | timeout | registry.timeout | int | 可选 | 5000 | 性能调优 | 注册中心请求超时时间(毫秒) | 2.0.0 以上版本 |
config-center | timeout | timeout | int | 可选 | 3000ms | 获取配置的超时时间 | 2.7.0 以上版本 | |
metadata-report-config | timeout | timeout | int | 可选 | 获取元数据超时时间(ms) | 2.7.0 以上版本 | ||
provider | timeout | default.timeout | int | 可选 | 1000 | 性能调优 | 远程服务调用超时时间(毫秒) | 2.0.5 以上版本 |
consumer | timeout | default.timeout | int | 可选 | 1000 | 性能调优 | 远程服务调用超时时间(毫秒) | 1.0.16 以上版本 |
method | timeout | <methodName>.timeout | int | 可选 | 缺省为的 timeout | 性能调优 | 方法调用超时时间(毫秒) | 1.0.8 以上版本 |
重试次数
属性 | 对应 URL 参数 | 类型 | 是否必填 | 默认值 | 作用 | 描述 | 兼容性 | |
service | retries | retries | int | 可选 | 2 | 性能调优 | 远程服务调用重试次数,不包括第一次调用,不需要重试请设为 0 | 2.0.0 以上版本 |
reference | retries | retries | int | 可选 | 缺省使用 <dubbo:consumer> 的 retries | 性能调优 | 远程服务调用重试次数,不包括第一次调用,不需要重试请设为 0 | 2.0.0 以上版本 |
provider | retries | default.retries | int | 可选 | 2 | 性能调优 | 远程服务调用重试次数,不包括第一次调用,不需要重试请设为 0 | 2.0.5 以上版本 |
consumer | retries | default.retries | int | 可选 | 2 | 性能调优 | 远程服务调用重试次数,不包括第一次调用,不需要重试请设为 0,仅在 cluster 为 failback / failover 时有效 | 1.0.16 以上版本 |
method | retries | <methodName>.retries | int | 可选 | 缺省为<dubbo:reference>的retries | 性能调优 | 远程服务调用重试次数,不包括第一次调用,不需要重试请设为 0 | 2.0.0 以上版本 |
服务分版本
2、按照以下的步骤进行版本迁移
(1)在低压力时间段,先升级一半提供者为新版本
(2)再将所有消费者升级为新版本
(3)然后将剩下的一半提供者升级为新版本
3、使用场景:当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用
4、使用方式
(1)服务提供者
<!-- 老版本服务提供者配置 -->
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
<!-- 新版本服务提供者配置 -->
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
(2)服务消费者
<!-- 老版本服务消费者配置 -->
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
<!-- 新版本服务消费者配置 -->
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />
(3)不区分版本
<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />
本地存根
1、特性说明
(1)远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑
2、使用场景
(1)做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等
(2)此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub(Stub 必须有可传入 Proxy 的构造函数),然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy
3、使用方式
(1)Spring 配置文件配置
<!-- 可以配置在消费者端 -->
<dubbo:consumer interface="com.foo.BarService" stub="true" />
<!-- 或 -->
<dubbo:consumer interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
<!-- 可以配置在服务者端 -->
<dubbo:service interface="com.foo.BarService" stub="true" />
<!-- 或 -->
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
(2)提供 Stub 的实现:在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数
package com.foo;
public class BarServiceStub implements BarService {
private final BarService barService;
// 构造函数传入真正的远程代理对象
public BarServiceStub(BarService barService){
this.barService = barService;
}
public String sayHello(String name) {
//before:此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
//此处 Proxy 发起调用
String name = barService.sayHello(name)
//after-returning:在返回过程之前执行自己的逻辑
return name;
} catch (Exception e) {
//after-throwing:可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
(3)在实际开发中,本地存根一般放在接口所在位置,服务端的接口实现在另一工程
4、本地存根执行顺序
(1)服务消费者发起调用
(2)如果服务消费者端存在本地存根 Stub,会先执行本地存根
(3)本地存根 Stub 持有远程服务的 Proxy 对象,Stub 在执行时,会先执行自己的逻辑(before),然后通过 Proxy 发起远程调用,最后在返回过程之前,也会执行自己的逻辑(after-returning)
(4)如果远程服务的 Proxy 对象在执行过程中抛出 Exception,会执行服务消费端的本地伪装 Mock 的逻辑(after-throwing),返回容错数据,从而达到服务降级的目的
地址缓存
1、dubbo 服务消费者在第一次调用时,会将服务提供方地址缓存到本地,以后在调用则不会访问注册中心
2、当服务提供者地址发生变化时,注册中心会通知服务消费者
3、健壮性
(1)监控中心宕掉不影响使用,只是丢失部分采样数据
(2)数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
(3)注册中心对等集群,任意一台宕掉后,将自动切换到另一台
(4)注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
(5)服务提供者无状态,任意一台宕掉后,不影响使用
(6)服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
4、dubbo 直连
(1)reference:服务消费者引用服务配置
属性 | 对应 URL 参数 | 类型 | 是否必填 | 缺省值 | 作用 | 描述 |
url | url | string | 可选 | 服务治理 | 点对点直连服务提供者地址,将绕过注册中心 |
序列化
1、dubbo 内部已经封装序列化、反序列化的过程
(1)参数及返回值需实现 Serializable 接口
(2)只需要在定义 POJO 类时,实现 Serializable 接口即可
(3)一般会定义一个公共 POJO 模块,让生产者和消费者都依赖该模块
2、在 dubbo RPC 中,同时支持多种序列化方式
(1)dubbo 序列化:阿里尚未开发成熟的高效 java 序列化实现,阿里不建议在生产环境使用它
(2)hessian2 序列化:hessian 是一种跨语言的高效二进制序列化方式,但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,它是 dubbo RPC 默认启用的序列化方式
(3)json 序列化:目前有两种实现,一种是采用的阿里的 fastjson 库,另一种是采用 dubbo 中自己实现的简单 json 库,但其实现都不是特别成熟,而且 json 这种文本序列化性能一般不如上面两种二进制序列化
(4)java 序列化:主要是采用 JDK 自带的 Java 序列化实现,性能很不理想
(5)在通常情况下,这四种主要序列化方式的性能从上到下依次递减
(6)对于dubbo RPC这种追求高性能的远程调用方式来说,实际上只有 1、2 两种高效序列化方式比较般配,而第 1 个 dubbo 序列化由于还不成熟,所以实际只剩下 2 可用,所以 dubbo RPC 默认采用 hessian2 序列化
(7)但 hessian 是一个比较老的序列化实现,而且它是跨语言的,所以不是单独针对 java 进行优化的。而 dubbo RPC 实际上完全是一种 Java to Java 的远程调用,其实没有必要采用跨语言的序列化方式(当然肯定也不排斥跨语言的序列化)
3、最近几年,各种新的高效序列化方式层出不穷,不断刷新序列化性能的上限,最典型的包括
(1)专门针对 Java 语言的:Kryo,FST 等等
(2)跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack 等等
(3)这些序列化方式的性能多数都显著优于 hessian2(甚至包括尚未成熟的 dubbo 序列化)
(4)有鉴于此,为 dubbo 引入 Kryo 和 FST 这两种高效 Java 序列化实现,来逐步取代 hessian2
(5)其中,Kryo 是一种非常成熟的序列化实现,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如:Hive、Storm)中广泛的使用。而 FST 是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例
(6)在面向生产环境的应用中,建议目前更优先选择 Kryo
负载均衡
1、在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用
2、具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个 Provider 实例
3、目前 Dubbo 内置了如下负载均衡算法,用户可直接配置使用
算法 | 特性 | 备注 |
---|---|---|
RandomLoadBalance | 加权随机 | 默认算法,默认权重相同 |
RoundRobinLoadBalance | 加权轮询 | 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同, |
LeastActiveLoadBalance | 最少活跃优先 + 加权随机 | 背后是能者多劳的思想 |
ShortestResponseLoadBalance | 最短响应优先 + 加权随机 | 更加关注响应速度 |
ConsistentHashLoadBalance | 一致性 Hash | 确定的入参,确定的提供者,适用于有状态请求 |
4、Random
(1)加权随机,按权重设置随机概率
(2)在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重
(3)缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上
5、RoundRobin
(1)加权轮询,按公约后的权重设置轮询比率,循环调用节点
(2)缺点:同样存在慢的提供者累积请求的问题
(3)加权轮询过程过程中,如果某节点权重过大,会存在某段时间内调用过于集中的问题
(4)例如 ABC 三节点有如下权重:{A: 3, B: 2, C: 1}
(5)那么按照最原始的轮询算法,调用过程将变成:A A A B B C
(6)对此,Dubbo 借鉴 Nginx 的平滑加权轮询算法,对此做了优化,调用过程可抽象成下表
轮前加和权重(上轮权重 + 初始配置权重) | 本轮胜者 | 合计权重 | 轮后权重(胜者减去合计权重) |
---|---|---|---|
起始轮:currentWeight 全为 0 | \ | \ | A(0), B(0), C(0) |
A(3), B(2), C(1) | A | 6 | A(-3), B(2), C(1) |
A(0), B(4), C(2) | B | 6 | A(0), B(-2), C(2) |
A(3), B(0), C(3) | A | 6 | A(-3), B(0), C(3) |
A(0), B(2), C(4) | C | 6 | A(0), B(2), C(-2) |
A(3), B(4), C(-1) | B | 6 | A(3), B(-2), C(-1) |
A(6), B(0), C(0) | A | 6 | A(0), B(0), C(0) |
(7)经过合计权重(3+2+1)轮次后,循环又回到了起点,整个过程中节点流量是平滑的,且哪怕在很短的时间周期内,概率都是按期望分布的
(8)如果用户有加权轮询的需求,可放心使用该算法
6、LeastActive
(1)加权最少活跃调用优先,活跃数越低,越优先调用,相同活跃数的进行加权随机
(2)活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强
(3)使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求
7、ShortestResponse
(1)加权最短响应优先,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机
(2)使得响应时间越快的提供者,处理更多的请求
(3)缺点:可能会造成流量过于集中于高性能节点的问题
(4)此处响应时间 = 某个提供者在窗口时间内的平均响应时间,窗口时间默认是 30s
8、ConsistentHash
(1)一致性 Hash,相同参数的请求总是发到同一提供者
(2)当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动
(3)缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
(4)缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
9、配置
(1)服务端服务级别
<dubbo:service interface="..." loadbalance="roundrobin" />
(2)客户端服务级别
<dubbo:reference interface="..." loadbalance="roundrobin" />
(3)服务端方法级别
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
(4)客户端方法级别
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
10、负载均衡扩展
(1)扩展接口:org.apache.dubbo.rpc.cluster.LoadBalance
(2)扩展配置
<dubbo:protocol loadbalance="xxx" />
<!-- 缺省值设置,当<dubbo:protocol>没有配置loadbalance时,使用此配置 -->
<dubbo:provider loadbalance="xxx" />
服务降级
1、特性说明
(1)推荐使用相关限流降级组件(如 Sentinel)以达到最佳体验
(2)服务降级是指服务在非正常情况下进行降级应急处理
2、使用场景
(1)某服务或接口负荷超出最大承载能力范围,需要进行降级应急处理,避免系统崩溃
(2)调用的某非关键服务或接口暂时不可用时,返回模拟数据或空,业务还能继续可用
(3)降级非核心业务的服务或接口,腾出系统资源,尽量保证核心业务的正常运行
(4)某上游基础服务超时或不可用时,执行能快速响应的降级预案,避免服务整体雪崩
3、使用方式
(1)以下将 xml 配置为例:(通过注解方式配置类似)
(2)配置 mock="true"
<!-- 这种方式需要在相同包下有类名 + Mock后缀的实现类,即com.xxx.service包下有DemoServiceMock类 -->
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="true" />
(3)配置 mock="com.xxx.service.DemoServiceMock"
<!-- 这种方式指定 Mock 类的全路径 -->
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="com.xxx.service.DemoServiceMock" />
(4)配置 mock="[fail|force]return|throw xxx"
fail 或 force 关键字可选,表示调用失败或不调用强制执行 mock 方法,如果不指定关键字默认为 fail
return 表示指定返回结果
throw 表示抛出指定异常
xxx 根据接口的返回类型解析,可以指定返回值或抛出自定义的异常
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="return" />
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="return null" />
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="fail:return aaa" />
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="force:return true" />
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="fail:throw" />
<dubbo:reference id="demoService" interface="com.xxx.service.DemoService" mock="force:throw java.lang.NullPointException" />
4、配合 dubbo-admin 使用
(1)应用消费端引入 dubbo-mock-admin 依赖
(2)应用消费端启动时设置 JVM 参数,-Denable.dubbo.admin.mock=true
(3)启动 dubbo-admin,在服务 Mock -> 规则配置菜单下设置 Mock 规则
(4)以服务方法的维度设置规则,设置返回模拟数据,动态启用 / 禁用规则
5、注意事项
(1)Dubbo 启动时会检查配置,当 mock 属性值配置有误时会启动失败,可根据错误提示信息进行排查
(2)配置格式错误,如:return+null 会报错,被当做 mock 类型处理,return 后面可省略不写或者跟空格后再跟返回值
(3)类型找不到错误,如:自定义 mock 类、throw 自定义异常,请检查类型是否存在或是否有拼写错误
集群容错
1、在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试
2、各节点关系
(1)此处 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装 Provider 地址及 Service 接口信息
(2)Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
(3)Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
(4)Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
(5)LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
3、Failover Cluster
(1)该配置为缺省配置
(2)失败自动切换,当出现失败,重试其它服务器
(3)通常用于读操作,但重试会带来更长延迟
(4)可通过 retries="2" 来设置重试次数(不含第一次)
(5)重试次数配置如下
<dubbo:service retries="2" />
<dubbo:reference retries="2" />
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</dubbo:reference>
4、Failfast Cluster
(1)快速失败,只发起一次调用,失败立即报错
(2)通常用于非幂等性的写操作,比如新增记录
5、Failsafe Cluster
(1)失败安全,出现异常时,直接忽略
(2)通常用于写入审计日志等操作
6、Failback Cluster
(1)失败自动恢复,后台记录失败请求,定时重发
(2)通常用于消息通知操作
7、Forking Cluster
(1)并行调用多个服务器,只要一个成功即返回
(2)通常用于实时性要求较高的读操作,但需要浪费更多服务资源
(3)可通过 forks="2" 来设置最大并行数
8、Broadcast Cluster
(1)2.1.0 开始支持
(2)广播调用所有提供者,逐个调用,任意一台报错则报错
(3)通常用于通知所有提供者更新缓存或日志等本地资源信息
(4)现在广播调用中,可以通过 broadcast.fail.percent 配置节点调用失败的比例,当达到这个比例后,BroadcastClusterInvoker 将不再调用其他节点,直接抛出异常
(5)broadcast.fail.percent 取值在 0~100 范围内,默认情况下当全部调用失败后,才会抛出异常
(6)broadcast.fail.percent 只是控制的当失败后是否继续调用其他节点,并不改变结果(任意一台报错则报错)
(7)broadcast.fail.percent 参数在 dubbo 2.7.10 及以上版本生效
(8)Broadcast Cluster 配置 broadcast.fail.percent
(9)broadcast.fail.percent=20 代表了当 20% 的节点调用失败就抛出异常,不再调用其他节点
@reference(cluster = "broadcast", parameters = {"broadcast.fail.percent", "20"})
9、Available Cluster
(1)调用目前可用的实例(只调用一个),如果当前没有可用的实例,则抛出异常
(2)通常用于不需要负载均衡的场景
10、Mergeable Cluster
(1)将集群中的调用结果聚合起来返回结果,通常和 group 一起配合使用
(2)通过分组对结果进行聚合并返回聚合后的结果,比如菜单服务,用 group 区分同一接口的多种实现,现在消费方需从每种 group 中调用一次并返回结果,对结果进行合并之后返回,这样就可以实现聚合菜单项
11、ZoneAware Cluster
(1)多注册中心订阅的场景,注册中心集群间的负载均衡
(2)对于多注册中心间的选址策略有如下四种
(3)指定优先级:preferred="true" 注册中心的地址将被优先选择
<dubbo:registry address="zookeeper://127.0.0.1:2181" preferred="true" />
(4)同中心优先:检查当前请求所属的区域,优先选择具有相同区域的注册中心
<dubbo:registry address="zookeeper://127.0.0.1:2181" zone="beijing" />
(5)权重轮询:根据每个注册中心的权重分配流量
<dubbo:registry id="beijing" address="zookeeper://127.0.0.1:2181" weight="100" />
<dubbo:registry id="shanghai" address="zookeeper://127.0.0.1:2182" weight="10" />
(6)缺省值:选择一个可用的注册中心
12、集群模式配置
(1)服务提供方配置集群模式
<dubbo:service cluster="failsafe" />
(2)服务消费方配置集群模式
<dubbo:reference cluster="failsafe" />
13、集群扩展
(1)扩展接口:org.apache.dubbo.rpc.cluster.Cluster
(2)扩展配置
<dubbo:protocol cluster="xxx" />
<!-- 缺省值配置,如果<dubbo:protocol>没有配置cluster时,使用此配置 -->
<dubbo:provider cluster="xxx" />
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战