Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
文章目录
- 定义一个示例
- 高级特性
- 装饰器
- 概述
- 简单案例
- 多种装饰方式
- 无框架请求
- 概述
- 使用方式
- 阻塞任务处理器
- 背景
- 概述
- 多种使用方式
- rpc 异常统一处理
- 使用方式
- 更详细的异常信息
- Armeria 提供 gRPC 客户端多种调用方式
- 同步调用
- 异步调用
- 使用装饰器
- 负载均衡
- 简单案例
- Armeria 提供的所有负载均衡策略
- 进阶使用
- Nacos 集成 Armeria
- 概述
- 实现步骤
- 文档服务
- 概述
- 实现步骤
定义一个示例
Note:本文所讲的所有特性围绕此例展开
1)定义一个简单的 proto:
syntax = "proto3";package org.cyk.armeria.grpc.hello;
option java_package = "org.cyk.armeria.grpc.hello";service HelloService {rpc Hello (HelloReq) returns (HelloResp) {}
}message HelloReq {string name = 1;
}message HelloResp {string msg = 1;
}
2)实现服务端
class HelloServiceGrpcFacade: HelloServiceImplBase() {override fun hello(request: Hello.HelloReq,responseObserver: StreamObserver<HelloResp>) {val resp = HelloResp.newBuilder().setMsg("hello ${request.name} ~").build()responseObserver.onNext(resp)responseObserver.onCompleted()}}
3)服务启动配置
object ArmeriaGrpcBean {fun newServer(port: Int): Server {return Server.builder().http(port) // 1.配置端口号.service(GrpcService.builder().addService(HelloServiceGrpcFacade()) // 2.添加服务示例.build()).build()}}companion object {private lateinit var stub: HelloServiceBlockingStubprivate lateinit var server: Server@JvmStatic@BeforeAllfun beforeAll() {server = ArmeriaGrpcBean.newServer(9000)server.start()//这里启动不是异步的,所以不用 Thread.sleep 等待stub = GrpcClients.newClient("http://127.0.0.1:9000/",HelloServiceBlockingStub::class.java,)}}
高级特性
装饰器
概述
装饰器主要作用是为了给 服务 或 方法 添加切面逻辑,也就是说在不改变核心业务逻辑的情况下,添加例如 日志、监控、限流、身份认证 功能,最大的好处就是统一处理,逻辑复用.
简单案例
例如在调用 HelloServiceGrpcFacade 下的 hello 方法时记录一下日志
那么首先需要先实现一个自定义装饰器:
Armeria 默认提供了一些装饰器,例如 专门处理日志的 com.linecorp.armeria.server.logging.LoggingService,但是为了满足客制化,我就根据 LoggingService 源码实现了一个自定义的装饰器
/*** 自定义装饰器* @author yikang.chen*/
class CustomDecorator(delegate: HttpService,
) : SimpleDecoratingHttpService(delegate) {companion object {private val log = LoggerFactory.getLogger(CustomDecorator::class.java)/*** 这里为了迎合 Armeria 的 Java API,只能先这样处理*/fun newDecorator(): Function<in HttpService, out CustomDecorator> {return Function { delegate ->CustomDecorator(delegate)}}}override fun serve(ctx: ServiceRequestContext, req: HttpRequest): HttpResponse {log.info("======================================================")log.info("收到客户端 rpc header: ${req.headers()}")req.aggregate().thenApply { req ->log.info("收到客户端 rpc body: ${req.contentUtf8()}")}log.info("======================================================")return unwrap().serve(ctx, req)}}
然后添加到服务配置中
fun newServer(port: Int): Server {return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade()).build(),listOf(CustomDecorator.newDecorator()) // 👈👈👈).build()}
客户端调用如下:
@Testfun test1() {val req = HelloReq.newBuilder().setName("cyk").build()val resp = stub.hello(req)assertTrue { resp.msg.isNotBlank() }}
在执行结果中就可以看到 装饰器 的处理信息:
======================================================
23:25:42.779 [armeria-common-worker-nio-3-3] INFO component.CustomDecorator -- 收到客户端 rpc header: [:method=POST, :authority=127.0.0.1:9000, :scheme=http, :path=/org.cyk.armeria.grpc.hello.HelloService/Hello, content-type=application/grpc+proto, te=trailers, grpc-accept-encoding=gzip, grpc-timeout=15000000u, user-agent=armeria/1.30.1, content-length=10]
23:25:42.781 [armeria-common-worker-nio-3-3] INFO component.CustomDecorator -- 收到客户端 rpc body: cyk
23:25:42.781 [armeria-common-worker-nio-3-3] INFO component.CustomDecorator -- ======================================================
多种装饰方式
1)在 GrpcServiceBuilder 中给单个服务指定装饰器
fun newServer(port: Int): Server {return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade(), listOf(CustomDecorator.newDecorator())) // 👈👈👈.build()).build()}
2)直接在服务类或者方法上使用 @Decorator
@Decorator(Custom2::class) // 👈👈👈 对该类下的所有方法都管用
class HelloServiceGrpcFacade: HelloServiceImplBase() {@Decorator(Custom2::class) // 👈👈👈 仅对该方法管用override fun hello(request: Hello.HelloReq,responseObserver: StreamObserver<HelloResp>) {val resp = HelloResp.newBuilder().setMsg("hello ${request.name} ~").build()responseObserver.onNext(resp)responseObserver.onCompleted()}}
值得注意的是,要使用这种注解的方式,那么自定义的装饰器必须要实现 DecoratingHttpServiceFunction 接口,如下:
class Custom2 : DecoratingHttpServiceFunction {override fun serve(delegate: HttpService, ctx: ServiceRequestContext, req: HttpRequest): HttpResponse {println("另一种装饰器 ...")return delegate.serve(ctx, req)}}
Ps:那为什么在 Server.builder().service 中没有直接用 Custom2 这种呢?因为作者没有提供这种重载… 你需要实现 DecoratingHttpServiceFunction 并继承 SimpleDecoratingHttpService,才能达到这两种效果.
无框架请求
概述
GrpcService 支持无框架的请求,也就是说,你可以使用传统的 protobuf 或 JSON API,而无需使用 gRPC 的二进制格式 来调用 gRPC 服务. 这对于将现有 HTTP POST API 迁移到 gRPC 非常有用(几乎无缝迁移).
使用方式
使用 Armeria 的 GrpcService,可以通过开启 enableUnframedRequests(true) 来支持无框架请求:
fun newServer(port: Int): Server {return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade()).enableUnframedRequests(true) // 👈👈👈 启用无框请求.build(),CustomDecorator.newDecorator(),).build()}
客户端请求方式如下:
- 二进制 Protobuf:
- 请求类型:HTTP POST
- URL:
/org.cyk.armeria.grpc.hello.HelloService/Hello - Content-Type: application/protobuf
- 请求体:使用二进制的 protobuf 格式
- JSON:
- 请求类型: HTTP POST
- URL:
/org.cyk.armeria.grpc.hello.HelloService/Hello - Content-Type: application/json; charset=utf-8
- 请求体:使用 JSON 格式.
Ps:注意上述 URL 分为三个部分:
- 包名:org.cyk.armeria.grpc.hello
- 服务名:HelloService
- 方法名:Hello
例如这里使用 JSON 请求:
curl -X POST http://localhost:9000/org.cyk.armeria.grpc.hello.HelloService/Hello \-H "Content-Type: application/json" \-d '{"name": "cyk"}'
响应如下:
{"msg": "hello cyk ~"
}
阻塞任务处理器
背景
Armeria 默认是非阻塞的,采用 事件循环模型 来处理请求.
什么是事件循环模型?实际上可以类比为 生产者 消费者 模式.
- 生产者(事件循环线程
EventLoop):负责监听网络 I/O 事件,把收到的客户端请求放入到一个任务队列中,此时生产者不会阻塞,而是继续处理下一个请求. - 消费者:监听队列中是否有新任务,如果有就从队列中取出任务并处理.
当执行的任务都是非阻塞任务时(不耗时),Aemeria 这种架构可以处理大量的并发请求,但是,如果来了一些阻塞任务(耗时任务),就会拖慢整个事件驱动的处理速度.
例如有 4 个事件循环线程在处理(假设两个生产者,两个消费者),此时来了一个 5s 的数据库查询任务,那么拿到整个任务的生产者不会有什么事(只需要把任务放队列中),而拿到整个任务的消费者,就会阻塞 5s,也就意味这这 5s 里,只剩下一个 消费者 在处理 队列中的任务,也就相当于整个事件驱动模型的任务处理速度大大下降.
概述
阻塞任务处理器是一个 可以缓存的线程池,行为类似于 Executors.newCachedThreadPool(),线程会根据任务需求动态创建,并在任务完成后回收空闲线程. 目的就是为了将 耗时任务 与 事件循环分离.
例如某个方法被标注为需要使用阻塞处理器,那么该方法就会交给阻塞处理器管理,而其他方法还是基于事件循环模型来处理任务.
多种使用方式
1)注解式,将某个类或者某个方法交给阻塞处理器.
@Blocking // 👈 让整个类中的方法都在阻塞任务执行器中运行(可以标注类,也可以标注方法)
class HelloServiceGrpcFacade: HelloServiceImplBase() {override fun hello(request: Hello.HelloReq,responseObserver: StreamObserver<HelloResp>) {Thread.sleep(2000) //模拟耗时任务val resp = HelloResp.newBuilder().setMsg("hello ${request.name} ~").build()responseObserver.onNext(resp)responseObserver.onCompleted()}}
2)全局配置服务,使得一个 GrpcService 下的所有服务都会在阻塞任务处理器中执行.
object ArmeriaGrpcBean {fun newServer(port: Int): Server {return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade()).enableUnframedRequests(true).useBlockingTaskExecutor(true) // 👈👈👈 这个 grpc 服务下的所有方法都会使用阻塞执行器.build(),CustomDecorator.newDecorator(),).build()}}
3)编程式,控制某一段逻辑使用阻塞任务处理器.
override fun hello(request: Hello.HelloReq,responseObserver: StreamObserver<HelloResp>) {// 👈👈👈 注意: 所有交给阻塞处理器执行的任务都是异步的(线程池),这样使用的前提是异步不干扰后续的业务逻辑ServiceRequestContext.current().blockingTaskExecutor().submit {Thread.sleep(2000) //模拟耗时任务println("耗时任务处理完成")}val resp = HelloResp.newBuilder().setMsg("hello ${request.name} ~").build()responseObserver.onNext(resp)responseObserver.onCompleted()}
rpc 异常统一处理
使用方式
自定义异常类 HelloException.
自定义异常处理实现 GrpcExceptionHandlerFunction 接口
import com.linecorp.armeria.common.RequestContext
import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction
import io.grpc.Metadata
import io.grpc.Status/*** 自定义异常* @author: yikang.chen*/
class HelloException (errorMsg: String
): IllegalStateException(errorMsg)/*** 统一异常处理* @author: yikang.chen*/
class GrpcExceptionHandler: GrpcExceptionHandlerFunction {override fun apply(ctx: RequestContext, status: Status, cause: Throwable, metadata: Metadata): Status? {when (cause) {is HelloException -> Status.NOT_FOUND.withCause(cause).withDescription(cause.message)is IllegalArgumentException -> return Status.INVALID_ARGUMENT.withCause(cause)else -> return null}return null}}
rpc 方法引发异常如下:
override fun hello(request: Hello.HelloReq,responseObserver: StreamObserver<HelloResp>) {if (1 + 1 == 2) {throw HelloException("异常 :( ")}val resp = HelloResp.newBuilder().setMsg("hello ${request.name} ~").build()responseObserver.onNext(resp)responseObserver.onCompleted()}
无框架模式调用后结果如下:

更详细的异常信息
如果觉得异常信息不够详细,还可以启用详细的异常响应:
fun newServer(port: Int): Server {// 启用详细异常响应 👈👈👈 System.setProperty("com.linecorp.armeria.verboseResponses", "true");return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade()).enableUnframedRequests(true).exceptionHandler(GrpcExceptionHandler()).build(),CustomDecorator.newDecorator(),).build()}
无框模式调用:

客户端调用(太长,这里只截取关键的一部分):
io.grpc.StatusRuntimeException: UNKNOWNat io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:268)at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:249)at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:167)at org.cyk.armeria.grpc.hello.HelloServiceGrpc$HelloServiceBlockingStub.hello(HelloServiceGrpc.java:160)at HelloServiceGrpcFacadeTests.test1(HelloServiceGrpcFacadeTests.kt:32)exception.HelloException: 异常 :(
com.linecorp.armeria.common.grpc.StatusCauseException: exception.HelloException: 异常 :( at service.HelloServiceGrpcFacade.hello(HelloServiceGrpcFacade.kt:16)at org.cyk.armeria.grpc.hello.HelloServiceGrpc$MethodHandlers.invoke(HelloServiceGrpc.java:210)
Armeria 提供 gRPC 客户端多种调用方式
同步调用
@Testfun test() {val client = GrpcClients.newClient("gproto+http://127.0.0.1:9000/",HelloServiceBlockingStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()val resp = client.hello(req)val expect = "hello cyk ~"require(resp.msg == expect ) { "expect: $expect, actual: ${resp.msg} "}}
异步调用
不用等待结果,使用回调函数来处理服务响应.
@Testfun testFutures() {val client = GrpcClients.newClient("gproto+http://127.0.0.1:9000/",HelloServiceFutureStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()val futureResp = client.hello(req)Futures.addCallback(futureResp, object : FutureCallback<HelloResp> {override fun onSuccess(result: HelloResp?) {assertNotNull(result)val expect = "hello cyk ~"require(result.msg == expect ) { "expect: $expect, actual: ${result.msg} "}}override fun onFailure(t: Throwable) {t.printStackTrace()}}, MoreExecutors.directExecutor())// Ps: MoreExecutors.directExecutor() 是 Guava 提供的特殊 Executor 实现,它不会为任务创建新的线程,也不会在线程池中执行任务。// 相反,它会直接在调用任务提交方法的当前线程中执行任务。// 等待异步完成(仅为演示,实际可能需要更多的非阻塞方式处理)futureResp.get()}
使用装饰器
这里和 服务端类似,客户端也可以使用装饰器,例如可以使用 Armeria 内置的 日志服务 来记录详细日志.
@Testfun testDecorator() {val client = GrpcClients.builder("gproto+http://127.0.0.1:9000/").serializationFormat(GrpcSerializationFormats.PROTO) //使用 protobuf 序列化.responseTimeoutMillis(10000) // 响应超时时间为 10 秒.decorator(LoggingClient.newDecorator()) // 添加日志装饰器.build(HelloServiceBlockingStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()val resp = client.hello(req)val expect = "hello cyk ~"require(resp.msg == expect ) { "expect: $expect, actual: ${resp.msg} "}}
调用后,在客户端可以看到详细的请求和响应日志:
15:08:55.154 [armeria-common-worker-nio-3-2] DEBUG com.linecorp.armeria.client.logging.LoggingClient -- [creqId=44e997a8, chanId=6412712d, laddr=127.0.0.1:61322, raddr=127.0.0.1:9000][http://127.0.0.1:9000/org.cyk.armeria.grpc.hello.HelloService/Hello#POST] Request: {startTime=2024-10-01T07:08:54.969Z(1727766534969348), Connection: {total=2024-10-01T07:08:55.006348Z[78689µs(78689500ns)], socket=2024-10-01T07:08:55.013353Z[70385µs(70385500ns)]}, length=10B, duration=136ms(136383200ns), scheme=gproto+h2c, name=Hello, headers=[:method=POST, :path=/org.cyk.armeria.grpc.hello.HelloService/Hello, :authority=127.0.0.1:9000, content-type=application/grpc+proto, te=trailers, grpc-accept-encoding=gzip, grpc-timeout=10000000u, content-length=10, user-agent=armeria/1.30.1], content=DefaultRpcRequest{serviceType=GrpcLogUtil, serviceName=org.cyk.armeria.grpc.hello.HelloService, method=Hello, params=[name: "cyk"
]}}
15:08:55.155 [armeria-common-worker-nio-3-2] DEBUG com.linecorp.armeria.client.logging.LoggingClient -- [creqId=44e997a8, chanId=6412712d, laddr=127.0.0.1:61322, raddr=127.0.0.1:9000][http://127.0.0.1:9000/org.cyk.armeria.grpc.hello.HelloService/Hello#POST] Response: {startTime=2024-10-01T07:08:55.142Z(1727766535142599), length=18B, duration=2549µs(2549100ns), totalDuration=177ms(177998100ns), headers=[:status=200, content-type=application/grpc+proto, grpc-encoding=identity, grpc-accept-encoding=gzip, server=Armeria/1.30.1, date=Tue, 1 Oct 2024 07:08:55 GMT], content=CompletableRpcResponse{msg: "hello cyk ~"
}, trailers=[EOS, grpc-status=0]}
负载均衡
简单案例
在 Armeria 中,EndpointGroup 是管理多个服务实例的工具.
默认负载均衡策略为: EndpointSelectionStrategy.weightedRoundRobin() -> 加权轮询,每个实例的权重默认是 1000
@Testfun test() {// 定义多个服务实例// 默认负载均衡策略为: EndpointSelectionStrategy.weightedRoundRobin() -> 加权轮询,每个实例的权重默认是 1000val instanceGroup = EndpointGroup.of(Endpoint.of("localhost", 9001),Endpoint.of("localhost", 9002),)val clientLB = GrpcClients.builder("gproto+http://group/").endpointRemapper { instanceGroup }.build(HelloServiceBlockingStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()for (i in 1..3) {val resp = clientLB.hello(req)val expect = "hello cyk ~"require(resp.msg == expect ) { "expect: $expect, actual: ${resp.msg} "}}}
Armeria 提供的所有负载均衡策略
-
加权轮询策略(EndpointSelectionStrategy.weightedRoundRobin())
- 解释:
默认采用的策略,每个节点权重默认为 1000. 加权轮询策略会尝试在长时间内,公平的根据权重分配请求,在短时间内可能会有偏差(短时间内几乎为轮询,特别时请求量小,而权重分配特别大时),随着请求量的增加,会越来越解决预期的分配比例.当 请求量 等于 所有实例的权重之和 时,就可以看到请求的量完全匹配权重的比例. - 使用场景:这个比较玄学… 所有实例权重之和
大于请求量时,几乎是轮询或者偏差;而所有实例权重之和小于请求量时,几乎严格按照权重分配的比例. 所以得看你怎么分配了.
- 解释:
-
普通轮询策略(EndpointSelectionStrategy.roundRobin())
- 解释:不考虑权重,
完全平均分配. - 使用场景:所有节点性能都差不多,或者你只是希望流量能均匀分布到各个机器.
- 解释:不考虑权重,
-
逐步提升权重策略(EndpointSelectionStrategy.rampingUp())
- 解释:
这个策略是为新加入节点设计的,并且不会像 加权轮询策略那么玄学,无论请求和权重之间比例怎么样,都会尽量按照权重分配. 当有新的实例加入集群时,系统不会立即让他处理大量请求,而是逐渐提高他的负载,最后慢慢符合权重比例. - 使用场景:适合动态扩展服务器时使用,特别是不想新节点刚启动时,被过多请求压垮.
- 解释:
-
粘性负载均衡策略(EndpointSelectionStrategy.sticky())
- 解释:这个策略让特定的请求始终被分配到同一个节点。它会基于某个特定的条件(比如用户 ID 或者 cookie)生成一个哈希值,这个哈希值会决定某个请求固定发送到某个节点。这样,某些请求总是发送到同一个服务器,便于该服务器进行缓存等优化
- 使用场景::
适用于需要某类请求固定走同一台服务器的情况,比如同一个用户的请求总是被发送到相同的服务器,这样服务器可以缓存用户的相关数据,提升性能。
进阶使用
这里以 加权负载均衡策略 为例,其他策略使用方式也一样
@Testfun testCustom() {//使用加权负载均衡策略,默认权重为 1000val strategy = EndpointSelectionStrategy.weightedRoundRobin() // 这也是默认策略val instanceGroup = EndpointGroup.of(strategy,Endpoint.of("localhost", 9001).withWeight(1),Endpoint.of("localhost", 9002).withWeight(9),)var c9001 = 0var c9002 = 0// 客户端装饰器: 记录调用次数// Ps:这里偷了个懒,建议还是专门弄个类,然后实现 DecoratingHttpClientFunction 接口val decorator = DecoratingHttpClientFunction { delegate, ctx, req ->if (ctx.endpoint()!!.port() == 9001) {c9001++} else {c9002++}return@DecoratingHttpClientFunction delegate.execute(ctx, req)}val clientLB = GrpcClients.builder("gproto+http://group/").endpointRemapper { instanceGroup }.decorator(decorator).build(HelloServiceBlockingStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()for (i in 1..100) {val resp = clientLB.hello(req)val expect = "hello cyk ~"require(resp.msg == expect ) { "expect: $expect, actual: ${resp.msg} "}}println("9001调用次数: $c9001")println("9002调用次数: $c9002")}
日志如下:
9001调用次数: 10
9002调用次数: 90
Nacos 集成 Armeria
概述
此处 Nacos 作为服务注册和发现中心,Armeria 从 Nacos 对应的服务集群中获取健康的实例列表来实现负载均衡,最后通过 Armeria 的 gRPC 客户端将请求分发.
实现步骤
1)依赖配置
implementation ("com.alibaba.nacos:nacos-client:2.3.2")
2)Armeria 配置 gRPC 服务,并注册到 Nacos 中.
object NacosBean {fun newService(): NamingService = NacosFactory.createNamingService(Properties().apply {put("serverAddr", "100.64.0.0:8848")put("namespace", "0dc9a7f0-5f97-445a-87e5-9fe6869d6708") //可选,默认命名空间为 public (自定义命名空间需要提前在 nacos 客户端上创建,此处填写命名空间ID)})}fun main() {val server1 = ArmeriaGrpcBean.newServer(9001)val server2 = ArmeriaGrpcBean.newServer(9002)server1.start().join()server2.start().join()// 连接 nacos,并注册集群val nacos = NacosBean.newService()val instance1 = Instance().apply {ip = "100.94.135.96"port = 9001clusterName = "grpc-hello"}val instance2 = Instance().apply {ip = "100.94.135.96"port = 9002clusterName = "grpc-hello"}nacos.batchRegisterInstance("helloGrpcService", "DEFAULT", listOf(instance1, instance2))
}
3)Armeria 客户端从 Nacos 获取健康实例列表,实现负载均衡.
@Testfun testNacosLB() {// 从 nacos 中获取 helloGrpcService 服务下所有 健康 的服务实例val endpointGroup = NacosBean.newService().selectInstances("helloGrpcService", "DEFAULT", true) // healthy: true.map { Endpoint.of(it.ip, it.port) }.let { endpoints ->EndpointGroup.of(EndpointSelectionStrategy.roundRobin(),endpoints)}val clientLB = GrpcClients.builder("gproto+http://group/").endpointRemapper { endpointGroup }.decorator(DecoratingHttpClientFunction { delegate, ctx, req ->println("目标端点: ${ctx.endpoint()!!.port()}")return@DecoratingHttpClientFunction delegate.execute(ctx, req)}).build(HelloServiceBlockingStub::class.java)val req = HelloReq.newBuilder().setName("cyk").build()for (i in 0..10) {val resp = clientLB.hello(req)val expect = "hello cyk ~"require(resp.msg == expect ) { "expect: $expect, actual: ${resp.msg} "}}}
文档服务
概述
在 Armeria 中,文档服务会自动帮我们生成 API 文档,包括 gRPC、HTTP、Thrift 等服务的接口定义文档,不仅可以看到接口的定义方式、还可以对接口进行调试,非常方便.
实现步骤
只需要在配置 Server 的时候添加文档服务即可
object ArmeriaGrpcBean {fun newServer(port: Int): Server {return Server.builder().http(port).service(GrpcService.builder().addService(HelloServiceGrpcFacade()).enableUnframedRequests(true).exceptionHandler(GrpcExceptionHandler()).build(),).serviceUnder("/docs", DocService()) // 👈👈👈 添加文档服务.build()}}
启动后,访问 ip:port/docs 就可以看到对应的页面

点击对应的服务,右上角就可以进行 Debug.
Ps:
防伪签名 yikang.chen| 未经本人允许,不得转载.
相关文章:
Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
文章目录 定义一个示例高级特性装饰器概述简单案例多种装饰方式 无框架请求概述使用方式 阻塞任务处理器背景概述多种使用方式 rpc 异常统一处理使用方式更详细的异常信息 Armeria 提供 gRPC 客户端多种调用方式同步调用异步调用使用装饰器 负载均衡简单案例Armeria 提供的所有…...
如何制作一个企业网站,建设网站的基本步骤有哪些?
企业网站是企业的门面和名片,决定网民对企业的第一印象,因此,现在很多公司想做一个属于自己网站,但是不知道怎么做,更不知道从何做起,更别说做成了。为了能够让大家清楚如何做一个企业网站,现在…...
01-python+selenium自动化测试-基础学习
前言 基于python3和selenium3做自动化测试,俗话说:工欲善其事必先利其器;没有金刚钻就不揽那瓷器活,磨刀不误砍柴工,因此你必须会搭建基本的开发环境,掌握python基本的语法和一个IDE来进行开发,…...
【redis-05】redis保证和mysql数据一致性
redis系列整体栏目 内容链接地址【一】redis基本数据类型和使用场景https://zhenghuisheng.blog.csdn.net/article/details/142406325【二】redis的持久化机制和原理https://zhenghuisheng.blog.csdn.net/article/details/142441756【三】redis缓存穿透、缓存击穿、缓存雪崩htt…...
写一个登录判断机制py
创建一个简单的登录机制涉及到用户输入的验证和与数据库中存储的凭证的比较。以下是一个使用Python语言和SQLite数据库的示例。这个例子仅用于教学目的,实际应用中应该使用更安全的方法来存储和验证密码,比如使用密码哈希。 首先,你需要安装…...
特征点检测与匹配是计算机视觉中的基础任务之一,广泛应用于图像配准、物体识别、运动估计、三维重建等领域。
特征点检测与匹配是计算机视觉中的基础任务之一,广泛应用于图像配准、物体识别、运动估计、三维重建等领域。下面是一些关键的知识点: 1. 特征点检测 特征点检测的目的是从图像中找到独特的、稳定的点,这些点在图像变化(如旋转、…...
python——Echarts现交互式动态可视化
数据展示 20192018201720162015201420132012北京5817.15785.91765430.78755081.264723.864027.16093661.10973314.934天津2410.252106.23972310.35522723.52667.112390.35182079.07161760.0201河北3742.673513.86433233.83322849.872649.182446.61662295.62032084.2825山西234…...
【含开题报告+文档+PPT+源码】基于SSM框架的民宿酒店预定系统的设计与实现
开题报告 随着人们旅游需求的增加,民宿行业呈现出快速发展的趋势。传统的住宿方式逐渐无法满足人们对个性化、舒适、便捷的需求,而民宿作为一种新型的住宿选择,逐渐受到人们的青睐。民宿的特点是具有独特的风格、便捷的地理位置、相对亲近的…...
正确理解协程
import asyncio# 定义一个异步函数(协程) async def say_after(delay, what):# 等待指定的时间await asyncio.sleep(delay)# 打印消息print(what)# 定义另一个异步函数 async def main():# 同时启动两个协程,并等待这2个协程结束await say_af…...
蒙特卡罗方法 - 采样和蒙特卡罗方法篇
序言 蒙特卡罗( Monte Carlo \text{Monte Carlo} Monte Carlo)方法,也被称为计算机随机模拟方法,是一种基于“随机数”的计算方法。这一方法源于美国在第二次世界大战期间研制原子弹的“曼哈顿计划”。其核心思想是使用随机数&am…...
论文阅读:InternVL v1.5| How Far Are We to GPT-4V? 通过开源模型缩小与商业多模式模型的差距
论文地址:https://arxiv.org/abs/2404.16821 Demo: https://internvl.opengvlab.com Model:https://huggingface.co/OpenGVLab/InternVL-Chat-V1-5 公开时间:2024年4月29日 InternVL1.5,是一个开源的多模态大型语言模…...
什么是电能表PTB认证
电能表PTB认证是指电能表产品经过德国国家计量研究所(Physikalisch-Technische Bundesanstalt,简称PTB)的认证和审核过程。PTB是德国联邦政府在计量、物理、材料和测试领域的技术专家和合作伙伴,拥有世界领先的技术水平和专业知识…...
C# 单例模式继承
简介:单例模式是软件工程中最著名的模式之一。从本质上讲,singleton 是一个只允许创建自身的单个实例的类,并且通常提供对该实例的简单访问。最常见的是,单例不允许在创建实例时指定任何参数 - 否则,对实例进行第二次请…...
ESP8266模块(WIFI STM32)
目录 一、介绍 二、传感器原理 1.原理图 2.引脚描述 3.ESP8266基础AT指令介绍 4.ESP8266基础工作模式 三、程序设计 main.c文件 esp8266.h文件 esp8266.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 ESP8266是一款嵌入式系统级芯片,它集成了Wi…...
微信小程序学习实录9:掌握wx.chooseMedia实现多图片文件上传功能(选择图片、预览图片、上传图片)
要实现多图片上传到服务器,需要在小程序前端和PHP后端分别进行相应的设置。 基本流程 微信小程序提供了丰富的API来支持多图片上传功能。在微信小程序中实现多图片的选择、预览以及上传到服务器的功能: 1. 选择图片 使用 wx.chooseImage API 可以让用…...
助动词的分类及其缩略形式
助动词的分类及其缩略形式 1. 助动词 (auxiliary verb)2. 基本助动词 (primary auxiliary)2.1. 基本助动词 be、do 和 have2.2. 实义动词 be、do 和 have 3. 情态助动词 (modal auxiliary)3.1. 情态助动词取代情态动词 4. 半助动词 (semi-auxiliary)4.1. 不能与 it ... that-cl…...
Redis——分布式锁
在一个分布式系统中,只要涉及到多个节点访问同一个公共资源的时候,就需要加锁来实现互斥,从而达到线程安全的问题。 但是呢,分布式系统不同一些,因为分布式系统部署在不同的服务器上,很可能大量的请求打到…...
C++面试速通宝典——13
208. class里面定义int a,如果不实现构造函数,实例化这个类,a的值是? 答:a的值是未定义的(在C标准中成为“未初始化”)。 解释: 在C中,如果一…...
数据结构(二叉树)
1. 树相关术语 父结点/双亲结点:如果一个结点有子结点那么它就是父结点或者双亲结点;例如A是BCDEFG的父结点,J是PQ的父结点等等;子结点:一个结点含有的子树的根节点称为该结点的子结点;如上图的H是D的子结点…...
Windows 通过 Docker 安装 GitLab
1. 安装 Docker Desktop 下载网站:Windows | Docker Docs 2. 拉取 GitLab Docker 镜像 打开 PowerShell 或 命令提示符,拉取 GitLab 镜像: docker pull gitlab/gitlab-ee:latest或则使用社区版: docker pull gitlab/gitlab-ce…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...

