前言:前文【spring源码分析】IOC容器初始化(九)中分析了AbstractAutowireCapableBeanFactory#createBeanInstance方法中通过工厂方法创建bean对象的流程,这里接着分析createBeanInstance方法中的剩余流程。


直接看createBeanInstance中剩余的流程:

 // AbstractAutowireCapableBeanFactory
boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
// 做同步
synchronized (mbd.constructorArgumentLock) {
// 如果已缓存的解析构造函数或者工厂方法不为null,则可以利用构造函数解析
// 因为需要根据参数确认到底使用哪个构造函数,该过程比较消耗性能,所以采用缓存机制
if (mbd.resolvedConstructorOrFactoryMethod != null) {
resolved = true;
autowireNecessary = mbd.constructorArgumentsResolved;
}
}
}
// 已经解析好了,直接注入即可
if (resolved) {
// autowire自动注入,调用构造函数自动注入
if (autowireNecessary) {
return autowireConstructor(beanName, mbd, null, null);
} else {
// 使用默认构造函数构造
return instantiateBean(beanName, mbd);
}
} // Need to determine the constructor...
// 确定解析的构造函数
// 主要是检查已经注册的SmartInstantiationAwareBeanPostProcessor
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
// 确定构造方法进行bean创建
if (ctors != null ||
mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
} // 有参数,又没有获取到构造方法,则只能调用无参构造方法来创建实例
// 这是一个兜底的方法
// No special handling: simply use no-arg constructor.
return instantiateBean(beanName, mbd);

分析:

剩余流程还剩下两大分支:构造函数自动注入初始化bean和默认构造函数初始化bean。

#1 AbstractAutowireCapableBeanFactory#autowireConstructor

     protected BeanWrapper autowireConstructor(
String beanName, RootBeanDefinition mbd, @Nullable Constructor<?>[] ctors, @Nullable Object[] explicitArgs) { return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs);
}

可以看到这里还是委托给ConstructorResolver来执行的:

 public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) { // 封装BeanWrapperImpl并完成初始化
BeanWrapperImpl bw = new BeanWrapperImpl();
this.beanFactory.initBeanWrapper(bw); // 获取constructorToUse、argsHolderToUse、argsToUse属性
Constructor<?> constructorToUse = null; // 构造函数
ArgumentsHolder argsHolderToUse = null; // 构造参数
Object[] argsToUse = null; // 构造参数 // 确定构造参数
// 如果getBean()已传递,则直接使用
if (explicitArgs != null) {
argsToUse = explicitArgs;
} else {
// 尝试从缓存中获取
Object[] argsToResolve = null;
// 同步
synchronized (mbd.constructorArgumentLock) {
// 缓存中的构造函数或工厂方法
constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod;
if (constructorToUse != null && mbd.constructorArgumentsResolved) {
// Found a cached constructor...
// 缓存中的构造参数
argsToUse = mbd.resolvedConstructorArguments;
if (argsToUse == null) {
// 获取缓存中的构造函数参数 包括可见字段
argsToResolve = mbd.preparedConstructorArguments;
}
}
}
// 缓存中存在,则解析存储在BeanDefinition中的参数
// 如给定方法的构造函数 f(int,int),则通过此方法后就会把配置文件中的("1","1")转换为 (1,1)
// 缓存中的值可能是原始值也有可能是最终值
if (argsToResolve != null) {
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
}
} // 没有缓存,则尝试从配置文件中获取参数
if (constructorToUse == null || argsToUse == null) {
// Take specified constructors, if any.
Constructor<?>[] candidates = chosenCtors;
// 如果chosenCtors未传入,则获取构造方法
if (candidates == null) {
Class<?> beanClass = mbd.getBeanClass();
try {
candidates = (mbd.isNonPublicAccessAllowed() ?
beanClass.getDeclaredConstructors() : beanClass.getConstructors());
} catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Resolution of declared constructors on bean Class [" + beanClass.getName() +
"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
}
}
// 创建bean
if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
Constructor<?> uniqueCandidate = candidates[0];
if (uniqueCandidate.getParameterCount() == 0) {
synchronized (mbd.constructorArgumentLock) {
mbd.resolvedConstructorOrFactoryMethod = uniqueCandidate;
mbd.constructorArgumentsResolved = true;
mbd.resolvedConstructorArguments = EMPTY_ARGS;
}
bw.setBeanInstance(instantiate(beanName, mbd, uniqueCandidate, EMPTY_ARGS));
return bw;
}
} // 是否需要解析构造器
// Need to resolve the constructor.
boolean autowiring = (chosenCtors != null ||
mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
// 用于承载解析后的构造函数参数值
ConstructorArgumentValues resolvedValues = null; int minNrOfArgs;
if (explicitArgs != null) {
minNrOfArgs = explicitArgs.length;
} else {
// 从BeanDefinition中获取构造参数,也就是从配置文件中提取构造参数
ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
resolvedValues = new ConstructorArgumentValues();
// 解析构造函数的参数
// 将该bean的构造函数参数解析为resolvedValues对象,其中会涉及到其他bean
minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
} // 对构造函数进行排序处理
// public构造函数优先,参数数量降序,非public构造函数参数数量降序
AutowireUtils.sortConstructors(candidates); // 最小参数类型权重
int minTypeDiffWeight = Integer.MAX_VALUE;
Set<Constructor<?>> ambiguousConstructors = null;
LinkedList<UnsatisfiedDependencyException> causes = null; // 遍历所有构造函数
for (Constructor<?> candidate : candidates) {
// 获取该构造函数的参数类型
Class<?>[] paramTypes = candidate.getParameterTypes(); // 如果已经找到选用的构造函数且需要的参数个数大于当前构造函数参数个数,则break
if (constructorToUse != null && argsToUse != null && argsToUse.length > paramTypes.length) {
// Already found greedy constructor that can be satisfied ->
// do not look any further, there are only less greedy constructors left.
break;
}
// 参数个数不等,继续循环
if (paramTypes.length < minNrOfArgs) {
continue;
} // 参数持有者ArgumentsHolder对象
ArgumentsHolder argsHolder;
if (resolvedValues != null) {
try {
// 注释上获取参数名称
String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length);
if (paramNames == null) {
// 获取构造函数、方法参数的探测器
ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
if (pnd != null) {
// 通过探测器获取构造函数的参数名称
paramNames = pnd.getParameterNames(candidate);
}
}
// 根据构造函数和构造参数,创建参数持有者ArgumentsHolder对象
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
} catch (UnsatisfiedDependencyException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex);
}
// Swallow and try next constructor.
if (causes == null) {
causes = new LinkedList<>();
}
// 若产生UnsatisfiedDependencyException异常,则添加到causes中
causes.add(ex);
continue;
}
} else {
// Explicit arguments given -> arguments length must match exactly.
// 构造函数中没有参数,则continue
if (paramTypes.length != explicitArgs.length) {
continue;
}
// 根据explicitArgs创建ArgumentsHolder对象
argsHolder = new ArgumentsHolder(explicitArgs);
} // isLenientConstructorResolution 判断解析构造函数的时候是否以宽松模式还是严格模式
// 严格模式:解析构造函数时,必须所有的都需要匹配,否则抛出异常
// 宽松模式:使用具有"最接近的模式"进行匹配
// typeDiffWeight:类型差异权重
int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
// Choose this constructor if it represents the closest match.
// 如果它代表着当前最接近的匹配则选择其作为构造函数
if (typeDiffWeight < minTypeDiffWeight) {
constructorToUse = candidate;
argsHolderToUse = argsHolder;
argsToUse = argsHolder.arguments;
minTypeDiffWeight = typeDiffWeight;
ambiguousConstructors = null;
} else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
if (ambiguousConstructors == null) {
ambiguousConstructors = new LinkedHashSet<>();
ambiguousConstructors.add(constructorToUse);
}
ambiguousConstructors.add(candidate);
}
} // 没有可执行的工厂方法,则抛出异常
if (constructorToUse == null) {
if (causes != null) {
UnsatisfiedDependencyException ex = causes.removeLast();
for (Exception cause : causes) {
this.beanFactory.onSuppressedException(cause);
}
throw ex;
}
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Could not resolve matching constructor " +
"(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)");
} else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Ambiguous constructor matches found in bean '" + beanName + "' " +
"(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " +
ambiguousConstructors);
} if (explicitArgs == null && argsHolderToUse != null) {
// 将解析的构造函数加入缓存
argsHolderToUse.storeCache(mbd, constructorToUse);
}
}
// 创建bean对象,并设置到BeanWrapperImpl中
Assert.state(argsToUse != null, "Unresolved constructor arguments");
bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
return bw;
}

分析:

该函数可简单理解为是带有参数的构造方法来进行bean的初始化。可以看到该函数与【spring源码分析】IOC容器初始化(九)中分析的instantiateUsingFactoryMethod函数一样方法体都非常的大,如果理解了instantiateUsingFactoryMethod中的初始化过程,其实对autowireConstructor函数的理解就不是很难了,该函数主要是确定构造函数参数、构造函数然后调用相应的初始化策略对bean进行初始化,其大部分逻辑都与instantiateUsingFactoryMethod函数一致,如有不明之处,请移步到上篇文章,但需但需注意其中的instantiate是不一样的,这里我们来看下该函数。

instantiate

 private Object instantiate(
String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse) { try {
InstantiationStrategy strategy = this.beanFactory.getInstantiationStrategy();
if (System.getSecurityManager() != null) {
return AccessController.doPrivileged((PrivilegedAction<Object>) () ->
strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse),
this.beanFactory.getAccessControlContext());
} else {
return strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse);
}
} catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Bean instantiation via constructor failed", ex);
}
}

分析:

这里对bean进行实例化,会委托SimpleInstantiationStrategy#instantiate方法进行实现:

 public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
final Constructor<?> ctor, Object... args) {
// 没有覆盖,则直接使用反射实例化即可
if (!bd.hasMethodOverrides()) {
if (System.getSecurityManager() != null) {
// use own privileged to change accessibility (when security is on)
// 设置构造方法,可访问
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
ReflectionUtils.makeAccessible(ctor);
return null;
});
}
// 通过BeanUtils直接使用构造器对象实例化bean对象
return BeanUtils.instantiateClass(ctor, args);
} else {
// CGLIB创建bean对象
return instantiateWithMethodInjection(bd, beanName, owner, ctor, args);
}
}

分析:

  • 如果该bean没有配置lookup-method、replace-method或者@LookUp注解,则直接通过反射的方式实例化bean对象即可。
  • 如果存在覆盖,则需要使用CGLIB进行动态代理,因为在创建代理的同时可将动态方法织入类中。

#1 通过反射实例化bean对象

 // BeanUtils
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
Assert.notNull(ctor, "Constructor must not be null");
try {
// 设置构造方法,可访问
ReflectionUtils.makeAccessible(ctor);
// 使用构造方法创建对象
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
return KotlinDelegate.instantiateClass(ctor, args);
} else {
Class<?>[] parameterTypes = ctor.getParameterTypes();
Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
Object[] argsWithDefaultValues = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
Class<?> parameterType = parameterTypes[i];
argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
} else {
argsWithDefaultValues[i] = args[i];
}
}
return ctor.newInstance(argsWithDefaultValues);
}
}
// 各种异常,最终统一抛出BeanInstantiationException异常
catch (InstantiationException ex) {
throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
} catch (IllegalAccessException ex) {
throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex);
} catch (IllegalArgumentException ex) {
throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex);
} catch (InvocationTargetException ex) {
throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException());
}
}

#2 通过CGLIB创建Bean对象,这里会通过其子类实现

 // CglibSubclassingInstantiationStrategy
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
@Nullable Constructor<?> ctor, Object... args) { // Must generate CGLIB subclass...
// 通过CGLIB生成一个子类对象
return new CglibSubclassCreator(bd, owner).instantiate(ctor, args);
}

分析:

首先创建一个CglibSubclassCreator对象,然后调用其instantiate进行初始化bean对象。

CglibSubclassingInstantiationStrategy#instantiate

 public Object instantiate(@Nullable Constructor<?> ctor, Object... args) {
// 通过cglib创建一个代理类
Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
Object instance;
// 没有构造器,通过BeanUtils使用默认构造器创建一个bean实例
if (ctor == null) {
instance = BeanUtils.instantiateClass(subclass);
} else {
try {
// 获取代理类对应的构造器对象,并实例化bean
Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes());
instance = enhancedSubclassConstructor.newInstance(args);
} catch (Exception ex) {
throw new BeanInstantiationException(this.beanDefinition.getBeanClass(),
"Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex);
}
}
// SPR-10785: set callbacks directly on the instance instead of in the
// enhanced class (via the Enhancer) in order to avoid memory leaks.
// 为了避免memory leaks异常,直接在bean实例上设置回调对象
Factory factory = (Factory) instance;
factory.setCallbacks(new Callback[]{NoOp.INSTANCE,
new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
return instance;
}

分析:

这里是通过CGLIB来创建bean对象,这部分内容后续分析AOP的时候再详细分析。

至此通过构造函数自动注入初始化bean对象分析完毕,下面来看使用默认构造函数初始化bean。

#2 AbstractAutowireCapableBeanFactory#instantiateBean

 protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
try {
Object beanInstance;
final BeanFactory parent = this;
// 安全模式
if (System.getSecurityManager() != null) {
// 获得InstantiationStrategy对象,并使用它创建bean对象
beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () -> getInstantiationStrategy().instantiate(mbd, beanName, parent),
getAccessControlContext());
} else {
// 获得InstantiationStrategy对象,并使用它创建bean对象
beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
}
// 封装BeanWrapperImpl,并完成初始化
BeanWrapper bw = new BeanWrapperImpl(beanInstance);
initBeanWrapper(bw);
return bw;
} catch (Throwable ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}

分析:

在经历过前面大方法体的分析后,再看该方法就简单得多了,该方法不需要构造参数,所以不需要进行复杂的确定构造参数、构造器等步骤,主要通过instantiate实例化对象后,注入BeanWrapper中,然后初始化BeanWrapper。

SimpleInstantiationStrategy#instantiate

 public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
// Don't override the class with CGLIB if no overrides.
// 没有覆盖,直接使用反射实例化即可
if (!bd.hasMethodOverrides()) {
Constructor<?> constructorToUse;
synchronized (bd.constructorArgumentLock) {
// 获得构造方法 constructorToUse
constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
if (constructorToUse == null) {
final Class<?> clazz = bd.getBeanClass();
// 如果是接口,则抛出异常
if (clazz.isInterface()) {
throw new BeanInstantiationException(clazz, "Specified class is an interface");
}
try {
// 从clazz中,获得构造方法
if (System.getSecurityManager() != null) {
constructorToUse = AccessController.doPrivileged(
(PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
} else {
constructorToUse = clazz.getDeclaredConstructor();
}
// 标记resolvedConstructorOrFactoryMethod属性
bd.resolvedConstructorOrFactoryMethod = constructorToUse;
} catch (Throwable ex) {
throw new BeanInstantiationException(clazz, "No default constructor found", ex);
}
}
}
// 通过BeanUtils直接使用构造器实例化bean对象
return BeanUtils.instantiateClass(constructorToUse);
} else {
// 通过CGLIB创建子类对象
// Must generate CGLIB subclass.
return instantiateWithMethodInjection(bd, beanName, owner);
}
}

分析:

该方法也比较简单,通过判断BeanDefinition是否有覆盖方法进行分支:通过反射或CGLIB来创建bean实例。

最后,如果以上分支还不满足,则会通过instantiateBean方法兜底来完成bean的实例化。

总结

终于把AbstractAutowireCapableBeanFactory#createBeanInstance方法大致分析完了,该方法就是选择合适的实例化策略来为bean创建实例对象,具体策略有:

  • Supplier回调方式。
  • 工厂方法初始化。
  • 构造函数自动注入初始化。
  • 默认构造函数初始化。

其中工厂方法初始化与构造函数自动注入初始化方式非常复杂,需要大量的经历来确定构造函数和构造参数。

至此createBeanInstance过程已经分析完毕,下面将介绍doCreateBean的其他处理流程。


by Shawn Chen,2019.04.25,下午。

【spring源码分析】IOC容器初始化(十)的更多相关文章

  1. SPRING源码分析:IOC容器

    在Spring中,最基本的IOC容器接口是BeanFactory - 这个接口为具体的IOC容器的实现作了最基本的功能规定 - 不管怎么着,作为IOC容器,这些接口你必须要满足应用程序的最基本要求: ...

  2. Spring源码解析-ioc容器的设计

    Spring源码解析-ioc容器的设计 1 IoC容器系列的设计:BeanFactory和ApplicatioContext 在Spring容器中,主要分为两个主要的容器系列,一个是实现BeanFac ...

  3. spring源码分析---IOC(1)

    我们都知道spring有2个最重要的概念,IOC(控制反转)和AOP(依赖注入).今天我就分享一下spring源码的IOC. IOC的定义:直观的来说,就是由spring来负责控制对象的生命周期和对象 ...

  4. spring 源码之 ioc 容器的初始化和注入简图

    IoC最核心就是两个过程:IoC容器初始化和IoC依赖注入,下面通过简单的图示来表述其中的关键过程:

  5. Spring源码阅读-IoC容器解析

    目录 Spring IoC容器 ApplicationContext设计解析 BeanFactory ListableBeanFactory HierarchicalBeanFactory Messa ...

  6. Spring 源码剖析IOC容器(一)概览

    目录 一.容器概述 二.核心类源码解读 三.模拟容器获取Bean ======================= 一.容器概述 spring IOC控制反转,又称为DI依赖注入:大体是先初始化bean ...

  7. Spring源码解析-IOC容器的实现

    1.IOC容器是什么? IOC(Inversion of Control)控制反转:本来是由应用程序管理的对象之间的依赖关系,现在交给了容器管理,这就叫控制反转,即交给了IOC容器,Spring的IO ...

  8. Spring源码解析-IOC容器的实现-ApplicationContext

    上面我们已经知道了IOC的建立的基本步骤了,我们就可以用编码的方式和IOC容器进行建立过程了.其实Spring已经为我们提供了很多实现,想必上面的简单扩展,如XMLBeanFacroty等.我们一般是 ...

  9. Spring源码之IOC容器创建、BeanDefinition加载和注册和IOC容器依赖注入

    总结 在SpringApplication#createApplicationContext()执行时创建IOC容器,默认DefaultListableBeanFactory 在AbstractApp ...

随机推荐

  1. Python开发【内置函数篇】re正则表达式

    一.简介 正则表达式本身是一种小型的.高度专业化的编程语言,而在python中,通过内嵌集成re模块,程序媛们可以直接调用来实现正则匹配.正则表达式模式被编译成一系列的字节码,然后由用C编写的匹配引擎 ...

  2. 如何在你的项目中集成 CAP【手把手视频教程】

    前言 之前录制过一期关于CAP的视频,但是由于当时是直播时录制的视频,背景音比较杂所以质量有点差.这次的视频没有直播,直接录制的,视频质量会好很多,第一遍录制完成之后发现播放到一半没有声音,所以又重新 ...

  3. Socket模拟HTTP请求

    WEB服务器可以可以理解为socket的上层封装,其也是TCP/IP协议,只要知道其IP地址和端口号就可以与他进行通信了 与WEB服务器数据交互,其重点在于请求头,如果请求头不对则不能进行数据传输 简 ...

  4. DotNetCore跨平台~关于appsettings.json里各种配置项的读取

    回到目录 对于dotnet Core来说,依赖注入的集成无疑是最大的亮点,它主要用在服务注册与注入和配置文件注册与注入上面,我们一般会在程序入口先注册服务或者文件,然后在需要的地方使用注入即可,下面主 ...

  5. SQL优化 MySQL版 - 避免索引失效原则(一)

    避免索引失效原则(一) 精力有限,剩余的失效原则将会在 <避免索引失效原则(二)>中连载出来,请谅解 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 避免索引失效的一些原 ...

  6. Selenium的发展历史及原理

    目录 1. selenium1.0的产生 2. webdriver的产生 3. selenium和webdriver的合并 4. selenium3.0的产生 1. selenium1.0的产生 为什 ...

  7. Flutter 即学即用系列博客——10 混淆

    前言 之前的博客我们都是在 debug 的模式下进行开发的. 实际发布到市场或者给到用户的都是 release 包. 而对于 Android 来说,release 包一个重要的步骤就是混淆. Andr ...

  8. 简单多播委托Demo

    namespace ConsoleApp4 { class Program { static void Main(string[] args) { Mum mum = new Mum(); Dad d ...

  9. asp.net后台管理系统-登陆模块-是否自动登陆

    FormsAuthentication.SetAuthCookie(UserFlag, createPersistentCookie); createPersistentCookie是否永久保存coo ...

  10. C#中的CultureInfo类

    CultureInfo类位于System.Globalization命名空间内,这个类和命名空间许多人都不是很熟悉,实际我们在写程序写都经常间接性的接触这个类,当进行数字,日期时间,字符串匹配时,都会 ...