springboot2.x源码笔记-生成bean的实例以及初始化

springboot的源码(spring)主要分为几个部分

1、构造SpringApplication,完成spring.factories文件中Initializers与Listeners的加载
2、加载配置文件,通过ConfigFileApplicationListener
3、加载BeanDefinitionRegistryPostProcessor与BeanFactoryPostProcessor完成bean的定义包装(非生成实例)
4、生成bean实例以及初始化

本文主要针对第四点,主要看如何AbstractBeanFactory的getBean方法完成bean的实例化和初始化

基于springboot2.1.4

项目地址:https://gitee.com/eshin/springbootdemo.git#autotest

通过3中bean的定义可知有如下几类的bean定义

1、普通注解的bean(eg:@Component @Service @Controller)
beanclass是原始类名
2、@Configuration 注解的配置类的bean
beanclass是用EnhancerBySpringCGlib封装的,Enhancer可参考此处
3、@Bean注解生成的Bean
beanclass为空,但是有factoryBeanName和factoryMethodName
4、用@Scope注解的上述Bean
beanclass为ScopedFactoryBean,但会通过targetBeanName查找到真正的定义,而真正的定义name前面会加上scopedTarget的前缀

记住这几个特征,在getBean的时候会用得上

进入org.springframework.context.support.AbstractApplicationContext#refresh-->org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization-->org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons
一、先来看遍历beanDefinition生成实例

进入getBean方法--->org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
1、先来看第一个getSingleton,org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
2、getObjectForBeanInstance,这个在多个位置用到,除了首次调用在1中获取到的bean不为空时,其余都是在bean创建完之后

org.springframework.beans.factory.support.AbstractBeanFactory#getObjectForBeanInstance


因此,如果beanClass是ScopeProxyFactoryBean,缓存到beanFactory的单例实例就是beanName对应的ScopeProxyFactoryBean,不管是原型模式和Scope模式(注意,ScopeProxyFactoryBean都是以单例形式存在beanFactory中,但是根据ScopeProxyFactoryBean找到targetBeanName生成代理对象就不是单例了,Scope的介绍放在最后)


3、接下来是三种场景下的createBean

原型模式在每次调用的时候都会生成新的bean,而Scope模式,会通过scope.get(),不同的scope按照不同的逻辑,判断是否生成新的bean,比如,SessionScope,在调用scope.get()的时候,会判断是否是新的session,如果是新的session就生成新的bean,否则使用已有的bean。本文重点在于解读singleton模式。

进入org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)

现在重点关注如何创建bean-->org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
3.1、创建bean的实例

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance


创建@Bean方法的bean与通过带参构造方法创建bean的原理类似,org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#instantiateUsingFactoryMethodorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#autowireConstructor两个方法都是又臭又长,就不展开了(可以设置多个重载方法带不同的参数来debug)通过工厂方法和构造方法获取Bean可参考此处,大概说下做了什么:
instantiateUsingFactoryMethod 如果bean的定义中factoryMethodName不为空进入该方法
3.1.1.1、获取factoryBean,也就是@Bean方法所在的类的实例,由于在定义的时候被enhancer封装了,所以是个代理对象,如果factoryBean还没创建,先创建,否则直接获取。
3.1.1.2、在factoryBean 的原始class中通过org.springframework.beans.factory.support.ConstructorResolver#getCandidateMethods查找到所有的方法,然后从中找到目标方法。
3.1.1.3、如果目标方法有参数,调用org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray-->org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument-->org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency--->org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency完成参数的bean的注入
3.1.1.4、调用org.springframework.beans.factory.support.ConstructorResolver#instantiate(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object, java.lang.reflect.Method, java.lang.Object[])-->org.springframework.beans.factory.support.SimpleInstantiationStrategy#instantiate(org.springframework.beans.factory.support.RootBeanDefinition, java.lang.String, org.springframework.beans.factory.BeanFactory, java.lang.Object, java.lang.reflect.Method, java.lang.Object...)执行目标方法,返回的实例就是需要的bean
由于factoryBean是enhancer封装的,执行factoryMethod.invoke(factoryBean, args)调用目标方法的时候会经过org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#intercept--->org.springframework.cglib.proxy.MethodProxy#invokeSuper (Enhancer代理拦截的原理)

autowireConstructor如果检测到beanClass包含有带参构造方法进入该方法并传入检测到的构造方法
3.1.2.1、筛选出匹配的构造方法
3.1.2.2、如果目标构造方法有参数,调用org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray-->org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument-->org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency--->org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency完成参数的bean的注入
3.1.2.3、调用org.springframework.beans.factory.support.ConstructorResolver#instantiate(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.reflect.Constructor<?>, java.lang.Object[])-->org.springframework.beans.factory.support.SimpleInstantiationStrategy#instantiate(org.springframework.beans.factory.support.RootBeanDefinition, java.lang.String, org.springframework.beans.factory.BeanFactory, java.lang.reflect.Constructor<?>, java.lang.Object...)完成使用构造方法创建实例

注意:不管是通过@bean方法还是构造方法生成目标bean,在执行目标方法前(即创建实例前),就需要对参数bean完成注入,此时在BeanFactory中是没有这个目标bean的引用的,所以如果参数bean中如果也要注入目标bean,那此时参数bean就无法从singletonFactories,或者earlySingletonObject或者singletonObjects中找到相关引用,就出现循环注入的异常。也就是说,两个bean如果相互注入,则需要先开始处理的bean先实例化并在singletonsCurrentlyInCreation中标识已经创建,并且在singletonFactories,或者earlySingletonObject中能够找到。这也是为何说org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingletonorg.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingletonFactory能够解决循环注入问题

有图可知,相互注入的两个bean中先处理的bean除了不能使用构造方法和@bean方法之外,还不能在创建实例后经过代理封装处理,如果设置allowRawInjectionDespiteWrapping=true,不会抛异常,但存在隐患,因为允许注入到后者中的前者,不是最终的版本。因此开发过程中要尽量避免循环注入的情况,可以在两者都成为完全体的bean后,手动在应用代码中互相调对方的set方法。

最后一个无参构造方法的构造就简单多了直接调用org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#instantiateBean

3.2、bean的成员变量的填充

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean


完成注入的processor如:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties,完成@value和@autowired属性的注入

注入线索->postProcessPropertiest-->org.springframework.beans.factory.annotation.InjectionMetadata#inject--->org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement#inject--->org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency--->org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency具体的注入逻辑,不看了,大概就是通过org.springframework.context.support.PropertySourcesPlaceholderConfigurer#processProperties(ConfigurableListableBeanFactory, ConfigurablePropertyResolver)中定义的StringValueResolver解析@value注解的String变量(配置项解析参考此处第3点),通过getBean方法获取需要autowired的bean。
如:org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessProperties完成@resource注解的属性注入

注入线索org.springframework.beans.factory.annotation.InjectionMetadata.InjectedElement#inject--->org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.ResourceElement#getResourceToInject--->org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#getResource-->org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#autowireResource--->org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#resolveBeanByName具体的逻辑也不看了,反正就是到getBean方法继续获取需要注入的bean

执行完所有的BeanPostProcessor的postProcessProperties方法后,需要填充的变量的值就获取完了,再调用org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyPropertyValues把变量值设置回bean中整个填充过程就完成了

3.3、bean的初始化,一系列初始化方法的调用

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean()

因此初始化方法的执行顺序1、@PostConstruct注解的方法->2、实现了InitializingBean的Bean(如果有)的afterPropertiesSet方法->3、自定义指定的初始化方法

对于自定义初始化方法,xml配置方法可以通过 init-method="initMethod"指定,但是注解方式就没必要了,可以通过@PostConstruct注解再不行,还能实现InitializingBean,在调用下afterPropertiesSet方法。是在手痒可以修改bean的定义,方法如下

/**
 * BeanFactoryPostProcessor主要用于修改bean定义,对beanFactory中的相关参数进行修改
 */
@Component
public class CustomBeanFactoryPostProcessor implements  BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.getBeanDefinition("helloController").setInitMethodName("initController");
    }

}

到这里bean的实例化和初始化就完成了
现在回到org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean中的下面部分

// Create bean instance.
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }

getObjectForBeanInstance(sharedInstance, name, beanName, mbd);在本文第2点中已经有说明
继续回到org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons
在完成bean的实例化初始化后,调用SmartInitializingSingleton的实例,具体可参考这里第3点其中一个SmartInitializingSingleton实例是EventListenerMethodProcessor完成对@EventListener注解的方法封装成listener注册到beanfactory,其具体解析可参考@EventListener注解解析部分

至此,应用启动后,非懒加载的bean就都加载好了。
现在回来说说最开始的几种bean的定义,configuration类生成的bean带enhancer的后缀,以及从configuration类生成的bean中执行@bean注解的方法生成bean,前面都已说明,还有种在开启了异步的情况下,bean的定义中还是普通注解的bean,但在生成bean的时候如果有方法使用了@Async注解,则也会通过enhancer封装,具体对@Async解析可参考此处
懒加载的bean,只有第一次调用的时候才会通过getBean方法进行实例化初始化(第一次手动getBean,或者第一次被注入到其他的bean),只有也都是缓存在beanfactory的singleObjects中

二、最后来看看用@Scope注解的bean

先上一段demo(完整代码从文章开头位置下载)

@Configuration
public class HelloConfigurationScope {

    @PostConstruct
    public void init(){
        System.out.println("HelloConfigurationScope init...");
    }

    @Bean
    @Scope(value = "singleton",proxyMode = ScopedProxyMode.TARGET_CLASS)
    public OrderBean orderBeanScope(){
        System.out.println("create default orderBeanScope in HelloConfigurationScope");
        return new OrderBean();
    }
    @Bean
    @Scope(value = "thread",proxyMode = ScopedProxyMode.TARGET_CLASS)
    public RefundBean refundBeanScope(){
        System.out.println("create default refundBeanScope in HelloConfigurationScope");
        return new RefundBean();
    }

@RunWith(SpringRunner.class)
@SpringBootTest
public class GetBeanTests {

    @Autowired
    ApplicationContext context;

    @Test
    public void loadBean() throws InterruptedException {

        Object bean = context.getBean("orderBean1");
        ((OrderBean)bean).setName("orderBean1");
        Object obean = context.getBean("orderBeanScope");
        ((OrderBean)obean).setName("orderBeanScope");
        Object rbean = context.getBean("refundBeanScope");
        /**
         * 强转会出发拦截器的拦截,提前实例化,等到setName拦截事,threadlocal已经有值
         * 此处要debug不能单步执行,得现在org.springframework.aop.target.SimpleBeanTargetSource#getTarget()打断点,
         * 然后按F8才能得到第一次getBean的过程,等到setName都是第二次了
         */
        ((RefundBean)rbean).setName("refundBeanScope");//
//        第二次調用
        Object bean1 = context.getBean("orderBean1");
        ((OrderBean)bean1).setName("orderBean1");
        Object obean1 = context.getBean("orderBeanScope");
        ((OrderBean)obean1).setName("orderBeanScope");
        Object rbean1 = context.getBean("refundBeanScope");
        ((RefundBean)rbean1).setName("refundBeanScope");//

       Thread tt =  new Thread(new Runnable() {
            @Override
            public void run() {
                Object rbean = context.getBean("refundBeanScope");
                ((RefundBean)rbean).setName("refundBeanScope");
            }
        });
        tt.start();
        tt.join();
    }

}

由最开始的beanDefinition图可知,用@Scope(value = "xxx",proxyMode = ScopedProxyMode.TARGET_CLASS)注解的bean,会有两个bean的definition,其中beanName对应的beanclass是org.springframework.aop.scope.ScopedProxyFactoryBean,另外一个scopedTarget.beanName,带有scopeTarget前缀的名字,这个对应的是bean的原始定义。不管是xxx=singleton还是其他,都是会有这两个定义,但是要设置proxyMode = ScopedProxyMode.TARGET_CLASS

1、singleton的场景@Scope(value = "singleton",proxyMode = ScopedProxyMode.TARGET_CLASS)

beanName对应beanclass为ScopedProxyFactoryBean的beanDefinition,是不设置scope的值,默认为空,空代表是singleton。
scopedTarget.beanName对应的beanDefinition是原始的定义,scope值也是singleton。因此,beanFactory的singletonObjects中两者的实例都有。

bean的实例化过程前面已经讲述。但是缺少了ScopedProxyFactoryBean如何生成代理bean的过程。
ScopedProxyFactoryBean实现了BeanFactoryAware接口,在bean实例化之后,初始化阶段进入org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)--->org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#invokeAwareMethods


进入对应的setBeanFactory方法

通过ProxyFactory获取代理,以及拦截过程可以参考这里第3点这里第2点

1.1、先看看如何获取到代理对象

有前面可以知道,当某个beanName对应的beanDefinition的beanclass是ScopedProxyFactoryBean,那在实例化非懒加载对象阶段,会通过&+beanName的name去获取ScopedProxyFactoryBean,从而根据beanName存入到beanFactory的singletonObjects的对象时scopedProxyFactoryBean,而不是scopedProxyFactoryBean.getObject()获取的proxy对象(其实存proxy对象也是可以的,proxy对象也是个单例(即便是scope模式,proxy对象也是个单例,只不过targetSource.getTarget()获取的bean可能不一样)),当通过context.getBean(beanName)获取bean是才获取到scopedProxyFactoryBean中的proxy对象

由图,此时图中get到的singleton肯定是不为空(先不管懒加载方式)那么进入org.springframework.beans.factory.support.AbstractBeanFactory#getObjectForBeanInstance方法后,判断是个factoryBean,且不是带&的name就进入org.springframework.beans.factory.support.FactoryBeanRegistrySupport#getObjectFromFactoryBean

1.2、现在主要看看targetSource.getTarget();

当上面获取到的代理对象执行目标方法的时候,就会被拦截,到执行targetSource.getTarget()的位置。由于构造ScopedProxyFactoryBean对象时,使用的SimpleBeanTargetSource,因此此时进入的org.springframework.aop.target.SimpleBeanTargetSource#getTarget

@Override
    public Object getTarget() throws Exception {
        return getBeanFactory().getBean(getTargetBeanName());
    }

此时就通过带scopeTarget前缀的beanName获取到bean,由于单例的时候,beanFactory中的singletonObjects中已经有对应的实例(懒加载的如果是第一次调用则在此时加载),然后执行其目标方法。

2、非singleton场景(以ThreadScope为例)

ThreadScope,spring启动的时候并没有注册,先注册下

@Component
public class CustomBeanFactoryPostProcessor implements  BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("thread",new SimpleThreadScope());
    }

}

非singleton场景下,ScopeProxyFactoryBean的生成,以及从中获取proxy对象都是和singleton一样的,不同的在于targetSource.getTarget(),由于非singleton,因此beanFactory中的singletonObjects并不会有scopeTarget前缀的beanName对应bean,因此每次都需要经过scope.get()如图:

以SimpleThreadScope为例:


由图,两个线程获取的代理bean都是同一个,获取到不同的bean需要通过targetSource.getTarget()

总结:

1、所有的@Configuration类的bean都是经过enhancer封装的
2、@EnableAsync(proxyTargetClass=true)情况下,非@Configuration的类的方法如果使用了@Async,那这个类也会被enhancer封装(跟1的封装不同,具体可看上面的内容)
3、所有需要实例化的bean都是(能通过getBean获取)的bean,都要求beanFactory中有对应的beanDefinition,所以如果有bean在定义过程中,被条件注解过滤,后续无法创建其实例。
4、spring允许两个bean相互注入,但是先处理的bean,不能通过构造方法或者工厂方法注入后者,否则会出现cycle异常,后者可以用工厂方法或者构造方法注入前者。相互注入的bean,最好都通过成员变量注入的方式。如果前者存在@Async注解的方法等有被封装成代理的bean的情况下,也会导致相互注入失败,此时建议通过set方法手动设置原本需要注入的bean。
5、当bean类有多个构造方法时,需要使用@Autowired指定用来构建实例的构造方法,不允许有多个构造方式注解@Autowired,当无定义构造方法或者只有一个构造方法,无需使用@Autowired
6、当有多个工厂方法重载(都是用@Bean,没有@Bean的重载方法不参与比较),首先public的有限,然后看参数个数,个数多的优先,然后看参数类型匹配度。
7、初始化方法的执行顺序1、@PostConstruct注解的方法->2、实现了InitializingBean的Bean(如果有)的afterPropertiesSet方法->3、自定义指定的初始化方法,执行这些初始化方法的时候,可以在这些方法中使用注入的对象
8、实现了ApplicationListener接口的bean,会注册到context的listeners列表中,而没有实现该接口的bean,如果有方法注解了@EvenListener,则该方法会被封装成一个listener并注册
9、当@Scope的值时singleton,beanFactory中会有beanName对应的ScopeProxyFactoryBean实例,和一个scopeTarget.beanName对应的目标bean实例。scope值为其他则没有scopeTarget.beanName对应的目标bean实例,需要在从ScopeProxyFactoryBean获取到proxy实例后,再执行目标方法时,才会从scope.get()中获取到实例。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270