• 【spring源码分析】IOC容器初始化(十)


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


    直接看createBeanInstance中剩余的流程:

     1 // AbstractAutowireCapableBeanFactory
     2 boolean resolved = false;
     3         boolean autowireNecessary = false;
     4         if (args == null) {
     5             // 做同步
     6             synchronized (mbd.constructorArgumentLock) {
     7                 // 如果已缓存的解析构造函数或者工厂方法不为null,则可以利用构造函数解析
     8                 // 因为需要根据参数确认到底使用哪个构造函数,该过程比较消耗性能,所以采用缓存机制
     9                 if (mbd.resolvedConstructorOrFactoryMethod != null) {
    10                     resolved = true;
    11                     autowireNecessary = mbd.constructorArgumentsResolved;
    12                 }
    13             }
    14         }
    15         // 已经解析好了,直接注入即可
    16         if (resolved) {
    17             // autowire自动注入,调用构造函数自动注入
    18             if (autowireNecessary) {
    19                 return autowireConstructor(beanName, mbd, null, null);
    20             } else {
    21                 // 使用默认构造函数构造
    22                 return instantiateBean(beanName, mbd);
    23             }
    24         }
    25 
    26         // Need to determine the constructor...
    27         // 确定解析的构造函数
    28         // 主要是检查已经注册的SmartInstantiationAwareBeanPostProcessor
    29         Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    30         // 确定构造方法进行bean创建
    31         if (ctors != null ||
    32                 mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
    33                 mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
    34             return autowireConstructor(beanName, mbd, ctors, args);
    35         }
    36 
    37         // 有参数,又没有获取到构造方法,则只能调用无参构造方法来创建实例
    38         // 这是一个兜底的方法
    39         // No special handling: simply use no-arg constructor.
    40         return instantiateBean(beanName, mbd);

    分析:

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

    #1 AbstractAutowireCapableBeanFactory#autowireConstructor

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

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

      1 public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
      2                                            @Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {
      3 
      4         // 封装BeanWrapperImpl并完成初始化
      5         BeanWrapperImpl bw = new BeanWrapperImpl();
      6         this.beanFactory.initBeanWrapper(bw);
      7 
      8         // 获取constructorToUse、argsHolderToUse、argsToUse属性
      9         Constructor<?> constructorToUse = null; // 构造函数
     10         ArgumentsHolder argsHolderToUse = null; // 构造参数
     11         Object[] argsToUse = null;  // 构造参数
     12 
     13         // 确定构造参数
     14         // 如果getBean()已传递,则直接使用
     15         if (explicitArgs != null) {
     16             argsToUse = explicitArgs;
     17         } else {
     18             // 尝试从缓存中获取
     19             Object[] argsToResolve = null;
     20             // 同步
     21             synchronized (mbd.constructorArgumentLock) {
     22                 // 缓存中的构造函数或工厂方法
     23                 constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod;
     24                 if (constructorToUse != null && mbd.constructorArgumentsResolved) {
     25                     // Found a cached constructor...
     26                     // 缓存中的构造参数
     27                     argsToUse = mbd.resolvedConstructorArguments;
     28                     if (argsToUse == null) {
     29                         // 获取缓存中的构造函数参数 包括可见字段
     30                         argsToResolve = mbd.preparedConstructorArguments;
     31                     }
     32                 }
     33             }
     34             // 缓存中存在,则解析存储在BeanDefinition中的参数
     35             // 如给定方法的构造函数 f(int,int),则通过此方法后就会把配置文件中的("1","1")转换为 (1,1)
     36             // 缓存中的值可能是原始值也有可能是最终值
     37             if (argsToResolve != null) {
     38                 argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
     39             }
     40         }
     41 
     42         // 没有缓存,则尝试从配置文件中获取参数
     43         if (constructorToUse == null || argsToUse == null) {
     44             // Take specified constructors, if any.
     45             Constructor<?>[] candidates = chosenCtors;
     46             // 如果chosenCtors未传入,则获取构造方法
     47             if (candidates == null) {
     48                 Class<?> beanClass = mbd.getBeanClass();
     49                 try {
     50                     candidates = (mbd.isNonPublicAccessAllowed() ?
     51                             beanClass.getDeclaredConstructors() : beanClass.getConstructors());
     52                 } catch (Throwable ex) {
     53                     throw new BeanCreationException(mbd.getResourceDescription(), beanName,
     54                                                     "Resolution of declared constructors on bean Class [" + beanClass.getName() +
     55                                                             "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
     56                 }
     57             }
     58             // 创建bean
     59             if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
     60                 Constructor<?> uniqueCandidate = candidates[0];
     61                 if (uniqueCandidate.getParameterCount() == 0) {
     62                     synchronized (mbd.constructorArgumentLock) {
     63                         mbd.resolvedConstructorOrFactoryMethod = uniqueCandidate;
     64                         mbd.constructorArgumentsResolved = true;
     65                         mbd.resolvedConstructorArguments = EMPTY_ARGS;
     66                     }
     67                     bw.setBeanInstance(instantiate(beanName, mbd, uniqueCandidate, EMPTY_ARGS));
     68                     return bw;
     69                 }
     70             }
     71 
     72             // 是否需要解析构造器
     73             // Need to resolve the constructor.
     74             boolean autowiring = (chosenCtors != null ||
     75                     mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
     76             // 用于承载解析后的构造函数参数值
     77             ConstructorArgumentValues resolvedValues = null;
     78 
     79             int minNrOfArgs;
     80             if (explicitArgs != null) {
     81                 minNrOfArgs = explicitArgs.length;
     82             } else {
     83                 // 从BeanDefinition中获取构造参数,也就是从配置文件中提取构造参数
     84                 ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
     85                 resolvedValues = new ConstructorArgumentValues();
     86                 // 解析构造函数的参数
     87                 // 将该bean的构造函数参数解析为resolvedValues对象,其中会涉及到其他bean
     88                 minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
     89             }
     90 
     91             // 对构造函数进行排序处理
     92             // public构造函数优先,参数数量降序,非public构造函数参数数量降序
     93             AutowireUtils.sortConstructors(candidates);
     94 
     95             // 最小参数类型权重
     96             int minTypeDiffWeight = Integer.MAX_VALUE;
     97             Set<Constructor<?>> ambiguousConstructors = null;
     98             LinkedList<UnsatisfiedDependencyException> causes = null;
     99 
    100             // 遍历所有构造函数
    101             for (Constructor<?> candidate : candidates) {
    102                 // 获取该构造函数的参数类型
    103                 Class<?>[] paramTypes = candidate.getParameterTypes();
    104 
    105                 // 如果已经找到选用的构造函数且需要的参数个数大于当前构造函数参数个数,则break
    106                 if (constructorToUse != null && argsToUse != null && argsToUse.length > paramTypes.length) {
    107                     // Already found greedy constructor that can be satisfied ->
    108                     // do not look any further, there are only less greedy constructors left.
    109                     break;
    110                 }
    111                 // 参数个数不等,继续循环
    112                 if (paramTypes.length < minNrOfArgs) {
    113                     continue;
    114                 }
    115 
    116                 // 参数持有者ArgumentsHolder对象
    117                 ArgumentsHolder argsHolder;
    118                 if (resolvedValues != null) {
    119                     try {
    120                         // 注释上获取参数名称
    121                         String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length);
    122                         if (paramNames == null) {
    123                             // 获取构造函数、方法参数的探测器
    124                             ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
    125                             if (pnd != null) {
    126                                 // 通过探测器获取构造函数的参数名称
    127                                 paramNames = pnd.getParameterNames(candidate);
    128                             }
    129                         }
    130                         // 根据构造函数和构造参数,创建参数持有者ArgumentsHolder对象
    131                         argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
    132                                                          getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
    133                     } catch (UnsatisfiedDependencyException ex) {
    134                         if (logger.isTraceEnabled()) {
    135                             logger.trace("Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex);
    136                         }
    137                         // Swallow and try next constructor.
    138                         if (causes == null) {
    139                             causes = new LinkedList<>();
    140                         }
    141                         // 若产生UnsatisfiedDependencyException异常,则添加到causes中
    142                         causes.add(ex);
    143                         continue;
    144                     }
    145                 } else {
    146                     // Explicit arguments given -> arguments length must match exactly.
    147                     // 构造函数中没有参数,则continue
    148                     if (paramTypes.length != explicitArgs.length) {
    149                         continue;
    150                     }
    151                     // 根据explicitArgs创建ArgumentsHolder对象
    152                     argsHolder = new ArgumentsHolder(explicitArgs);
    153                 }
    154 
    155                 // isLenientConstructorResolution 判断解析构造函数的时候是否以宽松模式还是严格模式
    156                 // 严格模式:解析构造函数时,必须所有的都需要匹配,否则抛出异常
    157                 // 宽松模式:使用具有"最接近的模式"进行匹配
    158                 // typeDiffWeight:类型差异权重
    159                 int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
    160                         argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
    161                 // Choose this constructor if it represents the closest match.
    162                 // 如果它代表着当前最接近的匹配则选择其作为构造函数
    163                 if (typeDiffWeight < minTypeDiffWeight) {
    164                     constructorToUse = candidate;
    165                     argsHolderToUse = argsHolder;
    166                     argsToUse = argsHolder.arguments;
    167                     minTypeDiffWeight = typeDiffWeight;
    168                     ambiguousConstructors = null;
    169                 } else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
    170                     if (ambiguousConstructors == null) {
    171                         ambiguousConstructors = new LinkedHashSet<>();
    172                         ambiguousConstructors.add(constructorToUse);
    173                     }
    174                     ambiguousConstructors.add(candidate);
    175                 }
    176             }
    177 
    178             // 没有可执行的工厂方法,则抛出异常
    179             if (constructorToUse == null) {
    180                 if (causes != null) {
    181                     UnsatisfiedDependencyException ex = causes.removeLast();
    182                     for (Exception cause : causes) {
    183                         this.beanFactory.onSuppressedException(cause);
    184                     }
    185                     throw ex;
    186                 }
    187                 throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    188                                                 "Could not resolve matching constructor " +
    189                                                         "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)");
    190             } else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) {
    191                 throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    192                                                 "Ambiguous constructor matches found in bean '" + beanName + "' " +
    193                                                         "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " +
    194                                                         ambiguousConstructors);
    195             }
    196 
    197             if (explicitArgs == null && argsHolderToUse != null) {
    198                 // 将解析的构造函数加入缓存
    199                 argsHolderToUse.storeCache(mbd, constructorToUse);
    200             }
    201         }
    202         // 创建bean对象,并设置到BeanWrapperImpl中
    203         Assert.state(argsToUse != null, "Unresolved constructor arguments");
    204         bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
    205         return bw;
    206     }

    分析:

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

    instantiate

     1 private Object instantiate(
     2             String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse) {
     3 
     4         try {
     5             InstantiationStrategy strategy = this.beanFactory.getInstantiationStrategy();
     6             if (System.getSecurityManager() != null) {
     7                 return AccessController.doPrivileged((PrivilegedAction<Object>) () ->
     8                                                              strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse),
     9                                                      this.beanFactory.getAccessControlContext());
    10             } else {
    11                 return strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse);
    12             }
    13         } catch (Throwable ex) {
    14             throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    15                                             "Bean instantiation via constructor failed", ex);
    16         }
    17     }

    分析:

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

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

    分析:

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

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

     1 // BeanUtils
     2 public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
     3         Assert.notNull(ctor, "Constructor must not be null");
     4         try {
     5             // 设置构造方法,可访问
     6             ReflectionUtils.makeAccessible(ctor);
     7             // 使用构造方法创建对象
     8             if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
     9                 return KotlinDelegate.instantiateClass(ctor, args);
    10             } else {
    11                 Class<?>[] parameterTypes = ctor.getParameterTypes();
    12                 Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
    13                 Object[] argsWithDefaultValues = new Object[args.length];
    14                 for (int i = 0; i < args.length; i++) {
    15                     if (args[i] == null) {
    16                         Class<?> parameterType = parameterTypes[i];
    17                         argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
    18                     } else {
    19                         argsWithDefaultValues[i] = args[i];
    20                     }
    21                 }
    22                 return ctor.newInstance(argsWithDefaultValues);
    23             }
    24         }
    25         // 各种异常,最终统一抛出BeanInstantiationException异常
    26         catch (InstantiationException ex) {
    27             throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
    28         } catch (IllegalAccessException ex) {
    29             throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex);
    30         } catch (IllegalArgumentException ex) {
    31             throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex);
    32         } catch (InvocationTargetException ex) {
    33             throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException());
    34         }
    35     }

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

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

    分析:

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

    CglibSubclassingInstantiationStrategy#instantiate

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

    分析:

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

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

    #2 AbstractAutowireCapableBeanFactory#instantiateBean

     1 protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
     2         try {
     3             Object beanInstance;
     4             final BeanFactory parent = this;
     5             // 安全模式
     6             if (System.getSecurityManager() != null) {
     7                 // 获得InstantiationStrategy对象,并使用它创建bean对象
     8                 beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () -> getInstantiationStrategy().instantiate(mbd, beanName, parent),
     9                                                              getAccessControlContext());
    10             } else {
    11                 // 获得InstantiationStrategy对象,并使用它创建bean对象
    12                 beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
    13             }
    14             // 封装BeanWrapperImpl,并完成初始化
    15             BeanWrapper bw = new BeanWrapperImpl(beanInstance);
    16             initBeanWrapper(bw);
    17             return bw;
    18         } catch (Throwable ex) {
    19             throw new BeanCreationException(
    20                     mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
    21         }
    22     }

    分析:

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

    SimpleInstantiationStrategy#instantiate

     1 public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
     2         // Don't override the class with CGLIB if no overrides.
     3         // 没有覆盖,直接使用反射实例化即可
     4         if (!bd.hasMethodOverrides()) {
     5             Constructor<?> constructorToUse;
     6             synchronized (bd.constructorArgumentLock) {
     7                 // 获得构造方法 constructorToUse
     8                 constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
     9                 if (constructorToUse == null) {
    10                     final Class<?> clazz = bd.getBeanClass();
    11                     // 如果是接口,则抛出异常
    12                     if (clazz.isInterface()) {
    13                         throw new BeanInstantiationException(clazz, "Specified class is an interface");
    14                     }
    15                     try {
    16                         // 从clazz中,获得构造方法
    17                         if (System.getSecurityManager() != null) {
    18                             constructorToUse = AccessController.doPrivileged(
    19                                     (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
    20                         } else {
    21                             constructorToUse = clazz.getDeclaredConstructor();
    22                         }
    23                         // 标记resolvedConstructorOrFactoryMethod属性
    24                         bd.resolvedConstructorOrFactoryMethod = constructorToUse;
    25                     } catch (Throwable ex) {
    26                         throw new BeanInstantiationException(clazz, "No default constructor found", ex);
    27                     }
    28                 }
    29             }
    30             // 通过BeanUtils直接使用构造器实例化bean对象
    31             return BeanUtils.instantiateClass(constructorToUse);
    32         } else {
    33             // 通过CGLIB创建子类对象
    34             // Must generate CGLIB subclass.
    35             return instantiateWithMethodInjection(bd, beanName, owner);
    36         }
    37     }

    分析:

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

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

    总结

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

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

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

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


    by Shawn Chen,2019.04.25,下午。

  • 相关阅读:
    asp.net mvc 从数据库中读取图片
    给折腾ramdisk的朋友们一点建议
    docker安装Jenkins和构建python容器
    docker笔记
    接口加密方式
    Dockerfile常用命令
    北斗七星小队团队展示
    固件程序设计实验内容1.4
    Myod实验 20181328祝维卿
    电子公文系统团队作业(四):描述设计
  • 原文地址:https://www.cnblogs.com/developer_chan/p/10763573.html
Copyright © 2020-2023  润新知