-- 以下内容均基于2.1.8.RELEASE版本

紧接着上一篇(三)SpringBoot启动过程的分析-创建应用程序上下文,本文将分析上下文创建完毕之后的下一步操作:预处理上下文容器。

预处理上下文容器

预处理上下文容器由prepareContext()方法完成,本篇内容全部都是基于这个方法所涉及的内容进行分析。

// SpringApplication.java

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {

	// 设置环境对象,传入的对象是解析完毕profiles的对象,Context内部则是不完整的对象
context.setEnvironment(environment);
// 设置上下文参数
postProcessApplicationContext(context);
// 加载ApplicationContextInitializers
applyInitializers(context);
// 触发开始准备上下文事件
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 将启动参数包装为名为springApplicationArguments的DefaultApplicationArguments对象,并以单例模式注册
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments); // 设置打印的Banner
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
} // 设置是否允许覆盖BeanDefinition
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
} // 加载资源
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0])); // 触发上下文加载完毕事件
listeners.contextLoaded(context);
}

设置上下文参数

// SpringApplication.java

protected void postProcessApplicationContext(ConfigurableApplicationContext context) {

	// 以单例模式注册beanNameGenerator
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
} // 为上下文设置资源加载器和类加载器
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext) {
((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader) {
((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
}
} // 为上下文设置转换服务
if (this.addConversionService) {
context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
}
}

将前面步骤初始化的属性赋值给上下文容器,代码中的this代表的是SpringApplication。

加载ApplicationContextInitializers

在SpringApplication.run()的一开始它就通过SPI获取到所有的ApplicationContextInitializers,在这里他们将被执行。

protected void applyInitializers(ConfigurableApplicationContext context) {
// 调用每一个实现类的initialize方法
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
} // 将之前获取到的集合进行排序并返回只读集合
public Set<ApplicationContextInitializer<?>> getInitializers() {
return asUnmodifiableOrderedSet(this.initializers);
}

DelegatingApplicationContextInitializer

用于初始化配置文件(属性名:context.initializer.classes)中指定的ApplicationContextInitializer实现类

public void initialize(ConfigurableApplicationContext context) {
// 获取环境对象
ConfigurableEnvironment environment = context.getEnvironment();
// 获取context.initializer.classes属性指定的实现类
List<Class<?>> initializerClasses = getInitializerClasses(environment);
if (!initializerClasses.isEmpty()) {
// 调用其initialize()方法
applyInitializerClasses(context, initializerClasses);
}
} private List<Class<?>> getInitializerClasses(ConfigurableEnvironment env) {
// 通过指定属性去获取,此处常量属性值为context.initializer.classes
String classNames = env.getProperty(PROPERTY_NAME);
List<Class<?>> classes = new ArrayList<>();
if (StringUtils.hasLength(classNames)) {
// 根据代码可以推断出,context.initializer.classes的值可以用逗号拼接
for (String className : StringUtils.tokenizeToStringArray(classNames, ",")) {
classes.add(getInitializerClass(className));
}
}
return classes;
}

这里看起来好像很奇怪,本身当前的类就是一个ApplicationContextInitializer, 它已经被上游代码调用了initializer()方法,在initializer()方法中它又去获取ApplicationContextInitializer,然后接着调用initializer(),好像很绕。不过它的类描述已经说明了问题,它用于加载从配置文件指定

的那些ApplicationContextInitializer。如果阅读过第一篇概览SpringApplication.java #构造方法的话就会明白,当前对象就是在SpringApplication类的构造方法中通过SPI方式获取到的,而当前方法则是通过配置文件指定的方式

来获取。由此就可以得出一个结论:在SpringBoot中实现ApplicationContextInitializer并确保其被加载有三种方法,一是通过SpringApplication公开的addInitializers()方法直接添加,二是以SPI方式配置,三是以配置文件方式配置。

在SpringBoot中三种配置ApplicationContextInitializer的方法:

  1. 直接以代码方式添加

    new SpringApplication(Example.class).addInitializers(new 实现类());
  1. 以SPI方式

    在/META-INF/spring.factories文件中配置org.springframework.context.ApplicationContextInitializer=实现类A, 实现类B
  1. 以配置文件方式(语法取决于你使用什么样的配置文件properties or xml or yml or yaml or 配置中心)

    context.initializer.classes = 实现类A, 实现类B

SharedMetadataReaderFactoryContextInitializer

在ConfigurationClassPostProcessor和Spring Boot之间创建共享的CachingMetadataReaderFactory, 干啥的我也不知道,后续debug在研究。

ContextIdApplicationContextInitializer

用于设置SpringApplication.getId()的值,如果配置文件中指定了spring.application.name,则本类不起作用,反之。

ConfigurationWarningsApplicationContextInitializer

添加BeanFactoryPostProcessor:ConfigurationWarningsPostProcessor用于打印配置错误的日志

ServerPortInfoApplicationContextInitializer

用于设置server.ports属性的值,它是Web服务器实际监听的应用程序端口。同时实现了ApplicationListener接口用于监听WebServerInitializedEvent事件,WebServer初始化完毕之后设置端口。

// ServerPortInfoApplicationContextInitializer.java

// ①
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(this);
} // ②
public void onApplicationEvent(WebServerInitializedEvent event) {
String propertyName = "local." + getName(event.getApplicationContext()) + ".port";
setPortProperty(event.getApplicationContext(), propertyName, event.getWebServer().getPort());
} // ③
private String getName(WebServerApplicationContext context) {
String name = context.getServerNamespace();
return StringUtils.hasText(name) ? name : "server";
} // ④
private void setPortProperty(ApplicationContext context, String propertyName, int port) {
if (context instanceof ConfigurableApplicationContext) {
setPortProperty(((ConfigurableApplicationContext) context).getEnvironment(), propertyName, port);
}
if (context.getParent() != null) {
setPortProperty(context.getParent(), propertyName, port);
}
} @SuppressWarnings("unchecked")
private void setPortProperty(ConfigurableEnvironment environment, String propertyName, int port) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get("server.ports");
if (source == null) {
source = new MapPropertySource("server.ports", new HashMap<>());
sources.addFirst(source);
}
((Map<String, Object>) source.getSource()).put(propertyName, port);
}

① - 向上下文监听器列表中添加当前类(作为监听器加入)

② - 在监听事件中设置端口,默认属性为local.server.port

③ - 若设置了服务器Namespace,则属性值为local.Namespace的值.port

④ - 将监听端口信息填入server.ports属性下

ConditionEvaluationReportLoggingListener

用于设置ConditionEvaluationReportListener监听器,此监听器监听ContextRefreshedEvent和ApplicationFailedEvent,分别打印出上下文容器成功刷新和失败的日志报告

在这一章节中,可以看到ApplicationContextInitializer扩展接口的实际应用。通过Spring框架内置的一些initializer可以实现框架功能,同理我们也可以利用这一特性在上下文容器还未刷新之前做一些扩展功能。

发布ApplicationContextInitializedEvent事件

由SpringApplicationRunListeners.contextPrepared(ConfigurableApplicationContext context)方法触发。

当上下文容器加载完毕所有的ApplicationContextInitializer之后,触发该事件,通知那些关注了此事件的监听器进行下一步操作。每次发布事件的时候可能已经有新的监听器被加进来,在上一章节中我们就看到会在ApplicationContextInitializer里面添加监听器

但我们只需要关注那些监听了当前事件的监听器即可。这里仅仅作为一个提示,监听器可能在任何地方被加进来。这里并未发现有监听器监听此事件。

记录活动的Profiles日志

日志打印的:The following profiles are active: dev,test 这一句就来自于此。代码比较简单一看便知。

if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
} protected void logStartupProfileInfo(ConfigurableApplicationContext context) {
Log log = getApplicationLog();
if (log.isInfoEnabled()) {
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
if (ObjectUtils.isEmpty(activeProfiles)) {
String[] defaultProfiles = context.getEnvironment().getDefaultProfiles();
log.info("No active profile set, falling back to default profiles: " + StringUtils.arrayToCommaDelimitedString(defaultProfiles));
}
else {
log.info("The following profiles are active: "+ StringUtils.arrayToCommaDelimitedString(activeProfiles));
}
}
}

注册启动SpringApplication时的参数

将参数对象注册为单例模式

// SpringApplication.java

ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);

是否允许覆盖Bean定义

这个是和spring.main.allow-bean-definition-overriding参数有关,默认情况下在DefaultListableBeanFactory中是true,但是SpringBoot在此处给设置成了false。

if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}

加载Bean到上下文中

加载需要两个必备条件:谁来加载Bean,从哪加载Bean;此处加载BeanDefinition是由BeanDefinitionLoader来完成。

// SpringApplication.java#prepareContext()

// 获取当前应用要加载的资源
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 加载Bean
load(context, sources.toArray(new Object[0])); // 创建Bean加载器,并加载
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
// 创建BeanDefinitionLoader
BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}

初始化BeanDefinitionLoader

用于创建BeanDefinitionLoader,并从source加载类,它是对加载Bean的整体功能做了封装,内部由不同的资源加载类来完成不同类型的资源加载,例如从基于注解的类来开始加载,从xml文件开始加载

// SpringApplication.java

protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry registry, Object[] sources) {
return new BeanDefinitionLoader(registry, sources);
} class BeanDefinitionLoader { // 类的来源
private final Object[] sources; // JavaConfig类读取器
private final AnnotatedBeanDefinitionReader annotatedReader; // xml文件类读取器
private final XmlBeanDefinitionReader xmlReader; // groovy类读取器
private BeanDefinitionReader groovyReader; // classpath类读取器
private final ClassPathBeanDefinitionScanner scanner; // 资源加载器
private ResourceLoader resourceLoader; BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
Assert.notNull(registry, "Registry must not be null");
Assert.notEmpty(sources, "Sources must not be empty");
this.sources = sources;
this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
this.xmlReader = new XmlBeanDefinitionReader(registry);
if (isGroovyPresent()) {
this.groovyReader = new GroovyBeanDefinitionReader(registry);
}
this.scanner = new ClassPathBeanDefinitionScanner(registry);
this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}
// ...省略部分代码
}

通过观察它的内部属性和构造方法可以看出,它支持加载基于编程方式配置的类,支持xml文件配置的类,支持groovy和xml混合定义的类的加载。它使用门面模式封装了这些具体的加载器。此处只需要先知道BeanDefinitionLoader用于加载Bean,且它内部封装了多个类读取器即可

不必深入。先了解大体的功能性即可。后续会开单独篇章介绍。

使用BeanDefinitionLoader加载Bean

// BeanDefinitionLoader.java

// ①
public int load() {
int count = 0;
for (Object source : this.sources) {
count += load(source);
}
return count;
} // ②
private int load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class<?>) {
return load((Class<?>) source);
}
if (source instanceof Resource) {
return load((Resource) source);
}
if (source instanceof Package) {
return load((Package) source);
}
if (source instanceof CharSequence) {
return load((CharSequence) source);
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}

① - 记录加载的资源数量

② - 根据传入的资源类型选择不同的加载方式

此处我们选择SpringBoot典型的资源加载方式Class方式来分析,在应用程序启动入口我们使用的是SpringApplication.run(Example.class, args);而Example.class是我们的main函数所在类,以它作为资源来加载。

// BeanDefinitionLoader.java

private int load(Class<?> source) {
// ①
if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
// Any GroovyLoaders added in beans{} DSL can contribute beans here
GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
load(loader);
}
// ②
if (isComponent(source)) {
this.annotatedReader.register(source);
return 1;
}
return 0;
}

① - 判断是否支持Groovy

② - 判断是否包含@Component注解,如果包含才开始注册

// BeanDefinitionLoader.java

private boolean isComponent(Class<?> type) {

	if (AnnotationUtils.findAnnotation(type, Component.class) != null) {
return true;
}
if (type.getName().matches(".*\\$_.*closure.*") || type.isAnonymousClass() || type.getConstructors() == null || type.getConstructors().length == 0) {
return false;
}
return true;
} // AnnotationUtils.java public static <A extends Annotation> A findAnnotation(Class<?> clazz, @Nullable Class<A> annotationType) {
return findAnnotation(clazz, annotationType, true);
} @SuppressWarnings("unchecked")
@Nullable
private static <A extends Annotation> A findAnnotation(Class<?> clazz, @Nullable Class<A> annotationType, boolean synthesize) { Assert.notNull(clazz, "Class must not be null");
if (annotationType == null) {
return null;
} // ①
AnnotationCacheKey cacheKey = new AnnotationCacheKey(clazz, annotationType);
A result = (A) findAnnotationCache.get(cacheKey);
if (result == null) {
result = findAnnotation(clazz, annotationType, new HashSet<>());
if (result != null && synthesize) {
result = synthesizeAnnotation(result, clazz);
findAnnotationCache.put(cacheKey, result);
}
}
return result;
}

① - 从缓存获取被加载的资源类是否有@Component注解的缓存

默认情况下是没有缓存,此时将会去查找Example.class是否包含@Component注解。那在默认情况下我们的启动类并未直接标明这个注解, 一个典型的SpringBoot Web应用如下:


@RestController
@SpringBootApplication
public class Example { @RequestMapping("/")
String home() {
return "Hello World!";
} public static void main(String[] args) {
SpringApplication.run(Example.class, args); } }

实际上@RestController和@SpringBootApplication两个注解都包含了@Component注解。因此Example.class自然也就具有@Component注解,感兴趣的可以亲自点进去看看这两个注解内部。更多细节将来会单独分析。在确定了它支持@Component注解之后,开始加载Bean。

// BeanDefinitionLoader#load(Class<?> source)

this.annotatedReader.register(source);

// AnnotatedBeanDefinitionReader.java

<T> void doRegisterBean(Class<T> annotatedClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name,
@Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) { // ①
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
// ②
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
} // ③
abd.setInstanceSupplier(instanceSupplier);
// ④
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
// ⑤
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
// ⑥
if (qualifiers != null) {
for (Class<? extends Annotation> qualifier : qualifiers) {
if (Primary.class == qualifier) {
abd.setPrimary(true);
}
else if (Lazy.class == qualifier) {
abd.setLazyInit(true);
}
else {
abd.addQualifier(new AutowireCandidateQualifier(qualifier));
}
}
}
// ⑦
for (BeanDefinitionCustomizer customizer : definitionCustomizers) {
customizer.customize(abd);
} // ⑧
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

① - 创建一个带注解元数据的类定义。

② - 判断是否有@Conditional注解,以确定是否满足条件需要排除。

③ - 设置一个创建Bean的回调,基本都是空值。

④ - 获取当前Bean的作用域元数据,判断依据是是否包含@Scope注解。ScopeMetadata包含类的作用域描述(singleton | prototype)和代理模式描述ScopedProxyMode,默认为单例模式,不使用代理创建。

⑤ - 处理常用注解,包括@Lazy、@Primary、@Role、@Description、@DependsOn,他们将被设置到BeanDefinition的属性当中。

⑥ - 检查传入的Qualifiers。

⑦ - 暂时不知道干啥的,以后再说

⑧ - 确认好当前Bean的代理模式,并注册。

在真正将一个类以BeanDefinition来注册的时候需要把可能涉及到的一些特性全部都检查一遍。此处只加载了我们的示例类Example.class,并未加载其他类。

发布ApplicationPreparedEvent事件

由EventPublishingRunListener.contextLoaded(ConfigurableApplicationContext context)方法触发。

在真正触发事件之前,它处理了ApplicationContextAware实现,为其设置了上下文。

为ApplicationContextAware扩展接口设置上下文

public void contextLoaded(ConfigurableApplicationContext context) {
for (ApplicationListener<?> listener : this.application.getListeners()) {
if (listener instanceof ApplicationContextAware) {
((ApplicationContextAware) listener).setApplicationContext(context);
}
// 将所有监听器赋值给上下文
context.addApplicationListener(listener);
}
this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
}

ConfigFileApplicationListener

添加了一个BeanFactoryPostProcessor:PropertySourceOrderingPostProcessor,用于重排序defaultProperties

LoggingApplicationListener

以单例模式注册注册当前日志系统

总结

在预处理上下文时将先前加载的一些属性赋值给context,执行了ApplicationContextInitializers的实现类,为ApplicationContextAware接口填充context对象。

(四)SpringBoot启动过程的分析-预处理ApplicationContext的更多相关文章

  1. (五)SpringBoot启动过程的分析-刷新ApplicationContext

    -- 以下内容均基于2.1.8.RELEASE版本 紧接着上一篇[(四)SpringBoot启动过程的分析-预处理ApplicationContext] (https://www.cnblogs.co ...

  2. (三)SpringBoot启动过程的分析-创建应用程序上下文

    -- 以下内容均基于2.1.8.RELEASE版本 紧接着上一篇(二)SpringBoot启动过程的分析-环境信息准备,本文将分析环境准备完毕之后的下一步操作:ApplicationContext的创 ...

  3. (一)SpringBoot启动过程的分析-启动流程概览

    -- 以下内容均基于2.1.8.RELEASE版本 通过粗粒度的分析SpringBoot启动过程中执行的主要操作,可以很容易划分它的大流程,每个流程只关注重要操作为后续深入学习建立一个大纲. 官方示例 ...

  4. (二)SpringBoot启动过程的分析-环境信息准备

    -- 以下内容均基于2.1.8.RELEASE版本 由上一篇SpringBoot基本启动过程的分析可以发现在run方法内部启动SpringBoot应用时采用多个步骤来实现,本文记录启动的第二个环节:环 ...

  5. SpringBoot启动过程原理

    最近这两年springboot突然火起来了,那么我们就来看看springboot的运行原理. 一.springboot的三种启动方式: 1.运行带有main方法的2.通过命令 Java -jar命令3 ...

  6. Spring Boot 学习笔记一(SpringBoot启动过程)

    SpringBoot启动 Spring Boot通常有一个名为*Application的入口类,在入口类里有一个main方法,这个main方法其实就是一个标准的java应用的入口方法. 在main方法 ...

  7. U-Boot启动过程完全分析

    U-Boot启动过程完全分析 1.1       U-Boot工作过程 U-Boot启动内核的过程可以分为两个阶段,两个阶段的功能如下: (1)第一阶段的功能 硬件设备初始化 加载U-Boot第二阶段 ...

  8. Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还需要有一个 Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home ...

  9. Android应用程序组件Content Provider的启动过程源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Andr ...

随机推荐

  1. Recoil & React official state management

    Recoil & React official state management Redux Recoil.js https://recoiljs.org/ A state managemen ...

  2. CSS style color all in one

    CSS style color all in one https://developer.mozilla.org/en-US/docs/Web/CSS/color_value /* Hexadecim ...

  3. window.locationbar

    window.locationbar demo https://cdn.xgqfrms.xyz/#/operate-tool/select-seat-system/select-detail?temp ...

  4. qt 取进程列表,读写内存, 写字节集

    导入库 pro win32:LIBS += -lpsapi win32:LIBS += -lkernel32 获取列表 #include "mainwindow.h" #inclu ...

  5. c#(winform)获取本地打印机

    引用 using System.Drawing.Printing; //代码 PrintDocument prtdoc = new PrintDocument(); string strDefault ...

  6. 微信小程序:解决小程序中有些格式如webpiPhone手机暂不支持的问题

    问题:小程序中有些格式是iPhone手机暂不支持的,如goods_introduce中的webp格式,在小程序的模拟器中是可以正常显示webp格式的,但是一旦你做真机调试,很可能某些iPhone手机是 ...

  7. docker 上传到docker hub 采坑

    前面是仓库名称 后面可以命名img名字 docker push gaodi2345/wj:docker_gui

  8. go mod包管理 加代理下载

    原始go.mod文件 module xxx go 1.14 报错 i/o timeout go mod init workorder go mod init: go.mod already exist ...

  9. Python切换版本工具pyenv

    目录 安装pyenv 安装与查看py版本 切换py版本 结合ide使用示例 和virtualenv的一些区别 参考文献 使用了一段时间,我发现这玩意根本不是什么神器,简直就是垃圾,安装多版本总是失败, ...

  10. 剑指 Offer 30. 包含min函数的栈 + 双栈实现求解栈中的最小值

    剑指 Offer 30. 包含min函数的栈 Offer_30 题目描述: 题解分析: 题目其实考察的是栈的知识,本题的目的是使用两个栈来求解最小值. 第二个栈主要用来维护第一个栈中的最小值,所以它里 ...