SpringBoot:异步任务基础与源码剖析

news2024/12/24 11:44:53

         官网文档:How To Do @Async in Spring | Baeldung。

@Async注解

Spring框架基于@Async注解提供了对异步执行流程的支持。

最简单的例子是:使用@Async注解修饰一个方法,那么这个方法将在一个单独的线程中被执行,即:从同步执行流程转换为异步执行流程。 

此外,Spring框架中,事件Event也是支持异步处理操作的。

 @EnableAsync注解|核心接口

通过在配置类上添加@EnableAsync注解,可以为Spring应用程序启用异步执行流程的支持

@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }

        该注解提供了一些可配置属性,

 annotation:默认情况下,@EnableAsync会告诉Spring程序去探查所有被@Async注解修饰的、以及@javax.ejb.Asynchronous.注解;

mode:指定异步流程生效的方式:JDK Proxy还是AspectJ;

proxtTargetClass:只有在mode为AdviceMode.PROXY(JDK动态代理)时才会生效,用于指定要使用的动态代理类型:CGLIB或者JDK;

order:设置AsyncAnnotationBeanPostProcessor执行异步调用的顺序。

mode可选值
AsyncAnnotationBeanPostProcessor-执行异步流程的调用

        而在AsyncAnnotationBeanPostProcessor类的内部,则是通过TaskExecutor提供了一个线程池,来更加具体的负责执行某一个异步流程的。

        再往深入查看,就会发现,该接口的父接口Executor,与之相关的就是我们经常谈论的和并发编程相关的Executor框架了。

        再看Spring框架内部提供的TaskExecutor接口的继承结构,如下图所示,

         因此,要使用@EnableAsync注解开启异步流程执行的支持,可能就需要去对TaskExecutor接口实例的线程池参数进行配置。

<task:executor id="myexecutor" pool-size="5"  />
<task:annotation-driven executor="myexecutor"/>

SpringBoot默认线程池配置

 根据以上解读,不难发现:其实SpringBoot内部是通过维护线程池的方式去执行异步任务的,那么,这个默认的线程池对应于Exector框架的哪一个实现子类?相关的配置参数又是什么呢?

        要解决上述疑惑,需要先去找到TaskExecutionAutoConfiguration类,该类定义了默认注入的线程池实例及其配置参数。

默认线程池类型:ThreadPoolTaskExecutor

默认线程池配置参数:TaskExecutionProperties.Pool

        详细参数信息,对应于一个静态内部类Pool,源码如下,

public static class Pool {

		/**
		 * Queue capacity. An unbounded capacity does not increase the pool and therefore
		 * ignores the "max-size" property.
		 */
		private int queueCapacity = Integer.MAX_VALUE;

		/**
		 * Core number of threads.
		 */
		private int coreSize = 8;

		/**
		 * Maximum allowed number of threads. If tasks are filling up the queue, the pool
		 * can expand up to that size to accommodate the load. Ignored if the queue is
		 * unbounded.
		 */
		private int maxSize = Integer.MAX_VALUE;

		/**
		 * Whether core threads are allowed to time out. This enables dynamic growing and
		 * shrinking of the pool.
		 */
		private boolean allowCoreThreadTimeout = true;

		/**
		 * Time limit for which threads may remain idle before being terminated.
		 */
		private Duration keepAlive = Duration.ofSeconds(60);

		public int getQueueCapacity() {
			return this.queueCapacity;
		}

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

		public int getCoreSize() {
			return this.coreSize;
		}

		public void setCoreSize(int coreSize) {
			this.coreSize = coreSize;
		}

		public int getMaxSize() {
			return this.maxSize;
		}

		public void setMaxSize(int maxSize) {
			this.maxSize = maxSize;
		}

		public boolean isAllowCoreThreadTimeout() {
			return this.allowCoreThreadTimeout;
		}

		public void setAllowCoreThreadTimeout(boolean allowCoreThreadTimeout) {
			this.allowCoreThreadTimeout = allowCoreThreadTimeout;
		}

		public Duration getKeepAlive() {
			return this.keepAlive;
		}

		public void setKeepAlive(Duration keepAlive) {
			this.keepAlive = keepAlive;
		}

	}

从中可以找到默认的配置参数,

默认线程池配置参数

@Async注解使用

使用时的两个限制

1.它只能应用于公共方法

2.从同一个类中调用异步方法将不起作用(会绕过代理,而直接去调用底层方法本身)

注解修饰对象

查看@Async注解的源码,可看到:它用于修饰class、interface、method,并且提供了一个value属性,用于在@Autowired和@@Qualifier注解组合自动装配时,指定要使用哪一个线程池。因为原则上来讲,我们是可以通过@Bean注解,在一个Spring容器中注入多个线程池实例的。

package org.springframework.scheduling.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation that marks a method as a candidate for <i>asynchronous</i> execution.
 *
 * <p>Can also be used at the type level, in which case all the type's methods are
 * considered as asynchronous. Note, however, that {@code @Async} is not supported
 * on methods declared within a
 * {@link org.springframework.context.annotation.Configuration @Configuration} class.
 *
 * <p>In terms of target method signatures, any parameter types are supported.
 * However, the return type is constrained to either {@code void} or
 * {@link java.util.concurrent.Future}. In the latter case, you may declare the
 * more specific {@link org.springframework.util.concurrent.ListenableFuture} or
 * {@link java.util.concurrent.CompletableFuture} types which allow for richer
 * interaction with the asynchronous task and for immediate composition with
 * further processing steps.
 *
 * <p>A {@code Future} handle returned from the proxy will be an actual asynchronous
 * {@code Future} that can be used to track the result of the asynchronous method
 * execution. However, since the target method needs to implement the same signature,
 * it will have to return a temporary {@code Future} handle that just passes a value
 * through: e.g. Spring's {@link AsyncResult}, EJB 3.1's {@link javax.ejb.AsyncResult},
 * or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}.
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {

	/**
	 * A qualifier value for the specified asynchronous operation(s).
	 * <p>May be used to determine the target executor to be used when executing
	 * the asynchronous operation(s), matching the qualifier value (or the bean
	 * name) of a specific {@link java.util.concurrent.Executor Executor} or
	 * {@link org.springframework.core.task.TaskExecutor TaskExecutor}
	 * bean definition.
	 * <p>When specified on a class-level {@code @Async} annotation, indicates that the
	 * given executor should be used for all methods within the class. Method-level use
	 * of {@code Async#value} always overrides any value set at the class level.
	 * @since 3.1.2
	 */
	String value() default "";

}

使用方式1:修饰方法method

根据上面的使用限制,被@Async注解修饰的方法,和主调方法不能位于同一个类中,并且也必须是public类型的公共方法。

package com.example.soiladmin;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

//@Async注解测试类
@Component
public class AsyncTestMethod {

    @Async
    public void asyncTest(){
        try {
            System.out.println(String.format("start:%s",Thread.currentThread().getName()));
            Thread.sleep(1500);
            System.out.println(String.format("end:%s",Thread.currentThread().getName()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

        调用方法,

注意到:被@Async修饰的方法线程睡眠了1.5s,如果它是异步执行的,那么就不会阻塞后面for循环的执行。

    @Autowired
    private AsyncTestMethod asyncTestMethod;

    @Test
    public void asyncTestMethod_1(){
        asyncTestClass.asyncTest();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(300);
                System.out.println(String.format("i=%d\n",i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

        打印结果,

使用方式2:修饰类class

 为了方便,我们直接将使用方式1中的类拷贝一份,然后使用@Async注解修饰class类,而非Method方法。

package com.example.soiladmin;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Async
public class AsyncTestClass {

    public void asyncTest(){
        try {
            System.out.println(String.format("start:%s",Thread.currentThread().getName()));
            Thread.sleep(1500);
            System.out.println(String.format("end:%s",Thread.currentThread().getName()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

        主调方法,

    @Autowired
    private AsyncTestClass asyncTestClass;

    @Test
    public void asyncTestClass_2(){
        asyncTestClass.asyncTest();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(300);
                System.out.println(String.format("i=%d\n",i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

        打印结果,

使用方式3:带返回值的方法

以上测试案例都是不带返回值的,但是一般情况下,我们可能还希望获取异步执行的结果,然后对结果进行合并、分析等,那么就可以为@Async注解修饰的方法声明一java.util.concurrent接口类型的返回类型。

但是这里有一个注意点:就是要Future是一个接口,我们没办法直接去new一个接口,所以还要找到Future接口的实现子类。

比较常用的是Spring框架提供的实现子类AsyncResult, 

        我们继续简单看一下AsyncResult实现子类的基本结构,

基本上提供了获取异步执行结果的get方法、对成功/失败情况进行处理的回调函数addCallBack、将返回结果继续进行封装为AsyncResult类型值的forValue,简单来讲,就是对jdk原生的concurrent包下的Future接口进行了功能拓展和增强。

         异步方法如下,


    /**
     * 数列求和: An = 2 ^ n(n>=0),求累加和S(n)---这里为了测试效果(出于增加耗时考虑),不直接使用求和公式
     * @param n
     * @return
     */
    @Async
    public ListenableFuture<Double> asyncSequenceSum(int n){
        Double sum = 0.0;
        for (int i = 1; i <= n; i++) {
            sum += Math.pow(2,i);
        }
        return new AsyncResult<>(sum);
    }

        测试方法如下,

以下两种方式都可以拿到执行结果,调用回调函数。

官网给的是第二种写法,

@Test
    public void asyncTestMethodWithReturns_nor(){
        System.out.println("开始执行...");
        ListenableFuture<Double> asyncResult = asyncTestMethod.asyncSequenceSum(10);
        //添加回调函数
        asyncResult.addCallback(
                new SuccessCallback<Double>() {
                    @Override
                    public void onSuccess(Double result) {
                        System.out.println("执行成功:" + result.doubleValue());
                    }
                },
                new FailureCallback() {
                    @Override
                    public void onFailure(Throwable ex) {
                        System.out.println("执行失败:"+ex.getMessage());
                    }
                }
        );
        //直接尝试获取结果-[只能拿到结果,如果执行出错,就会抛出异常]
        try {
            Double aDouble = asyncResult.get();
            System.out.println("计算结果:"+aDouble.doubleValue());
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }


    @Test
    public void asyncTestMethodWithReturns_normal(){
        System.out.println("开始执行...");
        ListenableFuture<Double> asyncResult = asyncTestMethod.asyncSequenceSum(10);
        //等待执行结果
        while (true){
            if (asyncResult.isDone()){
                //直接尝试获取结果-[只能拿到结果,如果执行出错,就会抛出异常]
                try {
                    Double aDouble = asyncResult.get();
                    System.out.println("计算结果:"+aDouble.doubleValue());
                    //添加回调函数
                    asyncResult.addCallback(
                            new SuccessCallback<Double>() {
                                @Override
                                public void onSuccess(Double result) {
                                    System.out.println("执行成功:" + result.doubleValue());
                                }
                            },
                            new FailureCallback() {
                                @Override
                                public void onFailure(Throwable ex) {
                                    System.out.println("执行失败:"+ex.getMessage());
                                }
                            }
                    );
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
                //终止循环
                break;
            }
            System.out.println("Continue doing something else. ");
        }
    }

        执行结果,

自定义线程池配置参数

上面提到,SpringBoot内置了1个ThreadPoolTaskExecutor线程池实例,在实际开发中,根据需要,我们也可以结合@Configuration注解自定义新的线程池,也可以通过通过实现AsyncConfigurer接口直接替换掉原有的线程池。

定义新的线程池

这种情况下,Spring容器就会出现多个线程池实例,所以在使用@Async注解时,要通过value属性指定具体要使用哪一个线程池实例。

@Configuration
@EnableAsync
public class SpringAsyncConfig {
    
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

        使用示例,

@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - "
      + Thread.currentThread().getName());
}

替换默认线程池 

替换默认线程池需要实现AsyncConfigurer接口,通过重写getAsyncExecutor() ,从而让自定义的线程池变为Spring框架默认使用的线程池。

        示例代码如下,

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
   @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

配置线程池参数

除了上述自定义新的线程池的方法,也可以通过SpringBoot配置文件,重新对默认线程池的参数进行修改。

 

异步处理流程的异常处理

内置异常处理类:SimpleAsyncUncaughtExceptionHandler

SpringBoot框架内置的异常处理类为SimpleAsyncUncaughtExceptionHandler,仅仅是对异

常信息进行了打印处理。

package org.springframework.aop.interceptor;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A default {@link AsyncUncaughtExceptionHandler} that simply logs the exception.
 *
 * @author Stephane Nicoll
 * @author Juergen Hoeller
 * @since 4.1
 */
public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

	private static final Log logger = LogFactory.getLog(SimpleAsyncUncaughtExceptionHandler.class);


	@Override
	public void handleUncaughtException(Throwable ex, Method method, Object... params) {
		if (logger.isErrorEnabled()) {
			logger.error("Unexpected exception occurred invoking async method: " + method, ex);
		}
	}

}

当我们不做任何处理时,默认就是上述异常处理类在起作用。

继续向上扒拉源码,会发现它的父接口AsyncUncaughtExceptionHandler,其作用就是:指定异步方法执行过程中,抛出异常时的因对策略。

 

自定义异常处理类|配置

我们也可以通过实现接口AsyncUncaughtExceptionHandler,来自定义异常处理逻辑。

如下所示,为自定义的异常处理类:CustomAsyncExceptionHandler。

package com.example.soilcommon.core.async;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;


/**
 * 异步处理流程异常处理类:
 * [1]对于返回值为Future类型的异步执行方法,异常会被抛出给主调方法
 * [2]对于返回值为void类型的异步执行方法,异常不会被抛出,即:在主调方法中没办法通过try...catch捕获到异常信息
 * 当前配置类针对情况[2]进行统一的异常处理
 */
@Component("customAsyncExceptionHandler")
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    /**
     * Handle the given uncaught exception thrown from an asynchronous method.
     * @param throwable the exception thrown from the asynchronous method
     * @param method the asynchronous method
     * @param params the parameters used to invoke the method
     */
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... params) {
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : params) {
            System.out.println("Parameter value - " + param);
        }
    }
}

接下来我们对其进行配置,使其生效,需要重写AsyncConfigurer接口getAsyncUncaughtExceptionHandler方法,

package com.example.soilcommon.core.async;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    /**
     * 指定要使用哪一个具体的异常处理类
     * 原因:SpringBoot框架默认使用内置的SimpleAsyncUncaughtExceptionHandler进行异常处理
     */
    @Autowired
    @Qualifier("customAsyncExceptionHandler")
    private AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler;

    /**
     * The {@link AsyncUncaughtExceptionHandler} instance to be used
     * when an exception is thrown during an asynchronous method execution
     * with {@code void} return type.
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return this.asyncUncaughtExceptionHandler;
    }
}

        最终异步方法执行抛出异常时,打印的信息就是我们自定义的了,

        参考文章:How To Do @Async in Spring | Baeldung

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

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

相关文章

项目总体测试计划书

目的&#xff1a;编写此测试方案的目的在于明确测试内容、测试环境、测试人员、测试工作进度计划等&#xff0c;以保证测试工作能够在有序的计划安排进行。 测试目标&#xff1a;确保XXX项目的需求分析说明书中的所有功能需求都已实现&#xff0c;且能正常运行&#xff1b;确保…

4.22每日一题(累次积分的计算:交换次序)

注&#xff1a;因为 是积不出的函数&#xff0c;所以先不用算&#xff0c;最后发现&#xff0c;出现dx与dy可以相互抵消&#xff0c;即可算出答案

【Linux】xfs文件系统的xfs_info命令

xfs_info命令 ① 查看命令工具自身的版本号 xfs_info -V ② 查看指定XFS设备的详细信息 xfs_info <device_name> 其他的一些命令可以使用man xfs_info去查阅man手册&#xff1a;

信息系统的安全保护等级的五个级别

信息系统的安全保护等级分为五级&#xff1a;第一级为自主保护级、第二级为指导保护级、第三级为监督保护级、第四级为强制保护级、第五级为专控保护级。 法律依据&#xff1a;《信息安全等级保护管理办法》第四条 信息系统的安全保护等级分为以下五级&#xff1a;   &#…

nginx国密ssl测试

文章目录 文件准备编译部署nginx申请国密数字证书配置证书并测试 文件准备 下载文件并上传到服务器&#xff0c;这里使用centos 7.8 本文涉及的程序文件已打包可以直接下载。 点击下载 下载国密版openssl https://www.gmssl.cn/gmssl/index.jsp 下载稳定版nginx http://n…

微服务知识小结

1. SOA、分布式、微服务之间有什么关系和区别&#xff1f; 1.分布式架构指将单体架构中的各个部分拆分&#xff0c;然后部署到不同的机器或进程中去&#xff0c;SOA和微服务基本上都是分布式架构的 2. SOA是一种面向服务的架构&#xff0c;系统的所有服务都注册在总线上&#…

曲率半径的推导

参考文章 参考文章

Leetcode1410. HTML 实体解析器

Every day a Leetcode 题目来源&#xff1a;1410. HTML 实体解析器 解法1&#xff1a;模拟 遍历字符串 text&#xff0c;每次遇到 ’&‘&#xff0c;就判断以下情况&#xff1a; 双引号&#xff1a;字符实体为 &quot; &#xff0c;对应的字符是 " 。单引号&a…

kubernetes 部署 spinnaker

spinnaker简介 Spinnaker 是一个开源、多云持续交付平台&#xff0c;它将强大而灵活的管道管理系统与主要云提供商的集成相结合。Spinnaker 提供应用程序管理和部署&#xff0c;帮助您快速、自信地发布软件变更。 Spinnaker 提供了两组核心的功能&#xff1a; 应用管理与应用程…

Windows日常故障自我排查:用工具eventvwr.msc(事件查看器)分析问题故障

windows故障排查方法一&#xff1a; 工具用法 系统故障问题时&#xff0c;找不到解决方法 首先&#xff0c; 在搜索栏输入&#xff1a; 事件查看器(eventvwr.msc) 打开程序 根据程序找到程序运行的LOG 根据程序Operational筛选出错误日志&#xff1a; 日志中找错误原因&…

专注短视频账号矩阵系统源头开发---saas工具

专注短视频账号矩阵系统源头开发---saas营销化工具&#xff0c;目前我们作为一家纯技术开发团队目前已经专注打磨开发这套系统企业版/线下版两个版本的saas营销拓客工具已经3年了&#xff0c;本套系统逻辑主要是从ai智能批量剪辑、账号矩阵全托管发布、私信触单收录、文案ai智能…

微信小程序使用腾讯地图实现地点搜索并且随着地图的滑动加载滑动到区域的地点,本文地点使用医院关键词作为搜索地点

实现效果如下 1.页面加载时&#xff0c;根据getLocation方法获取用户当前经纬度获取20条医院位置信息 2.页面滑动时&#xff0c;根据滑动到的经纬度再次获取20条医院位置信息 获取到的医院位置信息 实现方法如下 1.在.wxml中添加触发滑动的方法bindregiοnchange“onMapRegio…

2023亚太赛B题详细讲解 玻璃温室中的微气候

Problem B Microclimate Regulation in Glass Greenhouses 问题B玻璃温室中的微气候法规 温室作物的产量受到各种气候因素的影响&#xff0c;包括温度、湿度和风速[1]。其中&#xff0c;适宜的温度和风速是植物生长[2]的关键。为了调节玻璃温室内的温度、风速等气候因素&…

GPU服务器常见故障修复记录

日常写代码写方案文档&#xff0c;偶尔遇上服务器出现问题的时候&#xff0c;也需要充当一把运维工程师&#xff0c;此帖用来记录GPU服务器报错的一些解决方案&#xff0c;仅供参考&#xff01; 文章目录 一、服务器简介二、机箱拆解三、基本操作四、常见故障4.1 电源开关键闪烁…

【精选】改进的YOLOv5:红外遥感图像微型目标的高效识别系统

1.研究背景与意义 随着科技的不断发展&#xff0c;红外遥感技术在军事、安防、环境监测等领域中得到了广泛应用。红外遥感图像具有独特的优势&#xff0c;可以在夜间或恶劣天气条件下获取目标信息&#xff0c;因此在小目标检测方面具有重要的应用价值。然而&#xff0c;由于红…

Unity中颜色空间Gamma与Linear

文章目录 前言一、人眼对光照的自适应1、光照强度与人眼所见的关系2、巧合的是&#xff0c;早期的电子脉冲显示屏也符合这条曲线3、这两条曲线都巧合的符合 y x^2.2^&#xff08;Gamma2.2空间&#xff09; 二、Gamma矫正1、没矫正前&#xff0c;人眼看电子脉冲显示屏&#xff…

数据结构与算法编程题13

设计算法将一个带头结点的单链表A分解为两个具有相同结构的链表B、C&#xff0c;其中B表的结点为A表中值小于零的结点&#xff0c;而C表的结点为A表中值大于零的结点&#xff08;链表A中的元素为非零整数&#xff0c;要求B、C表利用A表的结点&#xff09; for example: A -1 2 …

使用C语言统计一个字符串中每个字母出现的次数

每日一言 Wishing is not enough; we must do. 光是许愿望是不够的; 我们必须行动。 题目 输入一个字符串&#xff0c;统计在该字符串中每个字母出现的次数 例如&#xff1a; 输入&#xff1a;i am a student 输出&#xff1a;a:2 d:1 e:1 i:1 m:1 n:1 s:1 t:2 u:1 大体思路…

第十一章 docker swarm集群部署

文章目录 前言一、安装docker1.1 解压1.2 配置docker 存储目录和dns1.3 添加docker.service文件1.4 docker 启动验证 二、docker swarm 集群配置2.1 关闭selinux2.2 设置主机名称并加入/etc/hosts2.3 修改各个服务器名称&#xff08;uname -a 进行验证&#xff09;2.4 初始化sw…