Spring Cloud OpenFeign源码解析,代理的魅力一展无遗

news2024/11/27 22:42:42

文章目录

  • 一、OpenFeign简介
  • 二、OpenFeign中Bean的动态装载
    • 1、@EnableFeignClients
    • 2、registerFeignClients
    • 3、FeignClientFactoryBean.getObject
    • 4、loadBalance
    • 5、DefaultTarget.target
    • 6、ReflectiveFeign.newInstance生成代理
      • (1)targetToHandlersByName.apply(target)
    • 7、小总结
  • 三、OpenFeign调用过程
    • 1、SynchronousMethodHandler.invoke
    • 2、executeAndDecode
  • 总结

一、OpenFeign简介

关于OpenFeign的使用,请移步:
SpringCloud-OpenFeign官方文档使用大全详解

本文主要分析OpenFeign的源码。

二、OpenFeign中Bean的动态装载

关于SpringBoot的Enable模块设计,请参考:
Spring注解驱动原理及源码,深入理解Spring注解驱动

1、@EnableFeignClients

OpenFeign使用@EnableFeignClients 才会开启OpenFeign,我们从下面这个注解进行切入,这个注解开启了FeignClient的解析过程。

@EnableFeignClients("com.demo")

这个注解的声明如下,它用到了一个@Import注解,我们知道Import是用来导入一个配置类的,接下来去看一下FeignClientsRegistrar的定义。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
	// 省略属性
}

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar,它是一个动态注入bean的接口,Spring Boot启动的时候,会去调用这个类中的registerBeanDefinitions来实现动态Bean的装载。它的作用类似于ImportSelector。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
	BeanDefinitionRegistry registry) {
		//注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
		//在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
		//所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}
}

registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册。

registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClientBeanDeifinition 添加到 spring 容器中。

2、registerFeignClients

registerFeignClients 方法,这个方法主要是扫描类路径下所有的@FeignClient注解,然后进行动态Bean的注入。它最终会调用 registerFeignClient 方法。

// org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

	LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
	Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
	final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
	if (clients == null || clients.length == 0) {
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		scanner.setResourceLoader(this.resourceLoader);
		scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
		Set<String> basePackages = getBasePackages(metadata);
		for (String basePackage : basePackages) {
			candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
		}
	}
	else {
		for (Class<?> clazz : clients) {
			candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
		}
	}
	// 在这个方法中,就是去组装BeanDefinition,也就是Bean的定义,然后注册到Spring IOC容器。
	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"));
			// 注册bean
			registerFeignClient(registry, annotationMetadata, attributes);
		}
	}
}


private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
		Map<String, Object> attributes) {
	String className = annotationMetadata.getClassName();
	Class clazz = ClassUtils.resolveClassName(className, null);
	ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
			? (ConfigurableBeanFactory) registry : null;
	String contextId = getContextId(beanFactory, attributes);
	String name = getName(attributes);
	FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
	factoryBean.setBeanFactory(beanFactory);
	factoryBean.setName(name);
	factoryBean.setContextId(contextId);
	factoryBean.setType(clazz);
	factoryBean.setRefreshableClient(isClientRefreshEnabled());
	// BeanDefinitionBuilder是用来构建一个BeanDefinition的,它是通过genericBeanDefinition 来构建的,并且传入了一个FeignClientFactoryBean的类
	BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
		factoryBean.setUrl(getUrl(beanFactory, attributes));
		factoryBean.setPath(getPath(beanFactory, attributes));
		factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
		Object fallback = attributes.get("fallback");
		if (fallback != null) {
			factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
					: ClassUtils.resolveClassName(fallback.toString(), null));
		}
		Object fallbackFactory = attributes.get("fallbackFactory");
		if (fallbackFactory != null) {
			factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
					: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
		}
		// 获取虚拟客户端
		return factoryBean.getObject();
	});
	definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
	definition.setLazyInit(true);
	validate(attributes);

	AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
	beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
	beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

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

	beanDefinition.setPrimary(primary);

	String[] qualifiers = getQualifiers(attributes);
	if (ObjectUtils.isEmpty(qualifiers)) {
		qualifiers = new String[] { contextId + "FeignClient" };
	}

	BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
	BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

	registerOptionsBeanDefinition(registry, contextId);
}

3、FeignClientFactoryBean.getObject

我们可以发现,FeignClient被动态注册成了一个FactoryBean,Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会把所有的FeignClient的BeanDefinition设置为FeignClientFactoryBean类型,而FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。

在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。

工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。

简单来说,FeignClient标注的这个接口,会通过FeignClientFactoryBean.getObject()这个方法获得一个代理对象。

getObject调用的是getTarget方法,它从applicationContext取出FeignContext,FeignContext继承了NamedContextFactory,它是用来来统一维护feign中各个feign客户端相互隔离的上下文。

// org.springframework.cloud.openfeign.FeignClientFactoryBean#getObject
@Override
public Object getObject() {
	return getTarget();
}
<T> T getTarget() {
	// FeignContext注册到容器是在FeignAutoConfiguration上完成的
	// 在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。
	FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
			: applicationContext.getBean(FeignContext.class);
	// 构建feign.builder,在构建时会向FeignContext获取配置的Encoder,Decoder等各种信息。
	Feign.Builder builder = feign(context);

	if (!StringUtils.hasText(url)) { // //如果url为空,则走负载均衡,生成有负载均衡功能的代理类

		if (LOG.isInfoEnabled()) {
			LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
		}
		if (!name.startsWith("http")) {
			url = "http://" + name;
		}
		else {
			url = name;
		}
		url += cleanPath();
		// 判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。实际上他们最终调用的是Target.target()方法。
		return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
	}
	//如果指定了url,则生成默认的代理类
	if (StringUtils.hasText(url) && !url.startsWith("http")) {
		url = "http://" + url;
	}
	String url = this.url + cleanPath();
	Client client = getOptional(context, Client.class);
	if (client != null) {
		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();
		}
		if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
			// not load balancing because we have a url,
			// but Spring Cloud LoadBalancer is on the classpath, so unwrap
			client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
		}
		builder.client(client);
	}
	//生成默认代理类
	Targeter targeter = get(context, Targeter.class);
	// 最终调用
	return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

4、loadBalance

生成具备负载均衡能力的feign客户端,为feign客户端构建起绑定负载均衡客户端。

// org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
	// 从IOC容器中获取一个Client,注意这里的Client是根据serviceId进行隔离的,每个ServiceId获取的Client是不同的
	Client client = getOptional(context, Client.class);
	if (client != null) {
		builder.client(client);
		Targeter targeter = get(context, Targeter.class);
		return targeter.target(this, builder, context, target);
	}

	throw new IllegalStateException(
			"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
}

Client默认是LoadBalancerFeignClient,是在FeignRibbonClientAutoConfiguration中自动注册的:

@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })

protected <T> T loadBalance(Builder builder, FeignContext context,
	HardCodedTarget<T> target) {
		Client client = (Client)this.getOptional(context, Client.class);
		if (client != null) {
			builder.client(client);
			Targeter targeter = (Targeter)this.get(context, Targeter.class);
			return targeter.target(this, builder, context, target);
		} else {
			throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
	}
}

5、DefaultTarget.target

最后执行的target方法,调用DefaultTarget的target方法:

class DefaultTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
			Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}
}

build会返回一个ReflectiveFeign,携带者非常多的参数以及配置信息,最终调用了ReflectiveFeign的newInstance方法。

// feign.Feign.Builder#target(feign.Target<T>)
public <T> T target(Target<T> target) {
  return build().newInstance(target);
}

public Feign build() {
  Client client = Capability.enrich(this.client, capabilities);
  Retryer retryer = Capability.enrich(this.retryer, capabilities);
  List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
      .map(ri -> Capability.enrich(ri, capabilities))
      .collect(Collectors.toList());
  Logger logger = Capability.enrich(this.logger, capabilities);
  Contract contract = Capability.enrich(this.contract, capabilities);
  Options options = Capability.enrich(this.options, capabilities);
  Encoder encoder = Capability.enrich(this.encoder, capabilities);
  Decoder decoder = Capability.enrich(this.decoder, capabilities);
  InvocationHandlerFactory invocationHandlerFactory =
      Capability.enrich(this.invocationHandlerFactory, capabilities);
  QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);

  SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
      new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
          logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
  ParseHandlersByName handlersByName =
      new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
          errorDecoder, synchronousMethodHandlerFactory);
  return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}

6、ReflectiveFeign.newInstance生成代理

这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。

从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。

// feign.ReflectiveFeign#newInstance
@SuppressWarnings("unchecked")
@Override
public <T> T newInstance(Target<T> target) {
	//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
  Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

  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)));
    }
  }
  // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
  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;
}

创建的InvocationHandler 实际上默认是FeignInvocationHandler,也就是说,最终注入的FeignClient接口,实际上调用目标方法时会调用FeignInvocationHandler的invoke方法。

(1)targetToHandlersByName.apply(target)

根据Contract协议规则,解析接口类的注解信息,解析成内部表现:

targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。

// feign.ReflectiveFeign.ParseHandlersByName#apply
public Map<String, MethodHandler> apply(Target target) {
  List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
  Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
  for (MethodMetadata md : metadata) {
    BuildTemplateByResolvingArgs buildTemplate;
    if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
      buildTemplate =
          new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
      buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else {
      buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
    }
    if (md.isIgnored()) {
      result.put(md.configKey(), args -> {
        throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
      });
    } else {
      result.put(md.configKey(),
          factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
    }
  }
  return result;
}

7、小总结

通过上面的分析,我们知道,Feign定义的FeignClient接口,使用@Autowire自动注入时,最终会调用FactoryBean的getObject方法,而getObject方法最终通过jdk动态代理生成一个代理类,调用目标方法时会调用FeignInvocationHandler的invoke方法。

三、OpenFeign调用过程

以下是一个FeignClient的接口实例,当调用order方法时,就会执行FeignInvocationHandler的invoke方法。

@FeignClient(name = "order-service")
public interface OrderSurface {

    /**
     * 触发直播回放定时任务 task -live
     */
    @GetMapping(value = "/order")
    String order();
// feign.ReflectiveFeign.FeignInvocationHandler#invoke
@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();
  }
  // 最终调用的是SynchronousMethodHandler的invoke方法
  return dispatch.get(method).invoke(args);
}

在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。

1、SynchronousMethodHandler.invoke

这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下:

// feign.SynchronousMethodHandler#invoke
@Override
public Object invoke(Object[] argv) throws Throwable {
  // template包含url、method元数据等等所有数据
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  // 选项
  Options options = findOptions(argv);
  // 重试
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      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;
    }
  }
}

在这里插入图片描述

2、executeAndDecode

经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个 executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。

// feign.SynchronousMethodHandler#executeAndDecode
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  //转化为Http请求报文
  Request request = targetRequest(template);

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

  Response response;
  long start = System.nanoTime();
  try {
    //发起远程通信
    response = client.execute(request, options);
    // ensure the request is set. TODO: remove in Feign 12
    //获取返回结果
    response = response.toBuilder()
        .request(request)
        .requestTemplate(template)
        .build();
  } 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);


  if (decoder != null)
    return decoder.decode(response, metadata.returnType());

  CompletableFuture<Object> resultFuture = new CompletableFuture<>();
  asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
      metadata.returnType(),
      elapsedTime);

  try {
    if (!resultFuture.isDone())
      throw new IllegalStateException("Response handling not done");

    return resultFuture.join();
  } catch (CompletionException e) {
    Throwable cause = e.getCause();
    if (cause != null)
      throw cause;
    throw e;
  }
}

最终拼装http请求信息,将请求发送出去。

总结

大致上来说,Feign将接口进行了代理,通过代理类最终拼装http请求发送到指定的服务提供者,并且还有负载均衡的功能。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言字符及字符串讲解

文章目录 前言一、字符介绍和使用二、字符串介绍和使用三、字符串操作函数四、字符串的长度和字符串所占内存空间的大小总结 前言 C 语言中的字符和字符串是常用的数据类型。字符是一个单个的字母、数字、标点符号或者其他可打印的符号&#xff0c;使用单引号 ’ ’ 表示&…

Linux route命令实战:route 命令实战教程,配置静态路由,删除路由表项

前言 大家好&#xff0c;又见面了&#xff0c;我是沐风晓月&#xff0c;本文是专栏【linux基本功-基础命令实战】的第61篇文章。 专栏地址&#xff1a;[linux基本功-基础命令专栏] &#xff0c; 此专栏是沐风晓月对Linux常用命令的汇总&#xff0c;希望能够加深自己的印象&am…

使用大型语言模(LLM)构建系统(四):链式提示

今天我学习了DeepLearning.AI的 Building Systems with LLM 的在线课程&#xff0c;我想和大家一起分享一下该门课程的一些主要内容。 下面是我们访问大型语言模(LLM)的主要代码&#xff1a; import openai#您的openai的api key openai.api_key YOUR-OPENAI-API-KEY def get_…

word如何转化为pdf格式?分享四个方法给大家!

在工作和学习中&#xff0c;经常需要对文档进行转换&#xff0c;其中将Word文档转换为PDF是最常见的格式转换之一。下面介绍几种常用的转换方法&#xff0c;包括使用记灵在线工具。 方法一&#xff1a;使用Word软件直接转换 如果你使用的是电脑上的Word软件&#xff0c;可以直…

vue3ts安装sass(scss)

序 1、我附上个sass的github&#xff08;跟本教程无关&#xff09;地址GitHub - sass/sass: Sass makes CSS fun! 2、博主本地环境 "vue": "^3.2.47", "typescript": "^5.0.2" "vite": "^4.3.9", node18.12.1 3、…

外贸企业必看!这五种企业邮箱最适合你的跨国业务需求

在当今的数字世界中&#xff0c;电子邮件的使用对任何外贸企业的成功都至关重要。在技术的冲击下&#xff0c;企业开展运营以及与客户、潜在客户和合作伙伴沟通的方式发生了巨大变化&#xff0c;电子邮件迅速成为外贸中首选的沟通方式。 说到哪种企业邮箱最适合外贸企业使用&am…

MATLAB使用技巧之局部放大图的制作及文本箭头的便捷设置

MATLAB使用技巧之局部放大图的制作及文本箭头的便捷设置 文章目录 MATLAB使用技巧之局部放大图的制作及文本箭头的便捷设置制作局部放大图的方法文本箭头的便捷设置小结 本文主要介绍如何在MATLAB中绘制局部放大图和如何便捷地设置文本箭头的相关内容&#xff0c;以作后续回顾之…

CISP-PTE2022最新考试经验分享

CISP_PTE2022年10月份考试心得体会 2022年9月份由于公司需要&#xff0c;参加了中启航的CISPPTE培训&#xff0c;总培训时间八天&#xff0c;8师傅讲的很好&#xff0c;浅显易懂&#xff0c;经过4天的理论学习和4天的实操练习&#xff0c;经过十一假期的熟练&#xff0c;我在10…

2005-2021年全国及31省绿色信贷水平(含原始数据和测算过程)

1、时间&#xff1a;2005-2021年 2、范围&#xff1a;全国及31省市 4、内容说明&#xff1a;包含原始数据、计算结果、计算过程 5、来源&#xff1a;工业NJ、2018年经济普查、其中2017年缺失已采用插值法补齐 6、计算说明&#xff1a; 选取各省六大高耗能产业利息支出占工…

Unity3D:Project窗口

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 Project 窗口 “项目”窗口显示与项目相关的所有文件&#xff0c;是您在应用程序中导航和查找资源和其他项目文件的主要方式。默认情况下&#xff0c;当您启动新项目时&#xff0c…

这里推荐几个前端icon网站(动图网站)

1. Loading.ioLoading.io 是一个免费的加载动效(Loading animations)图标库。它提供了多种风格的加载动效图标,包括 SVG、CSS 和 Lottie 动画格式。这些加载图标可以增强用户体验,为网站和应用程序添加更佳的视觉效果。 网站地址:loading.io - Your SVG GIF PNG Ajax Loading…

Vue 中如何处理树形结构数据渲染与操作?

Vue 中如何处理树形结构数据渲染与操作&#xff1f; 在实际开发中&#xff0c;我们经常会遇到需要渲染树形结构数据的情况&#xff0c;例如商品分类、组织架构、地区选择等等。Vue 提供了一些便捷的方法和工具来处理树形结构数据的渲染和操作&#xff0c;本文将介绍 Vue 处理树…

web前端要怎么样自学?

前言 前端入门相关的路线图以及资源都帮大噶准备好啦&#xff0c;希望对想要入门前端的小伙伴们有所帮助~ 先放上前端学习的思维导图 &#xff1a; 学习前准备&#xff1a;编译器 编译器方面的选择推荐HBuilder X或者Vscode&#xff0c;运行环境在浏览器&#xff0c;推荐是ch…

Mysql锁机制简介

一、什么是锁 锁是系数据库统区别于文件系统的一个关键特性。 锁机制用于管理对共享资源的并发访问&#xff0c;提供数据的完整性和一致性。 InnoDB存储引擎不仅会在行级别上对表数据上锁&#xff0c;还会在数据库内部其他多个地方使用锁&#xff0c;从而允许对多种不同资源…

【项目实战】一、Spring boot整合JWT、Vue案例

前言 通过Spring boot整合JWT、Vue案例&#xff0c;其中融合了微服务网关、微服务等。 1、若无公共模块&#xff0c;先添加公共模块 1.1、创建模块&#xff1a;common-service 1.2、修改父项的pom文件 1.2.1、给springCloud父项添加子模块 1.2.2、添加common-service的全局…

FinClip | 日子过的飞快,又来汇报了

FinClip 的使命是使您&#xff08;业务专家和开发人员&#xff09;能够通过小程序解决关键业务流程挑战&#xff0c;并完成数字化转型的相关操作。不妨让我们看看在本月的产品与市场发布亮点&#xff0c;看看是否有助于您实现目标。 产品方面的相关动向&#x1f447;&#x1f…

【021】C/C++字符串处理函数

C/C字符串处理函数 引言一、字符串操作函数1.1、测量字符串的长度strlen1.2、字符串拷贝函数strcpy1.3、字符串追加函数strcat1.4、字符串比较函数strcmp 二、字符串查找函数2.1、字符串查找字符函数strchr2.2、字符串查找子串函数strstr 三、其他字符串处理函数3.1、字符串分割…

结构型设计模式04-适配器模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 适配器模式 1、适配器模式介绍 适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计…

chatgpt赋能python:用Python实现文本数字转换:从123到一二三

用Python实现文本数字转换&#xff1a;从123到一二三 在网站开发中&#xff0c;我们经常需要将数字转换成文字&#xff0c;比如将123转成“一百二十三”。这种数字转文字的需求&#xff0c;既方便了用户的阅读&#xff0c;也提高了网站的可读性和SEO效果。 在本文中&#xff…

定时任务原理方案综述 | 京东云技术团队

本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行&#xff0c;并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持&#xff0c;总结当前各种定时任务原理&#xff0c;从定时任务基础原理、单…