SpringIOC的理解及源码解读(二)

153 阅读8分钟

refresh方法

@Override
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

      // Prepare this context for refreshing.
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         postProcessBeanFactory(beanFactory);

         StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
         // Invoke factory processors registered as beans in the context.
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         registerBeanPostProcessors(beanFactory);
         beanPostProcess.end();

         // Initialize message source for this context.
         initMessageSource();

         // Initialize event multicaster for this context.
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         onRefresh();

         // Check for listener beans and register them.
         registerListeners();

         // Instantiate all remaining (non-lazy-init) singletons.
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
         contextRefresh.end();
      }
   }
}

OK 一行一行代码的看
第一行

synchronized (this.startupShutdownMonitor) {

这一行很明显了,给startupShutdownMonitor这个成员变量加锁,这个成员变量是什么呢?

image.png 锁住一个对象,防止多线程同时执行初始化的操作
第二行

StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

创建一个静态内部类对象DefaultStartupStep image.png
第三行

prepareRefresh();

在刷新之前设置一些参数,比如设置开始时间戳,上下文是否激活的标志,输出刷新上下文的信息,验证一些必要的属性。

protected void prepareRefresh() {
   // Switch to active.
   this.startupDate = System.currentTimeMillis();
   this.closed.set(false);
   this.active.set(true);

   if (logger.isDebugEnabled()) {
      if (logger.isTraceEnabled()) {
         logger.trace("Refreshing " + this);
      }
      else {
         logger.debug("Refreshing " + getDisplayName());
      }
   }

   // Initialize any placeholder property sources in the context environment.
   initPropertySources();

   // Validate that all properties marked as required are resolvable:
   // see ConfigurablePropertyResolver#setRequiredProperties
   getEnvironment().validateRequiredProperties();

   // Store pre-refresh ApplicationListeners...
   if (this.earlyApplicationListeners == null) {
      this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
   }
   else {
      // Reset local application listeners to pre-refresh state.
      this.applicationListeners.clear();
      this.applicationListeners.addAll(this.earlyApplicationListeners);
   }

   // Allow for the collection of early ApplicationEvents,
   // to be published once the multicaster is available...
   this.earlyApplicationEvents = new LinkedHashSet<>();
}

设置开始时间,计时用。设置标记,输出日志,初始化监听器,检查环境变量,创建事件LinkedHashSet等。 其中检查环境变量的核心方法为 getEnvironment().validateRequiredProperties(),简单来说就是如果存在环境变量的value为空的时候就抛异常,然后停止启动Spring
第四行

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

点进去看

image.png 我们再来看refreshBeanFactory方法

image.png 正是在这个方法中销毁了应用上下文的原有BeanFactory并新建了DefaultListableBeanFactory,这个哥们的背景相当大,所有关于容器的接口、抽象类他都继承了。 看一下customizeBeanFactory方法

image.png 通过名字我们可以看得出来,就是用来设置 BeanFactory 的两个配置属性:是否允许 Bean 覆盖、是否允许循环引用。 接下来就是一个超级重要的方法loadBeanDefinitions 在看这个方法之前,我们必须了解一个东西,那就是:BeanDefinition。
我们知道BeanFactory是一个Bean容器,而BeanDefinition就是Bean的一 种形式(它里面包含了Bean指向的类、是否单例、是否懒加载、Bean的依赖关系等相关的属性)。BeanFactory中就是保存的BeanDefinition。
看一下BeanDefinition接口

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
   // Bean的生命周期,默认只提供sington和prototype两种,
   //在WebApplicationContext中还会有request, session,
   //globalSession, application, websocket 等   
   String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;
   String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;
   // 设置父Bean  
   void setParentName(String parentName);
   // 获取父Bean   
   String getParentName();
   // 设置Bean的类名称  
   void setBeanClassName(String beanClassName);
   // 获取Bean的类名称  
   String getBeanClassName();
   // 设置bean的scope   
   void setScope(String scope);
   String getScope();
   // 设置是否懒加载   
   void setLazyInit(boolean lazyInit);
   boolean isLazyInit();
   // 设置该Bean依赖的所有Bean   
   void setDependsOn(String... dependsOn);
   // 返回该Bean的所有依赖   
   String[] getDependsOn();
   // 设置该Bean是否可以注入到其他Bean中   
   void setAutowireCandidate(boolean autowireCandidate);
   // 该Bean是否可以注入到其他Bean中   
   boolean isAutowireCandidate();
   // 同一接口的多个实现,如果不指定名字的话,Spring会优先选择设置primary为true的bean   
   void setPrimary(boolean primary);
   // 是否是primary的   
   boolean isPrimary();
   // 指定工厂名称   
   void setFactoryBeanName(String factoryBeanName);   // 获取工厂名称        String getFactoryBeanName();   // 指定工厂类中的工厂方法名称   void setFactoryMethodName(String factoryMethodName);   // 获取工厂类中的工厂方法名称   String getFactoryMethodName();
   // 构造器参数   
   ConstructorArgumentValues getConstructorArgumentValues();
   // Bean 中的属性值,后面给 bean 注入属性值的时候会说到  
   MutablePropertyValues getPropertyValues();
   // 是否 singleton   
   boolean isSingleton();
   // 是否 prototype   
   boolean isPrototype();
   // 如果这个 Bean 是被设置为 abstract,那么不能实例化,常用于作为 父bean 用于继承   
   boolean isAbstract();
   int getRole();   
   String getDescription();  
   String getResourceDescription();   
   BeanDefinition getOriginatingBeanDefinition();
}

接下来继续看loadBeanDefinitions方法

@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
   // Create a new XmlBeanDefinitionReader for the given BeanFactory.
   XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

   // Configure the bean definition reader with this context's
   // resource loading environment.
   beanDefinitionReader.setEnvironment(this.getEnvironment());
   beanDefinitionReader.setResourceLoader(this);
   beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

   // Allow a subclass to provide custom initialization of the reader,
   // then proceed with actually loading the bean definitions.
   initBeanDefinitionReader(beanDefinitionReader);
   loadBeanDefinitions(beanDefinitionReader);
}

这里首先实例化XmlBeanDefinitionReader,因为这里使用的是xml文件的形式,所以使用XmlBeanDefinitionReader来加载。它还有三个兄弟。 image.png 此对象中包含了对xml的验证,对各种标签的解析等。 之后此方法对属性进行了填充。后执行了初始化,主要工作如下 image.png 继续看loadBeanDefinitions(beanDefinitionReader)方法

image.png 第一个if是看有没有系统指定的配置文件,如果没有的话就走第二个if加载我们最开始传入的classpath:application-ioc.xml进入loadBeanDefinitions中

image.png

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
    ResourceLoader resourceLoader = this.getResourceLoader();
    if (resourceLoader == null) {
        throw new BeanDefinitionStoreException("Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
    } else {
        int count;
        if (resourceLoader instanceof ResourcePatternResolver) {
            try {
                Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
                count = this.loadBeanDefinitions(resources);
                if (actualResources != null) {
                    Collections.addAll(actualResources, resources);
                }

                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
                }

                return count;
            } catch (IOException var6) {
                throw new BeanDefinitionStoreException("Could not resolve bean definition resource pattern [" + location + "]", var6);
            }
        } else {
            Resource resource = resourceLoader.getResource(location);
            count = this.loadBeanDefinitions((Resource)resource);
            if (actualResources != null) {
                actualResources.add(resource);
            }

            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
            }

            return count;
        }
    }
}

具体细节就不解释了,进入到下图的方法中

image.png

image.png

image.png 我们会来到XmlBeanDefinitionReader的loadBeanDefinitions方法。

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Loading XML bean definitions from " + encodedResource);
    }

    Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    } else {
        int var6;
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            Throwable var4 = null;

            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }

                var6 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            } catch (Throwable var24) {
                var4 = var24;
                throw var24;
            } finally {
                if (inputStream != null) {
                    if (var4 != null) {
                        try {
                            inputStream.close();
                        } catch (Throwable var23) {
                            var4.addSuppressed(var23);
                        }
                    } else {
                        inputStream.close();
                    }
                }

            }
        } catch (IOException var26) {
            throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var26);
        } finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }

        }

        return var6;
    }
}

此方法中包含doLoadBeanDefinitions方法

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
    try {
        Document doc = this.doLoadDocument(inputSource, resource);
        int count = this.registerBeanDefinitions(doc, resource);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Loaded " + count + " bean definitions from " + resource);
        }

        return count;
    } catch (BeanDefinitionStoreException var5) {
        throw var5;
    } catch (SAXParseException var6) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + var6.getLineNumber() + " in XML document from " + resource + " is invalid", var6);
    } catch (SAXException var7) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", var7);
    } catch (ParserConfigurationException var8) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, var8);
    } catch (IOException var9) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, var9);
    } catch (Throwable var10) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, var10);
    }
}

这个方法十分重要,这个方法将xml文件转成Document文件,并注册bean。接下来看这个关键的解析方法registerBeanDefinitions。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {   
//构建读取Document的工具类   
BeanDefinitionDocumentReader documentReader =createBeanDefinitionDocumentReader();   
//获取已注册的bean数量  
int countBefore = getRegistry().getBeanDefinitionCount();  
// 在这接着往下看  
documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); 
//总注册的bean减去之前注册的bean就是本次注册的bean   
return getRegistry().getBeanDefinitionCount() - countBefore;}

registerBeanDefinitions->doRegisterBeanDefinitions

protected void doRegisterBeanDefinitions(Element root) {
    BeanDefinitionParserDelegate parent = this.delegate;
    this.delegate = this.createDelegate(this.getReaderContext(), root, parent);
    if (this.delegate.isDefaultNamespace(root)) {
        String profileSpec = root.getAttribute("profile");
        if (StringUtils.hasText(profileSpec)) {
            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, ",; ");
            if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + this.getReaderContext().getResource());
                }

                return;
            }
        }
    }

    this.preProcessXml(root);
    this.parseBeanDefinitions(root, this.delegate);
    this.postProcessXml(root);
    this.delegate = parent;
}

image.png 接下来看parseBeanDefinitions()

image.png 看parseDefaultElement()方法

image.png 解析import、alias、bean、beans 看解析Bean中processBeanDefinition方法

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);

        try {
            BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.getReaderContext().getRegistry());
        } catch (BeanDefinitionStoreException var5) {
            this.getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, var5);
        }

        this.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }

}

这个方法最主要的事情就是创建了BeanDefinition。 看parseBeanDefinitionElement

@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
    String id = ele.getAttribute("id");
    String nameAttr = ele.getAttribute("name");
    List<String> aliases = new ArrayList();
    if (StringUtils.hasLength(nameAttr)) {
        String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, ",; ");
        aliases.addAll(Arrays.asList(nameArr));
    }

    String beanName = id;
    if (!StringUtils.hasText(id) && !aliases.isEmpty()) {
        beanName = (String)aliases.remove(0);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases");
        }
    }

    if (containingBean == null) {
        this.checkNameUniqueness(beanName, aliases, ele);
    }

    AbstractBeanDefinition beanDefinition = this.parseBeanDefinitionElement(ele, beanName, containingBean);
    if (beanDefinition != null) {
        if (!StringUtils.hasText(beanName)) {
            try {
                if (containingBean != null) {
                    beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true);
                } else {
                    beanName = this.readerContext.generateBeanName(beanDefinition);
                    String beanClassName = beanDefinition.getBeanClassName();
                    if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
                        aliases.add(beanClassName);
                    }
                }

                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Neither XML 'id' nor 'name' specified - using generated bean name [" + beanName + "]");
                }
            } catch (Exception var9) {
                this.error(var9.getMessage(), ele);
                return null;
            }
        }

        String[] aliasesArray = StringUtils.toStringArray(aliases);
        return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
    } else {
        return null;
    }
}

主要看这么几行

image.png parseBeanDefinitionElement方法

public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName,BeanDefinition containingBean) {
   this.parseState.push(new BeanEntry(beanName));
   String className = null;  
   if (ele.hasAttribute(CLASS_ATTRIBUTE)) {      
   className = ele.getAttribute(CLASS_ATTRIBUTE).trim();   
   }
   try {
    String parent = null;        
    if (ele.hasAttribute(PARENT_ATTRIBUTE)) {        
    parent = ele.getAttribute(PARENT_ATTRIBUTE);     
    }     
    // 创建 BeanDefinition,然后设置类信息     
    AbstractBeanDefinition bd = createBeanDefinition(className, parent);
   // 设置 BeanDefinition 的一堆属性,这些属性定义在 AbstractBeanDefinition 中
    parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);    
    bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
    
      /*** 下面的一堆是解析 <bean>......</bean> 
      内部的子元素* 解析出来以后的信息都放到 bd 的属性中 */
      // 解析 <meta />     
      parseMetaElements(ele, bd);     
      // 解析 <lookup-method />      
      parseLookupOverrideSubElements(ele, bd.getMethodOverrides());      
      // 解析 <replaced-method />     
      parseReplacedMethodSubElements(ele, bd.getMethodOverrides());    
      // 解析 <constructor-arg />      
      parseConstructorArgElements(ele, bd);     
      // 解析  <property />      
      parsePropertyElements(ele, bd);      
      // 解析 <qualifier />      
      parseQualifierElements(ele, bd);
      bd.setResource(this.readerContext.getResource());     
      bd.setSource(extractSource(ele));
      return bd;   
      }   
      catch (ClassNotFoundException ex) {     
      error("Bean class [" + className + "] not found", ele, ex);  
      }   
      catch (NoClassDefFoundError err) {      
      error("Class that bean class [" + className + "] depends on not found", ele, err);   }  
      catch (Throwable ex) {      
      error("Unexpected failure during bean definition parsing", ele, ex); 
      }   
      finally {     
      this.parseState.pop();   
      }
   return null;
  }

至此BeanDefinition创建完毕。

image.png 下面查看注册registerBeanDefinition

image.png

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
    Assert.hasText(beanName, "Bean name must not be empty");
    Assert.notNull(beanDefinition, "BeanDefinition must not be null");
    if (beanDefinition instanceof AbstractBeanDefinition) {
        try {
            ((AbstractBeanDefinition)beanDefinition).validate();
        } catch (BeanDefinitionValidationException var8) {
            throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", var8);
        }
    }

    BeanDefinition existingDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
    if (existingDefinition != null) {
        if (!this.isAllowBeanDefinitionOverriding()) {
            throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
        }

        if (existingDefinition.getRole() < beanDefinition.getRole()) {
            if (this.logger.isInfoEnabled()) {
                this.logger.info("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
            }
        } else if (!beanDefinition.equals(existingDefinition)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
            }
        } else if (this.logger.isTraceEnabled()) {
            this.logger.trace("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
        }

        this.beanDefinitionMap.put(beanName, beanDefinition);
    } else {
        if (this.hasBeanCreationStarted()) {
            synchronized(this.beanDefinitionMap) {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                List<String> updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
                updatedDefinitions.addAll(this.beanDefinitionNames);
                updatedDefinitions.add(beanName);
                this.beanDefinitionNames = updatedDefinitions;
                this.removeManualSingletonName(beanName);
            }
        } else {
            this.beanDefinitionMap.put(beanName, beanDefinition);
            this.beanDefinitionNames.add(beanName);
            this.removeManualSingletonName(beanName);
        }

        this.frozenBeanDefinitionNames = null;
    }

    if (existingDefinition == null && !this.containsSingleton(beanName)) {
        if (this.isConfigurationFrozen()) {
            this.clearByTypeCache();
        }
    } else {
        this.resetBeanDefinition(beanName);
    }

}

这个方法看不下去...

image.png

image.png

不愧是高富帅DefaultListableBeanFactory,成员变量如下。

// JSR-330 支持
private static Class<?> javaxInjectProviderClass;
// DefaultListableBeanFactory引用的缓存
private static final Map<String, Reference<DefaultListableBeanFactory>> serializableFactories = new ConcurrentHashMap<>(8);
// 序列号id
private String serializationId;
// 是否允许用相同的名称重新注册不同的定义
private boolean allowBeanDefinitionOverriding = true;
// 是否允许懒加载
private boolean allowEagerClassLoading = true;
// 依赖排序顺序
private Comparator<Object> dependencyComparator;
// 解析器,用于检查bean定义是否为自动装配候选
private AutowireCandidateResolver autowireCandidateResolver = new SimpleAutowireCandidateResolver();
// 依赖类型和自动注入值的映射
private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
// BeanDefinition和beanName的映射
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
// 依赖类型和单例、非单例bean的映射
private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
// 依赖类型和单例bean的映射
private final Map<Class<?>, String[]> singletonBeanNamesByType = new ConcurrentHashMap<>(64);
// bean定义名称列表,按注册顺序排列。
private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
// 手动注册的单例,按注册顺序排列
private volatile Set<String> manualSingletonNames = new LinkedHashSet<>(16);
// 固定配置的缓存的bean定义名数组
private volatile String[] frozenBeanDefinitionNames;
// 是否可以缓存所有bean的bean定义元数据
private volatile boolean configurationFrozen = false;

到这里已经初始化了 Bean 容器,的配置也相应的转换为了一个个BeanDefinition,然后注册了所有的BeanDefinition到beanDefinitionMap。

image.png 看得我眼花缭乱,还好我年轻还顶得住.. 明天继续更!