gRPC-transport包源码分析

本文深入解析gRPC结合HTTP/2标准的工作原理,重点介绍流控机制及其实现细节。从客户端发送请求到服务器端处理的全流程进行剖析,揭示gRPC RPC调用的内部机制。

gRPC是基于HTTP/2标准和proto协议开发的,gRPC的很多特性都依赖于HTTP/2标准提供。gRPC设计的四种模式是基于底层HTTP/2的流的概念。transport包是基于HTTP/2标准的实现,提供了流控等特性。

流控

transport提供基于connection和stream的两级流控。

-------------------------------------gRPC流控默认值----------------------------------------------
    defaultWindowSize = 65535 //64K
    initialWindowSize     = defaultWindowSize      // for an RPC
    initialConnWindowSize = defaultWindowSize * 16 // for a connection
-------------------------------------流控数据结构------------------------------------------------
type inFlow struct {
    //流控限制未处理的数据的数量
    limit uint32
    mu sync.Mutex
    //pendingData包含所有收到但未被应用消费的数据
    pendingData uint32
    //pendingUpdate包含被消费但为发送更新窗口的数量,减少窗口更新的频率
    pendingUpdate uint32
}
//真实的流控处理函数,server在接收到client的请求后会先
//检查pendingData+pendingUpdate是否超过limit限制
func (f *inFlow) onData(n uint32) error {
    f.mu.Lock()
    defer f.mu.Unlock()
    f.pendingData += n
    if f.pendingData+f.pendingUpdate > f.limit {
        return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", f.pendingData+f.pendingUpdate, f.limit)
    }
    return nil
}
//http2标准中规定:针对控制类的frame,为了确保能够得到高优先级的处理不做流控。DataFrame的流控处理在如下的函数中进行处理。
----------------------------------server端处理流------------------------------------------------
//server端handleData用于接收dataFrame
func (t *http2Server) handleData(f *http2.DataFrame) {
    size := len(f.Data())
    //针对connection的流控,如果client和server在该connection的负载大于16 * 64K,server会主动断开与client之间的连接。
    if err := t.fc.onData(uint32(size)); err != nil {
        //onData函数实现见流控的数据结构
        grpclog.Printf("transport: http2Server %v", err)
        //超过负载,直接关闭connection
        t.Close()
        return
    }
    // 选择正确的流进行处理
    s, ok := t.getStream(f)
    if !ok {
        if w := t.fc.onRead(uint32(size)); w > 0 {
          //更新流控窗口的大小
            t.controlBuf.put(&windowUpdate{0, w})
        }
        return
    }
    if size > 0 {
        s.mu.Lock()
        if s.state == streamDone {
            s.mu.Unlock()
            // stream已经被关闭,需要更新流控窗口
            if w := t.fc.onRead(uint32(size)); w > 0 {
                t.controlBuf.put(&windowUpdate{0, w})
            }
            return
        }
      //同一连接上的不同stream具有竞争关系,提供了strean级的流控
        if err := s.fc.onData(uint32(size)); err != nil {
            //onData()函数实现见流控数据结构
            s.mu.Unlock()
            //关闭超过流控限制的stream
            t.closeStream(s)
            //通知client再建立streamID相同的stream
            t.controlBuf.put(&resetStream{s.id, http2.ErrCodeFlowControl})
            return
        }
        s.mu.Unlock()
        data := make([]byte, size)
        copy(data, f.Data())
        s.write(recvMsg{data: data})
    }
    if f.Header().Flags.Has(http2.FlagDataEndStream) {
        s.mu.Lock()
        if s.state != streamDone {
            s.state = streamReadDone
        }
        s.mu.Unlock()
        s.write(recvMsg{err: io.EOF})
    }
}

RPC调用的执行过程

以unary模式的rpc调用为例分析一次RPC请求在gRPC中的流转过程,其他三种模式底层调用的函数与unary模式相同(四种模式从底层的HTTP/2分析都是stream,并且仍然是一套request和response的实现)。

: 以下源码分析部分均是以grpc/example/route_guide为例进行分析。对其他模式感兴趣的读者可自行分析。

unary模式的RPC请求在gRPC中的执行过程
------------------------------------------proto的声明-------------------------------------------
service RouteGuide {
  rpc GetFeature(Point) returns (Feature) {}
}
------------------------------------------pb.go源码---------------------------------------------
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) {
    out := new(Feature)
  // -->/routeguide.RouteGuide/GetFeature ->/package/server/method
    err := grpc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, c.cc, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}
//以下代码去掉错误处理和非关键函数的调用
//以下代码分析的是grpc client端如何发送request到server
-----------------------------------------grpc-client代码----------------------------------------
func invoke(ctx context.Context, method string, args, reply interface{}, cc *ClientConn, opts ...CallOption) (err error) {
    c := defaultCallInfo //构造rpc调用的defaultCallInfo并根据用户传入的信息进行填充
    topts := &transport.Options{
        Last:  true,
        Delay: false,
    }
    for {
        var (
            err    error
            t      transport.ClientTransport
            stream *transport.Stream
            put func()
        )
        //callHdr携带详细的RPC调用信息,如Method->/routeguide.RouteGuide/GetFeature
        callHdr := &transport.CallHdr{
            Host:   cc.authority,
            Method: method,
        }
        gopts := BalancerGetOptions{
            BlockingWait: !c.failFast,
        }
        t, put, err = cc.getTransport(ctx, gopts)
        if err != nil {
            if _, ok := err.(*rpcError); ok {
                return err
            }
            //非failFast情况下,err为以下两种情况会重试
            if err == errConnClosing || err == errConnUnavailable {
                if c.failFast {
                    return Errorf(codes.Unavailable, "%v", err)
                }
                continue
            }
            return Errorf(codes.Internal, "%v", err)
        }
        //将client请求信息发送,并等待server返回
        stream, err = sendRequest(ctx, cc.dopts.codec, cc.dopts.cp, callHdr, t, args, topts)
        if err != nil {
            if put != nil {
                put()
                put = nil
            }
            if _, ok := err.(transport.ConnectionError); ok || err == transport.ErrStreamDrain {
                if c.failFast {
                    return toRPCErr(err)
                }
                continue
            }
            return toRPCErr(err)
        }
        //在sendRequest创建的stream上等待server返回response
        err = recvResponse(cc.dopts, t, &c, stream, reply)
        if err != nil {
            if put != nil {
                put()
                put = nil
            }
            if _, ok := err.(transport.ConnectionError); ok || err == transport.ErrStreamDrain {
                if c.failFast {
                    return toRPCErr(err)
                }
                continue
            }
            return toRPCErr(err)
        }
        //关闭创建的stream
        t.CloseStream(stream, nil)
        if put != nil {
            put()
            put = nil
        }
        return Errorf(stream.StatusCode(), "%s", stream.StatusDesc())
    }
}
----------------------------------------------sendRequest()说明--------------------------------
func sendRequest(ctx context.Context, codec Codec, compressor Compressor, callHdr *transport.CallHdr, t transport.ClientTransport, args interface{}, opts *transport.Options) (_ *transport.Stream, err error) {  
     //根据callHdr中包含的host和method信息创建对应的stream
    //函数具体实现-transport/http2_client.go/http2Client.NewStream()
    stream, err := t.NewStream(ctx, callHdr)
    //序列化消息并定义消息头
    //消息头=5yte=1byte(msg是否压缩) + 4byte(msg长度)
    //函数具体实现-rpc_util.go
    outBuf, err := encode(codec, args, compressor, cbuf)
    //将outBuf按照http2帧的大小分帧并发送到对端,下面会对该函数具体分析
    err = t.Write(stream, outBuf, opts)
    //发送成功,返回该stream,用于接收response
    return stream, nil
}
------------------------------------ClientTransport.Write()说明---------------------------------
//真正将message分帧在指定的stream上传输的函数如下,将对该函数进行详细分析
func (t *http2Client) Write(s *Stream, data []byte, opts *Options) error {
    r := bytes.NewBuffer(data)
    for {
        var p []byte
        if r.Len() > 0 {
            size := http2MaxFrameLen
            s.sendQuotaPool.add(0)
            // 等待stream的流控上有配额发送数据,stream.sendQuotaPool=65535
            sq, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, s.sendQuotaPool.acquire())
            if err != nil {
                return err
            }
            t.sendQuotaPool.add(0)
            // 等待connection的流控有配额去发送数据,t.sendQuotaPool= 65535 * 16
            tq, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, t.sendQuotaPool.acquire())
            if err != nil {
                if _, ok := err.(StreamError); ok || err == io.EOF {
                    t.sendQuotaPool.cancel()
                }
                return err
            }
            if sq < size {
                size = sq
            }
            if tq < size {
                size = tq
            }
            p = r.Next(size)
            ps := len(p)
            if ps < sq {
                // 返回stream预留超额的配额数量
                s.sendQuotaPool.add(sq - ps)
            }
            if ps < tq {
                // 返回connection预留超额的配额数量
                t.sendQuotaPool.add(tq - ps)
            }
        }
        var (
            endStream  bool
            forceFlush bool
        )
        //判断是否为最后一帧l
        if opts.Last && r.Len() == 0 {
            endStream = true
        }
        // 表明这将有一个writer将要去写data frame
        t.framer.adjustNumWriters(1)
        // 释放t.writableChan上加的锁,获得在该transport上写的权利,确保只有一个调用者可以调用t.framer.writeData()函数。
        if _, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, t.writableChan); err != nil {
            if _, ok := err.(StreamError); ok || err == io.EOF {
                // 释放connection上预留的配额数量
                t.sendQuotaPool.add(len(p))
            }
            if t.framer.adjustNumWriters(-1) == 0 {
                // 如果该Writer是这一批的最后一个有责任去刷新http2.frames的缓存区
                //将刷新的请求排入一个队列而不是直接刷新合一避免和其他的Writer或者刷新请求的竞争
                t.controlBuf.put(&flushIO{})
            }
            return err
        }
        select {
        case <-s.ctx.Done():
            t.sendQuotaPool.add(len(p))
            if t.framer.adjustNumWriters(-1) == 0 {
                t.controlBuf.put(&flushIO{})
            }
           //再次为该transport加锁
            t.writableChan <- 0
            return ContextErr(s.ctx.Err())
        default:
        }
        if r.Len() == 0 && t.framer.adjustNumWriters(0) == 1 {
            // 强制刷新因为这是grpc message的最后一个数据帧
            //对于调用者来说此刻仅仅只有一个writer
            forceFlush = true
        }
        //如果t.framer.writeData失败,所有等待处理的stream将会在http2Clinet.Close()函数中进行处理,此处不必显示调用CloseStream()
      //writeData()不会并发被调用,确保server端收到的frame不会乱序(不会出现dataframe早于headerframe先到)
        if err := t.framer.writeData(forceFlush, s.id, endStream, p); err != nil {
          //writeData()增加二进制帧的头部,函数实现-net/http2/frame.go
            t.notifyError(err)
            return connectionErrorf(true, err, "transport: %v", err)
        }
        if t.framer.adjustNumWriters(-1) == 0 {
            t.framer.flushWrite()
        }
      //再次为该transport加锁
        t.writableChan <- 0
        if r.Len() == 0 {
            break
        }
    }
    if !opts.Last {
        return nil
    }
    s.mu.Lock()
    if s.state != streamDone {
      //更新stream的状态
        s.state = streamWriteDone
    }
    s.mu.Unlock()
    return nil
}
//以下代码是分析grpc-server接收client的请求后内部的处理流程
---------------------------------------grpc-server代码------------------------------------------
//serve函数在net.Listener接收客户端的连接,创建一个新的ServerTransport和service goroutine为每个连接,服务goroutine读取gRPC请求,然后调用server中注册的函数。
func (s *Server) Serve(lis net.Listener) error {

    s.lis[lis] = true

    for {
        rawConn, err := lis.Accept()
        if err != nil {
            s.mu.Lock()
            s.printf("done serving; Accept = %v", err)
            s.mu.Unlock()
            return err
        }
        //开始一个单独的goroutine处理client的连接-rawConn
        //继续for循环等待其他client的到来
        go s.handleRawConn(rawConn)
    }
}
//handleRawConn运行在独立的goroutine,并且处理已经接收连接但未执行任何I/O操作的连接
func (s *Server) handleRawConn(rawConn net.Conn) {
    conn, authInfo, err := s.useTransportAuthenticator(rawConn)
    if err != credentials.ErrConnDispatched {
            rawConn.Close()
        }
        return
    }
    if s.opts.useHandlerImpl {
        s.serveUsingHandler(conn)
    } else {
        s.serveNewHTTP2Transport(conn, authInfo)
    }
}
//serveNewHTTP2Transport建立一个新的HTTP/2 tranport并且为在该transport上的流提供服务
func (s *Server) serveNewHTTP2Transport(c net.Conn, authInfo credentials.AuthInfo) {
    //调用transport/http2_server.go
    st, err := transport.NewServerTransport("http2", c, 2, authInfo)
    if !s.addConn(st) {
        st.Close()
        return
    }
   //在transport上接收client发送stream并进行处理的函数
    s.serveStreams(st)
}

func (s *Server) serveStreams(st transport.ServerTransport) {
    defer s.removeConn(st)
    defer st.Close()
    var wg sync.WaitGroup
   //transport.ServerTranport下的st.HandleStreams处理client发送的stream
    st.HandleStreams(func(stream *transport.Stream) {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s.handleStream(st, stream, s.traceInfo(st, stream))
        }()
    })
    wg.Wait()
}
----------------------------transport/http2Server.HanleStreams()分析----------------------------
func (t *http2Server) HandleStreams(handle func(*Stream)) {
    // 检查client 发送的preface是否合法
    preface := make([]byte, len(clientPreface))
    if _, err := io.ReadFull(t.conn, preface); err != nil {
        grpclog.Printf("transport: http2Server.HandleStreams failed to receive the preface from client: %v", err)
        t.Close()
        return
    }
    if !bytes.Equal(preface, clientPreface) {
        grpclog.Printf("transport: http2Server.HandleStreams received bogus greeting from client: %q", preface)
        t.Close()
        return
    }

    frame, err := t.framer.readFrame()
    if err == io.EOF || err == io.ErrUnexpectedEOF {
        t.Close()
        return
    }
    if err != nil {
        grpclog.Printf("transport: http2Server.HandleStreams failed to read frame: %v", err)
        t.Close()
        return
    }
    //读取client发送的SettingFrame
    sf, ok := frame.(*http2.SettingsFrame)
    if !ok {
        grpclog.Printf("transport: http2Server.HandleStreams saw invalid preface type %T from client", frame)
        t.Close()
        return
    }
    //根据SettingFrame的内容进行设置
    t.handleSettings(sf)
    //读取client发送的request内容
    for {
        frame, err := t.framer.readFrame()
        if err != nil {
            if se, ok := err.(http2.StreamError); ok {
                t.mu.Lock()
                s := t.activeStreams[se.StreamID]
                t.mu.Unlock()
                if s != nil {
                    t.closeStream(s)
                }
                t.controlBuf.put(&resetStream{se.StreamID, se.Code})
                continue
            }
            if err == io.EOF || err == io.ErrUnexpectedEOF {
                t.Close()
                return
            }
            grpclog.Printf("transport: http2Server.HandleStreams failed to read frame: %v", err)
            t.Close()
            return
        }
        switch frame := frame.(type) {
        case *http2.MetaHeadersFrame:
            //t.operateHeaders函数解码headers内容,并将传输该frame的stream进行记录
            //函数实现包括根据stream携带的callHdr信息,如何路由到grpc.Server中注册server具体实现method的过程
            //函数实现-transport/http2_server.go operateHeader()函数
            if t.operateHeaders(frame, handle) {
                t.Close()
                break
            }
        case *http2.DataFrame:
            t.handleData(frame)
        case *http2.RSTStreamFrame:
            t.handleRSTStream(frame)
        case *http2.SettingsFrame:
            t.handleSettings(frame)
        case *http2.PingFrame:
            t.handlePing(frame)
        case *http2.WindowUpdateFrame:
            t.handleWindowUpdate(frame)
        case *http2.GoAwayFrame:
        default:
            grpclog.Printf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame)
        }
    }
}

func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream)) (close bool) {
    buf := newRecvBuffer()
    //保存client传输的stream信息
    s := &Stream{
        id:  frame.Header().StreamID,
        st:  t,
        buf: buf,
        fc:  &inFlow{limit: initialWindowSize},
    }

    var state decodeState
    for _, hf := range frame.Fields {
        state.processHeaderField(hf)
    }
    if err := state.err; err != nil {
        if se, ok := err.(StreamError); ok {
            t.controlBuf.put(&resetStream{s.id, statusCodeConvTab[se.Code]})
        }
        return
    }

    if frame.StreamEnded() {
        s.state = streamReadDone
    }
    s.recvCompress = state.encoding
    if state.timeoutSet {
        s.ctx, s.cancel = context.WithTimeout(context.TODO(), state.timeout)
    } else {
        s.ctx, s.cancel = context.WithCancel(context.TODO())
    }

    if uint32(len(t.activeStreams)) >= t.maxStreams {
        t.mu.Unlock()
        t.controlBuf.put(&resetStream{s.id, http2.ErrCodeRefusedStream})
        return
    }
    //对stream的合法性进行检查
    if s.id%2 != 1 || s.id <= t.maxStreamID {
        t.mu.Unlock()
        grpclog.Println("transport: http2Server.HandleStreams received an illegal stream id: ", s.id)
        return true
    }
    t.maxStreamID = s.id
    s.sendQuotaPool = newQuotaPool(int(t.streamSendQuota))
    t.activeStreams[s.id] = s
    t.mu.Unlock()
    s.windowHandler = func(n int) {
        t.updateWindow(s, uint32(n))
    }
    //调用server.go serveStreams()传入的handle去处理server端接收的stream
    //handle()会调用server.go handleStream()路由到server端真正实现的函数
    handle(s)
    return
}
//handleData处理server端接收到数据帧
func (t *http2Server) handleData(f *http2.DataFrame) {
    size := len(f.Data())
    //检查transport的流控
    if err := t.fc.onData(uint32(size)); err != nil {
        grpclog.Printf("transport: http2Server %v", err)
        t.Close()
        return
    }
    s, ok := t.getStream(f)
    if !ok {
        if w := t.fc.onRead(uint32(size)); w > 0 {
            t.controlBuf.put(&windowUpdate{0, w})
        }
        return
    }
    if size > 0 {
        s.mu.Lock()
        if s.state == streamDone {
            s.mu.Unlock()
            //检查stream的流控
            if w := t.fc.onRead(uint32(size)); w > 0 {
                t.controlBuf.put(&windowUpdate{0, w})
            }
            return
        }
        if err := s.fc.onData(uint32(size)); err != nil {
            s.mu.Unlock()
            t.closeStream(s)
            t.controlBuf.put(&resetStream{s.id, http2.ErrCodeFlowControl})
            return
        }
        s.mu.Unlock()
        data := make([]byte, size)
        copy(data, f.Data())
        s.write(recvMsg{data: data})
    }
    if f.Header().Flags.Has(http2.FlagDataEndStream) {
        s.mu.Lock()
        if s.state != streamDone {
            s.state = streamReadDone
        }
        s.mu.Unlock()
        s.write(recvMsg{err: io.EOF})
    }
}

以上源码分析一次gRPC调用,从client端如何发送请求到grpc.server端如何路由到server端注册函数的所有过程。

问题总结:

1.grpc的http/2的stream流是如何变化的?

答:unary模式的stream的创建、删除都是由gRPC控制的,剩下的三种模式是将stream的很多操作暴露给用户层,由用户自行控制,但sendRequset和recvResponse的流程和unary模式处理相同。笔者测试发现grpc用到的都是client端的stream,server端的stream在gRPC中并未使用。client端发起的stream都是基数开始的,并且最大值为2^31-1,如果client的streamID超过限制,server端会断开与client的连接。测试结果如下:

shell
//2^31的最大取值2147483648
client stream id 2147483649
2017/08/04 10:44:17 transport: http2Client.notifyError got notified that the client transport was broken invalid stream ID.
2017/08/04 10:44:17 &{0xc4201787e0}.RouteChat(_) = _, rpc error: code = 13 desc = transport: invalid stream ID
exit status 1

D:\application\JDK\bin\java.exe -ea -Djacoco-agent.destfile=D:\workspace\CCC00910_MspAI_GCH\target/jacoco.exec -Didea.test.cyclic.buffer.size=1048576 -javaagent:C:\Users\itw00668\AppData\Roaming\JetBrains\IdeaIC2024.2\plugins\mysql-log-plugin\lib\mysql-log-agent-1.1.jar -Dmysql-log.projectId=9bad6d48 "-javaagent:D:\application\Idea\Idea2024\IntelliJ IDEA Community Edition 2024.2.3\lib\idea_rt.jar=59275:D:\application\Idea\Idea2024\IntelliJ IDEA Community Edition 2024.2.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Users\itw00668\.m2\repository\org\junit\platform\junit-platform-launcher\1.8.2\junit-platform-launcher-1.8.2.jar;C:\Users\itw00668\.m2\repository\org\junit\platform\junit-platform-engine\1.8.2\junit-platform-engine-1.8.2.jar;C:\Users\itw00668\.m2\repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;C:\Users\itw00668\.m2\repository\org\junit\platform\junit-platform-commons\1.8.2\junit-platform-commons-1.8.2.jar;C:\Users\itw00668\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;D:\application\Idea\Idea2024\IntelliJ IDEA Community Edition 2024.2.3\lib\idea_rt.jar;D:\application\Idea\Idea2024\IntelliJ IDEA Community Edition 2024.2.3\plugins\junit\lib\junit5-rt.jar;D:\application\Idea\Idea2024\IntelliJ IDEA Community Edition 2024.2.3\plugins\junit\lib\junit-rt.jar;D:\application\JDK\jre\lib\charsets.jar;D:\application\JDK\jre\lib\deploy.jar;D:\application\JDK\jre\lib\ext\access-bridge-64.jar;D:\application\JDK\jre\lib\ext\cldrdata.jar;D:\application\JDK\jre\lib\ext\dnsns.jar;D:\application\JDK\jre\lib\ext\jaccess.jar;D:\application\JDK\jre\lib\ext\jfxrt.jar;D:\application\JDK\jre\lib\ext\localedata.jar;D:\application\JDK\jre\lib\ext\nashorn.jar;D:\application\JDK\jre\lib\ext\sunec.jar;D:\application\JDK\jre\lib\ext\sunjce_provider.jar;D:\application\JDK\jre\lib\ext\sunmscapi.jar;D:\application\JDK\jre\lib\ext\sunpkcs11.jar;D:\application\JDK\jre\lib\ext\zipfs.jar;D:\application\JDK\jre\lib\javaws.jar;D:\application\JDK\jre\lib\jce.jar;D:\application\JDK\jre\lib\jfr.jar;D:\application\JDK\jre\lib\jfxswt.jar;D:\application\JDK\jre\lib\jsse.jar;D:\application\JDK\jre\lib\management-agent.jar;D:\application\JDK\jre\lib\plugin.jar;D:\application\JDK\jre\lib\resources.jar;D:\application\JDK\jre\lib\rt.jar;D:\workspace\CCC00910_MspAI_GCH\target\test-classes;D:\workspace\CCC00910_MspAI_GCH\target\classes;D:\application\Maven\repository\ZA21\bee-starter-logging\5.10.21\bee-starter-logging-5.10.21.jar;D:\application\Maven\repository\ZA21\bee-commons\5.10.21\bee-commons-5.10.21.jar;D:\application\Maven\repository\com\fasterxml\jackson\core\jackson-databind\2.13.3\jackson-databind-2.13.3.jar;D:\application\Maven\repository\com\fasterxml\jackson\core\jackson-annotations\2.13.3\jackson-annotations-2.13.3.jar;D:\application\Maven\repository\com\fasterxml\jackson\core\jackson-core\2.13.3\jackson-core-2.13.3.jar;D:\application\Maven\repository\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\application\Maven\repository\ZA21\bee-statement\5.10.21\bee-statement-5.10.21.jar;D:\application\Maven\repository\ZA21\bee-encryption\1.0.0\bee-encryption-1.0.0.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-log4j2\2.7.2\spring-boot-starter-log4j2-2.7.2.jar;D:\application\Maven\repository\org\apache\logging\log4j\log4j-slf4j-impl\2.17.2\log4j-slf4j-impl-2.17.2.jar;D:\application\Maven\repository\org\apache\logging\log4j\log4j-api\2.17.2\log4j-api-2.17.2.jar;D:\application\Maven\repository\org\apache\logging\log4j\log4j-core\2.17.2\log4j-core-2.17.2.jar;D:\application\Maven\repository\org\apache\logging\log4j\log4j-jul\2.17.2\log4j-jul-2.17.2.jar;D:\application\Maven\repository\org\slf4j\jul-to-slf4j\1.7.36\jul-to-slf4j-1.7.36.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-configuration-processor\2.7.2\spring-boot-configuration-processor-2.7.2.jar;D:\application\Maven\repository\com\auth0\java-jwt\3.18.2\java-jwt-3.18.2.jar;D:\application\Maven\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.13.3\jackson-datatype-jsr310-2.13.3.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter\2.7.2\spring-boot-starter-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot\2.7.2\spring-boot-2.7.2.jar;D:\application\Maven\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;D:\application\Maven\repository\org\yaml\snakeyaml\2.0\snakeyaml-2.0.jar;D:\application\Maven\repository\LT32\logsdk\2.5.5\logsdk-2.5.5.jar;D:\application\Maven\repository\org\springframework\spring-web\5.3.22\spring-web-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-beans\5.3.22\spring-beans-5.3.22.jar;D:\application\Maven\repository\io\opentracing\opentracing-api\0.33.0\opentracing-api-0.33.0.jar;D:\application\Maven\repository\io\opentracing\contrib\opentracing-spring-web\4.1.0\opentracing-spring-web-4.1.0.jar;D:\application\Maven\repository\io\opentracing\contrib\opentracing-web-servlet-filter\0.4.0\opentracing-web-servlet-filter-0.4.0.jar;D:\application\Maven\repository\io\opentracing\opentracing-util\0.33.0\opentracing-util-0.33.0.jar;D:\application\Maven\repository\io\opentracing\opentracing-noop\0.33.0\opentracing-noop-0.33.0.jar;D:\application\Maven\repository\io\jaegertracing\jaeger-client\1.8.1\jaeger-client-1.8.1.jar;D:\application\Maven\repository\io\jaegertracing\jaeger-core\1.8.1\jaeger-core-1.8.1.jar;D:\application\Maven\repository\io\jaegertracing\jaeger-tracerresolver\1.8.1\jaeger-tracerresolver-1.8.1.jar;D:\application\Maven\repository\io\opentracing\contrib\opentracing-tracerresolver\0.1.8\opentracing-tracerresolver-0.1.8.jar;D:\application\Maven\repository\org\aspectj\aspectjweaver\1.9.7\aspectjweaver-1.9.7.jar;D:\application\Maven\repository\ZA21\bee-starter-tracer\5.10.21\bee-starter-tracer-5.10.21.jar;D:\application\Maven\repository\LT31\09\unique-sql-hashid\9.0.0\unique-sql-hashid-9.0.0.jar;D:\application\Maven\repository\org\apache\commons\commons-lang3\3.12.0\commons-lang3-3.12.0.jar;D:\application\Maven\repository\LT31\09\shindata-druid\1.1.15\shindata-druid-1.1.15.jar;D:\application\Maven\repository\commons-io\commons-io\2.11.0\commons-io-2.11.0.jar;D:\application\Maven\repository\ZA21\bee-starter-management\5.10.21\bee-starter-management-5.10.21.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-actuator-autoconfigure\2.7.2\spring-boot-actuator-autoconfigure-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-actuator\2.7.2\spring-boot-actuator-2.7.2.jar;D:\application\Maven\repository\ZA21\bee-starter-metrics\5.10.21\bee-starter-metrics-5.10.21.jar;D:\application\Maven\repository\io\opentelemetry\opentelemetry-api-metrics\1.9.1-alpha\opentelemetry-api-metrics-1.9.1-alpha.jar;D:\application\Maven\repository\io\opentelemetry\opentelemetry-api\1.9.1\opentelemetry-api-1.9.1.jar;D:\application\Maven\repository\io\opentelemetry\opentelemetry-context\1.9.1\opentelemetry-context-1.9.1.jar;D:\application\Maven\repository\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-web\2.7.2\spring-boot-starter-web-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-json\2.7.2\spring-boot-starter-json-2.7.2.jar;D:\application\Maven\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.13.3\jackson-datatype-jdk8-2.13.3.jar;D:\application\Maven\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.13.3\jackson-module-parameter-names-2.13.3.jar;D:\application\Maven\repository\org\springframework\spring-webmvc\5.3.22\spring-webmvc-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-aop\5.3.22\spring-aop-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-context\5.3.22\spring-context-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-expression\5.3.22\spring-expression-5.3.22.jar;D:\application\Maven\repository\com\cvicse\embedded\spring-boot-starter-inforsuite\10.0.2.5-CMB01\spring-boot-starter-inforsuite-10.0.2.5-CMB01.jar;D:\application\Maven\repository\org\springframework\cloud\spring-cloud-starter-openfeign\3.1.1\spring-cloud-starter-openfeign-3.1.1.jar;D:\application\Maven\repository\org\springframework\cloud\spring-cloud-starter\3.1.1\spring-cloud-starter-3.1.1.jar;D:\application\Maven\repository\org\springframework\cloud\spring-cloud-context\3.1.1\spring-cloud-context-3.1.1.jar;D:\application\Maven\repository\org\springframework\security\spring-security-rsa\1.0.10.RELEASE\spring-security-rsa-1.0.10.RELEASE.jar;D:\application\Maven\repository\org\springframework\cloud\spring-cloud-openfeign-core\3.1.1\spring-cloud-openfeign-core-3.1.1.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-aop\2.7.2\spring-boot-starter-aop-2.7.2.jar;D:\application\Maven\repository\io\github\openfeign\form\feign-form-spring\3.8.0\feign-form-spring-3.8.0.jar;D:\application\Maven\repository\io\github\openfeign\form\feign-form\3.8.0\feign-form-3.8.0.jar;D:\application\Maven\repository\commons-fileupload\commons-fileupload\1.4\commons-fileupload-1.4.jar;D:\application\Maven\repository\org\springframework\cloud\spring-cloud-commons\3.1.1\spring-cloud-commons-3.1.1.jar;D:\application\Maven\repository\org\springframework\security\spring-security-crypto\5.7.2\spring-security-crypto-5.7.2.jar;D:\application\Maven\repository\io\github\openfeign\feign-core\11.8\feign-core-11.8.jar;D:\application\Maven\repository\io\github\openfeign\feign-slf4j\11.8\feign-slf4j-11.8.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-webflux\2.7.2\spring-boot-starter-webflux-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-reactor-netty\2.7.2\spring-boot-starter-reactor-netty-2.7.2.jar;D:\application\Maven\repository\io\projectreactor\netty\reactor-netty-http\1.0.21\reactor-netty-http-1.0.21.jar;D:\application\Maven\repository\io\netty\netty-codec-http\4.1.79.Final\netty-codec-http-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-common\4.1.79.Final\netty-common-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-buffer\4.1.79.Final\netty-buffer-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-transport\4.1.79.Final\netty-transport-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-codec\4.1.79.Final\netty-codec-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-handler\4.1.79.Final\netty-handler-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-codec-http2\4.1.79.Final\netty-codec-http2-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-resolver-dns\4.1.79.Final\netty-resolver-dns-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-resolver\4.1.79.Final\netty-resolver-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-codec-dns\4.1.79.Final\netty-codec-dns-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-resolver-dns-native-macos\4.1.79.Final\netty-resolver-dns-native-macos-4.1.79.Final-osx-x86_64.jar;D:\application\Maven\repository\io\netty\netty-resolver-dns-classes-macos\4.1.79.Final\netty-resolver-dns-classes-macos-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-transport-native-epoll\4.1.79.Final\netty-transport-native-epoll-4.1.79.Final-linux-x86_64.jar;D:\application\Maven\repository\io\netty\netty-transport-native-unix-common\4.1.79.Final\netty-transport-native-unix-common-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-transport-classes-epoll\4.1.79.Final\netty-transport-classes-epoll-4.1.79.Final.jar;D:\application\Maven\repository\io\projectreactor\netty\reactor-netty-core\1.0.21\reactor-netty-core-1.0.21.jar;D:\application\Maven\repository\io\netty\netty-handler-proxy\4.1.79.Final\netty-handler-proxy-4.1.79.Final.jar;D:\application\Maven\repository\io\netty\netty-codec-socks\4.1.79.Final\netty-codec-socks-4.1.79.Final.jar;D:\application\Maven\repository\org\springframework\spring-webflux\5.3.22\spring-webflux-5.3.22.jar;D:\application\Maven\repository\io\projectreactor\reactor-core\3.4.21\reactor-core-3.4.21.jar;D:\application\Maven\repository\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;D:\application\Maven\repository\ZA21\bee-starter-web\5.10.21\bee-starter-web-5.10.21.jar;D:\application\Maven\repository\ZA21\bee-starter-healthcheck\5.10.21\bee-starter-healthcheck-5.10.21.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-validation\2.7.2\spring-boot-starter-validation-2.7.2.jar;D:\application\Maven\repository\org\apache\tomcat\embed\tomcat-embed-el\9.0.82\tomcat-embed-el-9.0.82.jar;D:\application\Maven\repository\org\hibernate\validator\hibernate-validator\6.2.3.Final\hibernate-validator-6.2.3.Final.jar;D:\application\Maven\repository\jakarta\validation\jakarta.validation-api\2.0.2\jakarta.validation-api-2.0.2.jar;D:\application\Maven\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\application\Maven\repository\com\fasterxml\classmate\1.5.1\classmate-1.5.1.jar;D:\application\Maven\repository\H0010038\04\config-client-integration-springboot\1.0.0-SNAPSHOT\config-client-integration-springboot-1.0.0-20250626.033408-18.jar;D:\application\Maven\repository\H0010038\04\config-client\1.0.0-SNAPSHOT\config-client-1.0.0-20250626.033408-19.jar;D:\application\Maven\repository\H0010038\04\infra-utils-spi\1.1.0-SNAPSHOT\infra-utils-spi-1.1.0-20240301.071119-6.jar;D:\application\Maven\repository\H0010038\04\config-client-grpc\1.0.0-SNAPSHOT\config-client-grpc-1.0.0-20250626.033408-19.jar;D:\application\Maven\repository\H0010038\04\grpc-client-notify\1.0.0-SNAPSHOT\grpc-client-notify-1.0.0-20250609.084419-19.jar;D:\application\Maven\repository\H0010038\04\grpc-client-lib\1.0.0-SNAPSHOT\grpc-client-lib-1.0.0-20250609.084419-19.jar;D:\application\Maven\repository\com\google\protobuf\protobuf-java\3.19.4\protobuf-java-3.19.4.jar;D:\application\Maven\repository\io\grpc\grpc-protobuf\1.51.0\grpc-protobuf-1.51.0.jar;D:\application\Maven\repository\com\google\api\grpc\proto-google-common-protos\2.9.0\proto-google-common-protos-2.9.0.jar;D:\application\Maven\repository\io\grpc\grpc-protobuf-lite\1.51.0\grpc-protobuf-lite-1.51.0.jar;D:\application\Maven\repository\com\google\guava\guava\20.0\guava-20.0.jar;D:\application\Maven\repository\io\grpc\grpc-stub\1.51.0\grpc-stub-1.51.0.jar;D:\application\Maven\repository\io\grpc\grpc-netty-shaded\1.30.2\grpc-netty-shaded-1.30.2.jar;D:\application\Maven\repository\io\grpc\grpc-core\1.30.2\grpc-core-1.30.2.jar;D:\application\Maven\repository\io\grpc\grpc-api\1.30.2\grpc-api-1.30.2.jar;D:\application\Maven\repository\com\google\android\annotations\4.1.1.4\annotations-4.1.1.4.jar;D:\application\Maven\repository\org\codehaus\mojo\animal-sniffer-annotations\1.18\animal-sniffer-annotations-1.18.jar;D:\application\Maven\repository\io\perfmark\perfmark-api\0.19.0\perfmark-api-0.19.0.jar;D:\application\Maven\repository\io\grpc\grpc-context\1.30.2\grpc-context-1.30.2.jar;D:\application\Maven\repository\H0010038\04\notifier-client\1.1.6-SNAPSHOT\notifier-client-1.1.6-20241011.023044-1.jar;D:\application\Maven\repository\org\apache\httpcomponents\httpmime\4.5.13\httpmime-4.5.13.jar;D:\application\Maven\repository\org\apache\curator\curator-recipes\5.1.0\curator-recipes-5.1.0.jar;D:\application\Maven\repository\org\apache\curator\curator-framework\5.1.0\curator-framework-5.1.0.jar;D:\application\Maven\repository\org\apache\curator\curator-client\5.1.0\curator-client-5.1.0.jar;D:\application\Maven\repository\org\apache\zookeeper\zookeeper\3.6.0\zookeeper-3.6.0.jar;D:\application\Maven\repository\commons-lang\commons-lang\2.6\commons-lang-2.6.jar;D:\application\Maven\repository\org\apache\zookeeper\zookeeper-jute\3.6.0\zookeeper-jute-3.6.0.jar;D:\application\Maven\repository\org\apache\yetus\audience-annotations\0.5.0\audience-annotations-0.5.0.jar;D:\application\Maven\repository\io\netty\netty-transport-native-epoll\4.1.79.Final\netty-transport-native-epoll-4.1.79.Final.jar;D:\application\Maven\repository\commons-codec\commons-codec\1.15\commons-codec-1.15.jar;D:\application\Maven\repository\com\google\code\gson\gson\2.9.0\gson-2.9.0.jar;D:\application\Maven\repository\ZA21\bee-starter-persistence\5.10.21\bee-starter-persistence-5.10.21.jar;D:\application\Maven\repository\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.2.2\mybatis-spring-boot-starter-2.2.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-jdbc\2.7.2\spring-boot-starter-jdbc-2.7.2.jar;D:\application\Maven\repository\org\springframework\spring-jdbc\5.3.22\spring-jdbc-5.3.22.jar;D:\application\Maven\repository\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.2.2\mybatis-spring-boot-autoconfigure-2.2.2.jar;D:\application\Maven\repository\org\mybatis\mybatis\3.5.9\mybatis-3.5.9.jar;D:\application\Maven\repository\org\mybatis\mybatis-spring\2.0.7\mybatis-spring-2.0.7.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-autoconfigure\2.7.2\spring-boot-autoconfigure-2.7.2.jar;D:\application\Maven\repository\org\apache\tomcat\tomcat-jdbc\9.0.82\tomcat-jdbc-9.0.82.jar;D:\application\Maven\repository\org\apache\tomcat\tomcat-juli\9.0.82\tomcat-juli-9.0.82.jar;D:\application\Maven\repository\LT99\09\tdsql-mysql-connector-java8\1.5.0\tdsql-mysql-connector-java8-1.5.0.jar;D:\application\Maven\repository\ZA21\bee-starter-redis\5.10.21\bee-starter-redis-5.10.21.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-data-redis\2.7.2\spring-boot-starter-data-redis-2.7.2.jar;D:\application\Maven\repository\org\springframework\data\spring-data-redis\2.7.2\spring-data-redis-2.7.2.jar;D:\application\Maven\repository\org\springframework\data\spring-data-keyvalue\2.7.2\spring-data-keyvalue-2.7.2.jar;D:\application\Maven\repository\org\springframework\data\spring-data-commons\2.7.2\spring-data-commons-2.7.2.jar;D:\application\Maven\repository\org\springframework\spring-tx\5.3.22\spring-tx-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-oxm\5.3.22\spring-oxm-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-context-support\5.3.22\spring-context-support-5.3.22.jar;D:\application\Maven\repository\redis\clients\jedis\3.10.0\jedis-3.10.0.jar;D:\application\Maven\repository\org\apache\commons\commons-pool2\2.11.1\commons-pool2-2.11.1.jar;D:\application\Maven\repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;D:\application\Maven\repository\org\apache\commons\commons-collections4\4.4\commons-collections4-4.4.jar;D:\application\Maven\repository\org\apache\commons\commons-csv\1.10.0\commons-csv-1.10.0.jar;D:\application\Maven\repository\com\alibaba\transmittable-thread-local\2.11.4\transmittable-thread-local-2.11.4.jar;D:\application\Maven\repository\com\github\ben-manes\caffeine\caffeine\2.8.5\caffeine-2.8.5.jar;D:\application\Maven\repository\org\checkerframework\checker-qual\3.4.1\checker-qual-3.4.1.jar;D:\application\Maven\repository\com\google\errorprone\error_prone_annotations\2.4.0\error_prone_annotations-2.4.0.jar;D:\application\Maven\repository\org\apache\httpcomponents\httpclient\4.5.13\httpclient-4.5.13.jar;D:\application\Maven\repository\org\apache\httpcomponents\httpcore\4.4.15\httpcore-4.4.15.jar;D:\application\Maven\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;D:\application\Maven\repository\H0010038\04\ccc-metric-dependency\1.0.9-SNAPSHOT\ccc-metric-dependency-1.0.9-20250220.060300-3.jar;D:\application\Maven\repository\org\slf4j\slf4j-api\1.7.36\slf4j-api-1.7.36.jar;D:\application\Maven\repository\H0010038\04\security-protocol\1.2.0-SNAPSHOT\security-protocol-1.2.0-20250311.063350-1.jar;D:\application\Maven\repository\H0010038\04\security-gct\1.2.0-RELEASE\security-gct-1.2.0-RELEASE.jar;D:\application\Maven\repository\org\bouncycastle\bcprov-jdk15on\1.60\bcprov-jdk15on-1.60.jar;D:\application\Maven\repository\org\bouncycastle\bcpkix-jdk15on\1.60\bcpkix-jdk15on-1.60.jar;D:\application\Maven\repository\ZA33\PCJava\1.17.20220616\PCJava-1.17.20220616.jar;D:\application\Maven\repository\com\payneteasy\ber-tlv\1.0-11\ber-tlv-1.0-11.jar;D:\application\Maven\repository\com\github\jnr\jnr-ffi\2.0.9\jnr-ffi-2.0.9.jar;D:\application\Maven\repository\com\github\jnr\jffi\1.2.11\jffi-1.2.11.jar;D:\application\Maven\repository\com\github\jnr\jffi\1.2.11\jffi-1.2.11-native.jar;D:\application\Maven\repository\org\ow2\asm\asm\5.0.3\asm-5.0.3.jar;D:\application\Maven\repository\org\ow2\asm\asm-commons\5.0.3\asm-commons-5.0.3.jar;D:\application\Maven\repository\org\ow2\asm\asm-analysis\5.0.3\asm-analysis-5.0.3.jar;D:\application\Maven\repository\org\ow2\asm\asm-tree\5.0.3\asm-tree-5.0.3.jar;D:\application\Maven\repository\org\ow2\asm\asm-util\5.0.3\asm-util-5.0.3.jar;D:\application\Maven\repository\com\github\jnr\jnr-x86asm\1.0.2\jnr-x86asm-1.0.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-starter-test\2.7.2\spring-boot-starter-test-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-test\2.7.2\spring-boot-test-2.7.2.jar;D:\application\Maven\repository\org\springframework\boot\spring-boot-test-autoconfigure\2.7.2\spring-boot-test-autoconfigure-2.7.2.jar;D:\application\Maven\repository\com\jayway\jsonpath\json-path\2.7.0\json-path-2.7.0.jar;D:\application\Maven\repository\net\minidev\json-smart\2.4.8\json-smart-2.4.8.jar;D:\application\Maven\repository\net\minidev\accessors-smart\2.4.8\accessors-smart-2.4.8.jar;D:\application\Maven\repository\jakarta\xml\bind\jakarta.xml.bind-api\2.3.3\jakarta.xml.bind-api-2.3.3.jar;D:\application\Maven\repository\jakarta\activation\jakarta.activation-api\1.2.2\jakarta.activation-api-1.2.2.jar;D:\application\Maven\repository\org\assertj\assertj-core\3.22.0\assertj-core-3.22.0.jar;D:\application\Maven\repository\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar;D:\application\Maven\repository\org\junit\jupiter\junit-jupiter\5.8.2\junit-jupiter-5.8.2.jar;D:\application\Maven\repository\org\junit\jupiter\junit-jupiter-api\5.8.2\junit-jupiter-api-5.8.2.jar;D:\application\Maven\repository\org\junit\jupiter\junit-jupiter-params\5.8.2\junit-jupiter-params-5.8.2.jar;D:\application\Maven\repository\org\junit\jupiter\junit-jupiter-engine\5.8.2\junit-jupiter-engine-5.8.2.jar;D:\application\Maven\repository\org\mockito\mockito-junit-jupiter\4.5.1\mockito-junit-jupiter-4.5.1.jar;D:\application\Maven\repository\org\skyscreamer\jsonassert\1.5.1\jsonassert-1.5.1.jar;D:\application\Maven\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;D:\application\Maven\repository\org\springframework\spring-core\5.3.22\spring-core-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-jcl\5.3.22\spring-jcl-5.3.22.jar;D:\application\Maven\repository\org\springframework\spring-test\5.3.22\spring-test-5.3.22.jar;D:\application\Maven\repository\org\xmlunit\xmlunit-core\2.9.0\xmlunit-core-2.9.0.jar;D:\application\Maven\repository\org\junit\vintage\junit-vintage-engine\5.8.2\junit-vintage-engine-5.8.2.jar;D:\application\Maven\repository\org\junit\platform\junit-platform-engine\1.8.2\junit-platform-engine-1.8.2.jar;D:\application\Maven\repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;D:\application\Maven\repository\org\junit\platform\junit-platform-commons\1.8.2\junit-platform-commons-1.8.2.jar;D:\application\Maven\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;D:\application\Maven\repository\junit\junit\4.13.2\junit-4.13.2.jar;D:\application\Maven\repository\org\hamcrest\hamcrest-core\2.2\hamcrest-core-2.2.jar;D:\application\Maven\repository\org\mockito\mockito-core\2.23.4\mockito-core-2.23.4.jar;D:\application\Maven\repository\net\bytebuddy\byte-buddy\1.12.12\byte-buddy-1.12.12.jar;D:\application\Maven\repository\net\bytebuddy\byte-buddy-agent\1.12.12\byte-buddy-agent-1.12.12.jar;D:\application\Maven\repository\org\objenesis\objenesis\2.6\objenesis-2.6.jar;D:\application\Maven\repository\org\powermock\powermock-core\2.0.2\powermock-core-2.0.2.jar;D:\application\Maven\repository\org\powermock\powermock-reflect\2.0.2\powermock-reflect-2.0.2.jar;D:\application\Maven\repository\org\javassist\javassist\3.24.0-GA\javassist-3.24.0-GA.jar;D:\application\Maven\repository\org\powermock\powermock-api-mockito2\2.0.2\powermock-api-mockito2-2.0.2.jar;D:\application\Maven\repository\org\powermock\powermock-api-support\2.0.2\powermock-api-support-2.0.2.jar;D:\application\Maven\repository\org\powermock\powermock-module-junit4\2.0.2\powermock-module-junit4-2.0.2.jar;D:\application\Maven\repository\org\powermock\powermock-module-junit4-common\2.0.2\powermock-module-junit4-common-2.0.2.jar;D:\application\Maven\repository\org\jacoco\org.jacoco.agent\0.8.4\org.jacoco.agent-0.8.4-runtime.jar" com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit5 com.cmbchina.ccc.msp.mspai.service.DbLogServiceImplTest,testInsertApiLog_shouldCallDaoAndLog Process finished with exit code -1 org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. For example: when(mock.getArticles()).thenReturn(articles); Also, this error might show up because: 1. you stub either of: final/private/equals()/hashCode() methods. Those methods *cannot* be stubbed/verified. Mocking methods declared on non-public parent classes is not supported. 2. inside when() you don't call method on mock but on some other object. at com.cmbchina.ccc.msp.mspai.service.DbLogServiceImplTest.testInsertApiLog_shouldCallDaoAndLog(DbLogServiceImplTest.java:73) at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:68) at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:89) at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:97) at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:87) at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:50) at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:34) at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:44) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at org.junit.runner.JUnitCore.run(JUnitCore.java:115) at org.junit.vintage.engine.execution.RunnerExecutor.execute(RunnerExecutor.java:42) at org.junit.vintage.engine.VintageTestEngine.executeAllChildren(VintageTestEngine.java:80) at org.junit.vintage.engine.VintageTestEngine.execute(VintageTestEngine.java:72) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
07-08
你遇到的异常是: ``` org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. ``` ### ❌ 错误原因分析 这个错误说明你在使用 `when(...).thenReturn(...)` 时,**没有传入一个对 mock 对象的方法调用**。 比如正确写法: ```java when(mockObject.someMethod()).thenReturn(result); ``` 而如果你写了类似下面的代码就会报错: ```java when(JsonUtil.toJson(logAdapterPo)).thenReturn(json); // ❌ 静态方法 or 非mock对象方法 ``` 在你的测试类中,这一行导致了问题: ```java when(jsonUtil.toJson(logApiPo)).thenReturn(json); ``` 虽然 `jsonUtil` 是一个 mock 对象,但你并没有真正 mock 它的行为。这是因为你在测试类中声明了: ```java @Mock private JsonUtil jsonUtil; ``` 但是你可能没有在 SUT(被测类)中注入它 —— 实际上,`DbLogServiceImpl` 并没有使用 `JsonUtil` 这个依赖,而是直接调用了它的静态方法 `JsonUtil.toJson(...)`。 --- ## ✅ 正确修复方式 ### 方法一:重构代码使其可测试(推荐) 将 `JsonUtil.toJson()` 改为非静态方法,并通过依赖注入传入到 `DbLogServiceImpl` 中。 #### ✅ 修改后的 `DbLogServiceImpl` 示例: ```java @Service @Slf4j public class DbLogServiceImpl implements DbLogService { private final LogAdapterDao logAdapterDao; private final LogApiDao logApiDao; private final JsonUtil jsonUtil; public DbLogServiceImpl(LogAdapterDao logAdapterDao, LogApiDao logApiDao, JsonUtil jsonUtil) { this.logAdapterDao = logAdapterDao; this.logApiDao = logApiDao; this.jsonUtil = jsonUtil; } @Override public void insertAdapterLog(final LogAdapterPo logAdapterPo) { log.debug(jsonUtil.toJson(logAdapterPo)); logAdapterDao.insert(logAdapterPo); } @Override public void insertApiLog(final LogApiPo logApiPo) { log.debug(jsonUtil.toJson(logApiPo)); logApiDao.insert(logApiPo); } } ``` 这样你可以像之前一样在测试中 mock `jsonUtil.toJson()`。 --- ### 方法二:不修改源码,仅修改测试逻辑(临时方案) 如果你无法修改 `DbLogServiceImpl` 的实现(如它是三方库或不能改动业务逻辑),可以考虑以下两种替代方案: #### ✅ 方案 A:使用 PowerMock + Mockito 模拟静态方法(适用于 Java 8) 添加依赖(Maven): ```xml <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> ``` 然后修改测试类如下: ```java import static org.powermock.api.mockito.PowerMockito.*; @RunWith(PowerMockRunner.class) @PrepareForTest({JsonUtil.class}) // 声明要 mock 的静态类 public class DbLogServiceImplTest { @InjectMocks private DbLogServiceImpl dbLogService; @Mock private LogAdapterDao logAdapterDao; @Mock private LogApiDao logApiDao; private final Logger logger = mock(Logger.class); @Before public void setUp() { // 替换日志实例为 mock 对象 Field loggerField = null; try { loggerField = DbLogServiceImpl.class.getDeclaredField("log"); loggerField.setAccessible(true); loggerField.set(dbLogService, logger); } catch (Exception e) { e.printStackTrace(); } MockitoAnnotations.openMocks(this); } @Test public void testInsertApiLog_shouldCallDaoAndLog() throws Exception { // Given LogApiPo logApiPo = new LogApiPo(); String json = "{\"key\":\"value\"}"; mockStatic(JsonUtil.class); // mock 静态方法 when(JsonUtil.toJson(any())).thenReturn(json); // When dbLogService.insertApiLog(logApiPo); // Then verify(logger).debug(json); verify(logApiDao).insert(logApiPo); } } ``` > ⚠️ 注意:PowerMock 不支持 Java 17+,只适合遗留项目使用。 --- ### ✅ 方案 B:使用 Mockito Inline(适用于 Java 17+) 从 Mockito 3.4 开始,支持 mock 静态方法。 添加依赖(Maven): ```xml <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>4.8.0</version> <scope>test</scope> </dependency> ``` 测试代码示例: ```java import static org.mockito.Mockito.mockStatic; @Test public void testInsertApiLog_shouldCallDaoAndLog() { // Given LogApiPo logApiPo = new LogApiPo(); String json = "{\"key\":\"value\"}"; try (MockedStatic<JsonUtil> utilities = mockStatic(JsonUtil.class)) { utilities.when(() -> JsonUtil.toJson(any())).thenReturn(json); // When dbLogService.insertApiLog(logApiPo); // Then verify(logger).debug(json); verify(logApiDao).insert(logApiPo); } } ``` --- ## ✅ 总结 | 方法 | 是否推荐 | 要求 | |------|----------|------| | 修改源码注入 JsonUtil | ✅ 推荐 | 可维护性好、便于测试 | | 使用 PowerMock | ❌ 不推荐 | 仅限 Java 8、旧项目可用 | | 使用 Mockito Inline | ✅ 推荐 | Java 17+,现代项目首选 | --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值