美文网首页SpringJava技术我爱编程
Spring源码分析之Bean的加载

Spring源码分析之Bean的加载

作者: 一字马胡 | 来源:发表于2017-12-29 23:56 被阅读415次

    作者: 一字马胡
    转载标志 【2017-12-29】

    更新日志

    日期 更新内容 备注
    2017-12-29 创建分析文档 Spring源码分析系列文章(二)

    前言

    在Spring源码分析的第一篇文章Spring源码分析之Bean的解析中,主要梳理了Spring Bean解析的链路,当然只是梳理了主干部分,细节的内容没有涉及太多。本文接着上一篇文章,开始进行Spring Bean加载的源码分析,bean解析完成之后,还不是可用的对象实例,需要进行加载,所谓bean的加载,就是将解析好的BeanDefinition变为可用的Object。在进行源码分析之前,可以猜测一下Spring bean加载到底做了些什么工作。首先,Spring bean解析的工作将我们在xml中配置的bean解析成BeanDefinition并且放在内存中,现在,我们可以拿到每一个bean的完整的信息,其中最重要的应该是Class信息,因为实例化对象需要知道具体需要实例化的Class信息。还需要知道具体bean的属性值等信息,而这些数据目前都是可以拿到的,所以接下来的事情就是根据这些信息首先new一个对于Class类型的Object,然后进行一些初始化,然后返回。

    当然,Spring bean加载的实现远远没有这么简单,这中间涉及的内容很多,本文依然不会涉及太多细节的内容,主要梳理Spring bean加载的主干流程,知道一个bean是如何被解析,以及如何加载的,是本文以及第一篇文章内容所想要表达的内容。

    Spring Bean加载流程分析

    分析Spring bean加载的第一步是找到分析的入口。接着上一篇文章的例子,从下面的代码开始分析:

    
            String file = "applicationContext.xml";
    
            ApplicationContext context = new ClassPathXmlApplicationContext(file);
    
            ModelA modelA = (ModelA) context.getBean("modelA");
    
    

    跟进context.getBean,可以在AbstractBeanFactory中找到具体的实现:

    
        public Object getBean(String name) throws BeansException {
            return doGetBean(name, null, null, false);
        }
    
    

    可以看到实际执行的方法是doGetBean,因为这个方法较为复杂,所以拆开来分析实现的具体细节。首先关注下面的代码:

    
        final String beanName = transformedBeanName(name);
    
        protected String transformedBeanName(String name) {
            return canonicalName(BeanFactoryUtils.transformedBeanName(name));
        }
    
        public static String transformedBeanName(String name) {
            Assert.notNull(name, "'name' must not be null");
            String beanName = name;
            while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
                beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
            }
            return beanName;
        }   
    

    我们传递进去的name就是我们在xml中配置的id,首先会调用transformedBeanName对传递的name做一次处理,在transformedBeanName中又调用了BeanFactoryUtils.transformedBeanName方法来进行做处理,具体的内容就是判断我们传递的name是不是以“&”开头的,要是的需要去掉再返回。需要注意的是,我们传递的name可能是bean的别名,也可能是BeanFactory,所以transformedBeanName的操作的目的就是取到真正的bean的名字,以便开始后续的流程。

    获取到bean的name之后,开始走下面的流程,需要注意下面的方法调用:

    
            // Eagerly check singleton cache for manually registered singletons.
            Object sharedInstance = getSingleton(beanName);
    
    

    具体的getSingleton的实现细节可以参考下面的代码:

    因为我们的bean在配置的时候可能会被配置为单例或者原型模式,单例模式的bean全局只会生成一次,所以getSingleton方法的目的是去Spring的单例bean缓存中寻找是否该bean已经被初始化了,如果已经被初始化了那么直接从缓存取到就可以了,就没有必要走下面的流程了。上面贴出的getSingleton代码实现的就是尝试从缓存map中取bean,当然,这里面还是涉及一些其他的流程的。如果发现需要取的bean实例没有在缓存中,那么就不能走缓存了,就需要走接下来的流程了。方法isSingletonCurrentlyInCreation实现的内容是判断当前bean是否是正在被加载的bean,判断方法是使用一个set来实现,没当在进行加载单例bean之前,Spring会将该bean的name放到set中。earlySingletonObjects是一个map,存储的是还没有加载完成的bean的信息。如果发现从earlySingletonObjects中取到的bean实例为null的话,getSingleton方法就会进行该单例bean的加载。在实现上,首先通过取到用于创建该bean的BeanFactory,然后调用BeanFactory的getObject方法获取到bean实例。

    singletonFactories存储着bean name以及创建该bean的BeanFactory。那用于获取bean实例的BeanFactory是什么时候被什么对象放到map中来的呢?可以发现是一个叫做addSingletonFactory的方法在做这件事情,下面是这个方法的实现细节:

    那是什么地方调用该方法的呢?可以在AbstractAutowireCapableBeanFactory类中的doCreateBean方法中找到该方法的调用细节,相关的上下文代码如下:

    接着回到getSingleton方法,获取到用于创建bean实例的BeanFactory之后,就可以使用singletonFactory.getObject()来获取到实例对象了。下面来分析一下这条链路的具体流程。这里需要注意一下,我们需要知道这个BeanFactory到底是什么样的实现,才能分析具体的内容,所以解铃还得系铃人,回头看看到底放到singletonFactories中的是一个什么样的BeanFactory。最后发现,实际执行的方法是getEarlyBeanReference,见下面的代码:

    
        /**
         * Obtain a reference for early access to the specified bean,
         * typically for the purpose of resolving a circular reference.
         * @param beanName the name of the bean (for error handling purposes)
         * @param mbd the merged bean definition for the bean
         * @param bean the raw bean instance
         * @return the object to expose as bean reference
         */
        protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
            Object exposedObject = bean;
            if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
                for (BeanPostProcessor bp : getBeanPostProcessors()) {
                    if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                        SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                        exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                    }
                }
            }
            return exposedObject;
        }
    
    

    实际上我们需要关心的是第三个参数bean,这也就是实际的bean实例对象,那这个bean到底是什么呢?是怎么样被创建出来的呢?这就得回头看调用getEarlyBeanReference方法的上下文了,所以回头看doCreateBean方法中的具体内容。主要看下面这段代码:

    主要的流程是看缓存里面有没有,如果有则不需要再次加载,否则需要调用方法createBeanInstance来创建bean实例,为了简化问题,主要分析createBeanInstance这个方法的实现细节。首先调用方法resolveBeanClass获取bean的Class,然后判断权限等操作,首先来看instantiateBean方法的具体细节,该方法代表使用默认构造函数来进行bean的实例化,而autowireConstructor方法则代表使用有参数的构造函数来进行bean的实例化。下面是instantiateBean的实现细节:

    主要关注:beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent)这一句代码,然后看instantiate这个方法的实现细节:

    主要关注:return BeanUtils.instantiateClass(constructorToUse)这行代码以及else分支的return语句,闲来看return BeanUtils.instantiateClass(constructorToUse)这句代码的实现细节:

    其实就是根据类的构造函数以及构造函数参数来new一个对象,具体的细节可以再跟进去详细研究。下面来看else分支中的instantiateWithMethodInjection(bd, beanName, owner),下面是具体的实现代码:

    具体的内容就是使用CGlib来进行对象的创建,具体的内容可以参考其他的资料,或者再跟下去来详细研究。下面来看使用有参构造函数来进行bean的实例化的具体实现细节,对应的方法是autowireConstructor,该方法相对于使用默认构造函数来进行bean的实例化来说,需要做的事情是,因为bean可能有多个构造函数,并且可能权限不同,所以Spring需要根据参数个数以及类型选取一个合适的构造函数来进行bean的实例化,这部分代码较多,但是大概的流程就是这样,代码就不再贴出来了。

    在new出来一个bean实例之后,还没有真正完成Spring的bean加载内容,因为除了new出来一个Object之外,还需要做其他的事情,下面来分析一下Spring都做了一些什么事情。首先是populateBean方法,该方法实现的功能是对bean进行属性注入,下面是该方法的关键代码:

    
            if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||
                    mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
                MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
    
                // Add property values based on autowire by name if applicable.
                if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
                    autowireByName(beanName, mbd, bw, newPvs);
                }
    
                // Add property values based on autowire by type if applicable.
                if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
                    autowireByType(beanName, mbd, bw, newPvs);
                }
    
                pvs = newPvs;
            }
    
    

    autowireByName代表根据名字自动注入属性,而autowireByType实现的是根据类型自动注入属性,根据名字注入相对简单一些,而根据类型注入要复杂很多,下面先来分析根据名字自动注入属性的分析。下面是autowireByName的代码:

    
        protected void autowireByName(
                String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
    
            String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
            for (String propertyName : propertyNames) {
                if (containsBean(propertyName)) {
                    Object bean = getBean(propertyName);
                    pvs.add(propertyName, bean);
                    registerDependentBean(propertyName, beanName);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Added autowiring by name from bean name '" + beanName +
                                "' via property '" + propertyName + "' to bean named '" + propertyName + "'");
                    }
                }
                else {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName +
                                "' by name: no matching bean found");
                    }
                }
            }
        }
    
    

    unsatisfiedNonSimpleProperties方法首先获取所有需要进行依赖注入的属性名称,然后保存起来,特别需要注意的是registerDependentBean这个方法的调用,该方法实现的是维护bean的依赖管理。接着是根据类型注入autowireByType的实现。该方法的实现还是很复杂的,暂时不对该方法进行分析。

    当执行完上面两个方法之后,已经获取到了所有需要注入的属性信息,pvs持有这些信息,接着,applyPropertyValues方法会实际将这些依赖注入。下面来分析一下applyPropertyValues方法的具体实现流程。具体的实现细节可以参考下面给出的索引:


    AbstractAutowireCapableBeanFactory#applyPropertyValues


    主要关注核心代码:bw.setPropertyValues(mpvs),bw是bean实例对象,mpvs是属性信息。如果再追踪下去,就可以看到如下的代码链路:

    
        public void setPropertyValues(PropertyValues pvs) throws BeansException {
            setPropertyValues(pvs, false, false);
        }
    
        public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
                throws BeansException {
    
            List<PropertyAccessException> propertyAccessExceptions = null;
            List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
                    ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
            for (PropertyValue pv : propertyValues) {
                setPropertyValue(pv);
            }
        }
        
    

    这里面已经去掉了一些异常处理的代码,只留下最核心的代码,主要关注setPropertyValue这个方法,再继续跟踪下去:

    
        public void setPropertyValue(PropertyValue pv) throws BeansException {
            setPropertyValue(pv.getName(), pv.getValue());
        }
    
    
    

    到这里就应该很清晰了,pv.getName()是属性名称,而pv.getValue()就是实际上注入的依赖。在完成依赖注入之后,应该说bean就可以使用了,但是因为Spring还提供了一些其他的标签让用户进行个性化设置,比如初始化方法、销毁方法等,接着看exposedObject = initializeBean(beanName, exposedObject, mbd)这行代码的实际工作流程。

    
        protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                    invokeAwareMethods(beanName, bean);
                    return null;
                }, getAccessControlContext());
            }
            else {
                invokeAwareMethods(beanName, bean);
            }
    
            Object wrappedBean = bean;
            if (mbd == null || !mbd.isSynthetic()) {
                wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
            }
    
            try {
                invokeInitMethods(beanName, wrappedBean, mbd);
            }
            catch (Throwable ex) {
                throw new BeanCreationException(
                        (mbd != null ? mbd.getResourceDescription() : null),
                        beanName, "Invocation of init method failed", ex);
            }
            if (mbd == null || !mbd.isSynthetic()) {
                wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            }
    
            return wrappedBean;
        }
    
    

    这其中调用了几个个性化的方法,第一个是invokeAwareMethods,是去访问子类的Aware,BeanClassLoaderAware, BeanFactoryAware等,下面是该方法的实现细节:

    
        private void invokeAwareMethods(final String beanName, final Object bean) {
            if (bean instanceof Aware) {
                if (bean instanceof BeanNameAware) {
                    ((BeanNameAware) bean).setBeanName(beanName);
                }
                if (bean instanceof BeanClassLoaderAware) {
                    ClassLoader bcl = getBeanClassLoader();
                    if (bcl != null) {
                        ((BeanClassLoaderAware) bean).setBeanClassLoader(bcl);
                    }
                }
                if (bean instanceof BeanFactoryAware) {
                    ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
                }
            }
        }
    
    

    接着是applyBeanPostProcessorsBeforeInitialization方法,是访问子类的后处理器内容,下面是该方法的详细实现:

    
        public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
                throws BeansException {
    
            Object result = existingBean;
            for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
                Object current = beanProcessor.postProcessBeforeInitialization(result, beanName);
                if (current == null) {
                    return result;
                }
                result = current;
            }
            return result;
        }
    
    

    接着是invokeInitMethods方法,是调用用户设定的init方法,下面是该方法的实现细节:

    
    protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
                throws Throwable {
    
            boolean isInitializingBean = (bean instanceof InitializingBean);
            if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
                }
                if (System.getSecurityManager() != null) {
                    try {
                        AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                            ((InitializingBean) bean).afterPropertiesSet();
                            return null;
                        }, getAccessControlContext());
                    }
                    catch (PrivilegedActionException pae) {
                        throw pae.getException();
                    }
                }
                else {
                    ((InitializingBean) bean).afterPropertiesSet();
                }
            }
    
            if (mbd != null && bean.getClass() != NullBean.class) {
                String initMethodName = mbd.getInitMethodName();
                if (StringUtils.hasLength(initMethodName) &&
                        !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                        !mbd.isExternallyManagedInitMethod(initMethodName)) {
                    invokeCustomInitMethod(beanName, bean, mbd);
                }
            }
        }
    
    

    这其中需要注意的一点是,在该方法中调用了afterPropertiesSet方法,然后是获取了用户设定的init方法的名字,然后调用invokeCustomInitMethod方法执行了这个init方法。

    到此,单例bean的加载好像分析到头了,确实是,当然这只是一个很小的分支,还有其他的分支没有走,现在回到doGetBean方法,继续分析剩下的代码。为了分析简单,直接从下面的代码开始分析,其他没有分析到的代码以后再详细分析,现在主要是希望走通整个流程,所以只会挑选关键的代码进行分析。首先是下面的代码:

    
                    // Guarantee initialization of beans that the current bean depends on.
                    String[] dependsOn = mbd.getDependsOn();
                    if (dependsOn != null) {
                        for (String dep : dependsOn) {
                            if (isDependent(beanName, dep)) {
                                throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                        "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                            }
                            registerDependentBean(dep, beanName);
                            getBean(dep);
                        }
                    }
    
    

    因为我们的bean可能依赖其他的bean,所以需要在加载当前bean之前加载它所依赖的bean。所以这里面又调用了getBean方法来循环加载依赖的bean。接着看下面的代码:

    如果bean是一个单例bean,那么会走到该分支里面。会调用getSingleton方法来加载该bean。getSingleton方法的第一个参数是beanName,第二个参数比较关键,是用于创建bean的BeanFactory,所以有必要仔细分析一下该BeanFactory。因为在getSingleton方法中关键的步骤就是调用BeanFactory的getObject方法来获取到一个bean的实例,当然还有很多其他的细节需要判断,但不再本文的叙述范围之内。下面来看这个BeanFactory的具体实现。根据上面贴出的代码,其实内部关键调用了一个方法createBean。下面来看这个方法的实现细节。该方法的关键内容如下:

    首先会调用resolveBeforeInstantiation方法,这是一个试探性的方法调用,根据描述,该方法是给BeanPostProcessors一个机会来返回bean实例,具体看看该方法的细节:

    
        protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
            Object bean = null;
            if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
                // Make sure bean class is actually resolved at this point.
                if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
                    Class<?> targetType = determineTargetType(beanName, mbd);
                    if (targetType != null) {
                        bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
                        if (bean != null) {
                            bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
                        }
                    }
                }
                mbd.beforeInstantiationResolved = (bean != null);
            }
            return bean;
        }
    
    

    该方法关键会先调applyBeanPostProcessorsBeforeInstantiation方法来调用实现类的postProcessBeforeInstantiation方法,如果返回内容还为null,就会继续调用applyBeanPostProcessorsAfterInitialization方法来调用实现类的postProcessAfterInitialization。所以这里需要注意一下,这也是一个Spring使用技巧点,可以继承BeanPostProcessor来实现个性化的Spring bean定制。

    当resolveBeforeInstantiation方法返回null的时候,就会继续调用doCreateBean方法来加载bean的实例。该方法中首先一个比较关键的方法调用是createBeanInstance,下面的流程在上文中已经进行过了,就不再赘述了。只要知道doCreateBean方法返回的就是一个bean的实例就好了。

    当然,我们获取到的bean实例可能是一个FactoryBean,那么就需从FactoryBean中获取到Object。实现该流程的是方法getObjectForBeanInstance。主要看该方法中调用的getObjectFromFactoryBean方法。进到getObjectFromFactoryBean方法之后主要注意方法doGetObjectFromFactoryBean。该方法的主要流程代码如下:

    主要关注代码:object = factory.getObject(),继续看下面的代码:

    
        public final T getObject() throws Exception {
            if (isSingleton()) {
                return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
            }
            else {
                return createInstance();
            }
        }
    
    

    这里区分了单例和原型两种bean,如果bean被配置为单例,则会走第一个分支,原型则会走第二个分支。对于单例bean,首先会判断是否已经初始化过了,如果已经初始化过了,那么就直接取到bean实例,否则会调用getEarlySingletonInstance来获取到bean的实例,下面来具体分析一下getEarlySingletonInstance这个方法的执行流程。跟踪进去最后执行的代码是下面的方法:

    
        private T getEarlySingletonInstance() throws Exception {
            Class<?>[] ifcs = getEarlySingletonInterfaces();
            if (ifcs == null) {
                throw new FactoryBeanNotInitializedException(
                        getClass().getName() + " does not support circular references");
            }
            if (this.earlySingletonInstance == null) {
                this.earlySingletonInstance = (T) Proxy.newProxyInstance(
                        this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());
            }
            return this.earlySingletonInstance;
        }
    
    

    到这里就很清晰了,还是会判断是否已经加载过了,如果加载过了,那么就不会重复实例化bean了,否则就使用java的动态代理技术生成一个bean实例。

    对于原型bean来说,会调用createInstance方法进行bean的实例化过程,对不同类型的FactoryBean会使用不同的子类的方法来获取实例,比如AbstractFactoryBean的子类等等。

    到这里需要说明一下FactoryBean这个类,FactoryBean是一个Bean,实现了FactoryBean接口的类有能力改变bean,FactoryBean希望你实现了它之后返回一些内容,Spring会按照这些内容
    去注册bean,这也给了程序员一个定制bean的机会,继承了FactoryBean的子类的bean的加载会调用FactoryBean的getObject方法获取bean实例,而且,我们使用getBean获取到的是FactoryBean的getObject方法调用的结果,如果想要获取到FactoryBean本身,需要在bean前面加上“&”标志,这个逻辑可以在方法getObjectForBeanInstance中找到:

    
            // Now we have the bean instance, which may be a normal bean or a FactoryBean.
            // If it's a FactoryBean, we use it to create a bean instance, unless the
            // caller actually wants a reference to the factory.
            if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
                return beanInstance;
            }
    
    

    如果对BeanFactory和FactoryBean傻傻分不清,可以参考文章Spring的BeanFactory和FactoryBean,主要思路就是看后缀,BeanFactory是一种FactoryBean,是用来生成bean的,而FactoryBean是一个bean,它是用来改变FactoryBean生成bean的路径的。

    需要注意的一点是,上文的分析中并没有涉及缓存的内容,这部分内容比较离散,我们在走主流程的时候只要知道它会在何时的时候填充缓存(主要关注单例bean),以及在需要的时候从缓存中获取就可以了。

    接着回到doGetBean方法中,看下面的分支:

    如果不是一个单例的话,那么可能是一个Prototype类型的bean,所谓Prototype类型的bean,就是每次都会创建一个bean实例,这是和单例区分开来说的,除此之外,和单例bean的加载没有区别,所以也不再赘述了。

    doGetBean方法还有后续的流程没有走完,但是到此为止我们已经明白了一个bean是如何被加载的,它现在几乎可以被使用了,后面就是一些收尾工作,具体流程就不再分析了,后续会进行一些策略性的补充。

    本文涉及的内容时Spring bean的加载,内容较多,而且较为复杂,很多内容都没有提及,只是大概梳理了一下主干流程,接着上一篇文章来继续分析,在上一篇文章中分析了Spring bean解析的流程,解析完成之后需要加载才能使用,在加载的过程中设计很多内容,其中包括bean的实例化,以及bean的依赖注入等内容。bean的加载大体上分为单例和原型,本文主要分析了单例bean的加载,原型bean的加载与单例bean的加载流程是一样的,只是单例全局只有一个bean实例,所有可以从缓存中取到,而原型bean每次都是新创建一个bean实例。总体来讲,分析Spring的源码还是比较复杂的,本文中欠缺的内容将在未来陆续补充完善,但主要目的还是梳理流程,走一遍Spring容器的方方面面,这样使用起来就会多几分底气了。

    相关文章

      网友评论

        本文标题:Spring源码分析之Bean的加载

        本文链接:https://www.haomeiwen.com/subject/cftggxtx.html