趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了

news2024/10/6 14:28:19
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

在这里插入图片描述

文章目录

    • 一、引言
    • 二、SPI是什么
    • 三、使用介绍
    • 四、原理介绍
      • 1、SPI注解
    • 五、源码剖析
      • 1、Loader的创建
      • 2、获取实例
        • 2.1 解析文件配置
        • 2.2 实例化创建
        • 2.3 前置处理
        • 2.4 依赖注入
        • 2.5 后置操作
        • 2.6 Wrapper 的包装
          • 2.6.1 Wrapper缓存
          • 2.6.2 Wrapper实现
        • 2.7 生命周期管理
    • 六、流程图
    • 七、总结

一、引言

兄弟们,上次的故障结果出来了

还好销售团队给力,没有让客户几千万的单子丢掉,成功挽回了本次损失

image-20230717003322489

不过内部处罚还是相对严重,年终奖悬了

image-20230717003338736

这也告诫我们 要对生产保持敬畏之情!

恰巧最近领导看我在写 Dubbo 源码系列,看到我们的项目中用了 SPI 扩展

于是给我一个将功补过的机会,让我好好的分析分析 DubboSPI 的扩展机制,进行组内技术分享

作为一个常年分享 源码系列 文章的选手,当然不会拒绝!

乾坤未定,你我皆是黑马,冲!

二、SPI是什么

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

img

Java SPI 实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

所以 SPI 的核心思想就是解耦

三、使用介绍

我们定义一个接口:City

@SPI
public interface City {
    String getCityName();
}

实现其两个类:

  • BeijingCity
public class BeijingCity implements City{
    @Override
    public String getCityName() {
        return "北京";
    }
}
  • TianjinCity
public class TianjinCity implements City{
    @Override
    public String getCityName() {
        return "天津";
    }
}

重点来了:我们要在 resources 文件夹下面建立一个路径:META-INF/dubbo

然后我们建立一个 txt 名为:com.dubbo.provider.SPI.Dubbo.City,如下:

我们在这个文件中写上各实现类的路径:

beijing=com.dubbo.provider.SPI.Dubbo.BeijingCity
tianjin=com.dubbo.provider.SPI.Dubbo.TianjinCity

有的朋友可能会问,这里为什么和 Java SPI 的实现不同?

这也正是 Dubbo 实现精准实例化的原因,我们后面也会聊到

测试方法:

public class DubboSPITest {
    public static void main(String[] args) {
        ExtensionLoader<City> loader = ExtensionLoader.getExtensionLoader(City.class);
        City tianjin = loader.getExtension("beijing");
        System.out.println(tianjin.getCityName());
    }
}

测试结果:

北京

从这里我们可以看出,Dubbo 可以通过 loader.getExtension("beijing") 精确的生成我们需要的实例

精确生成是如何实现的呢?我们继续往下看

四、原理介绍

在源码介绍之前,我们先说几个原理细节,防止大家后面的源码看迷糊

1、SPI注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     */
    String value() default "";

    /**
     * scope of SPI, default value is application scope.
     */
    ExtensionScope scope() default ExtensionScope.APPLICATION;
}

SPI 注解中,存在两个参数:valuescope

value

  • 作用:如果某个 SPI 扩展没有指定实现类名称,则会使用 @SPI 注解中指定的默认值

scope:指定 SPI 扩展实现类的作用域( Constants.SINGLETON

  • Constants.FRAMEWORK(框架作用域):实现类在 Dubbo 框架中只会创建一个实例,并且在整个应用程序中共享。
  • Constants.APPLICATION(应用程序作用域):实现类在应用程序上下文中只会创建一个实例,并且在整个应用程序中共享。
  • Constants.MODULE(模块作用域):实现类在模块上下文中只会创建一个实例,并且在整个模块中共享。
  • Constants.SELF(自定义作用域):实现类的作用范围由用户自行定义,可以是任何范围。

当然,这里 Dubbo 默认的是 Constants.APPLICATION,我们也只需要关注这个即可。

五、源码剖析

1、Loader的创建

我们 DubboSPIExtensionLoader.getExtensionLoader(City.class) 开始,看一看其实现方案

public <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    // 1、校验
    checkDestroyed();

    // 2、是否有本地Loader缓存
    ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoadersMap.get(type);

    // 3、是否有本地Scope缓存
    ExtensionScope scope = extensionScopeMap.get(type);
    
    // 4、如果当前的Scope为空
    // 4.1 获取当前接口类的SPI注解
    // 4.2 获取当前注解的scope
    // 4.3 放入scope缓存
    if (scope == null) {
        SPI annotation = type.getAnnotation(SPI.class);
        scope = annotation.scope();
        extensionScopeMap.put(type, scope);
    }

    // 5、如果加载器为空且当前是SELF,直接创建loader
    if (loader == null && scope == ExtensionScope.SELF) {
        loader = createExtensionLoader0(type);
    }

    // 6、如果当前加载器为空,去父类找加载器
    if (loader == null) {
        if (this.parent != null) {
            loader = this.parent.getExtensionLoader(type);
        }
    }

    // 7、如果父类也没有实例化,那么实例化并放入缓存
    if (loader == null) {
        loader = createExtensionLoader(type);
    }
	
    // 8、返回加载器
    return loader;
}

从上面的源码我们可以看到,获取 ExtensionLoader 采用了 缓存 + 父类继承 的模式

这种继承机制设计得比较巧妙,可以避免重复加载类,提高系统性能。

2、获取实例

Dubbo 通过 loader.getExtension("tianjin") 获取对应的实例

public T getExtension(String name) {
    T extension = getExtension(name, true);
    return extension;
}

public T getExtension(String name, boolean wrap) {
    // 1、校验
    checkDestroyed();
    
    // 2、参数为true,表明采用默认的实现类
    // 2.1 我们上面SPI中的value参数,若指定tianjin,则采用tianjin的实现类
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    
    String cacheKey = name;
    if (!wrap) {
        cacheKey += "_origin";
    }
    // 3、查看当前缓存中是否含有该实例
    // 3.1 如果当前的cacheKey没有Holder的话,创建一个
    final Holder<Object> holder = getOrCreateHolder(cacheKey);
    
    // 4、如果实例为空,采用DCL机制创建实例
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name, wrap);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

private Holder<Object> getOrCreateHolder(String name) {
    // 1、获取当前name的Holder
    Holder<Object> holder = cachedInstances.get(name);
    // 2、没有则创建并扔进缓存
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<>());
        holder = cachedInstances.get(name);
    }
    // 3、返回
    return holder;
}

Holder 类是一个简单的容器类,用于保存某个对象的引用

DubboExtensionLoader 类中,Holder 类被用于实现对 SPI 扩展实现类的缓存

Holder 结构如下:

public class Holder<T> {
    private volatile T value;
    public void set(T value) {
        this.value = value;
    }
    public T get() {
        return value;
    }
}

我们创建实例一共有以下几部分:

  • 解析文件配置得到对应的类
  • 通过实例化创建相关的类
  • 初始化之前前置操作
  • 依赖注入
  • 初始化之后后置操作
  • Wrapper 的包装
  • 是否具有生命周期管理的能力

我们挨个的讲解

2.1 解析文件配置

Class<?> clazz = getExtensionClasses().get(name);

private Map<String, Class<?>> getExtensionClasses() {
    // 1、从缓存中获取类的信息
    Map<String, Class<?>> classes = cachedClasses.get();
    
    // 2、DCL创建(经典的单例设计模式)
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 3、加载类信息并放至缓存中
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {
    // 1、校验
    checkDestroyed();
    
    // 2、是否有默认的类
    // 2.1 我们之前聊过的SPI注解的value机制
    cacheDefaultExtensionName();
	  
    // 3、这里有三个文件解析器
    // 3.1 DubboInternalLoadingStrategy:解析META-INF/dubbo/internal/
    // 3.2 DubboLoadingStrategy:解析META-INF/dubbo/
    // 3.3 ServicesLoadingStrategy:解析META-INF/services/
    // 3.4 解析文件并放至缓存
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy, type.getName());
		
        if (this.type == ExtensionInjector.class) {
            loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
        }
    }
	
    // tianjin:"class com.msb.dubbo.provider.SPI.Dubbo.TianjinCity"
    // beijing:"class com.msb.dubbo.provider.SPI.Dubbo.BeijingCity"
    return extensionClasses;
}

2.2 实例化创建

// 1、从缓存中获取
T instance = (T) extensionInstances.get(clazz);
if (instance == null) {
    // 2、缓存为空则创建并放至缓存
    extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
    instance = (T) extensionInstances.get(clazz);
}

// 1、获取当前类的所有的构造方法
// 2、判断是否有符合的构造方法,若没有则报错
// 3、有符合的构造犯法,返回即可
private Object createExtensionInstance(Class<?> type) throws ReflectiveOperationException {
    return instantiationStrategy.instantiate(type);
}

2.3 前置处理

类似 Spirng 的前置处理器,之前也说过,感兴趣的可以看一下,整体思路区别不大

instance = postProcessBeforeInitialization(instance, name);

private T postProcessBeforeInitialization(T instance, String name) throws Exception {
    if (extensionPostProcessors != null) {
        for (ExtensionPostProcessor processor : extensionPostProcessors) {
            instance = (T) processor.postProcessBeforeInitialization(instance, name);
        }
    }
    return instance;
}

2.4 依赖注入

  • 首先,如果依赖注入器为 null,则直接返回传入的实例。
  • 然后,遍历传入实例的所有方法,找到所有的 setter 方法。
  • 对于每个 setter 方法,如果标注了 @DisableInject 注解,则跳过该方法,不进行注入。
  • 如果 setter 方法的参数类型是基本类型,则跳过该方法,不进行注入。
  • 如果 setter 方法的参数类型不是基本类型,则尝试从依赖注入器中获取该类型对应的实例,并调用该 setter 方法进行注入。
  • 如果获取实例失败,则记录错误日志。
  • 最后,返回注入后的实例。
injectExtension(instance);

private T injectExtension(T instance) {
    for (Method method : instance.getClass().getMethods()) {
        // 1、如果不是setter方法,直接跳过
        if (!isSetter(method)) {
            continue;
        }
        
        // 2、包含了DisableInject注解,直接跳过
        if (method.isAnnotationPresent(DisableInject.class)) {
                continue;
            }

            
            if (method.getDeclaringClass() == ScopeModelAware.class) {
                continue;
            }
        	
        	// 3、如果是基本数据类型,跳过
            if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {
                if (ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method))) {
                    continue;
                }
            }
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                // 4、依赖注入器中获取该类型对应的实例,并调用该 setter 方法进行注入
                // 4.1 这里直接拿取的ListableBeanFactory->DefaultListableBeanFactory
                String property = getSetterProperty(method);
                Object object = injector.getInstance(pt, property);
                
                // 5、将当前的对象注入到实例里面
                if (object != null) {
                    method.invoke(instance, object);
                }
            }
    return instance;
}

2.5 后置操作

  • 类似 Spirng 的后置处理器,之前也说过,感兴趣的可以看一下,整体思路区别不大
instance = postProcessAfterInitialization(instance, name);

private T postProcessAfterInitialization(T instance, String name) throws Exception {
    if (instance instanceof ExtensionAccessorAware) {
        ((ExtensionAccessorAware) instance).setExtensionAccessor(extensionDirector);
    }
    if (extensionPostProcessors != null) {
        for (ExtensionPostProcessor processor : extensionPostProcessors) {
            instance = (T) processor.postProcessAfterInitialization(instance, name);
        }
    }
    return instance;
}

2.6 Wrapper 的包装

2.6.1 Wrapper缓存

在讲该部分之前,我们先来看 cachedWrapperClasses 这个缓存的来历:

在我们上面解析文件配置时,会进行 loadClass,这里不仅会解析正常的类,也会解析 Wrapper 类,方便后面的包装

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,boolean overridden) {
    if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    }
}

从这里我们可以看到,最关键的当属判断当前的 Class 是不是属于 WrapperClass

protected boolean isWrapperClass(Class<?> clazz) {
    // 1、获取构造方法
    Constructor<?>[] constructors = clazz.getConstructors();
    // 2、从构造方法中取出参数为 1 且类型等于当前接口的
    for (Constructor<?> constructor : constructors) {
        if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == type) {
            return true;
        }
    }
    return false;
}

而具体的实现如下:

public class CityWrapper implements City{

    private City city;
    // 怎样判断扩展点还是aop切面呢?
    // 通过是否有这样的一个构造方法来判断
    public CityWrapper(City city) {
        this.city = city;
    }

    @Override
    public String getCityName() {
        return "文明城市" + city.getCityName();
    }
}

了解这个之后,我们再来看看 Dubbo 如何处理这些类似 AOP 的包装

2.6.2 Wrapper实现
if (wrap) {
    List<Class<?>> wrapperClassesList = new ArrayList<>();
	// 1、判断是否有Wrapper缓存
    // 1.1 将缓存放入当前
    // 1.2 排序 + 翻转
    if (cachedWrapperClasses != null) {
        wrapperClassesList.addAll(cachedWrapperClasses);
        wrapperClassesList.sort(WrapperComparator.COMPARATOR);
        Collections.reverse(wrapperClassesList);
    }
	
    // 2、当前的wrapper缓存不为空
    if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
        // 循环包装
        for (Class<?> wrapperClass : wrapperClassesList) {
            // 3、获取Wrapper注解,是否需要包装(正常都是包装的)
            Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
            // 4、判断下是否包装条件
            boolean match = (wrapper == null) ||
                ((ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) &&
                    !ArrayUtils.contains(wrapper.mismatches(), name));
            // 5、符合包装
            // 5.1 将当前类封装至wrapper中
            // 5.2 做一些后置处理
            if (match) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                instance = postProcessAfterInitialization(instance, name);
            }
        }
    }
}

通过这种方式,我们可以创建不同的 wrapper,实现 AOP 的作用

读过 从源码全面解析 dubbo 服务端服务调用的来龙去脉 和 从源码全面解析 dubbo 消费端服务调用的来龙去脉 的文章,这时候应该理解最后的那些 过滤器 怎么实现的了

比如,我们现在有两个 wrapper 类,分别是 CityWrapperCityWrapper2,实现类是 TianjinCity

那么,我们最终 TianjinCity 返回的实例如下:

  • CityWrapper
    • CityWrapper2
      • TianjinCity

image-20230716215711952

不得不说,这个包装还是有点秀秀的

2.7 生命周期管理

  • 实现 Lifecycle 的接口
initExtension(instance);

六、流程图

高清图片私聊博主获取

在这里插入图片描述

七、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

  • 美团二面:聊聊ConcurrentHashMap的存储流程
  • 从源码全面解析Java 线程池的来龙去脉
  • 从源码全面解析LinkedBlockingQueue的来龙去脉
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
  • 从源码全面解析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

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

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

相关文章

大数据平台测试-git常用操作(白盒测试基础)

一、前言 学习Git是非常有价值和重要的&#xff0c;无论是一个个人开发者还是在团队中进行协作开发。以下是一些学习Git的原因&#xff1a; 版本控制&#xff1a;Git是目前最流行的分布式版本控制系统&#xff0c;可以帮助你跟踪、管理和控制代码的版本。你可以轻松地回退到先…

gee架设教程

1:GameCenter 设置 1.1服务器控制 1.2 账号 1.3.1 配置向导 - 基本设置 1.3.2 配置向导 - 登录网关 1.3.3 配置向导 - 角色网关 1.3.4 配置向导 - 游戏网关 1.3.5 配置向导 - 登录服务器 1.3.6 配置向导 - 数据库服务器 1.3.7 配置向导 - 日志服务器 1.3.8 配置向导 - 主服务器…

SAP/ABAP(一)

一、什么是ERP ERP 是企业资源规划&#xff08;Enterprise Resource Planning&#xff09;的缩写&#xff0c;它指的是一种集成化的管理软件系统&#xff0c;用于协调企业内部各个部门的活动&#xff0c;并与外部供应商、客户以及其他利益相关方进行信息和业务流程的交互。 二、…

4.4.tensorRT基础(1)-模型推理时动态shape的具体实现要点

目录 前言1. 动态shape2. 补充知识总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 基础-模型推理时动态 shape 的…

科大讯飞星火认知大模型实在是太牛逼了吧,可以和人类进行自然交流,解答问题,高效完成各领域认知智能需求。

星火认知大模型&#xff1a;探索人工智能的无限可能 在21世纪的今天&#xff0c;人工智能技术已经逐渐成为推动社会发展的重要力量。作为一种模拟人类智能的技术手段&#xff0c;人工智能在各个领域都展现出了强大的应用潜力。而在这个领域中&#xff0c;星火认知大模型无疑是…

awk用法--一次性匹配文件中的多个文本,保存成不同的参数

功能描述&#xff1a; 需要从某个文件中读取多个指标的数据&#xff0c;并保存下来&#xff0c;读取的时候需要一次性读取出多个数据&#xff0c;之后将数据写入到结果文件 代码示例 主要逻辑&#xff1a; 1、 匹配包含MemTotal的字符串&#xff0c;并将匹配到的行的倒数第二列…

【电子学会】2023年05月图形化三级 -- 绘制多彩五角星

绘制多彩五角星 1. 准备工作 &#xff08;1&#xff09;选择背景stars、角色Pencil&#xff1b; &#xff08;2&#xff09;将角色Penci的中心点设为笔尖。 2. 功能实现 &#xff08;1&#xff09;将画笔粗细设为3&#xff0c;画笔的颜色和初始位置自定义&#xff0c;绘制边…

【gis插件】arcgis插件界址点编号工具、C#实现思路

数据&#xff1a;界址点图层、宗地图层 要求&#xff1a;找出宗地对应的所有界址点号&#xff0c;对这些界址点号以J1开始按顺序排列 要找出宗地所对应的所有界址点号&#xff0c;这里只要执行一个标识 即可得到这样得到的结果。 难点在于对界址点的编号&#xff0c;经过检查…

c语言小项目——通讯录初阶

通讯录中阶&#xff1a;点这里 通讯录&#xff08;初阶&#xff09; 项目简介项目中遇到的难点1.给复杂结构体初始化错误写法正确写法 2.枚举和switch可以结合一下&#xff0c;方便查看switch的case中是什么功能&#xff0c;double3.ShowContact中printf的新认知4.FindByName加…

Vue 打包到生产环境部署后图标不见了

问题描述&#xff1a;打包完后部署到正式环境有些图标不显示&#xff0c;如下图&#xff0c;显示为小方块。 解决方案&#xff1a; 这个问题可能是vue-cli脚手架配置问题 在build/webpack.prod.conf.js中 把extract&#xff1a;true 改为 fasle&#xff0c;然后再重新build就…

商城-学习整理-基础-项目简介和分布式概念(一)

目录 前言&#xff1a;一、项目简介1、项目背景2、项目架构图 二、分布式基础概念1、微服务2、集群&分布式&节点3、远程调用4、负载均衡5、服务注册/发现&注册中心6、配置中心7、服务熔断&服务降级8、APP网关 前言&#xff1a; 该项目基于逆向工程进行开发&am…

nginx+lua+redis环境搭建(文末赋上脚本)

目录 需求背景 环境搭建后nginx和redis版本 系统环境 搭建步骤 配置服务器DNS 安装ntpdate同步一下系统时间 安装网络工具、编译工具及依赖库 创建软件包下载目录、nginx和redis安装目录 下载配置安装lua解释器LuaJIT 下载nginx NDK&#xff08;ngx_devel_kit&#xff09…

51单片机学习--独立按键控制LED

功能&#xff1a;按下K1时D1亮&#xff0c;松开时D1灭&#xff0c;P3_1对应K1 , P2_0对应D1 #include <REGX52.H>void main() {while(1) {if(P3_1 0) //按下K1{P2_0 0;}else{P2_0 1;}}} 按下按钮和松开按钮时会有抖动&#xff0c;所以需要用延时函数来避免抖动造成的…

C国演义 [第十章]

第十章 最佳买卖股票时机含冷冻期题目理解步骤dp数组递推公式初始化遍历方向 代码 买卖股票的最佳时机含手续费题目理解步骤dp数组递推公式初始化遍历方向 代码 最佳买卖股票时机含冷冻期 力扣链接 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格…

瀚高数据库企业版V4单机版-安装手册(Windows)

目录 瀚高数据库企业版V4单机版-安装手册&#xff08;Windows&#xff09; 1. 环境准备 2. 软件安装 3.设置环境变量 4 配置数据库文件 瀚高数据库企业版V4单机版-安装手册&#xff08;Windows&#xff09; 1. 环境准备 ①.安装数据库之前&#xff0c;请确保vcredist_x6…

鸽了百万用户四年的赛博皮卡终于要来啦

作者 | Amy 编辑 | 德新 本月15号&#xff0c;特斯拉官方宣布&#xff0c;第一辆 赛博皮卡已在特斯拉得州工厂下线。 而就在本月初&#xff0c;马斯克还发推预热了一波&#xff0c;「开着赛博皮卡在奥斯汀&#xff08;特斯拉得州工厂所在地&#xff09;溜了一圈&#xff01…

网页动态表单 ,网页动态参数

有的时候因为参数太多 无法 一一 创建 所有采用动态创建 自己遇到的一个实际情况今天写个例子 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>form demo</title><link rel"stylesheet" href&quo…

Windows查看电脑出厂时间

方法一&#xff1a;CMD命令查询 CMD输入命令 >systeminfoBIOS版本时间大概就是出厂时间

基于Mybatis-Plus的代码自动生成器

代码自动生成器 由于在普通业务开发中大多数增删改查操作都是重复的大量的&#xff0c;修改的内容也是相当的少&#xff0c;就如一个模版一样。所以在此构造一个基于Mybatis-Plus的代码生成器&#xff0c;旨在于快速生成项目结构和基础代码。 1、搭建环境 新建一个Springboo…

C语言实现通讯录——动态内存

好与不好&#xff0c;干嘛从别人口中找答案 大家好&#xff0c;我是纪宁。 考试周过去了&#xff0c;刚放暑假也陆陆续续有一些事&#xff0c;这两天才开始静下心来好好学习。希望你我都能过一个充实且快乐的暑假&#xff01; 今天的文章是用C语言实现一个动态版的通讯录 文章…