`Spring Cloud OpenFeign`底层实现原理

Spring Cloud OpenFeign工作原理

一 、简介

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

二、OpenFeign的使用

1. 引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. 启动类添加@EnableFeignClients注解

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(basePackages = {"xx.xx"})
@ComponentScan(basePackages = {"xx.xx"})
public class TagApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(TagApplication.class);
    }
}

3. 编写业务类

@FeignClient(name = "user-service")
public interface BaseFeignApi {
    @GetMapping("/list")
    List<User> getList();
}

三、OpenFeign实现原理

1.@EnableFeignClients(basePackages = {"xx.xx"})注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	String[] value() default {};

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] defaultConfiguration() default {};

	Class<?>[] clients() default {};
}

2.FeignClientsRegistrar

2.1 FeignClientsRegistrar
class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    ....
}
2.2ImportBeanDefinitionRegistrar函数
public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        this.registerBeanDefinitions(importingClassMetadata, registry);
    }

    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }
}

FeignClientsRegistrar本质是一个ImportBeanDefinitionRegistrar,并且持有环境变量和资源加载器的能力,FeignClientsRegistrar重写了registerBeanDefinitions方法,该方法在容器上下文刷新(启动时调用refresh)时被调用,调用时机此处不展开分析,我们看一下.

2.3FeignClientsRegistrar#registerBeanDefinitions实现
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    // 处理@EnableFeignClients注解上的属性配置,将配置注册到容器仲
    registerDefaultConfiguration(metadata, registry);
    // 核心方法:注册@FeignClient对应的接口,奖@FeignClient注册到容器中
    registerFeignClients(metadata, registry);
}
2.4FeignClientsRegistrar#registerFeignClients方法
	public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {

        // 创建Spring内置的扫描器
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		scanner.setResourceLoader(this.resourceLoader);

		Set<String> basePackages;

		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
        // 获取 EnableFeignClients 注解中的 clients 属性值
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
            // 添加过滤器:过滤器所有被 @FeignClient 标记接口
			scanner.addIncludeFilter(annotationTypeFilter);
			basePackages = getBasePackages(metadata);
		}
		else {
			final Set<String> clientClasses = new HashSet<>();
			basePackages = new HashSet<>();
			for (Class<?> clazz : clients) {
				basePackages.add(ClassUtils.getPackageName(clazz));
				clientClasses.add(clazz.getCanonicalName());
			}
			AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
				@Override
				protected boolean match(ClassMetadata metadata) {
					String cleaned = metadata.getClassName().replaceAll("\\$", ".");
					return clientClasses.contains(cleaned);
				}
			};
			scanner.addIncludeFilter(
					new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
		}
		// 处理@FeignClient类,解析
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
					// verify annotated class is an interface
					AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
					AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
					Assert.isTrue(annotationMetadata.isInterface(),
							"@FeignClient can only be specified on an interface");

					Map<String, Object> attributes = annotationMetadata
							.getAnnotationAttributes(
									FeignClient.class.getCanonicalName());

					String name = getClientName(attributes);
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
					// 重点方法,注入FeignClient对象
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

第一部分是为了找到@Feignclient标识的接口类,第二部分就是对找出的接口类进行处理了处理,主要关注registerClientConfigurationregisterFeignClient函数。

其中registerClientConfiguration是为了处理@FeignClient#configuration属性的,在这个函数会往spirng容器中添加#{serviceName}.FeignClientSpecification作为名字的FeignClientSpecification类对象,例如user-center.FeignClientSpecification。

registerFeignClient函数则是处理接口类的主要方法了。我们在之前考虑到,我们在接口上填写了@FeignClient注解,在之后程序中我们可以直接引用这个接口对象来调用接口上的函数,理论分析一波:接口如果没有实现类,是不能直接在spring中直接进行注入并调用相应的方法的,一定需要我们去实现这个接口,那么我们可以想到,OpenFeign中一定做了这样的操作。

接下来我们看一下registerFeignClient函数

2.5registerFeignClient函数
	private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
        // 创建一个 BeanDefinitionBuilder 对象,用于构建并注入 FeignClientFactoryBean
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
        // 参数校验
		validate(attributes);
        // 设置BeanDefinition参数
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

		String alias = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

		boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
																// null

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
        // 注入接口实例
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

3. 接口代理对象的构建

OpenFeign 接口代理对象的构建,主要是通过 Spring 的扩展接口 FactoryBean 来实现的。在上面的代码中,通过解析 FeignClient 对象,构建成一个 FeignClientFactoryBean 对象,Spring 在注入对应接口是,会调用 FeignClientFactoryBean 对象中的 getObject() 方法,返回注入对应的代理对象。

3.1FeignClientFactoryBean

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    
    @Override
	public Object getObject() throws Exception {
        // 获取目标对象
		return getTarget();
	}

	/**
	 * 获取目标对象
	 * @param <T> the target type of the Feign client
	 * @return a {@link Feign} client created with the specified data and the context
	 * information
	 */
	<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
		// 判断当前 FeignClient 注解中的url是否为空,如果不为空,直接通过url的调用
		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
            // 返回目标对象
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
		if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
			this.url = "http://" + this.url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				// not load balancing because we have a url,
				// but ribbon is on the classpath, so unwrap
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			if (client instanceof FeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}
		Targeter targeter = get(context, Targeter.class);
        // 返回目标对象
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(this.type, this.name, url));
	}
}
3.2 Targeter#target函数

HystrixTargeter类实现了Targeter

class HystrixTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget<T> target) {
		if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
			return feign.target(target);
		}
		feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
		String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
				: factory.getContextId();
		SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
		if (setterFactory != null) {
			builder.setterFactory(setterFactory);
		}
		Class<?> fallback = factory.getFallback();
		if (fallback != void.class) {
			return targetWithFallback(name, context, target, builder, fallback);
		}
		Class<?> fallbackFactory = factory.getFallbackFactory();
		if (fallbackFactory != void.class) {
			return targetWithFallbackFactory(name, context, target, builder,
					fallbackFactory);
		}
		// 返回目标对象
		return feign.target(target);
	}
}
3.3 Feign对象

feign.target(target)函数

public abstract class Feign {
    
	public <T> T target(Target<T> target) {
		// 创建代理对象
        return this.build().newInstance(target);
    }
    
    public abstract <T> T newInstance(Target<T> target);

	/**
	 * 构建 Feign对象
	 */
    public Feign build() {
        Client client = (Client)Capability.enrich(this.client, this.capabilities);
        Retryer retryer = (Retryer)Capability.enrich(this.retryer, this.capabilities);
        List<RequestInterceptor> requestInterceptors = (List)this.requestInterceptors.stream().map((ri) -> {
            return (RequestInterceptor)Capability.enrich(ri, this.capabilities);
        }).collect(Collectors.toList());
        Logger logger = (Logger)Capability.enrich(this.logger, this.capabilities);
        Contract contract = (Contract)Capability.enrich(this.contract, this.capabilities);
        Options options = (Options)Capability.enrich(this.options, this.capabilities);
        Encoder encoder = (Encoder)Capability.enrich(this.encoder, this.capabilities);
        Decoder decoder = (Decoder)Capability.enrich(this.decoder, this.capabilities);
        // 创建代理对象的 InvocationHandler 工厂实例
        InvocationHandlerFactory invocationHandlerFactory = (InvocationHandlerFactory)Capability.enrich(this.invocationHandlerFactory, this.capabilities);
        QueryMapEncoder queryMapEncoder = (QueryMapEncoder)Capability.enrich(this.queryMapEncoder, this.capabilities);
        Factory synchronousMethodHandlerFactory = new Factory(client, retryer, requestInterceptors, logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy, this.forceDecoding);
        ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
        return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
}

点击进入newInstance()函数

3.4ReflectiveFeign 对象

ReflectiveFeign继承了Feign

public class ReflectiveFeign extends Feign {
/**
 * 创建代理对象
 */
  @Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
	// 解析模板:将方法解析,封装为MethodHandler
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 创建代理对象
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
}

看到这里就比较清晰了,把接口中的方法和默认实现放到Map<Method, MethodHandler>中然后使用InvocationHandlerFactory.Default()创建InvocationHandler,然后使用jdk动态代理生成接口的代理并返回,这里主要看一下FeignInvocationHandler实现:

  static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }
   }

四、服务调用

正如我们前边所说,通过@Autowired或者@Resource注入的时候,注入的是被封装之后的代理类实现,
jdk动态代理持有的是ReflectiveFeign.FeignInvocationHandler类型的InvocationHandler,那么具体调用的时候,会调用FeignInvocationHandler#invoke

4.1 FeignInvocationHandler对象
static class FeignInvocationHandler implements InvocationHandler { 
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }
	  // 执行
      return dispatch.get(method).invoke(args);
    }
   }

前几个判断分支都是调用了Object的基本方法,最后dispatch.get(method).invoke(args)才是接口的业务方法调用,dispatch的类型是Map<Method, MethodHandler>,是接口中方法与MethodHandler的映射关系,而MethodHandler又被SynchronousMethodHandler.Factory封装成SynchronousMethodHandler(实现了MethodHandler):

4.2 SynchronousMethodHandler

SynchronousMethodHandler类实现了MethodHandler。那么dispatch.get(method).invoke(args)最终调用的就是SynchronousMethodHandler#invoke:

final class SynchronousMethodHandler implements MethodHandler {
      @Override
  public Object invoke(Object[] argv) throws Throwable {
    // 构建请求的一个模版
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        // 可以看到调用的时候默认是带有重试能力,默认是5次,具体调用交给executeAndDecode来实现:
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }
}

可以看到调用的时候默认是带有重试能力,默认是5次,具体调用交给executeAndDecode来实现:

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
       // 通过 Client执行,进行远程通信
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

核心是client.execute,我们选择性看一下OkHttpClient的实现:

  @Override
  public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
  }

很明显,最终服务调用会委托给okhttp3.OkHttpClient来执行,然后组装结果和状态码返回调用,到这里openfeign的服务调用就分析完了,为了帮助理解和有更直观的概念,我们看一下服务调用时序图:

在这里插入图片描述

五、总结

  1. 通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描
  2. 解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
  3. Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy.
  4. 基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作

感觉这块的逻辑还是有点难度的,我debug源码三四次才搞懂OpenFeign底层是如何创建代理对象,如何实现调用的。

参考:

  • https://blog.csdn.net/weixin_41821642/article/details/129874394?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171325961116800188518011%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=171325961116800188518011&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-5-129874394-null-null.142v100pc_search_result_base4&utm_term=openfeign%E5%BA%95%E5%B1%82%E8%B0%83%E7%94%A8%E5%8E%9F%E7%90%86&spm=1018.2226.3001.4187

  • https://blog.csdn.net/bz120413/article/details/122215774?ops_request_misc=&request_id=&biz_id=102&utm_term=openfeign%E5%BA%95%E5%B1%82%E8%B0%83%E7%94%A8%E5%8E%9F%E7%90%86&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-122215774.142v100pc_search_result_base4&spm=1018.2226.3001.4187

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/549500.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Git】初识 Git

文章目录 1. 提出问题2. 如何解决&#xff1f;版本控制器3. 注意事项 1. 提出问题 不知道你工作或学习时&#xff0c;有没有遇到这样的情况&#xff1a;我们在编写各种文档时&#xff0c;为了防止文档丢失、更改失误、失误后能恢复到原来的版本&#xff0c;不得不复制出一个副…

Apifox接口测试教程(一)接口测试的原理与工具

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

「GO基础」GO程序组成要素

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

力扣爆刷第119天之CodeTop100五连刷81-85

力扣爆刷第119天之CodeTop100五连刷81-85 文章目录 力扣爆刷第119天之CodeTop100五连刷81-85一、14. 最长公共前缀二、718. 最长重复子数组三、169. 多数元素四、662. 二叉树最大宽度五、128. 最长连续序列 一、14. 最长公共前缀 题目链接&#xff1a;https://leetcode.cn/pro…

腾讯清华联合提出图像到视频生成方法-Follow-Your-Click:点击图像并加上简单提示词就可让图像动起来!

Follow-Your-Click只需单击一次和简短的提示就可以让图像的某一部分动起来&#xff0c;还支持不同的动作表达&#xff0c;比如微笑&#xff0c;悲伤&#xff0c;跳舞…… 相关链接 论文链接&#xff1a;https://arxiv.org/abs/2403.08268 项目链接&#xff1a;https://github…

【每日刷题】Day16

【每日刷题】Day16 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 2. 160. 相交链表 - 力扣&…

IGBT基本工作原理、主要参数及作用

IGBT是一种三端子的半导体开关器件&#xff0c;栅极&#xff0c;集电极和发射极。它结合了MOSFET的高输入阻抗和双极型三极管的低导通压降特性&#xff0c;广泛应用于变频器、电动汽车、电力传输等领域。 工作原理 IGBT由N沟道MOSFET和PNP双极型晶体管组成&#xff0c;其导通和…

前端ocr技术:electron+vue3中使用tesseract插件识别图片中字符

同学们可以私信我加入学习群&#xff01; 正文开始 前言一、electron各种csp问题二、试用插件总结 前言 项目需要ocr技术识别图片中的中文字符&#xff0c;本来这部分是后端的工作&#xff0c;但是因为各种原因&#xff0c;决定前端也做一个版本。 在ai时代之前&#xff0c;o…

conda新建环境报错An HTTP error occurred when trying to retrieve this URL.

conda新建环境报错如下 cat .condarc #将 .condarc文件中的内容删除&#xff0c;改成下面的内容 vi .condarc channels:- defaults show_channel_urls: true default_channels:- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main- https://mirrors.tuna.tsinghua.…

如何评估一个RAG(检索增强生成)系统

本文首发自博客文章 如何评估一个RAG&#xff08;检索增强生成&#xff09;系统 RAG 概念最初来源于 2020 年 Facebook 的一篇论文&#xff0c;这是 Facebook 博客对论文内容的进一步解释 &#x1f449;《检索增强生成&#xff1a;简化智能自然语言处理模型的创建》。大家都知…

Vitis HLS 学习笔记--readVec2Stream 函数-探究

目录 1. 高效内存存取的背景 2. readVec2Stream() 参数 3. 函数实现 4. 总结 1. 高效内存存取的背景 在深入研究《Vitis HLS 学习笔记--scal 函数探究》一篇文章之后&#xff0c;我们对于scal()函数如何将Y alpha * X这种简单的乘法运算复杂化有了深刻的理解。本文将转向…

商家转账到零钱全攻略:开通、使用、区别与常见问题解答

商家转账到零钱是什么&#xff1f; 【商家转账到零钱】可以说是【企业付款到零钱】的升级版&#xff0c;商家转账到零钱可以为商户提供同时向多个用户微信零钱转账的能力&#xff0c;支持分销返佣、佣金报酬、企业报销、企业补贴、服务款项、采购货款等自动向用户转账的场景。…

8个Python高效数据分析的技巧

这篇文章介绍了8个使用Python进行数据分析的方法&#xff0c;不仅能够提升运行效率&#xff0c;还能够使代码更加“优美”。 1 一行代码定义List 定义某种列表时&#xff0c;写For 循环过于麻烦&#xff0c;幸运的是&#xff0c;Python有一种内置的方法可以在一行代码中解决…

MDC使用手册精讲

MDC 背景&#xff1a; 线上排查问题时&#xff0c;请求在多个微服务之间进行调用&#xff0c;并发量较大的情况下&#xff0c;想跟踪某一个请求的链路&#xff0c;是需要花费一些时间才能梳理出来&#xff0c;而且还依赖于你的业务字段。而我们需要的是快速定位&#xff0c;快…

SpringSecurity登录时在哪里调用我们自定义的UserDetailsServiceImpl

SpringSecurity登录时在哪里调用我们自定义的UserDetailsServiceImpl 1、请求login方法 2、将用户的用户名和密码封装成一个对象&#xff0c;以便进行后续的认证操作 3、执行认证操作 4、调用providermanager类的authenticate 5.进入这一步就开始跟我们自定义实现的UserDet…

【云计算】云数据中心网络(四):IPv6 网关

云数据中心网络&#xff08;四&#xff09;&#xff1a;IPv6 网关 1.什么是 IPv6 网关2.IPv6 网关设计思路3.IPv6 网关的主要应用场景3.1 IPv6 私网通信3.2 IPv6 互联网通信3.3 IPv6 互联网通信&#xff08;仅主动访问&#xff09; 1.什么是 IPv6 网关 2017 年&#xff0c;中国…

OpenHarmony实战开发-Worker子线程中解压文件。

介绍 本示例介绍在Worker 子线程使用ohos.zlib 提供的zlib.decompressfile接口对沙箱目录中的压缩文件进行解压操作&#xff0c;解压成功后将解压路径返回主线程&#xff0c;获取解压文件列表。 效果图预览 使用说明 1.点击解压按钮&#xff0c;解压test.zip文件&#xff0c…

跟着Datawhale重学数据结构与算法

数据结构和算法之前学过&#xff0c;现在跟着Datawhale重学一下&#xff0c;就当是监督自己学习&#xff0c;重新拾起来养成一个好的习惯&#xff0c;以后可以一直坚持下去。 开源链接&#xff1a;【 教程地址 】【电子网站】 首先&#xff1a; #mermaid-svg-Cdr3rn9fGCVAiKS…

文献速递:深度学习胰腺癌诊断--胰腺癌在CT扫描中通过深度学习检测:一项全国性的基于人群的研究

Title 题目 Pancreatic Cancer Detection on CT Scans with Deep Learning: A Nationwide Population-based Study 胰腺癌在CT扫描中通过深度学习检测&#xff1a;一项全国性的基于人群的研究 01 文献速递介绍 胰腺癌&#xff08;PC&#xff09;的五年生存率是所有癌症中…

记一次奇妙的某个edu渗透测试

前话&#xff1a; 对登录方法的轻视造成一系列的漏洞出现&#xff0c;对接口确实鉴权造成大量的信息泄露。从小程序到web端网址的奇妙的测试就此开始。&#xff08;文章厚码&#xff0c;请见谅&#xff09; 1. 寻找到目标站点的小程序 进入登录发现只需要姓名加学工号就能成功…
最新文章