异步编程 - 09 Spring框架中的异步执行_@Async注解异步执行原理源码解析

news2024/11/29 4:28:13

文章目录

  • 概述
  • 小结
  • 好文推荐

在这里插入图片描述


概述

在Spring中调用线程将在调用含有@Async注释的方法时立即返回,Spring是如何做到的呢?其实是其对标注@Async注解的类做了代理,比如下面的类Async-AnnotationExample。

public class AsyncAnnotationExample {
    @Async
    public CompletableFuture<String> doSomething() {

        // 1.创建future
        CompletableFuture<String> result = new CompletableFuture<String>();
        // 2.模拟任务执行
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "doSomething");
        } catch (Exception e) {
            e.printStackTrace();
        }
        result.complete("done");

        // 3.返回结果
        return result;
    }
}

由于AsyncAnnotationExample类中方法doSomething被标注了@Async注解,所以Spring框架在开启异步处理后会对AsyncAnnotationExample的实例进行代理,代理后的类代码框架如下所示。

public class AsyncAnnotationExampleProxy {
    
    public AsyncAnnotationExample getAsyncTask() {
        return asyncTask;
    }

    public void setAsyncAnnotationExample(AsyncAnnotationExample asyncTask) {
        this.asyncTask = asyncTask;
    }

    private AsyncAnnotationExample asyncTask;
    private TaskExecutor executor = new SimpleAsyncTaskExecutor();
    public CompletableFuture<String> dosomthingAsyncFuture() {

        return CompletableFuture.supplyAsync(new Supplier<String>() {

            @Override
            public String get() {
                try {
                    return asyncTask.dosomthing().get();
                } catch (Throwable e) {
                    throw new CompletionException(e);
                }
            }
        },executor);
    }
}

如上代码所示,Spring会对AsyncAnnotationExample类进行代理,并且会把AsyncAnnotationExample的实例注入AsyncAnnotationExampleProxy内部,当我们调用AsyncAnnotationExample的dosomthing方法时,实际调用的是AsyncAnnotation ExampleProxy的dosomthing方法,后者使用CompletableFuture.supplyAsync开启了一个异步任务(其马上返回一个CompletableFuture对象),并且使用默认的SimpleAsync TaskExecutor线程池作为异步处理线程,然后在异步任务内具体调用了AsyncAnnotationExample实例的dosomthing方法。

默认情况下,Spring框架是使用Cglib对标注@Async注解的方法进行代理的,具体拦截器是AnnotationAsyncExecutionInterceptor,我们看看其invoke方法。

public Object invoke(final MethodInvocation invocation) throws Throwable {
    //1.被代理的目标对象
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    //2. 获取被代理的方法
    Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
    final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
    //3. 判断使用哪个执行器执行被代理的方法
    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
    if (executor == null) {
        throw new IllegalStateException(
                "No executor specified and no default executor set on AsyncExecutionInterceptor either");
    }
    //4. 使用Callable包装要执行的方法
    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;
    };
    //5. 提交包装的Callable任务到指定执行器执行
    return doSubmit(task, executor, invocation.getMethod().getReturnType());
}

·代码1获取被代理的目标对象的Class对象,本例中为class:com.artisan.async.AsyncProgram.AsyncAnnotationExample的Class对象;

·代码2获取被代理的方法,本例中为public java.util.concurrent.CompletableFuture:com.artisan.async.AsyncProgram.AsyncAnnotationExample.dosomthing();

·代码3根据规则获取使用哪个执行器TaskExecutor执行被代理的方法,其代码如下所示。

private final Map<Method, AsyncTaskExecutor> executors = new ConcurrentHashMap<>(16);
protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
    //4.1获取对应方法的执行器
    AsyncTaskExecutor executor = this.executors.get(method);
    //4.2不存在则按照规则查找
    if (executor == null) {
        //4.2.1 如果注解@Async中指定了执行器名称
        Executor targetExecutor;
        String qualifier = getExecutorQualifier(method);
        if (StringUtils.hasLength(qualifier)) {
            targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
        }
        //4.2.2 获取默认执行器
        else {
            targetExecutor = this.defaultExecutor;
            if (targetExecutor == null) {
                synchronized (this.executors) {
                    if (this.defaultExecutor == null) {
                        this.defaultExecutor = getDefaultExecutor(this.beanFactory);
                    }
                    targetExecutor = this.defaultExecutor;
                }
            }
        }
        //4.2.3
        if (targetExecutor == null) {
            return null;
        }
        //4.2.4 添加执行器到缓存
        executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
                (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
        this.executors.put(method, executor);
    }
    //4.3返回查找的执行器
    return executor;
}

代码4.1从缓存executors中尝试获取method方法对应的执行器,如果存在则直接执行代码4.3返回;否则执行代码4.2.1判断方法的注解@Async中是否指定了执行器名称,如果有则尝试从Spring的bean工厂内获取该名称的执行器的实例,否则执行代码4.2.2获取默认的执行器(SimpleAsyncTaskExecutor),然后代码4.2.4把执行器放入缓存。

到这里就探讨完成了AnnotationAsyncExecutionInterceptor的invoke方法内代码3是如何确定那个执行器,然后在invoke方法中的代码4使用Callable包装要执行的方法,代码5提交包装的Callable任务到指定执行器。

到这里所有的执行使用的都是调用线程,调用线程提交异步任务到执行器后就返回了,异步任务真正执行的是具体执行器中的线程。下面我们看看代码5 doSubmit的代码。

protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
    //5.1判断方法返回值是否为CompletableFuture类型或者是其子类
    if (CompletableFuture.class.isAssignableFrom(returnType)) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return task.call();
            }
            catch (Throwable ex) {
                throw new CompletionException(ex);
            }
        }, executor);
    }
    //5.2判断返回值类型是否为ListenableFuture类型或者是其子类
    else if (ListenableFuture.class.isAssignableFrom(returnType)) {
        return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
    }
    //5.3判断返回值类型是否为ListenableFuture类型或者是其子类
    else if (Future.class.isAssignableFrom(returnType)) {
        return executor.submit(task);
    }
    //5.4其他情况下没有返回值
    else {
        executor.submit(task);
        return null;
    }
}

·代码5.1判断方法返回值是否为CompletableFuture类型或者是其子类,如果是则把任务使用CompletableFuture.supplyAsync方法提交到线程池executor执行,该方法会马上返回一个CompletableFuture对象。

·代码5.2判断方法返回值是否为ListenableFuture类型或者是其子类,如果是则把任务提交到线程池executor执行,该方法会马上返回一个ListenableFuture对象。

·代码5.3判断方法返回值是否为Future类型或者是其子类,如果是则把任务提交到线程池executor执行,该方法会马上返回一个Future对象。

·代码5.4说明方法不需要返回值,直接提交任务到线程池executor后返回null。

上面我们讲解了代理拦截器AnnotationAsyncExecutionInterceptor的invoke方法如何对标注@Async的方法进行处理,实现异步执行的。其实还有一部分还没讲,前面说了要开始异步处理,必须使用@EnableAsync注解或者task:annotation-driven/来开启异步处理,那么这两个部分背后到底做了什么呢?下面我们就来一探究竟。

首先我们看看添加@EnableAsync注解后发生了什么?在Spring容器启动的过程中会有一系列扩展接口对Bean的元数据定义、初始化、实例化做拦截处理,也存在一些处理器类可以动态地向Spring容器添加一些框架需要使用的Bean实例。其中ConfigurationClassPostProcessor处理器类则是用来解析注解类,并把其注册到Spring容器中的,其可以解析标注@Configuration、@Component、@ComponentScan、@Import、@ImportResource等的Bean。当我们使用context:annotation-config/或者context:component-scan/时,Spring容器会默认把ConfigurationClassPostProcessor处理器注入Spring容器。

而@EnableAsync的定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
...
}

所以我们添加了@EnableAsync注解后,ConfigurationClassPostProcessor会解析其中的@Import(AsyncConfigurationSelector.class),并把AsyncConfigurationSelector的实例注入Spring容器,其代码如下所示。

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
            "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
  
    @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;
        }
    }
}

AsyncConfigurationSelector实现了ImportSelector接口的selectImports方法,根据AdviceMode参数返回需要导入到Spring容器的Bean的全路径包名。该方法会在ConfigurationClassPostProcessor中的ConfigurationClassParser类中调用。默认情况下的adviceMode为PROXY,所以会把ProxyAsyncConfiguration的实例注入Spring容器。

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("proxyTarget
Class"));
        bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
        return bpp;
    }

}

如上代码ProxyAsyncConfiguration的asyncAdvisor方法添加了@Bean注解,所以该方法返回的Bean也会被注入Spring容器。该方法创建了AsyncAnnotationBean PostProcessor处理器,所以AsyncAnnotationBeanPostProcessor的一个实例会被注入Spring容器中,由于其实现了BeanFactoryAware接口,所以Spring框架会调用其setBeanFactory(BeanFactory beanFactory)方法把Spring BeanFactory(存放bean的容器)注入该Bean,setBeanFactory方法代码如下所示。

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;
}

如上代码创建了一个AsyncAnnotationAdvisor的实例并保存到了AsyncAnnotation BeanPostProcessor的advisor变量。Spring中每个AsyncAnnotationAdvisor都包含一个Advice(切面逻辑)和一个PointCut(切点),也就是会对符合PointCut的方法使用Advice进行功能增强,对应Advice和PointCut是在AsyncAnnotationAdvisor构造函数内创建的。

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

    //6.1.异步注解类型
    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) {
    }
    //6.2创建切面逻辑
    this.advice = buildAdvice(executor, exceptionHandler);
    //6.3创建切点
    this.pointcut = buildPointcut(asyncAnnotationTypes);
}

如上代码6.1收集注解@Async和@javax.ejb.Asynchronous到asyncAnnotationTypes,代码6.2则创建Advice,其代码如下所示。

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

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

由上述代码可知,这里创建了AnnotationAsyncExecutionInterceptor拦截器作为切面逻辑。下面看看代码6.3如何创建切点。

protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {
    ComposablePointcut result = null;
    for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
        Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
        Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
        if (result == null) {
            result = new ComposablePointcut(cpc);
        }
        else {
            result.union(cpc);
        }
        result = result.union(mpc);
    }
    return (result != null ? result : Pointcut.TRUE);
}

在上述代码中使用收集的注解集合asyncAnnotationTypes,并在每个注解处创建了一个AnnotationMatchingPointcut作为切点,AnnotationMatchingPointcut内部的AnnotationClassFilter的方法matches则作为某个方法是否满足切点的条件,具体代码如下所示。

public boolean matches(Class<?> clazz) {
    return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
            clazz.isAnnotationPresent(this.annotationType));
}

由如上代码可知,判断方法通过是否有注解@Async为依据来判断方法是否符合切点。

到此我们把AsyncAnnotationBeanPostProcessor的setBeanFactory(BeanFactory bean-Factory)方法逻辑讲解完毕了,其内部保存了一个AsyncAnnotationAdvisor对象用来对Spring容器中符合条件(这里为含有@Async注解的方法的Bean)的Bean的方法进行功能增强,下面我们看看AsyncAnnotationBeanPostProcessor的postProcess AfterInitialization方法是如何对这些符合条件的Bean进行代理的。

public Object postProcessAfterInitialization(Object bean, String beanName) {
    ...

    if (isEligible(bean, beanName)) {
        //7.1
        ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
        if (!proxyFactory.isProxyTargetClass()) {
            evaluateProxyInterfaces(bean.getClass(), proxyFactory);
        }
        //7.2 设置拦截器
        proxyFactory.addAdvisor(this.advisor);
        customizeProxyFactory(proxyFactory);
        //7.3 获取代理类
        return proxyFactory.getProxy(getProxyClassLoader());
    }

    // No proxy needed.
    return bean;
}

如上代码7.1使用prepareProxyFactory创建了代理工厂,其代码如下所示。

protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
    proxyFactory.setTarget(bean);
    return proxyFactory;
}

代码7.2则设置在其setBeanFactory方法内创建的AsyncAnnotationAdvisor对象作为Advisor,代码7.3从代理工厂获取代理后的Bean实例并返回到Spring容器,所以当我们调用含有@Async注解的Bean的方法时候,实际调用的是被代理后的Bean。

当我们调用被代理的类的方法时,代理类内部会先使用AsyncAnnotationAdvisor中的PointCut进行比较,看其是否符合切点条件(方法是否含有@Async)注解,如果不符合则直接调用被代理的对象的原生方法,否则调用AsyncAnnotationAdvisor内部的AnnotationAsyncExecutionInterceptor进行拦截异步处理。

在了解添加@EnableAsync注解后会发生什么后,下面我们来看看当添加标签<task:annotation-driven/>开启异步处理时,背后又发生了什么?在Spring中对于标签<XXX:/>总是会存在名称为XXXTaskNamespaceHandler的处理器负责该标签的解析,所以对于标签,自然存在TaskNamespaceHandler处理器负责其解析,其代码如下所示。

public class TaskNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        this.registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
        this.registerBeanDefinitionParser("executor", new ExecutorBeanDefinitionParser());
        this.registerBeanDefinitionParser("scheduled-tasks", new ScheduledTasksBeanDefinitionParser());
        this.registerBeanDefinitionParser("scheduler", new SchedulerBeanDefinitionParser());
    }
}

由如上代码可知,<task:annotation-driven/>是使用AnnotationDrivenBeanDefinitionParser来进行解析的,下面我们看看其parse方法。

public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
...
    @Override
    @Nullable
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        Object source = parserContext.extractSource(element);

        ...
        //8.1 
        String mode = element.getAttribute("mode");
        if ("aspectj".equals(mode)) {
            // mode="aspectj"
            registerAsyncExecutionAspect(element, parserContext);
        }
        else {
            //8.2 mode="proxy"
            if (registry.containsBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)) {
                parserContext.getReaderContext().error(
                        "Only one AsyncAnnotationBeanPostProcessor may exist within the context.", source);
            }
            else {
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
                        "org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor");
                builder.getRawBeanDefinition().setSource(source);
                String executor = element.getAttribute("executor");
                if (StringUtils.hasText(executor)) {
                    builder.addPropertyReference("executor", executor);
                }
                String exceptionHandler = element.getAttribute("exception-handler");
                if (StringUtils.hasText(exceptionHandler)) {
                    builder.addPropertyReference("exceptionHandler", exceptionHandler);
                }
                if (Boolean.valueOf(element.getAttribute(AopNamespaceUtils.PROXY_TARGET_CLASS_ATTRIBUTE))) {
                    builder.addPropertyValue("proxyTargetClass", true);
                }
                registerPostProcessor(parserContext, builder, TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME);
            }
        }

    
        //8.3 Finally register the composite component.
        parserContext.popAndRegisterContainingComponent();

        return null;
    }
}

由如上代码可知,其主要是用来创建AsyncAnnotationBeanPostProcessor在Spring容器中的元数据定义,并注册到Spring容器中,剩下的流程就与基于@EnableAsync注解开启异步处理的流程一样了。

小结

我们梳理如何使用Spring框架中的@Async进行异步处理,以及其内部如何使用代理的方式来实现,并且可知使用@Async实现异步编程属于声明式编程,一般情况下不需要我们显式创建线程池并提交任务到线程池,这大大减轻了的负担

好文推荐

一文彻底讲透@Async注解的原理和使用方法

在这里插入图片描述

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

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

相关文章

Qt 5.15集成Crypto++ 8.8.0(MSVC 2019)笔记

一、背景 笔者已介绍过在Qt 5.15.x中使用MinGW&#xff08;8.10版本&#xff09;编译并集成Crypto 8.8.0。 但是该编译出来的库&#xff08;.a和.dll&#xff09;不适用MSVC&#xff08;2019版本&#xff09;构建环境&#xff0c;需要重新编译&#xff08;.lib或和.dll&#xf…

C++11新特性② | 左值、左值引用、右值与右值引用

目录 1、引言 2、值类别及相关概念 3、左值、右值 4、左值引用、右值引用 5、移动语义 5.1、为什么需要移动语义 5.2、移动语义定义 5.3、转移构造函数 5.4、转移赋值函数 6、标准库函数 std::move 7、完美转发 std::forward VC常用功能开发汇总&#xff08;专栏文章…

Mybatis 使用typeHandler自定义类型转换

之前我们介绍了使用Mybatis完成数据的增删改查操作&#xff1b;本篇我们介绍使用Mybatis提供的typeHandler自定义类型转换。 如果您对Mybatis的增删改查操作不太了解&#xff0c;可以参考&#xff1a; Mybatis 查询数据https://blog.csdn.net/m1729339749/article/details/13…

explain 实战-----查看hive sql执行计划

目录 1.join/left join/full join 语句会过滤关联字段 null 的值吗&#xff1f; &#xff08;1&#xff09;join &#xff08;2&#xff09; left join /full join 2.group by 分组语句会进行排序吗&#xff1f; 1.join/left join/full join 语句会过滤关联字段 null 的值吗…

Qt 5.15编译(MinGW)及集成Crypto++ 8.8.0笔记

一、背景 为使用AES加密库&#xff08;AES/CBC加解密&#xff09;&#xff0c;选用Crypto 库&#xff08;官网&#xff09;。   最新Crypto C库依次为&#xff1a;8.8.0版本&#xff08;2023-6-25&#xff09;、8.7.0&#xff08;2022-8-7&#xff09;和8.6.0&#xff08;202…

绘图系统三:支持散点图、极坐标和子图绘制

文章目录 新增散点图绘制绑定与回调极坐标功能子图绘制功能源代码 &#x1f4c8;一 三维绘图系统&#x1f4c8;二 多图绘制系统 新增散点图绘制 同一坐标系中绘制多个图像是很常见的需求&#xff0c;比如数据拟合的时候&#xff0c;用散点图表示原始数据&#xff0c;用曲线图…

vue3 webpack打包流程及安装 (1)

npm run build 也可以打包 如果没有特殊需求 可以使用 效果其实是差不多的 --------------------------------------------------------------------------------------------------------------------------------- webpack网址 &#xff1a; 起步 | webpack 中文文档 (docsc…

记录docker 部署nessus

1、开启容器 docker run -itd --nameramisec_nessus -p 8834:8834 ramisec/nessus 2、登录 &#xff1a;注意是https https://ip8843 3、修改admin密码 #进入容器 docker exec -it ramisec_nessus /bin/bash#列出用户名 /opt/nessus/sbin/nessuscli lsuser#修改密码&a…

(html+CSS)垂直居中

line-height 设置文字行高等于父元素的高度 vertical-align 用于设置一个元素的垂直对齐方式&#xff0c;但是它只针对于行内元素或者行内块元素有效。 属性值说明baseline默认&#xff0c;元素放置在父元素的基线上top把元素的顶端与行中最高元素的顶端对齐middle把此元素…

递归算法学习——黄金矿工,不同路径III

目录 ​编辑 一&#xff0c;黄金矿工 1.题意 2.题目分析 3.题目接口 4.解题思路及代码 二&#xff0c;不同路径III 1.题意 2.解释 3.题目接口 4.解题思路及代码 一&#xff0c;黄金矿工 1.题意 你要开发一座金矿&#xff0c;地质勘测学家已经探明了这座金矿中的资源…

YMatrix 5.0 与天翼云完成产品兼容性认证

近日&#xff0c;北京四维纵横数据技术有限公司与天翼云宣布完成产品兼容性认证。经过双方严格的测试验证&#xff0c;超融合数据库 YMatrix 5.0 与天翼云兼容性良好&#xff0c;可基于天翼云稳定运行。 数据库系统作为基础软件的核心&#xff0c;自主可控势在必行。在此背景下…

项目(智慧教室)第一部分:cubemx配置,工程文件的移植,触摸屏的检测,项目bug说明

第一章&#xff1a;需求与配置 一。项目需求 二。实现外设控制 注意&#xff1a; 先配置引脚&#xff0c;再配置外设。否则会出现一些不可预料的问题 1.时钟&#xff0c;串口&#xff0c;灯&#xff0c;蜂鸣器配置 &#xff08;1&#xff09;RCC配置为外部时钟&#xff0c;修…

Android 开发小贴士

Android 开发小贴士 Unable to merge dex 原因&#xff1a; 1. 包引用重复 2. 方法数超限 3. 或者几个库之间有重复代码块(特别是在整理module时容易犯) 解决&#xff1a; 1. app的build.gradle中 // 1. 添加配置 defaultConfig {......multiDexEnabled true }// 2. 清除缓…

pico学习进程记录已经开发项目

Pico pin脚定义 Pico 运行准备 下载uf2文件 https://pico.org.cn/ &#xff08;注意运行micropython的文件和运行c/c的不一样&#xff09; 装载uf2文件&#xff1a;按住pico的按键&#xff0c;然后通过micro usb连接电脑&#xff08;注意&#xff1a;如果用的线材&#xff0c;…

菜鸟教程《Python 3 教程》笔记(19):错误与异常

菜鸟教程《Python 3 教程》笔记&#xff08;19&#xff09; 19 错误和异常19.1 assert&#xff08;断言&#xff09;19.2 异常处理19.2.1 try/except19.2.2 try/except...else19.2.3 try-finally 语句 19.3 抛出异常19.4 用户自定义异常19.5 清理行为19.5.1 定义清理行为19.5.2…

生信分析Python实战练习 6 | 视频24

开源生信 Python教程 生信专用简明 Python 文字和视频教程 源码在&#xff1a;https://github.com/Tong-Chen/Bioinfo_course_python 目录 背景介绍 编程开篇为什么学习Python如何安装Python如何运行Python命令和脚本使用什么编辑器写Python脚本Python程序事例Python基本语法 数…

HTTP代理协议原理分析

HTTP代理协议是一种常见的网络协议&#xff0c;它可以在网络中传递HTTP协议的请求和响应。本文将介绍HTTP代理协议的分析和原理&#xff0c;包括HTTP代理的工作流程、HTTP代理的请求和响应格式、HTTP代理的优缺点等方面。 一、HTTP代理的工作流程 HTTP代理的工作流程如下&#…

OmniGraffle Pro for Mac 中文正式版(附注册码) 苹果电脑 思维导图软件

OmniGraffle Pro是OmniGraffle的高级版本&#xff0c;它提供了更多的功能和工具&#xff0c;可以帮助用户创建更为复杂和高级的图表和流程图。OmniGraffle Pro支持自定义形状、图形、线条和箭头等&#xff0c;可以让用户创建出更加精细的图表。此外&#xff0c;OmniGraffle Pro…

centos7做软连接及注意事项

centos7做软连接及注意事项 小白教程&#xff0c;一看就会&#xff0c;一做就成。 1.要求 /data/upload/目录软连接到/data/ydoa/webapps/defaultroot下&#xff0c;命名为upload 2.开始操作 #先进入到/data/ydoa/webapps/defaultroot cd /data/ydoa/webapps/defaultroot #…

调试心得:通过观察正常的程序行为来识别Bug

有时候&#xff0c;当我调试一个问题的时候&#xff0c;我会特意忽略掉某些线程。 这个时候&#xff0c;有人就问了&#xff1a;”这些线程是干什么的&#xff1f;你为什么知道要忽略它们&#xff1f;” 我的回答是&#xff1a;我也不清楚这些线程是干啥的&#xff0c;但是无论…