https://www.cnblogs.com/jyyzzjl/p/5459335.html

一、代码实例

  在我们分析spring的IOC源码的时候,发现除了配置标准的bean,并且通过getBean(beanName)的方法获取到一个bean的实例外,似乎还有这不少其他获取的方法,例如在第四节,我们发现得到bean实例后,在第26节,会判断是否继承了FactoryBean,然后调用它的方法获取真实的bean,在配置文件中我们发现一个factory-bean方法,这些都说明,我们应该可以使用一个beanFactory获取一个bean,此节重点讨论这部分的实现。

  代码如下:

  1、car类

复制代码
package com.zjl.factorybean;

public class Car {
    public Car(String name) {
        this.name=name;
    }
    String name;
    public void run(){
        System.out.println(this.name+" is running");
    }
}
复制代码
  2、person类

复制代码
package com.zjl.factorybean;

public class Person {
    public Person(String name) {
        this.name=name;
    }
    public String name;
    public int age;
    
    public Car car;
    
    public void sayHello(){
        System.out.println(this.name+" say hello");
    }
    public Car createCar(){
        return new Car("奥迪");
    }
}
复制代码
  3、用来获取person的FactoryBean,必须继承FactoryBean接口 

复制代码
package com.zjl.factorybean;
import org.springframework.beans.factory.FactoryBean;

public class PersonFactory implements FactoryBean<Person> {
    String name;

public String getName() {
        return name;
    }

public void setName(String name) {
        this.name = name;
    }

@Override
    public Person getObject() throws Exception {
        return new Person(name);
    }

@Override
    public Class<Person> getObjectType() {
        return Person.class;
    }

@Override
    public boolean isSingleton() {
        return true;
    }

}
复制代码
  4、配置文件

<bean id="personFactory" class="com.zjl.factorybean.PersonFactory">
        <property name="name" value="zhangsan"></property>
    </bean>  
    
    <bean id="car" factory-method="createCar" factory-bean="personFactory"></bean>
  5、测试类

复制代码
package com.zjl.factorybean;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;

public class Test {
    public static void main(String[] args) throws Exception {
        DefaultListableBeanFactory beanFacory=new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader=new XmlBeanDefinitionReader(beanFacory);
        reader.loadBeanDefinitions(new ClassPathResource("factorybean.xml"));
        
        Person person=(Person)beanFacory.getBean("personFactory");
        person.sayHello();
        Person person2=(Person)beanFacory.getBean("personFactory");
        System.out.println("person==person2 is "+(person==person2));
        
        Car car=(Car)beanFacory.getBean("car");
        car.run();
        Car car2=(Car)beanFacory.getBean("car");
        System.out.println("car==car2 is "+(car==car2));
    }
}
复制代码
  6、测试结果

zhangsan say hello
person==person2 is true
奥迪 is running
car==car2 is true
  7、结论

  我们可以看到:

  a)通过getBean(beanName)方法获取到的直接就是Person的实例,而不是BeanFactory或者PersonFactory的实例。

  b)每次获取到的Person实例都是同一个,根据接口中的方法isSingleton方法,猜测于此有关

  c)bean的id为car,并没有配置我们常见的class配置,他应该是执行了Person的createCar方法

  d)car也遵循单例模式

二、FactoryBean代码解析

  1、通过我们对bean的加载过程,发现所有的配置无论是spring默认bean的 配置,还是客户自定义的配置均无差别的被解析后存放在beanDefinitionMap中,所以解析配置文件过程不再重复。

  2、Person person=(Person)beanFacory.getBean("personFactory");生成bean过程与第四部分重复的跳过

  3、来到bean生成实例后的地方

bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd)
  4、进入方法,判断如果是bean不是FactoryBean的实例且beanName是&开头,抛出错误。是FactoryBean的实例,且以&开头,则直接返回实例。

    将实例转化为FactoryBean的实例,并且调用getObjectFromFactoryBean(factory, beanName, !synthetic)方法

  注:也就是我们要获得定义的通过personFactory返回PersonFactory的实例,可以使用beanFacory.getBean("&personFactory")进行获取,然后调用getObject也可以返回Person的实例,不过这个需要自己控制单例模式

复制代码
    protected Object getObjectForBeanInstance(
            Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {

// Don't let calling code try to dereference the factory if the bean isn't a factory.
        if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) {
            throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
        }//bean不是FactoryBean的实例且beanName是&开头,报错

// 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.
     //bean是FactoryBean的实例,且beanName以&开头,返回实例

if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
            return beanInstance;
        }

Object object = null;
        if (mbd == null) {
            object = getCachedObjectForFactoryBean(beanName);
        }
        if (object == null) {
            // Return bean instance from factory.
            FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
            // Caches object obtained from FactoryBean if it is a singleton.
            if (mbd == null && containsBeanDefinition(beanName)) {
                mbd = getMergedLocalBeanDefinition(beanName);
            }
            boolean synthetic = (mbd != null && mbd.isSynthetic());
            object = getObjectFromFactoryBean(factory, beanName, !synthetic);
        }
        return object;
    }
复制代码
  5、调用factory实例的isSingleton方法和containsSingleton(beanName),判断是否是单例模式,单例模式的话,从factoryBeanObjectCache中尝试读取,否则直接生成。

  注:我们可以看到,通过FactoryBean的对象是否是单例模式取决于bean定义的范围和方法isSingleton同时为单例才可以

复制代码
    protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
        if (factory.isSingleton() && containsSingleton(beanName)) {
            synchronized (getSingletonMutex()) {
                Object object = this.factoryBeanObjectCache.get(beanName);
                if (object == null) {
            //入口,调用getObject方法
                    object = doGetObjectFromFactoryBean(factory, beanName);
                    // Only post-process and store if not put there already during getObject() call above
                    // (e.g. because of circular reference processing triggered by custom getBean calls)
                    Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
                    if (alreadyThere != null) {
                        object = alreadyThere;
                    }
                    else {
                        if (object != null && shouldPostProcess) {
                            try {
                                object = postProcessObjectFromFactoryBean(object, beanName);
                            }
                            catch (Throwable ex) {
                                throw new BeanCreationException(beanName,
                                        "Post-processing of FactoryBean's singleton object failed", ex);
                            }
                        }
                        this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
                    }
                }
                return (object != NULL_OBJECT ? object : null);
            }
        }
        else {
            Object object = doGetObjectFromFactoryBean(factory, beanName);
            if (object != null && shouldPostProcess) {
                try {
                    object = postProcessObjectFromFactoryBean(object, beanName);
                }
                catch (Throwable ex) {
                    throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
                }
            }
            return object;
        }
    }
复制代码
  6、doGetObjectFromFactoryBean中调用getObject方法,返回实例

  7、调用bean后处理postProcessObjectFromFactoryBean并放入缓存factoryBeanObjectCache

  到此,我们已经获取到了真正的bean,并且也知道了怎么获取原来定义的FactoryBean的实例,但是,似乎少了一个方法,那就说FactoryBean中的getObjectType,我们需要回头去找哪里漏掉了

  我翻遍了源代码,并没有找到此方法调用的地方,事实上通过改动

@Override
    public Class<Car> getObjectType() {
        return Car.class;
    }
或者

@Override
    public Class<Person> getObjectType() {
        return null;
    }
都不会影响代码执行结果的正确性,那么我们猜想,是否仅仅是一个预留,并无实际用处,或者客户自定义使用方法。

三、FactoryBean实例

  在上一步查找源代码过程中,我们有了新的发现,spring自定义了抽象类AbstractFactoryBean和大量他的子类,包括我们常见的list,map,set,object等,我们也来研究下

1、首先是他的继承关系

public abstract class AbstractFactoryBean<T>
        implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
我们看到继承了几个熟悉的接口,包括FactoryBean和InitializingBean,还有几个BeanClassLoaderAware, BeanFactoryAware, DisposableBean,这些接口分别是:

FactoryBean:通过getObjectBean生成bean实例

InitializingBean:在实例化后执行afterPropertiesSet方法

以上我们比较熟悉,其余三个接口简单了解下

BeanClassLoaderAware:注入classLoad

BeanFactoryAware:注入一个BeanFactory

DisposableBean:销毁bean默认调用destroy方法

我们这里重点关注FactoryBean的三个接口实现:

2、是否单例,通过外部注入

public boolean isSingleton() {
        return this.singleton;
    }
3、getObject方法,如果是单例且已经创建,返回单例模式,未创建调用getEarlySingletonInstance方法,不是单例模式,调用createInstance方法

复制代码
    @Override
    public final T getObject() throws Exception {
        if (isSingleton()) {
            return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
        }
        else {
            return createInstance();
        }
    }
复制代码
4、getEarlySingletonInstance方法,通过第5步判断,是否为接口,如果是通过动态代理,创建对象

复制代码
    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;
    }
复制代码
5、此处调用getObjectType,判断是否为空或者是否为接口,如果是,返回,否则为空

protected Class<?>[] getEarlySingletonInterfaces() {
        Class<?> type = getObjectType();
        return (type != null && type.isInterface() ? new Class<?>[] {type} : null);
    }
6、动态代理的InvocationHandler 类,似乎什么都没干,存疑

复制代码
private class EarlySingletonInvocationHandler implements InvocationHandler {

@Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (ReflectionUtils.isEqualsMethod(method)) {
                // Only consider equal when proxies are identical.
                return (proxy == args[0]);
            }
            else if (ReflectionUtils.isHashCodeMethod(method)) {
                // Use hashCode of reference proxy.
                return System.identityHashCode(proxy);
            }
            else if (!initialized && ReflectionUtils.isToStringMethod(method)) {
                return "Early singleton proxy for interfaces " +
                        ObjectUtils.nullSafeToString(getEarlySingletonInterfaces());
            }
            try {
                return method.invoke(getSingletonInstance(), args);
            }
            catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
        }
    }
复制代码
分析到这里,似乎这个类什么都没做,我们具体看个实例类ListFactoryBean,此类注入了以下参数:

a、sourceList是一个list

b、targetListClass是一个Class

c、重写了createInstance(),将sourceList修改后注入

ListFactoryBean
7、配置文件

复制代码
        <bean id="list" class="org.springframework.beans.factory.config.ListFactoryBean">
            <property name="targetListClass">
                <value>java.util.ArrayList</value>
            </property>
            <property name="sourceList">
                <list>
                    <value>zhangsan</value>
                    <value>lisi</value>
                    <value>wangwu</value>
                </list>
            </property>
        </bean>
复制代码
8、测试代码

复制代码
    public static void main(String[] args) throws Exception {
        DefaultListableBeanFactory beanFacory=new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader=new XmlBeanDefinitionReader(beanFacory);
        reader.loadBeanDefinitions(new ClassPathResource("factorybean.xml"));
        
        @SuppressWarnings("unchecked")
        List<String> list=(List<String>)beanFacory.getBean("list");
        System.out.println(list.toString());

}
复制代码
四、factory-bean的源码解析

1、与之前解析类似,直到进入,如果有beancalss,直接返回beanClass,此处返回值为null

复制代码
protected Class<?> resolveBeanClass(final RootBeanDefinition mbd, String beanName, final Class<?>... typesToMatch)
            throws CannotLoadBeanClassException {
        try {
            if (mbd.hasBeanClass()) {
                return mbd.getBeanClass();
            }
            if (System.getSecurityManager() != null) {
                return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
                    @Override
                    public Class<?> run() throws Exception {
                        return doResolveBeanClass(mbd, typesToMatch);
                    }
                }, getAccessControlContext());
            }
            else {
                return doResolveBeanClass(mbd, typesToMatch);
            }
        }
        catch (PrivilegedActionException pae) {
            ClassNotFoundException ex = (ClassNotFoundException) pae.getException();
            throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex);
        }
        catch (ClassNotFoundException ex) {
            throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex);
        }
        catch (LinkageError err) {
            throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), err);
        }
    }
复制代码
2、开始创建bean实例,判断是否有beanClass,如果有FactoryMethodname,调用instantiateUsingFactoryMethod(beanName, mbd, args)

复制代码
    protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
        // Make sure bean class is actually resolved at this point.
        Class<?> beanClass = resolveBeanClass(mbd, beanName);

if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                    "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
        }

if (mbd.getFactoryMethodName() != null)  {
            return instantiateUsingFactoryMethod(beanName, mbd, args);
        }
复制代码
3、进入instantiateUsingFactoryMethod方法,判断factoryBeanName是否为空,如果不为空,判断是否为自身,自身则报错。不是自身,获取factoryBean和factoryClass,设定为非静态;如果factoryBeanName为空,且没有classname则报错,有的话,获得factoryClass,设定为静态

注:此处应该是由两种方式,一种通过其他bean来生成,一种是通过class的静态方法生成

复制代码
String factoryBeanName = mbd.getFactoryBeanName();
        if (factoryBeanName != null) {
            if (factoryBeanName.equals(beanName)) {
                throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                        "factory-bean reference points back to the same bean definition");
            }
            factoryBean = this.beanFactory.getBean(factoryBeanName);
            if (factoryBean == null) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                        "factory-bean '" + factoryBeanName + "' (or a BeanPostProcessor involved) returned null");
            }
            if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
                throw new IllegalStateException("About-to-be-created singleton instance implicitly appeared " +
                        "through the creation of the factory bean that its bean definition points to");
            }
            factoryClass = factoryBean.getClass();
            isStatic = false;
        }
        else {
            // It's a static factory method on the bean class.
            if (!mbd.hasBeanClass()) {
                throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                        "bean definition declares neither a bean class nor a factory-bean reference");
            }
            factoryBean = null;
            factoryClass = mbd.getBeanClass();
            isStatic = true;
        }
复制代码
4、从factoryClass中检查是否有FactoryBeanMethod,此处获得的是数组,说明可能可以根据重写的方法和参数生成不同的bean

复制代码
factoryClass = ClassUtils.getUserClass(factoryClass);

Method[] rawCandidates = getCandidateMethods(factoryClass, mbd);
            List<Method> candidateSet = new ArrayList<Method>();
            for (Method candidate : rawCandidates) {
                if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate)) {
                    candidateSet.add(candidate);
                }
            }
            Method[] candidates = candidateSet.toArray(new Method[candidateSet.size()]);
            AutowireUtils.sortFactoryMethods(candidates);
复制代码
5、进入instantiate,反射生成真正的bean

复制代码
public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner,
            Object factoryBean, final Method factoryMethod, Object... args) {

try {
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    @Override
                    public Object run() {
                        ReflectionUtils.makeAccessible(factoryMethod);
                        return null;
                    }
                });
            }
            else {
                ReflectionUtils.makeAccessible(factoryMethod);
            }

Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get();
            try {
                currentlyInvokedFactoryMethod.set(factoryMethod);
                return factoryMethod.invoke(factoryBean, args);
            }
            finally {
                if (priorInvokedFactoryMethod != null) {
                    currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod);
                }
                else {
                    currentlyInvokedFactoryMethod.remove();
                }
            }
        }
复制代码
到这里bean就真正的生成了

五、bean-factory验证

主要需要验证的有两点:

1、如过没有factory-bean,我们可以使用一个class的静态方法进行生成bean

2、可以使用多个重写的方法选择进行生成bean,参数可以从外部传递

我们增加一个CarFactory类,其中有一个静态方法createCar()

public class CarFactory {
    public static Car createCar(){
        return new Car();
    }
}
修改Car类,与一般的bean相同

复制代码
package com.zjl.factorybean;

public class Car {
    public Car() {
    }
    String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void run(){
        System.out.println(this.name+" is running");
    }
}
复制代码
编写配置文件

<bean id="car" factory-method="createCar" class="com.zjl.factorybean.CarFactory"><!-- 使用class的静态方法 -->
       <property name="name" value="奔驰"></property><!-- 属性注入 -->
   </bean>
测试类:

复制代码
public class Test {
    public static void main(String[] args) throws Exception {
        DefaultListableBeanFactory beanFacory=new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader=new XmlBeanDefinitionReader(beanFacory);
        reader.loadBeanDefinitions(new ClassPathResource("factorybean.xml"));

Car car=(Car)beanFacory.getBean("car");
        car.run();
        Car car2=(Car)beanFacory.getBean("car");
        System.out.println("car==car2 is "+(car==car2));
    }
}
复制代码
结果:

奔驰 is running
car==car2 is true
可以看到,结果跟我们在阅读源码时候的猜想完全一致

六、总结

1、FactoryBean与factory-bean的作用都是通过其他的一个bean工厂产生一个真实的bean,不同的是,FactoryBean是使用了spring默认的接口,具有一定侵入性,对框架造成依赖,factory-bean不会改变代码接口,属于注入方式。spring中很多类似的组队,比如init-method和InitializingBean。

2、从原则上,我们使用spring,很大的优点在于它没有侵略性。那么为什么会提供接口形式呢。接口形式更倾向于框架的使用,比如spirng的另一个重要的特性AOP,框架编写了AOPFactoryBean,我们不需要知道他内部实现,也不会获取他的实力,只需要配置它需要代理的类和接口,便可以成功返回一个真实的bean,也就是目标类的代理类,从而完成各种工作。

3、可以说spirng的很多扩展工作都是基于预留接口提供,同时新扩展的功能也会提供新的预留接口,比如aop的切面等。

[spring源码学习]六、IOC源码-BeanFactory和factory-bean相关推荐

  1. java中batch基础_详解Spring batch 入门学习教程(附源码)

    详解Spring batch 入门学习教程(附源码) 发布时间:2020-09-08 00:28:40 来源:脚本之家 阅读:99 作者:achuo Spring batch 是一个开源的批处理框架. ...

  2. 【Android 源码学习】SharedPreferences 源码学习

    第一章:SharedPreferences 源码学习 文章目录 第一章:SharedPreferences 源码学习 Android SharedPreferences的缺陷 MMKV.Jetpack ...

  3. RocketMQ源码学习(六)-Name Server

    问题列表: Name Server 的作用是什么? Name Server 存储了Broker的什么信息? Name Server 为Producer的提供些什么信息? Name Server 为Co ...

  4. Spring源码学习---------(最简单易懂的源码学习笔记)

  5. mybatis源码学习1--学习源码的目的

    在开始分析mybatis源码之前,需要定一个目标,也就是我们不是为了读源码而去读,一定是带着问题去读,在读的时候去寻找到答案,然后再读码的同时整理总结,学习一些高级的编码方式和技巧. 首先我们知道my ...

  6. Golang源码学习(二)----Go源码学习基础

    ### 本文源码版本为 GO 1.17.8 Windows/amd64: ### 可能参与对比的版本:GO 1.16.2 Linux/amd64一.Golang的编译器究竟是如何工作的? (学习源码有 ...

  7. Spark源码学习之IDEA源码阅读环境搭建

    软件准备 (1)Java 1.8 (2)Scala 2.11.12(需要在IDEA中安装) (3)Maven 3.8.2(需要在IDEA中配置) (4)Git 2.33 以上软件需要安装好,并进行环境 ...

  8. nimble源码学习——广播流程源码分析1

    广播流程源码分析1 在controller层有多种状态:广播.空闲.连接等等,这次分析的是广播这个状态或者叫 做角色.在前面controller层循环的分析中,可以明确controller层也有eve ...

  9. Spring IOC源码解析笔记

    小伙伴们,你们好,我是老寇 Spring最重要的概念就算IOC和AOP,本篇主要记录Spring IOC容器的相关知识,本文适合有spring相关基础并且想了解spring ioc的相关原理的人看 本 ...

最新文章

  1. css实现一个写信的格式
  2. 如何解决Asp.Net MVC和WebAPI的Controller名称不能相同的问题
  3. open wrt 跟换主题_Openwrt编译进阶-修改密码、路由连接数、时区及主题
  4. Java数据结构和算法:数组、单链表、双链表
  5. Spring Boot 微信点餐开源系统
  6. 这样也可以,insert,,
  7. ElasticSearch-7.10版本最新万字长文教程【距离搞懂ELK核心你只差这一片文章】
  8. apache默认网站
  9. JS过滤表单数据中的特殊字符
  10. spring boot 集成Mybatis时 Invalid bound statement (not found)
  11. mysql索引 红黑树_为什么MySql索引使用B+树?
  12. vscode 运行python代码没有输出(Code runner)
  13. 调试信息清除小工具的编写
  14. php简单登陆,PHP简单实现单点登录
  15. ectouch微信支付,带微信H5支付
  16. 营业执照统一社会信用代码Java正则表达式
  17. python 根据x的值和函数y=20+x2,计算y_new,算出y_new和y的差,记为delta_y。¶绘制x和delt_y的点图,并计算y的方差。有关方差的计算参阅数学资料。
  18. 有了它,将大大丰富VR内容,3D VR摄像机Vuze VR开启预定
  19. 海康威视SDK控制台程序consoleDemo获取视频通道参数
  20. 你连《Android高级UI与FrameWork源码》都搞不懂学什么Android?还敢面试阿里P7!

热门文章

  1. 云南省计算机一级考试题7,计算机(一级B类)云南省计算机一级考试题库.doc
  2. 1.13正版服务器,我的世界Minecraft梦幻世界服务器(1.7-1.13版本)
  3. 小米洪锋:跟7000万MIUI用户谈谈
  4. 暄桐教室的50本必读书 | 39《前朝梦忆》
  5. mac 设置优先连接的wifi
  6. mysql 计算自然周
  7. 【微信小程序】表单校验
  8. 关于vue弹窗自定义
  9. windy数(数位dp)
  10. 07-----给音视频文件添加字幕流