go 进阶 go-zero相关: 七. 拦截器与熔断拦截器

2023-11-20

一. 拦截器的基础使用

  1. 在go-zero 中提供了拦截器功能
  2. go-zero中拦截器可以在两个角度分类
  1. 类型角度分为: 一元拦截器, 流式拦截
  2. 服务角度分为: 服务端拦截器, 客户端拦截器
  1. 什么是一元拦截器, 流式拦截
  1. 一元拦截器是指拦截一元RPC调用的拦截器,一元RPC调用是指客户端发送一个请求,服务端返回一个响应的调用。一元拦截器可以在请求和响应之间执行一些逻辑,例如日志、认证、限流等。一元拦截器是一个函数类型,它接收一个上下文、一个方法名、一个请求、一个响应、一个客户端连接、一个调用器和一些调用选项作为参数,返回一个错误作为结果
  2. 流式拦截器是指拦截流式RPC调用的拦截器,流式RPC调用是指客户端和服务端可以互相发送多个消息的调用。流式拦截器可以在流开始时执行一些逻辑,也可以在每个消息发送或接收时执行一些逻辑,例如日志、认证、限流等。流式拦截器是一个函数类型,它接收一个上下文、一个流描述、一个客户端连接、一个方法名、一个流创建器和一些调用选项作为参数,返回一个客户端流和一个错误作为结果

1. 服务端拦截器

  1. 代码示例
  1. 编写服务端一元拦截器函数, 流式拦截器函数
  2. 将一元拦截器,流式拦截器注册到服务Server中
import (
	"context"
	"errors"
	"flag"
	"fmt"
	"github.com/zeromicro/go-zero/core/logx"
	"google.golang.org/grpc/metadata"
	"log"
	"time"
	"go_cloud_demo/rpc/internal/config"
	"go_cloud_demo/rpc/internal/server"
	"go_cloud_demo/rpc/internal/svc"
	"go_cloud_demo/rpc/types/user"
	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

// 命令行参数读取配置文件所在路径
var configFile = flag.String("f", "rpc/etc/user.yaml", "the config file")

func main() {
	flag.Parse()
	//1.读取配置文件解析到Config结构体上
	var c config.Config
	conf.MustLoad(*configFile, &c)
	//2.创房服务运行上下文
	ctx := svc.NewServiceContext(c)

	//3.将服务注册到rpc服务器,并且监听指定端口启动服务
	//参数一"c.RpcServerConf":保存了当前rpc服务配置信息
	//参数二"func(grpcServer *grpc.Server)"一个函数,当执行该函数时
	//会调用通过proto生成的RegisterXXXServer(),将当前rpc服务实现注册到rpc服务器
	s := zrpc.MustNewServer(c.RpcServerConf,
		func(grpcServer *grpc.Server) {
			user.RegisterUserServer(grpcServer, server.NewUserServer(ctx))

			if c.Mode == service.DevMode || c.Mode == service.TestMode {
				reflection.Register(grpcServer)
			}
		})

	//添加UnaryInterceptor一元拦截器
	s.AddUnaryInterceptors(interceptor)
	//添加StreamInterceptor流式拦截器
	s.AddStreamInterceptors(StreamLoggerInterceptor)

	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}

// 自定义一元拦截器函数
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	// 获取metadata 这里的metadata类似http的header,不合法直接return
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("获取metadata失败")
	}
	if values, ok := md["header"]; ok {
		// ...
		fmt.Printf("接收请求头: %v", values)
	}

	logx.Info("拦截器前...")
	// 记录开始时间和请求的方法
	log.Println("start:" + time.Now().Format("2006-01-02 15:04:05") + " " + info.FullMethod)

	resp, err := handler(ctx, req)
	logx.Info("拦截器后...")
	// 正常结束,记录结束时间和方法
	log.Println("end:" + time.Now().Format("2006-01-02 15:04:05") + " " + info.FullMethod)
	return resp, err
}

// 自定义流式拦截器函数
func StreamLoggerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {

	logx.Info("拦截器前...")
	log.Println(time.Now().Format("2006-01-02 15:04:05") + " " + info.FullMethod)
	err := handler(srv, ss)
	logx.Info("拦截器后...")
	if err != nil {
		log.Println(time.Now().Format("2006-01-02 15:04:05") + " " + err.Error())
		return err
	}
	log.Println(time.Now().Format("2006-01-02 15:04:05") + " " + info.FullMethod)
	return nil
}

2. 客户端拦截器

  1. 编写客户端一元拦截器,流式拦截器函数
import (
	"context"
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/zrpc"
	"go_cloud_demo/rpc/types/user"
	"go_cloud_demo/rpc/userclient"
	"go_cloud_demo/user/internal/config"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

type ServiceContext struct {
	Config config.Config
	//用来创建rpc客户端的结构体
	RpcUser userclient.User
	//访问rpc服务接口返回的数据
	UserAuthResp *user.UserAuthResp
}

func NewServiceContext(c config.Config) *ServiceContext {
	zrpc.WithUnaryClientInterceptor(interceptor)
	return &ServiceContext{
		Config: c,
		//添加初始化rpc客户端逻辑
		//注册客户端拦截器
		RpcUser: userclient.NewUser(zrpc.MustNewClient(
			c.RpcClientConf,
			zrpc.WithUnaryClientInterceptor(interceptor),                    //添加一元拦截
			zrpc.WithStreamClientInterceptor(ClientStreamLoggerInterceptor), //添加流式拦截器

		)),
	}
}

// 客户端一元拦截器函数
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	md := metadata.New(map[string]string{"name": "lsz"})
	ctx = metadata.NewOutgoingContext(ctx, md)
	logx.Info("调用rpc服务前")
	err := invoker(ctx, method, req, reply, cc)
	if err != nil {
		return err
	}
	logx.Info("调用rpc服务后")
	return nil
}

// 客户端流式拦截器函数(示例,函数内部为空,不能实际使用)
func ClientStreamLoggerInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
	return nil, nil
}

二. 拦截器底层底层执行原理

三. go-zero默认添加的拦截器

客户端

  1. 在使用 go-zero rpc 客户端时, 调用github.com/zeromicro/go-zero/zrp下的MustNewClient()初始化rpc客户端,示例代码
func NewServiceContext(c config.Config) *ServiceContext {
	zrpc.WithUnaryClientInterceptor(interceptor)
	return &ServiceContext{
		Config: c,
		//添加初始化rpc客户端逻辑
		RpcUser: userclient.NewUser(zrpc.MustNewClient(
			c.RpcClientConf,
			zrpc.WithUnaryClientInterceptor(interceptor),                    //添加自定义一元拦截
			zrpc.WithStreamClientInterceptor(ClientStreamLoggerInterceptor), //添加自定义流式拦截器

		)),
	}
}
  1. 查看MustNewClient()源码,内部的调用链路上,最终会调用到client结构体上的buildDialOptions()方法
MustNewClient()
	--->NewClient()
			--->github.com/zeromicro/go-zero/zrpc/internal下的NewClient()
					--->client结构体上的dial()
						--->client结构体上的buildDialOptions()
//可以参考查看"go 进阶 go-zero相关: 五. 服务发现" 文档
  1. 查看client结构体上buildDialOptions()源码, 通过该方法默认注册了6个拦截器
func (c *client) buildDialOptions(opts ...ClientOption) []grpc.DialOption {
	var cliOpts ClientOptions
	for _, opt := range opts {
		opt(&cliOpts)
	}

	var options []grpc.DialOption
	if !cliOpts.Secure {
		options = append([]grpc.DialOption(nil), grpc.WithTransportCredentials(insecure.NewCredentials()))
	}

	if !cliOpts.NonBlock {
		options = append(options, grpc.WithBlock())
	}

	options = append(options,
		//五个一元拦截器
		WithUnaryClientInterceptors(
			clientinterceptors.UnaryTracingInterceptor,
			clientinterceptors.DurationInterceptor,
			clientinterceptors.PrometheusInterceptor,
			clientinterceptors.BreakerInterceptor, //熔断器相关拦截器
			clientinterceptors.TimeoutInterceptor(cliOpts.Timeout),
		),
		//一个流式拦截器
		WithStreamClientInterceptors(
			clientinterceptors.StreamTracingInterceptor,
		),
	)

	return append(options, cliOpts.DialOptions...)
}

// WithStreamClientInterceptors uses given client stream interceptors.
func WithStreamClientInterceptors(interceptors ...grpc.StreamClientInterceptor) grpc.DialOption {
	return grpc.WithChainStreamInterceptor(interceptors...)
}

// WithUnaryClientInterceptors uses given client unary interceptors.
func WithUnaryClientInterceptors(interceptors ...grpc.UnaryClientInterceptor) grpc.DialOption {
	return grpc.WithChainUnaryInterceptor(interceptors...)
}
  1. 接下来我们重点看一下熔断器拦截器

1. 熔断器拦截器 BreakerInterceptor

  1. zRPC中熔断器的实现参考了Google Sre过载保护算法,该算法的原理如下
  1. 请求数量(requests):调用方发起请求的数量总和
  2. 请求接受数量(accepts):被调用方正常处理的请求数量
  1. 正常情况下这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就回打开,新的请求会在本地以一定的概率被抛弃直接返回错误,通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 * acceptst 调整为 requests = 1.1 * accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断
  2. 服务调用方为每一个调用服务(调用路径)维护一个状态机,熔断器的三种状态:
  1. 关闭(Closed):该状态下,需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态,在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进去断开状态
  2. 打开(Open):该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
  3. 半打开(Half-Open):该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,可以认为被调用服务已经恢复正常,熔断器切换到关闭状态,同时重置计数,如果仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮
  1. 查看熔断拦截器BreakerInterceptor()源码
  1. 基于请求方法进行熔断,所以该函数中首先会拼接拦截器名target+method
  2. 然后执行github.com/zeromicro/go-zero/core/breaker下的DoWithAcceptable()函数
//在github.com/zeromicro/go-zero/zrpc/internal/clientinterceptors下
func BreakerInterceptor(ctx context.Context, method string, req, reply interface{},
    cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    //1.基于请求方法进行熔断,所以拼接拦截器名target+method 
    breakerName := path.Join(cc.Target(), method)
    return breaker.DoWithAcceptable(
    	breakerName, 
    	func() error {//发起一次grpc请求函数
        	return invoker(ctx, method, req, reply, cc, opts...)
    	},
    	codes.Acceptable //定义哪些错误码为需要拦截的
    	)
}

//定义哪些错误码为需要拦截的函数,在github.com/zeromicro/go-zero/zrpc/internal/codes下
//用来判断哪些error会计入失败计数
func Acceptable(err error) bool {
	switch status.Code(err) {
	case codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss, codes.Unimplemented:
		return false
	default:
		return true
	}
}
  1. 查看github.com/zeromicro/go-zero/core/breaker下的DoWithAcceptable()函数,该函数执行需要三个参数
  1. name: 拦截器名称
  2. req func() error: 发起一次实际grpc请求函数
  3. acceptable Acceptable: 返回哪些代码需要拦截的函数
func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error {
	//执行获取拦截器函数do()
	return do(name, func(b Breaker) error {
		//最终执行Breaker下的DoWithAcceptable()方法
		return b.DoWithAcceptable(req, acceptable)
	})
}

//获取拦截器
func do(name string, execute func(b Breaker) error) error {
    return execute(GetBreaker(name))
}

// GetBreaker returns the Breaker with the given name.
func GetBreaker(name string) Breaker {
    lock.RLock()
    b, ok := breakers[name]
    lock.RUnlock()
    if ok {
        return b
    }

    lock.Lock()
    b, ok = breakers[name]
    if !ok {
        b = NewBreaker(WithName(name)) 
        breakers[name] = b
    }
    lock.Unlock()

    return b
}
  1. 查看Breaker下的DoWithAcceptable()方法,Breaker是一个接口,默认情况下执行circuitBreaker实现的DoWithAcceptable(),该方法中重点关注调用执行了circuitBreaker下throttle的doReq()方法, 总结就是:

go-zero默认情况下针对这个Breaker接口提供了circuitBreaker实现,执行该结构体上的doReq()方法, 通过执行该方法最终执行到googleBreaker结构体上的doReq(), googleBreaker才是熔断的核心

func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error {
	//执行circuitBreaker下throttle的doReq()方法
	return cb.throttle.doReq(req, nil, acceptable)
}
  1. throttle是一个接口内部有allow与doReq两个抽象方法,在上方执行时,默认情况下会执行loggedThrottle这个实现类的
throttle interface {
	allow() (Promise, error)
	doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error
}

//实现上方throttle 接口
type loggedThrottle struct {
	name string
	internalThrottle
	errWin *errorWindow
}

func newLoggedThrottle(name string, t internalThrottle) loggedThrottle {
	return loggedThrottle{
		name:             name,
		internalThrottle: t,
		errWin:           new(errorWindow),
	}
}

func (lt loggedThrottle) allow() (Promise, error) {
	promise, err := lt.internalThrottle.allow()
	return promiseWithReason{
		promise: promise,
		errWin:  lt.errWin,
	}, lt.logError(err)
}

//拦截器执行的方法
func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
	//重点关注会获取loggedThrottle上的internalThrottle属性,执行它的doReq()方法
	return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool {
		accept := acceptable(err)
		if !accept && err != nil {
			lt.errWin.add(err.Error())
		}
		return accept
	}))
}
  1. 查看googleBreaker是针对internalThrottle接口的实现, go-zero通过它提供了默认的熔断逻辑
type googleBreaker struct {
	k     float64 //倍值 默认1.5
	stat  *collection.RollingWindow //滑动时间窗口,用来对请求失败和成功计数
	proba *mathx.Proba //动态概率
}

func newGoogleBreaker() *googleBreaker {
	bucketDuration := time.Duration(int64(window) / int64(buckets))
	st := collection.NewRollingWindow(buckets, bucketDuration)
	return &googleBreaker{
		stat:  st,
		k:     k,
		proba: mathx.NewProba(),
	}
}

//上方会调用该方法
func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
	//执行accept(),判断是否触发熔断
	if err := b.accept(); err != nil {
		if fallback != nil {
			return fallback(err)
		}

		return err
	}

	defer func() {
		if e := recover(); e != nil {
			b.markFailure()
			panic(e)
		}
	}()
	//执行真正的调用
	err := req()
	//正常请求计数
	if acceptable(err) {
		//实际执行:b.stat.Add(1)
    	//也就是说:内部指标统计成功+1
		b.markSuccess()
	} else {
		//异常请求计数
		//原理同上
		b.markFailure()
	}

	return err
}

func (b *googleBreaker) accept() error {
	//请求接受数量和请求总量
	accepts, total := b.history()
	weightedAccepts := b.k * float64(accepts)
	//计算丢弃请求概率 
	//https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
	dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
	if dropRatio <= 0 {
		return nil
	}
	//动态判断是否触发熔断
	if b.proba.TrueOnProba(dropRatio) {
		return ErrServiceUnavailable
	}

	return nil
}

func (b *googleBreaker) allow() (internalPromise, error) {
	if err := b.accept(); err != nil {
		return nil, err
	}
	return googlePromise{
		b: b,
	}, nil
}

func (b *googleBreaker) markSuccess() {
	b.stat.Add(1)
}

func (b *googleBreaker) markFailure() {
	b.stat.Add(0)
}
  1. 那么是怎么滑动计算的, 继续向下追查看markSuccess()或markFailure()方法中执行的Add(),默认情况下执行RollingWindow的Add()方法

参考博客

// Add adds value to current bucket.
func (rw *RollingWindow) Add(v float64) {
	rw.lock.Lock()
	defer rw.lock.Unlock()
	//滑动的动作发生在此
	rw.updateOffset()
	rw.win.add(rw.offset, v)
}

// Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set.
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
	rw.lock.RLock()
	defer rw.lock.RUnlock()

	var diff int
	span := rw.span()
	// ignore current bucket, because of partial data
	if span == 0 && rw.ignoreCurrent {
		diff = rw.size - 1
	} else {
		diff = rw.size - span
	}
	if diff > 0 {
		offset := (rw.offset + span + 1) % rw.size
		rw.win.reduce(offset, diff, fn)
	}
}

func (rw *RollingWindow) span() int {
	offset := int(timex.Since(rw.lastTime) / rw.interval)
	if 0 <= offset && offset < rw.size {
		return offset
	}
	return rw.size
}

func (rw *RollingWindow) updateOffset() {
	span := rw.span()
	if span <= 0 {
		return
	}

	offset := rw.offset
	//重置过期的 bucket
	for i := 0; i < span; i++ {
		rw.win.resetBucket((offset + i + 1) % rw.size)
	}

	rw.offset = (offset + span) % rw.size
	now := timex.Now()
	//更新时间
	rw.lastTime = now - (now-rw.lastTime)%rw.interval
}

func (w *window) add(offset int, v float64) {
	往执行的 bucket 加入指定的指标
	w.buckets[offset%w.size].add(v)
}

服务端

  1. 在提供go-zero服务端时,执行zrpc.MustNewServer()创建RpcServer,内部最终会调用到一个NewServer()函数,该函数中:
  1. 执行c.HasEtcd()判断是否配置了etcd注册中心地址,如果配置了,执行NewRpcPubServer()函数
  2. 在NewRpcPubServer()中会创建一个名为registerEtcd的function函数,并将这个function封装到keepAliveServer结构体中
  3. 自此rpcServer创建成功,并封装了keepAliveServer结构体变量,内部持有一个注册服务的registerEtcd()函数,后续会通过这个函数是实现服务注册
  4. 执行setupInterceptors()函数注册拦截器
  1. 查看setupInterceptors()源码:这里要了解go-zero底层的keepAliveServer结构参考:go 进阶 go-zero相关: 四. 服务注册原理中提到
//setupInterceptors()根据配置信息为rpc服务添加一些拦截器
//入参: svr是一个rpc服务对象
//		c是一个RpcServerConf结构体,包含了rpc服务的配置信息
//		metrics是一个stat.Metrics对象,用于收集和报告统计指标
func setupInterceptors(svr internal.Server, c RpcServerConf, metrics *stat.Metrics) error {
	//1.如果配置中指定了CpuThreshold参数,表示要开启自适应限流功能
	if c.CpuThreshold > 0 {
		// 创建一个自适应限流器对象,设置CPU阈值为配置中的值
		shedder := load.NewAdaptiveShedder(load.WithCpuThreshold(c.CpuThreshold))
		// 为rpc服务添加一个一元拦截器,用于执行限流逻辑,并记录统计指标
		svr.AddUnaryInterceptors(serverinterceptors.UnarySheddingInterceptor(shedder, metrics))
	}

	//2.如果配置中指定了Timeout参数,表示要开启超时控制功能
	if c.Timeout > 0 {
		// 为rpc服务添加一个一元拦截器,用于执行超时控制逻辑,超时时间为配置中的值
		svr.AddUnaryInterceptors(serverinterceptors.UnaryTimeoutInterceptor(
			time.Duration(c.Timeout) * time.Millisecond))
	}

	//3.如果配置中指定了Auth参数,表示要开启鉴权功能
	if c.Auth {
		// 调用setupAuthInterceptors函数,为rpc服务添加鉴权相关的拦截器
		if err := setupAuthInterceptors(svr, c); err != nil {
			return err
		}
	}
	return nil
}
  1. 继续查看用来添加拦截器的AddUnaryInterceptors()方法,最终会将拦截器保存到baseRpcServer的streamInterceptors属性,或unaryInterceptors属性中(下方源码在网上搜的,不知道是不是我的版本不对还是操作有问题在go-zero源码包中没找到…不能确认是否正确)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 进阶 go-zero相关: 七. 拦截器与熔断拦截器 的相关文章

  • 如何让 Spring 控制器从 POJO 返回 CSV? [复制]

    这个问题在这里已经有答案了 给定一个简单的 Java 对象 public class Pojo private String x private String y private String z getters setters 是否有一些
  • 将特定项目移至列表末尾

    我有一个ArrayList in Java deleteItem createitem exportitem deleteItems createItems 我想移动包含的所有字符串delete到列表的末尾 所以我会得到下一个 create
  • spring-data-neo4j 基本一对多关系不持久

    EDIT 示例项目可在github https github com troig neo4jCustomRepository 我在后端项目中使用 Neo4J Rest 图形数据库 托管在 grapheneDb 中 和 Spring Data
  • 如何生成源代码来创建我正在调试的对象?

    我的典型场景 我处理的遗留代码有一个错误 只有生产中的客户端才会遇到 我附加了一个调试器并找出如何重现该问题their系统给定their输入 但是 我还不知道为什么会发生错误 现在我想在本地系统上编写一个自动化测试来尝试重现然后修复错误 最
  • Java中单击和双击的区别

    我搜索论坛并看到以下代码 public void mouseClicked MouseEvent e if e getClickCount 2 System out println and it s a double click wasDo
  • Tomcat:Java 静态变量作用域、应用程序范围还是会话范围?

    java 静态变量是否在使用相同 web 应用程序的所有会话之间共享 或者每个会话都有自己的静态变量版本吗 换句话说 Tomcat 是为每个会话创建一组新的类 还是为整个 Web 应用程序创建一组新的类 Tomcat 创建一个ClassLo
  • 由于 maven-surefire-plugin,Maven 构建失败

    我这里有类似的问题eclipse 中缺少 maven surefire plugin https stackoverflow com questions 23588957 maven surefire plugin missing in e
  • 传输级别信息与 SOAP 消息命名空间 URI 不匹配

    我收到错误 Transport level information does not match with SOAP Message namespace URI 要求您提供详细信息以解决问题 我在客户端设置了以下内容 HttpTranspo
  • 将 Flash 文件上传与 JSF 集成

    我看到我们可以通过flash文件上传来上传多个文件 喜欢SWF上传 http code google com p swfupload or YUI上传器 http yuilibrary com yui docs uploader 是否可以将
  • 无法为对象堆保留足够的空间

    每次尝试运行该程序时 我都会重复出现以下异常 VM初始化期间发生错误 无法为对象堆保留足够的空间 无法创建Java虚拟机 我尝试增加虚拟内存 页面大小 和 RAM 大小 但无济于事 我怎样才能消除这个错误 运行 JVM XX MaxHeap
  • 了解 Etc/GMT 时区

    Apple 在从 App Store 返回自动续订订阅收据时使用 Etc GMT 时区的理由是什么 Etc GMT 时区到底是什么 Java SDK 能理解这个时区吗 或者我是否必须使用其他第三方库 例如乔达时间 http www joda
  • 如何在 JUnit 中缩短(或隐藏)包名称?

    我在 JUnit 中有很长的包名称 这使得很难看到正在运行哪些测试 不幸的是 使用 Eclipse 的 缩写包名称 不起作用 有没有办法隐藏或者最好缩短它们 None
  • 无法实例化类对象的类型 (Java)

    这是我收到错误的代码 在 new 之后的第二个 Killer 处 String classes new String 5 kills 0 Brian Moser kills 1 James Doakes kills 2 Lila Tourn
  • Hibernate - 如何通过 Hibernate 将 java.net.URL 存储到数据库中

    我有一块田地URL countryURL in a Country班级 我想将其数据存储到COUNTRY通过 Hibernate 将表存储在数据库中 哪个休眠type我应该在休眠映射文件中使用
  • JPA中如何连接多个数据库?

    我有一个 Spring Boot 应用程序 当前使用 JPA 连接到单个数据库 application properties 文件中的连接详细信息 spring datasource url jdbc oracle thin localho
  • startDrag 方法 已弃用且无法编译程序

    startDrag android content ClipData android view View DragShadowBuilder java lang Object int 已弃用 如何解决这个问题而又不失去对旧版本的兼容性 还有
  • 根据另一个列表的顺序对列表进行排序[重复]

    这个问题在这里已经有答案了 我需要对列表进行排序Person对象 List
  • 运行外部进程的非阻塞线程

    我创建了一个 Java GUI 应用程序 它充当许多低级外部进程的包装器 该实用程序按原样运行 但迫切需要一项重大改进 我希望我的外部进程以非阻塞方式运行 这将允许我并行服务其他请求 简而言之 我希望能够在生成数据时处理来自外部进程的数据
  • Spring验证非空元素的字符串列表

    我有一个模型类 其中包含字符串列表 该列表可以为空 也可以包含元素 如果它有元素 这些元素不能为空 举个例子 假设我有一个名为 QuestionPaper 的类 它有一个 QuestionId 列表 其中每个都是一个字符串 class Qu
  • Spring Data JPA 存储库,具有规范、分页和标准 fetch-join

    我正在使用具有规范和分页功能的 Spring Data JPA 存储库实现实体列表的搜索 过滤服务 我正在尝试减少查询数量 n 1 问题 并使用条件获取机制获取嵌套数据 我有两个实体类 Entity Table name delegatio

随机推荐

  • HR 宏

    表TRMAC 程序DBPNPMAC 转载于 https www cnblogs com CtrlS p 10818285 html
  • Tableau 中的组(group)与集(set)

    使用tableau也好长时间了 最近有同事问我tableau中组和集有什么区别 那今天就谈谈我个人对组和集的一些理解 也算是一个回顾 理解不对之处还望大牛们及时指正 1 首先看看官方的定义 组是构成更高级别类别的维度成员的组合 单独理解的确
  • Error:(923) Apostrophe not preceded by \ (in %1$s's )

    问题描述 在使用Android的strings xml时 英文版会遇到许多简写 其中 s 和 t时经常使用的 如果直接使用编译无法通过就报标题所示错误 解决方法 主要原因是 是特殊字符 需要转义 加 既可解决 示例 xx s thing 修
  • VMware 中搭建 SylixOS 环境

    1 制作 x86 平台 U 盘启动盘 详细步骤见 RealEvo IDE 使用手册 第八章 制作成功后插入 U 盘 2 创建 VMware 虚拟机设备 打开 VMware 这里使用版本为 15 5 6 点击 创建新的虚拟机 按如下步骤创建虚
  • linux 如何查看进程端口号,在linux中查看进程占用的端口号

    在Linux 上的 etc services 文件可以查看到更多关于保留端口的信息 可以使用以下六种方法查看端口信息 ss 可以用于转储套接字统计信息 netstat 可以显示打开的套接字列表 lsof 可以列出打开的文件 nmap 是网络
  • srand(time(NULL))

    srand函数是随机数发生器的初始化函数 原型 void srand unsigned seed 用法 它初始化随机种子 会提供一个种子 这个种子会对应一个随机数 如果使用相同的种子后面的rand 函数会出现一样的随机数 如 srand 1
  • 什么是页面文件使用率

    你好 很高兴能看到你的问题 也很高兴我能够回答你的问题 你提问 什么是页面文件使用率 首先我们必须要了解什么叫 页面文件 页面文件是一个存放在硬盘上的文件 大多数情况下都放在系统磁盘 如C 盘 的根目录下 这个文件不允许用户访问 只能够被操
  • 简单的文件内容繁简体甄别

    在做国际化的时候 很多旧文件中的简体或者繁体 需要优化 一个一个找很麻烦 于是在查阅资料后 自己编写了一个简单的工具 废话不多说 上码子 插件 mui ui vue js jquery 項目是 hbuildx 直接創建的 change la
  • pip相关命令

    查看当前pip源 pip config list 更改pip源 pip config set global index url 清华源网址 升级pip python m pip install upgrade pip 安装模块 pip in
  • vue3中路由的使用

    路由是什么 vue中的路由是用来管理页面切换或跳转的一种方式 Vue Router是vue官方的路由管理器 1 Vue Router的安装 需要先弄好npm npm install vue router 4 s 在安装完成之后 开始尝试简单
  • BT蓝牙协议 — HFP/HSP的关联与区别

    一 前言 有时 我们能看到有的蓝牙产品标明支持HFP HSP 而有的产品却只标注了支持HFP 那么HFP or HSP是什么呢 又有什么样的关系呢 二 HSP协议 HSP Headset Profile 耳机模式 仅实现了最基本的通话操作
  • c语言判断一个数是否为偶数

    include
  • 图解五种磁盘调度算法, FCFS, SSTF, SCAN, C-SCAN, LOOK

    一 FCFS 调度 先来先服务 磁盘调度的最简单形式当然是先来先服务 FCFS 算法 虽然这种算法比较公平 但是它通常并不提供最快的服务 例如 考虑一个磁盘队列 其 I O 请求块的柱面的顺序如下 98 183 37 122 14 124
  • 华为OD机试 Python 【TLV解析】

    题目 题目简述 你收到了一串由两端设备传递的TLV格式的消息 现在你需要根据这串消息生成一个对应的 tag length valueOffset 列表 详细说明 这串消息其实是由许多小组成的 每一小组里包含了tag length value
  • win可以上网,但是右下方显示“无internet链接“

    使用了下面链接的方法 成功解决 Win10可以联网但右下角显示无法连接到Internet怎办 首先 打开控制面板 control 右上角 将查看方式切换为小图标 调整计算机的设置下 找到并点击网络和共享中心 网络和共享中心窗口 左侧点击更改
  • 苹果鼠标win10不能滑动_解决WIN10使用苹果鼠标滚轮不能使用的问题

    这个花费了蛮多时间却解决不了 网上流行各种各样的尝试 还有很多的安装包 都试了一遍 无一解决 绝望的时候 看到有个网友发的云盘链接 感谢 花小柏 一安装即可使用 太感谢了 最后也分享给大家 链接 https pan baidu com s
  • Linux运维脚本

    20200911 这里记录一些平时使用的脚本 免密登陆什么的 免密登陆 bin bash f root ssh id rsa pub ssh keygen t rsa P f root ssh id rsa gt dev null expe
  • 【2023版】最新stable diffusion安装教程,一键安装,永久使用,stable diffusion下载安装教程!

    关于现在非常红火的AI绘画 很多感兴趣的人不知道如何入手 如果你的电脑配置足够好 那么不要犹豫 让我来教你如何在本地电脑全免费运行当下最强大的AI绘画工具 Stable Diffusion 吧 一 Stable Diffusion 是什么
  • cmake Targets:CMake如何构建简单的Target

    CMake有三个基本命令 用于定义CMake Target 分别是 add executable 构建exe add library 构建库 add custom target 自定义构建目标在camke构建阶段运行的 add execut
  • go 进阶 go-zero相关: 七. 拦截器与熔断拦截器

    目录 一 拦截器的基础使用 1 服务端拦截器 2 客户端拦截器 二 拦截器底层底层执行原理 三 go zero默认添加的拦截器 客户端 1 熔断器拦截器 BreakerInterceptor 服务端 一 拦截器的基础使用 在go zero