Spring源码系列(五)——FactoryBean的使用与源码分析

一、FactoryBean介绍

1. FactoryBean使用

FactoryBean被称为工厂Bean。在Spring中是个接口,用它可以生成某一个类型Bean的实例,其最大的一个作用是:可以让我们自定义Bean的创建过程

在使用Spring时,我们通过在xml中配置<bean>标签或者在类上添加注解@Component来实例化Bean,这种方式下Spring是通过反射机制利用class属性来实例化Bean。我们就把这种使用方式称为传统方式吧。

但在某些情况下,如果实例化Bean过程比较复杂,再按照上面传统的方式去创建Bean,则需要在<bean>标签中或者注解@Component上提供大量的配置信息。此时传统配置的Bean方式就很繁琐,也有局限性。那除了上面的传统使用Bean的方式外,还有其他办法吗?

当然有了,我们还可以使用编码的方式创建Bean

Spring为此提供了一个org.springframework.bean.factory.FactoryBean接口,程序员可以通过实现该接口采用编码的方式自定义实例化Bean的逻辑。

FactoryBean接口在Spring框架具有极其重要的地位,Spring自身就提供了70多个FactoryBean的实现,这些FactoryBean隐藏了实例化一些复杂Bean的细节,给上层应用带来了极大便利,下面看FactoryBean源码:

// FactoryBean接口
public interface FactoryBean<T> {

    //返回对象实例
	T getObject() throws Exception;

    //创建bean的Class类型
	Class<?> getObjectType();

    //true是单例,false是非单例,可以看出默认为单例的
	default boolean isSingleton() {
		return true;
	}
}

FactoryBean是一个泛型类且只有三个方法,如果你想要的类是一个单例,那么只需要实现两个方法就可以了,getObject()方法返回一个对象的实例,getObjectType()方法返回对象的Class类型。

看下面一个例子:

// 配置类
@Configuration
@ComponentScan("com.scorpios")
public class AppConfig {
}

// FactoryBean工厂
@Component
public class MasterFactoryBean implements FactoryBean {
    
    @Override
    public Object getObject() throws Exception {
        // 创建一个类
        return new Master();
    }

    @Override
    public Class<?> getObjectType() {
        // 返回类的类型
        return Master.class;
    }

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

// 测试方法
public static void main( String[] args )
{
    AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
    annotationConfigApplicationContext.register(AppConfig.class);
    annotationConfigApplicationContext.refresh();
    Master master = (Master) annotationConfigApplicationContext.getBean("masterFactoryBean");
    
    MasterFactoryBean masterFactoryBean = (MasterFactoryBean) annotationConfigApplicationContext.getBean("&masterFactoryBean");
    System.out.println("-----------------------------");
    System.out.println(master);
    System.out.println(masterFactoryBean);
}

打印结果:

在这里插入图片描述
MasterFactoryBean类用@Component注解修饰,如果没有给组件添加别名,那么默认是类名第一个字母小写,即masterFactoryBean。当我们去容器中取masterFactoryBean的时候,就拿到Master类型。

在获取FactoryBean的时候,其实返回的是方法getObject()的值,如果想要获取MasterFactoryBean类本身,需要加上在前面加上&符号才能获取MasterFactoryBean工厂Bean本身。

对于FactoryBean是否有了基本的认识,但是不是会觉得这样做毫无意思啊,为什么要使用FactoryBean包裹一层呢?这样很麻烦啊

2. FactoryBean作用

为什么需要使用FactoryBean呢?

  • 考虑下,如果需要实例化一个复杂Bean,按照传统方式,我们需要在<bean>中提供大量的配置信息,但这些信息还是我们不需要去关注的,我们只想要创建一个Bean,并不想知道这个Bean是经过多么复杂的流程创建出来的,这时就可以使用FactoryBean来把一些配置信息、依赖项进行封装在内部,很简单的来实例化一个Bean

  • 和第三方框架的整合,首先在第三方框架内部是不会出现像@Component这样的Spring注解,为什么呢?因为如果使用了@Component注解,是不是就要依赖Spring。第三方框架与Spring框架整合,就是需要将第三方框架中的类转化为BeanDefinition放到Spring容器中去,以注解或者XML形式都行。往往第三方类都有一个非常重要的类,我们只需要将这个类注入到Spring就可以了,不过这个类想要正常工作可能需要其他依赖,所以我们在Spring中想注入这个类,就需要先知道它依赖的其他类,可是其他类还会有依赖,这样做就太麻烦了,所以第三方框架可以写一个FactoryBean类,提供一个简单的入口和Spring框架进行整合,我们只需要将这个FactoryBean类以注解或XML的格式放到容器就可以了。(Mybatis集成入Spring就是这样)

3. MyBatis中FactoryBean使用

我们在整合MyBatisSpring时需要在XML中配置SqlSessionFactory

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
</bean>

SpringBoot项目中整合MyBatis,我们不需要进行过多的额外配置,只需要引入MyBatis包就可以直接使用,我们并没有配置SqlSessionFactory,但是我们没配并不代表没有,而是SpringBoot帮我们写了,在MybatisAutoConfiguration自动配置类中有如下一个@Bean注解:

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  
    // 此处就是用FactoryBean
    // 使用SqlSessionFactoryBean来创建的SqlSessionFactory
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
        factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
	// 从this.properties中可以拿到在配置文件中 Mybatis 配置的信息
    Configuration configuration = this.properties.getConfiguration();
    if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
        configuration = new org.apache.ibatis.session.Configuration();
    }

	// ……
    return factory.getObject();
}

说明:上面方法中,从this.properties属性中可以拿到在application.yaml或者是application.properties中Mybatis 配置的信息,配置如下

mybatis:
  mapper-locations: classpath:/mapper/**/*.xml

在配置文件中,我们可以配置关于MyBatis的相关信息,但是不用每一个依赖项都配置,只需要配置你感兴趣的,然后关于其他的配置都会在factory.getObject()中由MyBatis自行去配置。

来看一下SqlSessionFactoryBean源码

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
  
    // 要为各种属性赋值,省略set方法
    private Resource configLocation;
    private Configuration configuration;
    private Resource[] mapperLocations;
    private DataSource dataSource;
    private TransactionFactory transactionFactory;
    private Properties configurationProperties;
    private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    private SqlSessionFactory sqlSessionFactory;
    private String environment = SqlSessionFactoryBean.class.getSimpleName();
    private boolean failFast;
    private Interceptor[] plugins;
    private TypeHandler<?>[] typeHandlers;
    private String typeHandlersPackage;
    private Class<?>[] typeAliases;
    private String typeAliasesPackage;
    private Class<?> typeAliasesSuperType;
    private DatabaseIdProvider databaseIdProvider;
    private Class<? extends VFS> vfs;
    private Cache cache;
    private ObjectFactory objectFactory;
    private ObjectWrapperFactory objectWrapperFactory;

    // 省略很多其他代码
    public void afterPropertiesSet() throws Exception {
        this.sqlSessionFactory = this.buildSqlSessionFactory();
    }

    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        XMLConfigBuilder xmlConfigBuilder = null;
        Configuration configuration;
        if (this.configuration != null) {
            configuration = this.configuration;
            if (configuration.getVariables() == null) {
                configuration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {
                configuration.getVariables().putAll(this.configurationProperties);
            }
        } else if (this.configLocation != null) {
            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            configuration = new Configuration();
            if (this.configurationProperties != null) {
                configuration.setVariables(this.configurationProperties);
            }
        }
    } 
    
    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
            this.afterPropertiesSet();
        }
		// 返回的是这个SqlSessionFactory对象
        return this.sqlSessionFactory;
    }

    public Class<? extends SqlSessionFactory> getObjectType() {
        return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
    }

    public boolean isSingleton() {
        return true;
    }
}

这个SqlSessionFactoryBean配置是不是太麻烦了,而现在呢,我们只需要配置我们感兴趣的,其他的交给MyBatis自己写的FactoryBean就可以。

二、FactoryBean源码分析

1. 知识点说明

在分析源码之前先说几个重要的点:

在之前的源码分析中,一直在强调的几个集合含义

beanDefinitionMap:存放Spring容器中的所有的BeanDefinition

beanDefinitionNames:存放Spring容器中的所有的beanName

singletonObjects:存放Spring容器中已经完成实例化的对象,这个集合里面存在的变量表示已经完成了实例化

singletonFactories:存放 bean工厂对象解决循环依赖

earlySingletonObjects:存放原始的bean对象用于解决循环依赖,注意:存到里面的对象还没有被填充属性

FactoryBean内部包裹的类是存在FactoryBeanRegistrySupport类中的factoryBeanObjectCache属性中,该属性是个MapFactoryBeanRegistrySupportDefaultSingletonBeanRegistry的子类。

在这里插入图片描述

从上面的断点图可以看到,在没有执行Master master = (Master) ac.getBean("masterFactoryBean")之前,factoryBeanObjectCache中是没有masterFactoryBean数据的。

下面看下这行代码执行之后的断点图

在这里插入图片描述

当放过上面第17行断点时,发现factoryBeanObjectCache有了一条数据,masterFactoryBean对应的class类型是Master

通过上面断点调试得知,当我们向Spring中注入FactoryBean类型的Bean,相当于注入了两个Bean,一个在singletonObjects中,另一个在factoryBeanObjectCache中,但factoryBeanObjectCache中也受Spring的管理,并且被FactoryBean包裹的Bean具有天然的懒加载功能。

singletonObjects中的masterFactoryBean类型为MasterFactoryBeanBean,和普通的Bean注入容器的方式一样,也可以从上图得知,在Spring进行扫描并注入到Spring容器的时候不会去考虑是不是FactoryBean类型的Bean

2. FactoryBean源码分析

// 测试方法
public static void main( String[] args ){
    
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    ac.register(AppConfig.class);
    ac.refresh();
    Master master = (Master) ac.getBean("masterFactoryBean");
    
    MasterFactoryBean masterFactoryBean = (MasterFactoryBean) ac.getBean("&masterFactoryBean");
    System.out.println("-----------------------------");
    System.out.println(master);
    System.out.println(masterFactoryBean);
}

下面来分析一下从Spring容器中拿FactoryBean时,为什么返回的是FactoryBean所包裹的对象,用上面测试方法作为源码分析的入口。

入口为ac.getBean("masterFactoryBean")方法,然后会调用到AbstractApplicationContext#getBean()方法:

@Override
public Object getBean(String name) throws BeansException {
    // 判断容器是否处于激活状态
	assertBeanFactoryActive();
    // 首先获得容器,然后调用容器的getBean方法
	return getBeanFactory().getBean(name);
}

断点跟踪会进入到AbstractBeanFactory类中的getBean()方法

protected <T> T doGetBean(
    String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly){

   /**
	* 下面这个方法是通过 name 去获取 beanName,这里为什么不使用 name 直接作为 beanName呢?
	* 有两个原因:
	* 1、name 的值可能会以 & 字符开头,表明调用者想获取 FactoryBean 本身,而非 FactoryBean 实现类所创建的 bean。
	* 在 BeanFactory 中,FactoryBean 的实现类和其他的 bean 存储方式是一致的,即 <beanName, bean>,而 beanName 中是没有 & 这个字符的。
	* 所以需要将 name 的首字符 & 移除,这样才能从缓存里取到 FactoryBean 实例。(此知识点涉及接口FactoryBean的使用,可自行查阅)
	* 2、还是别名的问题,转换需要 &beanName
	*/
    String beanName = transformedBeanName(name);

    Object bean;

   /**
	* 下面这个getSingleton()方法在Bean初始化的时候会调用,在getBean()的时候也会调用,
	* 为什么需要这么做呢?
	* 也就是说Spring容器在Bean初始化的时候先获取这个Bean对象,判断这个对象是否被实例化好了,
	* 即是不是已经被创建了。普通情况下绝对为空,但有一种情况可能不为空
	* 从Spring容器中获取一个Bean,由于Spring中Bean容器是用一个map(singletonObjects)来存储的
	* 所以你可以理解getSingleton(beanName)等于beanMap.get(beanName)
	* 由于getBean()方法会在Spring环境初始化的时候(就是对象被创建的时候调用一次)调用一次
	* 还会在Bean初始化的时候再调用一次
	*/
    Object sharedInstance = getSingleton(beanName);

    // 此处拿到sharedInstance,不为空,表示已经实例化了,但属性有没有填充不确定
    if (sharedInstance != null && args == null) {

       /**
		* 如果 sharedInstance 是普通的单例 bean,下面的方法会直接返回。但如果
		* sharedInstance 是 FactoryBean 类型的,则需调用 getObject 工厂方法获取真正的
		* bean 实例。如果用户想获取 FactoryBean 本身,这里也不会做特别的处理,直接返回
		* 即可。毕竟 FactoryBean 的实现类本身也是一种 bean,只不过具有一点特殊的功能而已。
		*/
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // sharedInstance为null,才会进入到这里,表示从singletonObjects中拿不到
    	// 省略从单例池中获取不到时,开始创建bean的过程
    } 
    return (T) bean;
}

下面看下这行代码getSingleton(beanName),看下是如何拿从缓存中拿取Bean的,会执行到DefaultSingletonBeanRegistry类中的getSingleton()方法

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从singletonObjects中获取bean,如果不为空直接返回,不再进行初始化工作
    Object singletonObject = this.singletonObjects.get(beanName);
    // isSingletonCurrentlyInCreation()方法判断当前beanName是不是正在创建中
    // 如果为null并且正在创建中,则会进入
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 去earlySingletonObjects中拿,注意这行集合里面存放的对象还没有被填充属性
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 如果也没拿到,说明还没有创建完成
            if (singletonObject == null && allowEarlyReference) {
                // 去存放Bean工厂的集合里拿,看看是不是Bean工厂
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

在获取到sharedInstance不为空的时候,会进入下面这行代码bean = getObjectForBeanInstance(sharedInstance, name, beanName, null)进行进一步判断

protected Object getObjectForBeanInstance(
        Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {
 
    // name不为空并且name是以"&"开始,进if
	if (BeanFactoryUtils.isFactoryDereference(name)) {
        
		if (beanInstance instanceof NullBean) {
			return beanInstance;
		}
        // beanInstance不是FactoryBean,则抛异常
		if (!(beanInstance instanceof FactoryBean)) {
			throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
		}
	}
 
    // 如果beanInstance不是FactoryBean(也就是普通bean),则直接返回beanInstance
    // 如果beanInstance是FactoryBean,并且name以“&”为前缀,则直接返回beanInstance(以“&”为前缀代表想获取的是FactoryBean本身)
    if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
        return beanInstance;
    }
 
    // 走到这边,代表beanInstance是FactoryBean,但name不带有“&”前缀,表示想要获取的是FactoryBean创建的对象实例
    Object object = null;
    if (mbd == null) {
        // 如果mbd为空,则尝试从factoryBeanObjectCache缓存中获取该FactoryBean创建的对象实例
        object = getCachedObjectForFactoryBean(beanName);
    }
 
    if (object == null) {
        // 只有beanInstance是FactoryBean才能走到这边,因此直接强转
        FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
        if (mbd == null && containsBeanDefinition(beanName)) {
            // mbd为空,但是该bean的BeanDefinition在缓存中存在,则获取该bean的MergedBeanDefinition
            mbd = getMergedLocalBeanDefinition(beanName);
        }
        // mbd是否是合成的(这个字段比较复杂,mbd正常情况都不是合成的,也就是false
        boolean synthetic = (mbd != null && mbd.isSynthetic());
        // 从FactoryBean获取对象实例,这句是关键代码
        object = getObjectFromFactoryBean(factory, beanName, !synthetic);
    }
    // 返回对象实例
    return object;
}
 
@Nullable
protected Object getCachedObjectForFactoryBean(String beanName) {
    // 可以看到就是从factoryBeanObjectCache里面取已经缓存的,这样第二次再去获取的时候不用再次创建
	return this.factoryBeanObjectCache.get(beanName);
}

下面看一下FactoryBeanRegistrySupport类中的getObjectFromFactoryBean()方法:

protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
	// 1.如果是单例,并且已经存在于单例对象缓存中,什么意思?
	// 就是说FactoryBean你可以在类上标注为多实例的bean,那么在多次获取FactoryBean的时候
	// 实际就是多次调用FactoryBean的getObject方法,如果是单实例的,则进行缓存起来,只创建一次
	if (factory.isSingleton() && containsSingleton(beanName)) {
		synchronized (getSingletonMutex()) {
			// 3.从FactoryBean创建的单例对象的缓存中获取该bean实例
			//关于这个factoryBeanObjectCache上面我们已经介绍过了
			Object object = this.factoryBeanObjectCache.get(beanName);
			if (object == null) {
				// 4.调用FactoryBean的getObject方法获取对象实例
				object = doGetObjectFromFactoryBean(factory, beanName);
                
				Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
				// 5.如果该beanName已经在缓存中存在,则将object替换成缓存中的
				if (alreadyThere != null) {
					object = alreadyThere;
				} else {
					//6.判断需要做后置处理,shouldPostProcess传过来的是!synthetic,即为true,不是合成的
					if (shouldPostProcess) {
						if (isSingletonCurrentlyInCreation(beanName)) {
							return object;
						}
						beforeSingletonCreation(beanName);
						try {
							// 7.对bean实例进行后置处理,执行所有已注册的BeanPostProcessor的postProcessAfterInitialization方法
							//此方法会进行AOP代理增强,所以我们在写AOP增强的时候,不只是会增强FactoryBean本身,还会增强FactoryBean所包裹的对象
							object = postProcessObjectFromFactoryBean(object, beanName);
						} catch (Throwable ex) {
							// 抛异常略
						} finally {
							afterSingletonCreation(beanName);
						}
					}
					if (containsSingleton(beanName)) {
						// 8.将beanName和object放到factoryBeanObjectCache缓存中
						this.factoryBeanObjectCache.put(beanName, object);
					}
				}
			}
			// 9.返回object对象实例
			return object;
		}
	} else {
		// 10.调用FactoryBean的getObject方法获取对象实例
		//可以看出来,如果是多实例的,那么每次是直接创建的
		Object object = doGetObjectFromFactoryBean(factory, beanName);
		if (shouldPostProcess) {
			try {
				// 11.对bean实例进行后置处理,执行所有已注册的BeanPostProcessor的postProcessAfterInitialization方法
				object = postProcessObjectFromFactoryBean(object, beanName);
			}
			catch (Throwable ex) {
				// 抛异常略
			}
		}
		// 12.返回object对象实例
		return object;
	}
}

上述代码中的4会调用FactoryBeangetObject()方法获取对象实例

上述代码中的7会对Bean实例进行后置处理

下面看下4中的doGetObjectFromFactoryBean()方法,位于FactoryBeanRegistrySupport类中,代码如下:

private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName) {
 
	Object object;
	try {
		// 1.调用FactoryBean的getObject方法获取bean对象实例
		if (System.getSecurityManager() != null) {
			AccessControlContext acc = getAccessControlContext();
			try {
				// 1.1 带有权限验证的
				object = AccessController.doPrivileged((PrivilegedExceptionAction<Object>) factory::getObject, acc);
			}
			catch (PrivilegedActionException pae) {
				throw pae.getException();
			}
		}
		else {
			// 1.2 不带权限,factory.getObject就会最终执行我们实现了FactoryBean的方法
			object = factory.getObject();
		}
	}
	catch (FactoryBeanNotInitializedException ex) {
		// 抛异常略
	}
 
	// 2.getObject返回的是空值,并且该FactoryBean正在初始化中,则直接抛异常,不接受一个尚未完全初始化的FactoryBean的getObject返回的空值
	if (object == null) {
		if (isSingletonCurrentlyInCreation(beanName)) {
			// 抛异常略
		}
		object = new NullBean();
	}
	// 3.返回创建好的bean对象实例
	return object;
}

上面的方法很简单,就是直接调用 FactoryBeangetObject()方法来获取到对象实例。

上面代码中的7调用 FactoryBeanRegistrySupport类中的postProcessObjectFromFactoryBean()方法,此步骤主要是对Bean进行后置后置增强的。

@Override
protected Object postProcessObjectFromFactoryBean(Object object, String beanName) {
    return applyBeanPostProcessorsAfterInitialization(object, beanName);
}
 
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
        throws BeansException {
 
    Object result = existingBean;
    // 1.遍历所有注册的BeanPostProcessor实现类,调用postProcessAfterInitialization方法
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
        // 2.在bean初始化后,调用postProcessAfterInitialization方法
        result = beanProcessor.postProcessAfterInitialization(result, beanName);
        if (result == null) {
            // 3.如果返回null,则不会调用后续的BeanPostProcessors
            return result;
        }
    }
    return result;
}

3. 小结

在取FactoryBean包裹的对象时,也就是不加&符号的流程如下:

首先会对beanName名字进行转换,获取真正的beanName,然后根据beanNamesingletonObjects中取出Bean实例,如果拿出来为null,则要进行创建步骤,如果不为空,接着判断这个Bean,如果不是FactoryBean类型的,或者名称中是以&开头,则直接返回取出的bean实例

为什么会直接返回呢?因为从singletonObjects中拿出来的Bean实例就是FactoryBean

如果是FactoryBean类型的,名称中不是以&开头,再判断是单实例还是多实例的,如果是多实例的,则直接调用FactoryBeangetObject()方法,如果单实例的,则先从factoryBeanObjectCache缓存中去拿,没有的话则进行创建

创建完成后,会判断这个FactoryBeanBeanDefinition是不是合成,如果不是,则执行BeanPostProcessor后置处理器的postProcessAfterInitialization进行增强,如果需要增强的话

FactoryBean包裹的对象是天然懒加载的,如果是单例的,则会在第一个调用getBean()的时候创建,然后放到缓存中

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

止步前行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值