zoukankan      html  css  js  c++  java
  • 【Spring源码分析】非懒加载的单例Bean初始化过程(上篇)

    代码入口

    上文【Spring源码分析】Bean加载流程概览,比较详细地分析了Spring上下文加载的代码入口,并且在AbstractApplicationContext的refresh方法中,点出了finishBeanFactoryInitialization方法完成了对于所有非懒加载的Bean的初始化。

    finishBeanFactoryInitialization方法中调用了DefaultListableBeanFactory的preInstantiateSingletons方法,本文针对preInstantiateSingletons进行分析,解读一下Spring是如何初始化Bean实例对象出来的。

    DefaultListableBeanFactory的preInstantiateSingletons方法

    DefaultListableBeanFactory的preInstantiateSingletons方法,顾名思义,初始化所有的单例Bean,看一下方法的定义:

     1 public void preInstantiateSingletons() throws BeansException {
     2     if (this.logger.isInfoEnabled()) {
     3         this.logger.info("Pre-instantiating singletons in " + this);
     4     }
     5     synchronized (this.beanDefinitionMap) {
     6         // Iterate over a copy to allow for init methods which in turn register new bean definitions.
     7         // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
     8         List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);
     9         for (String beanName : beanNames) {
    10             RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
    11             if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
    12                 if (isFactoryBean(beanName)) {
    13                     final FactoryBean factory = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName);
    14                     boolean isEagerInit;
    15                     if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
    16                         isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
    17                             public Boolean run() {
    18                                 return ((SmartFactoryBean) factory).isEagerInit();
    19                             }
    20                         }, getAccessControlContext());
    21                     }
    22                     else {
    23                         isEagerInit = (factory instanceof SmartFactoryBean &&
    24                                 ((SmartFactoryBean) factory).isEagerInit());
    25                     }
    26                     if (isEagerInit) {
    27                         getBean(beanName);
    28                     }
    29                 }
    30                 else {
    31                     getBean(beanName);
    32                 }
    33             }
    34         }
    35     }
    36 }

    这里先解释一下getMergedLocalBeanDefinition方法的含义,因为这个方法会常常看到。Bean定义公共的抽象类是AbstractBeanDefinition,普通的Bean在Spring加载Bean定义的时候,实例化出来的是GenericBeanDefinition,而Spring上下文包括实例化所有Bean用的AbstractBeanDefinition是RootBeanDefinition,这时候就使用getMergedLocalBeanDefinition方法做了一次转化,将非RootBeanDefinition转换为RootBeanDefinition以供后续操作

    解释完了getMergedLocalBeanDefinition方法的作用,第1行~第10行的代码就没什么好说的了,根据beanName拿到RootBeanDefinition而已。由于此方法实例化的是所有非懒加载的单例Bean,因此要实例化Bean,必须满足11行的三个定义:

    (1)不是抽象的

    (2)必须是单例的

    (3)必须是非懒加载的

    接着简单看一下第12行~第29行的代码,这段代码主要做的是一件事情:首先判断一下Bean是否FactoryBean的实现,接着判断Bean是否SmartFactoryBean的实现,假如Bean是SmartFactoryBean的实现并且eagerInit(这个单词字面意思是渴望加载,找不到一个好的词语去翻译,意思就是定义了这个Bean需要立即加载的意思)的话,会立即实例化这个Bean。Java开发人员不需要关注这段代码,因为SmartFactoryBean基本不会用到,我翻译一下Spring官网对于SmartFactoryBean的定义描述:

    • FactoryBean接口的扩展接口。接口实现并不表示是否总是返回单独的实例对象,比如FactoryBean.isSingleton()实现返回false的情况并不清晰地表示每次返回的都是单独的实例对象
    • 不实现这个扩展接口的简单FactoryBean的实现,FactoryBean.isSingleton()实现返回false总是简单地告诉我们每次返回的都是单独的实例对象,暴露出来的对象只能够通过命令访问
    • 注意:这个接口是一个有特殊用途的接口,主要用于框架内部使用与Spring相关。通常,应用提供的FactoryBean接口实现应当只需要实现简单的FactoryBean接口即可,新方法应当加入到扩展接口中去

    代码示例

    为了后面的代码分析方便,事先我定义一个Bean:

     1 package org.xrq.action;
     2 
     3 import org.springframework.beans.factory.BeanClassLoaderAware;
     4 import org.springframework.beans.factory.BeanNameAware;
     5 import org.springframework.beans.factory.InitializingBean;
     6 
     7 public class MultiFunctionBean implements InitializingBean, BeanNameAware, BeanClassLoaderAware {
     8 
     9     private int    propertyA;
    10     
    11     private int    propertyB;
    12     
    13     public int getPropertyA() {
    14         return propertyA;
    15     }
    16 
    17     public void setPropertyA(int propertyA) {
    18         this.propertyA = propertyA;
    19     }
    20 
    21     public int getPropertyB() {
    22         return propertyB;
    23     }
    24 
    25     public void setPropertyB(int propertyB) {
    26         this.propertyB = propertyB;
    27     }
    28     
    29     public void initMethod() {
    30         System.out.println("Enter MultiFunctionBean.initMethod()");
    31     }
    32 
    33     @Override
    34     public void setBeanClassLoader(ClassLoader classLoader) {
    35         System.out.println("Enter MultiFunctionBean.setBeanClassLoader(ClassLoader classLoader)");
    36     }
    37 
    38     @Override
    39     public void setBeanName(String name) {
    40         System.out.println("Enter MultiFunctionBean.setBeanName(String name)");
    41     }
    42 
    43     @Override
    44     public void afterPropertiesSet() throws Exception {
    45         System.out.println("Enter MultiFunctionBean.afterPropertiesSet()");
    46     }
    47     
    48     @Override
    49     public String toString() {
    50         return "MultiFunctionBean [propertyA=" + propertyA + ", propertyB=" + propertyB + "]";
    51     }
    52     
    53 }

    定义对应的spring.xml:

    1 <?xml version="1.0" encoding="UTF-8"?>
    2 <beans xmlns="http://www.springframework.org/schema/beans"
    3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4     xsi:schemaLocation="http://www.springframework.org/schema/beans
    5     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    6     
    7     <bean id="multiFunctionBean" class="org.xrq.action.MultiFunctionBean" init-method="initMethod" />
    8     
    9 </beans>

    利用这个MultiFunctionBean,我们可以用来探究Spring加载Bean的多种机制。

    doGetBean方法构造Bean流程

    上面把getBean之外的代码都分析了一下,看代码就可以知道,获取Bean对象实例,都是通过getBean方法,getBean方法最终调用的是DefaultListableBeanFactory的父类AbstractBeanFactory类的doGetBean方法,因此这部分重点分析一下doGetBean方法是如何构造出一个单例的Bean的。

    看一下doGetBean方法的代码实现,比较长:

      1 protected <T> T doGetBean(
      2         final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
      3         throws BeansException {
      4 
      5     final String beanName = transformedBeanName(name);
      6     Object bean;
      7 
      8     // Eagerly check singleton cache for manually registered singletons.
      9     Object sharedInstance = getSingleton(beanName);
     10     if (sharedInstance != null && args == null) {
     11         if (logger.isDebugEnabled()) {
     12             if (isSingletonCurrentlyInCreation(beanName)) {
     13                 logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
     14                         "' that is not fully initialized yet - a consequence of a circular reference");
     15             }
     16             else {
     17                 logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
     18             }
     19         }
     20         bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
     21     }
     22 
     23     else {
     24         // Fail if we're already creating this bean instance:
     25         // We're assumably within a circular reference.
     26         if (isPrototypeCurrentlyInCreation(beanName)) {
     27             throw new BeanCurrentlyInCreationException(beanName);
     28         }
     29 
     30         // Check if bean definition exists in this factory.
     31         BeanFactory parentBeanFactory = getParentBeanFactory();
     32         if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
     33             // Not found -> check parent.
     34             String nameToLookup = originalBeanName(name);
     35             if (args != null) {
     36                 // Delegation to parent with explicit args.
     37                 return (T) parentBeanFactory.getBean(nameToLookup, args);
     38             }
     39             else {
     40                 // No args -> delegate to standard getBean method.
     41                 return parentBeanFactory.getBean(nameToLookup, requiredType);
     42             }
     43         }
     44 
     45         if (!typeCheckOnly) {
     46             markBeanAsCreated(beanName);
     47         }
     48 
     49         final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
     50         checkMergedBeanDefinition(mbd, beanName, args);
     51 
     52         // Guarantee initialization of beans that the current bean depends on.
     53         String[] dependsOn = mbd.getDependsOn();
     54         if (dependsOn != null) {
     55             for (String dependsOnBean : dependsOn) {
     56                 getBean(dependsOnBean);
     57                 registerDependentBean(dependsOnBean, beanName);
     58             }
     59         }
     60 
     61         // Create bean instance.
     62         if (mbd.isSingleton()) {
     63             sharedInstance = getSingleton(beanName, new ObjectFactory() {
     64                 public Object getObject() throws BeansException {
     65                     try {
     66                         return createBean(beanName, mbd, args);
     67                     }
     68                     catch (BeansException ex) {
     69                         // Explicitly remove instance from singleton cache: It might have been put there
     70                         // eagerly by the creation process, to allow for circular reference resolution.
     71                         // Also remove any beans that received a temporary reference to the bean.
     72                         destroySingleton(beanName);
     73                         throw ex;
     74                     }
     75                 }
     76             });
     77             bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
     78         }
     79 
     80         else if (mbd.isPrototype()) {
     81             // It's a prototype -> create a new instance.
     82             Object prototypeInstance = null;
     83             try {
     84                 beforePrototypeCreation(beanName);
     85                 prototypeInstance = createBean(beanName, mbd, args);
     86             }
     87             finally {
     88                 afterPrototypeCreation(beanName);
     89             }
     90             bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
     91         }
     92 
     93         else {
     94             String scopeName = mbd.getScope();
     95             final Scope scope = this.scopes.get(scopeName);
     96             if (scope == null) {
     97                 throw new IllegalStateException("No Scope registered for scope '" + scopeName + "'");
     98             }
     99             try {
    100                 Object scopedInstance = scope.get(beanName, new ObjectFactory() {
    101                     public Object getObject() throws BeansException {
    102                             beforePrototypeCreation(beanName);
    103                         try {
    104                             return createBean(beanName, mbd, args);
    105                         }
    106                         finally {
    107                             afterPrototypeCreation(beanName);
    108                         }
    109                     }
    110                 });
    111                 bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    112             }
    113             catch (IllegalStateException ex) {
    114                 throw new BeanCreationException(beanName,
    115                         "Scope '" + scopeName + "' is not active for the current thread; " +
    116                         "consider defining a scoped proxy for this bean if you intend to refer to it from a singleton",
    117                         ex);
    118             }
    119         }
    120     }
    121 
    122     // Check if required type matches the type of the actual bean instance.
    123     if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
    124         try {
    125             return getTypeConverter().convertIfNecessary(bean, requiredType);
    126         }
    127         catch (TypeMismatchException ex) {
    128             if (logger.isDebugEnabled()) {
    129                 logger.debug("Failed to convert bean '" + name + "' to required type [" +
    130                         ClassUtils.getQualifiedName(requiredType) + "]", ex);
    131             }
    132             throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
    133         }
    134     }
    135     return (T) bean;
    136 }

    首先第9行~第21行的代码,第9行的代码就不进去看了,简单说一下:首先检查一下本地的单例缓存是否已经加载过Bean,没有的话再检查earlySingleton缓存是否已经加载过Bean(又是early,不好找到词语翻译),没有的话执行后面的逻辑。

    接着第26行~第50行,这里执行的都是一些基本的检查和简单的操作,包括bean是否是prototype的(prototype的Bean当前创建会抛出异常)、是否抽象的、将beanName加入alreadyCreated这个Set中等。

    接着第53行~第59行,我们经常在bean标签中看到depends-on这个属性,就是通过这段保证了depends-on依赖的Bean会优先于当前Bean被加载

    接着第62行~第78行、第80行~第91行、第93行~第120行有三个判断,显然上面的MultiFunctionBean是一个单例的Bean也是本文探究的重点,因此执行第62行~第78行的逻辑。getSingleton方法不贴了,有一些前置的判断,很简单的逻辑,重点就是调用了ObjectFactory的getObject()方法来获取到单例Bean对象,方法的实现是调用了createBean方法,createBean方法是AbstractBeanFactory的子类AbstractAutowireCapableBeanFactory的一个方法,看一下它的方法实现:

     1 protected Object createBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
     2         throws BeanCreationException {
     3 
     4     if (logger.isDebugEnabled()) {
     5         logger.debug("Creating instance of bean '" + beanName + "'");
     6     }
     7     // Make sure bean class is actually resolved at this point.
     8     resolveBeanClass(mbd, beanName);
     9 
    10     // Prepare method overrides.
    11     try {
    12         mbd.prepareMethodOverrides();
    13     }
    14     catch (BeanDefinitionValidationException ex) {
    15         throw new BeanDefinitionStoreException(mbd.getResourceDescription(),
    16                 beanName, "Validation of method overrides failed", ex);
    17     }
    18 
    19     try {
    20         // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
    21         Object bean = resolveBeforeInstantiation(beanName, mbd);
    22         if (bean != null) {
    23             return bean;
    24         }
    25     }
    26     catch (Throwable ex) {
    27         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    28                 "BeanPostProcessor before instantiation of bean failed", ex);
    29     }
    30 
    31     Object beanInstance = doCreateBean(beanName, mbd, args);
    32     if (logger.isDebugEnabled()) {
    33         logger.debug("Finished creating instance of bean '" + beanName + "'");
    34     }
    35     return beanInstance;
    36 }

    前面的代码都没什么意义,代码执行到第31行:

     1 protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
     2     // Instantiate the bean.
     3     BeanWrapper instanceWrapper = null;
     4     if (mbd.isSingleton()) {
     5         instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
     6     }
     7     if (instanceWrapper == null) {
     8         instanceWrapper = createBeanInstance(beanName, mbd, args);
     9     }
    10     final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
    11     Class beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
    12 
    13     // Allow post-processors to modify the merged bean definition.
    14     synchronized (mbd.postProcessingLock) {
    15         if (!mbd.postProcessed) {
    16             applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
    17             mbd.postProcessed = true;
    18         }
    19     }
    20 
    21     // Eagerly cache singletons to be able to resolve circular references
    22     // even when triggered by lifecycle interfaces like BeanFactoryAware.
    23     boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
    24             isSingletonCurrentlyInCreation(beanName));
    25     if (earlySingletonExposure) {
    26         if (logger.isDebugEnabled()) {
    27             logger.debug("Eagerly caching bean '" + beanName +
    28                     "' to allow for resolving potential circular references");
    29         }
    30         addSingletonFactory(beanName, new ObjectFactory() {
    31             public Object getObject() throws BeansException {
    32                 return getEarlyBeanReference(beanName, mbd, bean);
    33             }
    34         });
    35     }
    36 
    37     // Initialize the bean instance.
    38     Object exposedObject = bean;
    39     try {
    40         populateBean(beanName, mbd, instanceWrapper);
    41         if (exposedObject != null) {
    42             exposedObject = initializeBean(beanName, exposedObject, mbd);
    43         }
    44     }
    45     catch (Throwable ex) {
    46         if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
    47             throw (BeanCreationException) ex;
    48         }
    49         else {
    50             throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
    51         }
    52     }
    53 
    54     if (earlySingletonExposure) {
    55         Object earlySingletonReference = getSingleton(beanName, false);
    56         if (earlySingletonReference != null) {
    57             if (exposedObject == bean) {
    58                 exposedObject = earlySingletonReference;
    59             }
    60             else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
    61                 String[] dependentBeans = getDependentBeans(beanName);
    62                 Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
    63                 for (String dependentBean : dependentBeans) {
    64                     if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
    65                         actualDependentBeans.add(dependentBean);
    66                     }
    67                 }
    68                 if (!actualDependentBeans.isEmpty()) {
    69                     throw new BeanCurrentlyInCreationException(beanName,
    70                             "Bean with name '" + beanName + "' has been injected into other beans [" +
    71                                 StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
    72                             "] in its raw version as part of a circular reference, but has eventually been " +
    73                             "wrapped. This means that said other beans do not use the final version of the " +
    74                             "bean. This is often the result of over-eager type matching - consider using " +
    75                             "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
    76                 }
    77             }
    78         }
    79     }
    80 
    81     // Register bean as disposable.
    82     try {
    83         registerDisposableBeanIfNecessary(beanName, bean, mbd);
    84     }
    85     catch (BeanDefinitionValidationException ex) {
    86         throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    87     }
    88 
    89     return exposedObject;
    90 }

    代码跟踪到这里,已经到了主流程,接下来分段分析doCreateBean方法的代码。

    创建Bean实例

    第8行的createBeanInstance方法,会创建出Bean的实例,并包装为BeanWrapper,看一下createBeanInstance方法,只贴最后一段比较关键的:

     1 // Need to determine the constructor...
     2 Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
     3 if (ctors != null ||
     4         mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
     5         mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
     6     return autowireConstructor(beanName, mbd, ctors, args);
     7 }
     8 
     9 // No special handling: simply use no-arg constructor.
    10 return instantiateBean(beanName, mbd);

    意思是bean标签使用构造函数注入属性的话,执行第6行,否则执行第10行。MultiFunctionBean使用默认构造函数,使用setter注入属性,因此执行第10行代码:

     1 protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
     2     try {
     3         Object beanInstance;
     4         final BeanFactory parent = this;
     5         if (System.getSecurityManager() != null) {
     6             beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() {
     7                 public Object run() {
     8                     return getInstantiationStrategy().instantiate(mbd, beanName, parent);
     9                 }
    10             }, getAccessControlContext());
    11         }
    12         else {
    13             beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
    14         }
    15         BeanWrapper bw = new BeanWrapperImpl(beanInstance);
    16         initBeanWrapper(bw);
    17         return bw;
    18     }
    19     catch (Throwable ex) {
    20         throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
    21     }
    22 }

    代码执行到13行:

     1 public Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) {
     2     // Don't override the class with CGLIB if no overrides.
     3     if (beanDefinition.getMethodOverrides().isEmpty()) {
     4         Constructor<?> constructorToUse;
     5         synchronized (beanDefinition.constructorArgumentLock) {
     6             constructorToUse = (Constructor<?>) beanDefinition.resolvedConstructorOrFactoryMethod;
     7             if (constructorToUse == null) {
     8                 final Class clazz = beanDefinition.getBeanClass();
     9                 if (clazz.isInterface()) {
    10                     throw new BeanInstantiationException(clazz, "Specified class is an interface");
    11                 }
    12                 try {
    13                     if (System.getSecurityManager() != null) {
    14                         constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor>() {
    15                             public Constructor run() throws Exception {
    16                                 return clazz.getDeclaredConstructor((Class[]) null);
    17                             }
    18                         });
    19                     }
    20                     else {
    21                         constructorToUse = clazz.getDeclaredConstructor((Class[]) null);
    22                     }
    23                     beanDefinition.resolvedConstructorOrFactoryMethod = constructorToUse;
    24                 }
    25                 catch (Exception ex) {
    26                     throw new BeanInstantiationException(clazz, "No default constructor found", ex);
    27                 }
    28             }
    29         }
    30         return BeanUtils.instantiateClass(constructorToUse);
    31     }
    32     else {
    33         // Must generate CGLIB subclass.
    34         return instantiateWithMethodInjection(beanDefinition, beanName, owner);
    35     }
    36 }

    整段代码都在做一件事情,就是选择一个使用的构造函数。当然第9行顺带做了一个判断:实例化一个接口将报错。

    最后调用到30行,看一下代码:

     1 public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
     2     Assert.notNull(ctor, "Constructor must not be null");
     3     try {
     4         ReflectionUtils.makeAccessible(ctor);
     5         return ctor.newInstance(args);
     6     }
     7     catch (InstantiationException ex) {
     8         throw new BeanInstantiationException(ctor.getDeclaringClass(),
     9                 "Is it an abstract class?", ex);
    10     }
    11     catch (IllegalAccessException ex) {
    12         throw new BeanInstantiationException(ctor.getDeclaringClass(),
    13                 "Is the constructor accessible?", ex);
    14     }
    15     catch (IllegalArgumentException ex) {
    16         throw new BeanInstantiationException(ctor.getDeclaringClass(),
    17                 "Illegal arguments for constructor", ex);
    18     }
    19     catch (InvocationTargetException ex) {
    20         throw new BeanInstantiationException(ctor.getDeclaringClass(),
    21                 "Constructor threw exception", ex.getTargetException());
    22     }
    23 }

    通过反射生成Bean的实例。看到前面有一步makeAccessible,这意味着即使Bean的构造函数是private、protected的,依然不影响Bean的构造

    最后注意一下,这里被实例化出来的Bean并不会直接返回,而是会被包装为BeanWrapper继续在后面使用。

  • 相关阅读:
    tensorflow模型的保存与恢复
    Anaconda Linux端环境管理
    Windows环境下Redis集群部署
    Error fetching https://ruby.taobao.org/:RubySass淘宝镜源无效解决
    ORM 轻量级框架 Dapper(介绍)
    TypeScript 数据类型
    TypeScript 环境搭建
    微信小程序反编译
    利用Fiddler实现手机抓包
    SQL 优化常用查询
  • 原文地址:https://www.cnblogs.com/xrq730/p/6361578.html
Copyright © 2011-2022 走看看