springboot通过AsyncConfigurer接口实现异步线程池自动化配置组件

springboot提供了org.springframework.scheduling.annotation.AsyncConfigurer接口让开发人员可以自定义线程池执行器;框架默认提供了一个空的实现类AsyncConfigurerSupport,不过两个方法体内部提供的都是空实现;

1.首先看下AsyncConfigurer接口
public interface AsyncConfigurer {

	/**
	 * 方法返回一个实际执行线程的线程池
	 */
	@Nullable
	default Executor getAsyncExecutor() {
		return null;
	}

	/**
	 * 当线程池执行异步任务时会抛出AsyncUncaughtExceptionHandler异常,
	 * 此方法会捕获该异常
	 */
	@Nullable
	default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return null;
	}

}
2.通过上面的接口实现异步线程池

先看下我们的目标:

  • 自定义一个异步线程池替换掉springboot框架默认的线程池;
  • 自动捕获异步线程实例抛出的异常;
  • 线程池的核心指标可以通过配置文件自定义配置;
  • 封装成为一个组件,可以通过配置控制容器是否加载;

自动化配置类如下:

package com.sgrain.boot.autoconfigure.threadpool;

import com.sgrain.boot.common.exception.PrintExceptionInfo;
import com.sgrain.boot.common.utils.LoggerUtils;
import com.sgrain.boot.common.utils.constant.CharacterUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @program: spring-parent
 * @description: 异步线程池配置 AsyncConfigurer在applicationContext早期初始化,如果需要依赖于其它的bean,尽可能的将它们声明为lazy
 * @create: 2020/08/21
 */
@EnableAsync
@Configuration
@EnableConfigurationProperties(AsyncThreadPoolProperties.class)
@ConditionalOnProperty(prefix = "spring.sgrain.async-thread-pool", name = "enable", havingValue = "true", matchIfMissing = false)
public class AsyncThreadPoolAutoConfiguration implements AsyncConfigurer {

    @Autowired
    private AsyncThreadPoolProperties asyncThreadPoolProperties;

    /**
     * 定义线程池
     * 使用{@link java.util.concurrent.LinkedBlockingQueue}(FIFO)队列,是一个用于并发环境下的阻塞队列集合类
     * ThreadPoolTaskExecutor不是完全被IOC容器管理的bean,可以在方法上加上@Bean注解交给容器管理,这样可以将taskExecutor.initialize()方法调用去掉,容器会自动调用
     *
     * @return
     */
    @Bean("asyncTaskExecutor")
    @Override
    public Executor getAsyncExecutor() {
        //Java虚拟机可用的处理器数
        int processors = Runtime.getRuntime().availableProcessors();
        //定义线程池
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //核心线程数
        taskExecutor.setCorePoolSize(Objects.nonNull(asyncThreadPoolProperties.getCorePoolSize()) ? asyncThreadPoolProperties.getCorePoolSize() : processors);
        //线程池最大线程数,默认:40000
        taskExecutor.setMaxPoolSize(Objects.nonNull(asyncThreadPoolProperties.getMaxPoolSize()) ? asyncThreadPoolProperties.getMaxPoolSize() : 40000);
        //线程队列最大线程数,默认:80000
        taskExecutor.setQueueCapacity(Objects.nonNull(asyncThreadPoolProperties.getMaxPoolSize()) ? asyncThreadPoolProperties.getMaxPoolSize() : 80000);
        //线程名称前缀
        taskExecutor.setThreadNamePrefix(StringUtils.isNotEmpty(asyncThreadPoolProperties.getThreadNamePrefix()) ? asyncThreadPoolProperties.getThreadNamePrefix() : "Async-ThreadPool-");
        //线程池中线程最大空闲时间,默认:60,单位:秒
        taskExecutor.setKeepAliveSeconds(asyncThreadPoolProperties.getKeepAliveSeconds());
        //核心线程是否允许超时,默认:false
        taskExecutor.setAllowCoreThreadTimeOut(asyncThreadPoolProperties.isAllowCoreThreadTimeOut());
        //IOC容器关闭时是否阻塞等待剩余的任务执行完成,默认:false(必须设置setAwaitTerminationSeconds)
        taskExecutor.setWaitForTasksToCompleteOnShutdown(asyncThreadPoolProperties.isWaitForTasksToCompleteOnShutdown());
        //阻塞IOC容器关闭的时间,默认:10秒(必须设置setWaitForTasksToCompleteOnShutdown)
        taskExecutor.setAwaitTerminationSeconds(asyncThreadPoolProperties.getAwaitTerminationSeconds());
        /**
         * 拒绝策略,默认是AbortPolicy
         * AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
         * DiscardPolicy:丢弃任务但不抛出异常
         * DiscardOldestPolicy:丢弃最旧的处理程序,然后重试,如果执行器关闭,这时丢弃任务
         * CallerRunsPolicy:执行器执行任务失败,则在策略回调方法中执行任务,如果执行器关闭,这时丢弃任务
         */
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //初始化
        //taskExecutor.initialize();

        return taskExecutor;
    }

    /**
     * 异步方法执行的过程中抛出的异常捕获
     *
     * @return
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, objects) -> {
            String msg = StringUtils.EMPTY;
            if (ArrayUtils.isNotEmpty(objects) && objects.length > 0) {
                msg = StringUtils.join(msg, "参数是:");
                for (int i = 0; i < objects.length; i++) {
                    msg = StringUtils.join(msg, objects[i], CharacterUtils.ENTER);
                }
            }
            if (Objects.nonNull(throwable)) {
                msg = StringUtils.join(msg, PrintExceptionInfo.printErrorInfo(throwable));
            }
            LoggerUtils.error(method.getDeclaringClass(), msg);
        };
    }
}

线程池的自定义属性及异常处理代码中的注解已经标的很清晰了,不在重复说明;@Configuration注解描述当前类是一个配置类,@EnableAsync注解描述启动线程池执行器开启异步执行功能;@EnableConfigurationProperties注解描述启动对@ConfigurationProperties注解标注的bean的支持,该bean可以被注入到IOC容器之中;@ConditionalOnProperty是一个条件注解,用来控制容器是否将当前的配置类注入到IOC容器之中;

线程池使用到的外部属性配置类如下:

package com.sgrain.boot.autoconfigure.threadpool;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @program: spring-parent
 * @description: 异步线程池配置文件
 * @create: 2020/08/21
 */
@ConfigurationProperties(prefix = "spring.sgrain.async-thread-pool")
public class AsyncThreadPoolProperties {
    /**
     * 是否启动异步线程池,默认 false
     */
    private boolean enable;
    /**
     * 核心线程数,默认:Java虚拟机可用线程数
     */
    private Integer corePoolSize;
    /**
     * 线程池最大线程数,默认:40000
     */
    private Integer maxPoolSize;
    /**
     * 线程队列最大线程数,默认:80000
     */
    private Integer queueCapacity;
    /**
     * 自定义线程名前缀,默认:Async-ThreadPool-
     */
    private String threadNamePrefix;
    /**
     * 线程池中线程最大空闲时间,默认:60,单位:秒
     */
    private Integer keepAliveSeconds = 60;
    /**
     * 核心线程是否允许超时,默认false
     */
    private boolean allowCoreThreadTimeOut;
    /**
     * IOC容器关闭时是否阻塞等待剩余的任务执行完成,默认:false(必须设置setAwaitTerminationSeconds)
     */
    private boolean waitForTasksToCompleteOnShutdown;
    /**
     * 阻塞IOC容器关闭的时间,默认:10秒(必须设置setWaitForTasksToCompleteOnShutdown)
     */
    private int awaitTerminationSeconds = 10;

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public Integer getCorePoolSize() {
        return corePoolSize;
    }

    public void setCorePoolSize(Integer corePoolSize) {
        this.corePoolSize = corePoolSize;
    }

    public Integer getMaxPoolSize() {
        return maxPoolSize;
    }

    public void setMaxPoolSize(Integer maxPoolSize) {
        this.maxPoolSize = maxPoolSize;
    }

    public Integer getQueueCapacity() {
        return queueCapacity;
    }

    public void setQueueCapacity(Integer queueCapacity) {
        this.queueCapacity = queueCapacity;
    }

    public String getThreadNamePrefix() {
        return threadNamePrefix;
    }

    public void setThreadNamePrefix(String threadNamePrefix) {
        this.threadNamePrefix = threadNamePrefix;
    }

    public Integer getKeepAliveSeconds() {
        return keepAliveSeconds;
    }

    public void setKeepAliveSeconds(Integer keepAliveSeconds) {
        this.keepAliveSeconds = keepAliveSeconds;
    }

    public boolean isAllowCoreThreadTimeOut() {
        return allowCoreThreadTimeOut;
    }

    public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
        this.allowCoreThreadTimeOut = allowCoreThreadTimeOut;
    }

    public boolean isWaitForTasksToCompleteOnShutdown() {
        return waitForTasksToCompleteOnShutdown;
    }

    public void setWaitForTasksToCompleteOnShutdown(boolean waitForTasksToCompleteOnShutdown) {
        this.waitForTasksToCompleteOnShutdown = waitForTasksToCompleteOnShutdown;
    }

    public int getAwaitTerminationSeconds() {
        return awaitTerminationSeconds;
    }

    public void setAwaitTerminationSeconds(int awaitTerminationSeconds) {
        this.awaitTerminationSeconds = awaitTerminationSeconds;
    }
}

properties属性配置示例如下:

#异步线程池
#异步线程池组件开关,默认false
spring.sgrain.async-thread-pool.enable=true
#核心线程数,默认:Java虚拟机可用线程数
spring.sgrain.async-thread-pool.core-pool-size=4
#线程池最大线程数,默认:40000
spring.sgrain.async-thread-pool.max-pool-size=40000
#线程队列最大线程数,默认:80000
spring.sgrain.async-thread-pool.queue-capacity=80000
#自定义线程名前缀,默认:Async-ThreadPool-
spring.sgrain.async-thread-pool.thread-name-prefix=Async-ThreadPool-
#线程池中线程最大空闲时间,默认:60,单位:秒
spring.sgrain.async-thread-pool.keep-alive-seconds=60
#核心线程是否允许超时,默认false
spring.sgrain.async-thread-pool.allow-core-thread-time-out=false
#IOC容器关闭时是否阻塞等待剩余的任务执行完成,默认:false(必须设置setAwaitTerminationSeconds)
spring.sgrain.async-thread-pool.wait-for-tasks-to-complete-on-shutdown=false
#阻塞IOC容器关闭的时间,默认:10秒(必须设置setWaitForTasksToCompleteOnShutdown)
spring.sgrain.async-thread-pool.await-termination-seconds=10

上面的开发任务都做好之后还需要最后一步,将com.sgrain.boot.autoconfigure.threadpool.AsyncThreadPoolAutoConfiguration配置到resources/META-INF目录的spring.factories文件中:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.sgrain.boot.autoconfigure.threadpool.AsyncThreadPoolAutoConfiguration

看到配置的属性key是EnableAutoConfiguration大概你就可以猜测到是为了启用自动化配置功能;到现在整个异步线程池自动化配置组件已经开发完成了,那如何使用呢?我就不再举例说明了,网上有很多示例,只说几个重点;

  • 使用@Async注解标注方法为异步任务
  • 异步任务返回值为void
  • 异步任务方法必须使用@Override标注,即是一个接口的实现方法
3.异步线程池原理分析

springboot框架自带ThreadPoolTaskExecutor线程池,org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration自动化配置类自动的创建默认的线程池,如果没有自定义ThreadPoolTaskExecutor线程池,那么@EnableAsync异步线程自动关联默认的线程池;

  • 看下@EnableAsync的源码是如何启动异步执行器的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {

	Class<? extends Annotation> annotation() default Annotation.class;
	boolean proxyTargetClass() default false;
	AdviceMode mode() default AdviceMode.PROXY;
	int order() default Ordered.LOWEST_PRECEDENCE;

}

上述代码的核心是@Import注解导入的AsyncConfigurationSelector选择器类;

  • AsyncConfigurationSelector选择器类用来确定具体实现的配置类,一共有两种模式一种是使用JDK自带的动态代理模式实现动态代理,另外一种是使用ASPECTJ实现动态代理;默认使用JDK模式;
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

	private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
			"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";


	/**
	 * Returns {@link ProxyAsyncConfiguration} or {@code AspectJAsyncConfiguration}
	 * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableAsync#mode()},
	 * respectively.
	 */
	@Override
	@Nullable
	public String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {ProxyAsyncConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
			default:
				return null;
		}
	}

}
  • ProxyAsyncConfiguration配置类源码如下
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {

	@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
		Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
		AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
		bpp.configure(this.executor, this.exceptionHandler);
		Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
		if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
			bpp.setAsyncAnnotationType(customAsyncAnnotation);
		}
		bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
		bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
		return bpp;
	}

}

上面的类是AbstractAsyncConfiguration类的子类,该类通过setConfigurers方法注入实现了AsyncConfigurer配置的自定义线程池,源码如下:

	@Autowired(required = false)
	void setConfigurers(Collection<AsyncConfigurer> configurers) {
		if (CollectionUtils.isEmpty(configurers)) {
			return;
		}
		if (configurers.size() > 1) {
			throw new IllegalStateException("Only one AsyncConfigurer may exist");
		}
		AsyncConfigurer configurer = configurers.iterator().next();
		this.executor = configurer::getAsyncExecutor;
		this.exceptionHandler = configurer::getAsyncUncaughtExceptionHandler;
	}

ProxyAsyncConfiguration源码中的AsyncAnnotationBeanPostProcessor是一个Bean的后置处理器,所以会在setBeanFactory方法中对AOP切面类AsyncAnnotationAdvisor进行了初始化,源码如下:

	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		super.setBeanFactory(beanFactory);

		AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
		if (this.asyncAnnotationType != null) {
			advisor.setAsyncAnnotationType(this.asyncAnnotationType);
		}
		advisor.setBeanFactory(beanFactory);
		this.advisor = advisor;
	}

切面类的构造函数中有两个核心点,对通知和切点进行初始化,源码如下:

	public AsyncAnnotationAdvisor(
			@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {

		Set<Class<? extends Annotation>> asyncAnnotationTypes = new LinkedHashSet<>(2);
		asyncAnnotationTypes.add(Async.class);
		try {
			asyncAnnotationTypes.add((Class<? extends Annotation>)
					ClassUtils.forName("javax.ejb.Asynchronous", AsyncAnnotationAdvisor.class.getClassLoader()));
		}
		catch (ClassNotFoundException ex) {
			// If EJB 3.1 API not present, simply ignore.
		}
		this.advice = buildAdvice(executor, exceptionHandler);
		this.pointcut = buildPointcut(asyncAnnotationTypes);
	}

其中buildAdvice方法对拦截器AnnotationAsyncExecutionInterceptor进行了初始化,该类实现了MethodInterceptor接口,源码如下:

	protected Advice buildAdvice(
			@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {

		AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
		interceptor.configure(executor, exceptionHandler);
		return interceptor;
	}

上述拦截器类调用了configure方法,该方法对默认线程池和异常执行器进行了初始化,源码如下:

	public void configure(@Nullable Supplier<Executor> defaultExecutor,
			@Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {

		this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory));
		this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new);
	}

这个地方是一个关键点,如果没有自定义实现AsyncConfigurer接口,则此处获取到的是默认的线程池,如果自定义实现了AsyncConfigurer接口,则此处获取到的就是自定义线程池;

  • 接下来我们看下拦截器的invoke方法,此方法是在AnnotationAsyncExecutionInterceptor的父类AsyncExecutionInterceptor中,源码如下:
	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
		Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
		final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
		//此处是用来获取具体执行线程任务线程池
		AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
		if (executor == null) {
			throw new IllegalStateException(
					"No executor specified and no default executor set on AsyncExecutionInterceptor either");
		}

		Callable<Object> task = () -> {
			try {
				Object result = invocation.proceed();
				if (result instanceof Future) {
					return ((Future<?>) result).get();
				}
			}
			catch (ExecutionException ex) {
				handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
			}
			catch (Throwable ex) {
				handleError(ex, userDeclaredMethod, invocation.getArguments());
			}
			return null;
		};

		return doSubmit(task, executor, invocation.getMethod().getReturnType());
	}

上述代码的核心determineAsyncExecutor方法用来确定执行线程的线程池,源码如下:

@Nullable
	protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
		AsyncTaskExecutor executor = this.executors.get(method);
		if (executor == null) {
			Executor targetExecutor;
			String qualifier = getExecutorQualifier(method);
			if (StringUtils.hasLength(qualifier)) {
				targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
			}
			else {
				targetExecutor = this.defaultExecutor.get();
			}
			if (targetExecutor == null) {
				return null;
			}
			executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
					(AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
			this.executors.put(method, executor);
		}
		return executor;
	}

至此,通过@EnableAsync注解启动异步线程池,如何加载默认线程池配置,如何定义AOP切面及拦截器,通过@Async标注异步任务如何确定执行的线程池的原理及源码分析已经完成。

GitHub源码:https://github.com/mingyang66/spring-parent

### 回答1: 在Spring Boot中,我们可以通过自定义异步线程池来优化应用程序的性能。下面是一个简单的示例,演示如何在Spring Boot应用程序中自定义异步线程池: 1. 首先,我们需要在Spring Boot应用程序的配置文件中定义异步线程池的属性: ``` spring.task.execution.pool.core-size=10 spring.task.execution.pool.max-size=20 spring.task.execution.pool.queue-capacity=1000 ``` 上述配置定义了异步线程池的核心大小、最大大小和队列容量。 2. 然后,我们需要创建一个自定义的异步线程池配置类,如下所示: ``` @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("MyCustomExecutor-"); executor.initialize(); return executor; } } ``` 上述配置实现AsyncConfigurer接口,并覆盖了getAsyncExecutor()方法,该方法返回一个自定义的ThreadPoolTaskExecutor对象,其中设置了异步线程池的核心大小、最大大小、队列容量和线程名称前缀。 3. 最后,在需要异步执行的方法上添加@Async注解即可: ``` @Service public class MyService { @Async public void doSomethingAsync() { // 异步执行的代码 } } ``` 上述示例中,我们在MyService类的doSomethingAsync()方法上添加了@Async注解,表示该方法需要异步执行。 这样,我们就成功地自定义了Spring Boot应用程序中的异步线程池。 ### 回答2: 在Spring Boot中,可以通过配置来自定义异步线程池。我们可以利用`@EnableAsync`注解开启异步支持,并使用`@Async`注解来标识需要异步执行的方法。 首先,在Spring Boot的配置文件(如application.properties或application.yml)中配置线程池的相关属性,例如线程池核心线程数、最大线程数、队列容量等。示例配置如下: ``` spring.task.execution.pool.core-size=10 spring.task.execution.pool.max-size=20 spring.task.execution.pool.queue-capacity=100 ``` 接下来,在启动类上添加`@EnableAsync`注解,以开启异步支持。例如: ```java @SpringBootApplication @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 然后,我们可以在需要异步执行的方法上添加`@Async`注解,以告知Spring该方法需要异步执行。例如: ```java @Service public class MyService { @Async public void asyncMethod() { // 异步执行的方法体 } } ``` 在上述示例中,`asyncMethod()`方法会被Spring框架异步执行,Spring会自动从线程池中获取一个线程来执行该方法。 通过以上步骤,我们就可以在Spring Boot中自定义异步线程池。我们可以根据具体的业务需求来调整线程池配置,以达到最佳的性能和资源利用效果。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值