Hibernate 源码分析

1、Hibernate之生成SessionFactory源码追踪

Hibernate的所有session都是由sessionFactory来生成的,那么,sessionFactory是怎么得来的呢?它与我们配置的xxx.cfg.xml文件以及xxx.hbm.xml文件之间又有着怎么样的联系呢?

先看一小段生成sessionFactory的代码:

code_1:

public class HibernateTest {
    @Test
    public void test() {        
        System.out.println("test...");        
        
        //1. 创建一个 SessionFactory 对象
        SessionFactory sessionFactory = null;       
        
        //1). 创建 Configuration 对象: 对应 hibernate 的基本配置信息和 对象关系映射信息
        Configuration configuration = new Configuration().configure();
        
        //4.0 之前这样创建
        //sessionFactory = configuration.buildSessionFactory();
        
        //2). 创建一个 ServiceRegistry 对象: hibernate 4.x 新添加的对象
        //hibernate 的任何配置和服务都需要在该对象中注册后才能有效.
        ServiceRegistry serviceRegistry = 
            new ServiceRegistryBuilder().applySettings(configuration.getProperties())
            .buildServiceRegistry();
        
        //3). 利用serviceRegistry来创建sessionFactory实例
        sessionFactory = configuration.buildSessionFactory(serviceRegistry);        
        
        //2. 创建一个 Session 对象
        Session session = sessionFactory.openSession();
        
        //3. 开启事务
        Transaction transaction = session.beginTransaction();
        
        //4. 执行保存操作
        News news = new News("Java12345", "ATGUIGU", new Date(new java.util.Date().getTime()));
        session.save(news);
        
        //5. 提交事务 
        transaction.commit();
        
        //6. 关闭 Session
        session.close();
        
        //7. 关闭 SessionFactory 对象
        sessionFactory.close();
    }   
}

从上面的代码很清晰的可以看见,这一切的源头都在 Configuration configuration = new Configuration().configure() 这条语句上:创建ServiceRegistry 需要用到configuration,生成sessionFactory同样需要用到configuration。

Configuration的生成过程

从源代码中可以看到,Configuration的configure()方法共有5中重载方式:

code_2:

public Configuration configure();        //无参
public Configuration configure(String resource)
public Configuration configure(URL url)
public Configuration configure(File configFile)
public Configuration configure(org.w3c.dom.Document document)

现在从无参的configure()方法开始分析,它代表了一种默认的行为,默认读取类路径下的hibernate.cfg.xml文件作为hibernate的配置文件:

code_3:

public Configuration configure() throws HibernateException {
    configure( "/hibernate.cfg.xml" );    //默认读取classpath路径下的hibernate.cfg.xml文件
    return this;
}

继续追踪configure( “/hibernate.cfg.xml” )方法:

code_4:

1 public Configuration configure(String resource) throws HibernateException {
2     InputStream stream = getConfigurationInputStream( resource );    //通过传入的资源路径获取一个输入流
3     return doConfigure( stream, resource );    //这个方法会完成解析的第一步:将输入流转换成Document对象
4 }

继续追踪 return doConfigure( stream, resource ) 语句,可以发现底层会通过SAX解析工具将输入流转换成Document对象。然后调用然后调用doConfigure(Document doc)来继续解析这个文档:

code_5:

 1 protected Configuration doConfigure(InputStream stream, String resourceName) throws HibernateException {
 2         
 3     ErrorLogger errorLogger = new ErrorLogger( resourceName );  //将输入流转换成Document对象
 5     Document document = xmlHelper.createSAXReader( errorLogger,  entityResolver )
 6                 .read( new InputSource( stream ) );
 7         //具体解析document树,并将结果以键值对的形式存放到properties中
 8     doConfigure( document );
 9         
10     return this;
11 }

doConfigure(Document doc)是实际解析文档的方法,前面configure()的5种重载方法最后都要调用这个方法来完成实际的解析。我们看看它的解析思路:

code_6:

 1 protected Configuration doConfigure(Document doc) throws HibernateException {
 2     Element sfNode = doc.getRootElement().element( "session-factory" );
 3     String name = sfNode.attributeValue( "name" );
 4     if ( name != null ) {    //session-factory根节点是可以有name属性值的
 5         properties.setProperty( Environment.SESSION_FACTORY_NAME, name );
 6     }
 7     //遍历文档中所有的property节点,读取器name属性值以及节点的文本,
 8     //以name-value的形式放入到properties中
 9     addProperties( sfNode );    
10     //解析除了property之外的节点:mapping、class-cache、collection-cache
11     parseSessionFactory( sfNode, name );
12 
13     Element secNode = doc.getRootElement().element( "security" );
14     if ( secNode != null ) {
15         parseSecurity( secNode );
16     }
17 
18     LOG.configuredSessionFactory( name );
19     LOG.debugf( "Properties: %s", properties );
20 
21     return this;
22 }

doConfigure(Document doc)方法中对Document的解析主要分为两个步骤进行:

①解析xxx.cfg.xml配置文档中所有的property节点;

②解析xxx.cfg.xml配置文档中除了property节点之外的其它5种节点。

先来看看第

①步,它在addProperties(…)方法中完成(code_6代码段的第9行)思路很清晰,用一个迭代器来遍历文档中所有的property节点,并将name-value存放到Configuration的properties属性中:

code_7:

 1 private void addProperties(Element parent) {
 2     //指定,只会遍历property节点
 3     Iterator itr = parent.elementIterator( "property" );
 4     while ( itr.hasNext() ) {//循环遍历
 5         Element node = (Element) itr.next();
 6         //读取节点的name属性值
 7         String name = node.attributeValue( "name" );
 8         //读取节点的文本值
 9         String value = node.getText().trim();
10         LOG.debugf( "%s=%s", name, value );
11         //将name-value值存放如properties中
12         properties.setProperty( name, value );
13         //待研究...
14         if ( !name.startsWith( "hibernate" ) ) {
15             properties.setProperty( "hibernate." + name, value );
16         }
17     }
18         Environment.verifyProperties( properties );
19 }

再来看看第②步,它在parseSessionFactory( …)方法中进行(code_6代码段的第10行),它主要解析3类标签:mapping、class-cache、collection-cache:

code_8:

 1 private void parseSessionFactory(Element sfNode, String name) {
 2     Iterator elements = sfNode.elementIterator();
 3     while ( elements.hasNext() ) {
 4         Element subelement = (Element) elements.next();
 5         String subelementName = subelement.getName();
 6         //解析mapping节点,mapping可以指定hibernate的映射文件位置
 7         if ( "mapping".equals( subelementName ) ) {
 8             //具体解析mapping节点
 9             parseMappingElement( subelement, name );
10         }
11         //下面两个是和hibernate的二级缓存相关的配置,不做深入探讨
12         else if ( "class-cache".equals( subelementName ) ) {
13             String className = subelement.attributeValue( "class" );
14             Attribute regionNode = subelement.attribute( "region" );
15             final String region = ( regionNode == null ) ? className : regionNode.getValue();
16             boolean includeLazy = !"non-lazy".equals( subelement.attributeValue( "include" ) );
17             setCacheConcurrencyStrategy( className, subelement.attributeValue( "usage" ), region, includeLazy );
18         }
19         else if ( "collection-cache".equals( subelementName ) ) {
20             String role = subelement.attributeValue( "collection" );
21             Attribute regionNode = subelement.attribute( "region" );
22             final String region = ( regionNode == null ) ? role : regionNode.getValue();
23             setCollectionCacheConcurrencyStrategy( role, subelement.attributeValue( "usage" ), region );
24         }
25     }
26 }

后面两个标签class-cache和collection-cache是和hibernate的二级缓存相关,不作深入探讨。主要看看解析mapping的方法:

code_9:

 1 private void parseMappingElement(Element mappingElement, String name) {
 2     // 从源代码可以看出,mapping节点支持的属性值有5个
 3     final Attribute resourceAttribute = mappingElement.attribute( "resource" );
 4     final Attribute fileAttribute = mappingElement.attribute( "file" );
 5     final Attribute jarAttribute = mappingElement.attribute( "jar" );
 6     final Attribute packageAttribute = mappingElement.attribute( "package" );
 7     final Attribute classAttribute = mappingElement.attribute( "class" );
 8 
 9     if ( resourceAttribute != null ) {
10         final String resourceName = resourceAttribute.getValue();
11         LOG.debugf( "Session-factory config [%s] named resource [%s] for mapping", name, resourceName );
12         //将hibernate的映射文件作进一步的解析
13         addResource( resourceName );
14     }
15     else if ( fileAttribute != null ) {
16         final String fileName = fileAttribute.getValue();
17         LOG.debugf( "Session-factory config [%s] named file [%s] for mapping", name, fileName );
18         addFile( fileName );
19     }
20     else if ( jarAttribute != null ) {
21         final String jarFileName = jarAttribute.getValue();
22         LOG.debugf( "Session-factory config [%s] named jar file [%s] for mapping", name, jarFileName );
23         addJar( new File( jarFileName ) );
24     }
25     else if ( packageAttribute != null ) {
26         final String packageName = packageAttribute.getValue();
27         LOG.debugf( "Session-factory config [%s] named package [%s] for mapping", name, packageName );
28         addPackage( packageName );
29     }
30     else if ( classAttribute != null ) {
31         final String className = classAttribute.getValue();
32         LOG.debugf( "Session-factory config [%s] named class [%s] for mapping", name, className );
33         try {
34             addAnnotatedClass( ReflectHelper.classForName( className ) );
35         }
36         catch ( Exception e ) {
37             throw new MappingException(
38                     "Unable to load class [ " + className 
    					+ "] declared in Hibernate configuration <mapping/> entry",e);
41         }
42     }
43     else {
44         throw new MappingException( "<mapping> element in configuration specifies no known attributes" );
45     }
46 }

addResource( resourceName )是如何解析的呢?那么它是如何工作的呢?这里不再一步一步追踪源代码,因为嵌套太深,直接给出一个感性的认识即可:
在Configuration中定义了一个名为MetadataSourceQueue的内部内,同时Configuration中还有一个该队列的属性值:metadataSourceQueue。
addResource( resourceName )方法嵌套到最后会调用metadataSourceQueue.add(…)方法来将映射的元数据存储到metadataSourceQueue队列中。要使用的时候,从该队列中取就可以了。
metadataSourceQueue的底层存储是一个Map类型…

到现在为止,Configuration对象就得到了,总结一下,其重要的几个点:
1、configure()方法默认读取/hibernate.cfg.xml作为hibernate的配置文件。当然,configure()方法还有其它重载形式可用。
2、doConfigure(Document document)方法会调用两个重要的方法:addProperties( sfNode )和parseSessionFactory( sfNode, name );
3、addProperties( sfNode )方法会解析配置文件中的property节点,并将解析到的name-value放入到properties中
4、parseSessionFactory( sfNode, name )方法会解析配置文件中除了property节点外的其它3个类型的节点(4.2版本):mapping、class-cache和collection-cache
5、mapping配置是和映射相关的,class-cache和collection-cache是与二级缓存相关的。
6、mapping解析的结果会存放到metadataSourceQueue对象中。
7、所以,整个过程得到Configuration中两个重要的属性值:properties和metadataSourceQueue

img

2、Spring的LocalSessionFactoryBean创建过程源码分析

spring的LocalSessionFactoryBean生成过程与hibernate的SessionFactory生成过程是高度吻合的

为了后面源码分析,首先讲解一个接口,一个类的功能:
①、接口InitializingBean
接口的功能:这个接口专门为bean设计的,它只有一个方法。我们知道所有的bean都是由beanFactory来生成的,如果一个bean实现了该接口,在beanFactory为该bean装配好了所有的属性以后,在返回实际bean之前还会调用一次该接口的afterPropertiesSet(…)方法。其设计目的是为了实现个性化,或者是为了检查bean属性值的完整性等。

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

②、类LocalSessionFactoryBuilder

显然,LocalSessionFactoryBuilder继承自org.hibernate.cfg.Configuration,那么Configuration拥有的属性,LocalSessionFactoryBuilder也具有

public class LocalSessionFactoryBuilder extends Configuration{
    //...
}

从LocalSessionFactoryBean源码中分析出其于hibernate的sessionFactory和configuration之间的关系:

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

//②、类LocalSessionFactoryBuilder
//显然,LocalSessionFactoryBuilder继承自org.hibernate.cfg.Configuration,那么Configuration拥有的属性,LocalSessionFactoryBuilder也具有
public class LocalSessionFactoryBuilder extends Configuration{
    //...
}

//现在主要分析LocalSessionFactoryBean
//1、看一下几个非常重要的属性值定义
public class LocalSessionFactoryBean extends... implements InitializingBean, ...{
    //数据源
    private DataSource dataSource;
    //hibernate的配置文件Xxx.cfg.xml所在的
    //位置多个可以用","隔开
    private Resource[] configLocations;
    private String[] mappingResources;
    //hibernate的映射文件位置
    private Resource[] mappingLocations;
    //hibernate的properties属性,存放了配置
    //文件中解析property标签的结果
    private Properties hibernateProperties;
    //hibernate的configuration属性
    private Configuration configuration;
    //这个sessionfactory是hibernate的sessionFactory
    private SessionFactory sessionFactory;
    
    //省略其它的属性以及setter方法...
    
    //注意这个set方法,说明当只有一个配置文件
    //的时候可以使用configLocation属性来配置,
    //最终也会被转换成configLocations
    public void setConfigLocation(Resource configLocation) {
        this.configLocations = new Resource[] {configLocation};
    }
    
    @Override
    public void afterPropertiesSet() throws IOException {
        LocalSessionFactoryBuilder sfb = new LocalSessionFactoryBuilder(this.dataSource, this.resourcePatternResolver);

        if (this.configLocations != null) {
            for (Resource resource : this.configLocations) {
                //sfb.configure(...)实际上也就是调用其父类
                //org.hibernate.cfg.Configuration的configure(...)方法
                //这里完成xxx.cfg.xml文件中property节点
                //的解析,得到一个properties
                sfb.configure(resource.getURL());
            }
        }

        //mappingResources和mappingLocations效果是一样的,
        //都会将资源转换成输入流,并调用sfb.addInputStream(...)方法
        //sfb.addInputStream(...)最终会完成xxx.cfg.xml文件的
        //非property节点解析(主要有3类:mapping、class-cache和collection-cache)
        //将解析结果放入到metadataSourceQueue中
        if (this.mappingResources != null) {
            for (String mapping : this.mappingResources) {
                Resource mr = new ClassPathResource(mapping.trim(), this.resourcePatternResolver.getClassLoader());
                sfb.addInputStream(mr.getInputStream());
            }
        }
        if (this.mappingLocations != null) {
            for (Resource resource : this.mappingLocations) {
                sfb.addInputStream(resource.getInputStream());
            }
        }

        //sfb.addProperties(...)方法会调用properties.putAll( extraProperties )方法
        //说明我们可以配置一个properties对象来达到配置xxx.cfg.xml相同的效果!!
        //两方面的配置最终都会放入到properties对象中
        if (this.hibernateProperties != null) {
            sfb.addProperties(this.hibernateProperties);
        }
        
        //省略若干其它方法...

        // 将sfb向上转型,LocalSessionFactoryBean中的configuration属性实际上
        // 就是org.hibernate.cfg.Configuration
        this.configuration = sfb;
        //在得到configuration以后,通过它来创建一个sessionFactory,
        //并赋值给sessionFactory属性(LocalSessionFactoryBean的属性)
        //在底层会调用return super.buildSessionFactory()来的到一个
        //sessionFactory,由于其父类是org.hibernate.cfg.Configuration
        //所以,相当于调用了configuration.buildSessionFactory()来生成
        //sessionFactory,这是hibernate4.0版本之前的做法,新版本已经
        //被buildSessionFactory(ServiceRegistry)所取代。 
        this.sessionFactory = buildSessionFactory(sfb);
    }
}

总结:

spring的LocalSessionFactoryBean实际完成的工作有:
1、通过解析bean中的configLocation和mappingLocations等属性,得到一个hibernate的原生态的org.hibernate.cfg.Configuration属性
2、通过org.hibernate.cfg.Configuration生成一个hibernate原生态的org.hibernate.SessionFactory属性
3、可以在外部配置一个Properties对象,并将其配置为properties属性,可以达到与xxx.cfg.xml相同的配置效果

3、Spring整合Hibernate

我们可以得到这样一个结论,spring的LocalSessionFactoryBean具体是调用Hibernate的Configuration中configure(…)方法来读取并解析xxx.cfg.xml文件的,同样也会得到一个原生态的org.hibernate.cfg.Configuration 和 org.hibernate.SessionFactory属性。

所以,我们可以看见LocalSessionFactoryBean中保存有hibernate的configuration、sessionFactory、properties 以及 metadataSourceQueue。

从前面两节分析中我们知道,LocalSessionFactoryBean会读取并解析xxx.cfg.xml和xxx.hbm.xml文件,所以在配置LocalSessionFactoryBean的时候需要明确指定这两个文件的位置。当然,连接数据库的dataSource也要配置在spring的bean中:

下面是一个spring整合hibernate的实例:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4     xmlns:aop="http://www.springframework.org/schema/aop"
 5     xmlns:context="http://www.springframework.org/schema/context"
 6     xmlns:tx="http://www.springframework.org/schema/tx"
 7     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
 8         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
 9         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
10         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
11 
12     <!-- 配置自动扫描的包 -->
13     <!-- 实体和dao -->
14     <context:component-scan base-package="comentity,com.dao.impl"></context:component-scan>
15     <!-- service -->
16     <context:component-scan base-package="com.service.impl"></context:component-scan>
17 
18     <!-- 导入数据库资源文件 -->
19     <context:property-placeholder location="classpath:db.properties"/>
20     
21     <!-- 配置c3p0数据源 -->
22     <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
23         <property name="user" value="${jdbc.user}"></property>
24         <property name="password" value="${jdbc.password}"></property>
25         <property name="driverClass" value="${jdbc.driverClass}"></property>
26         <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
27         
28         <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
29         <property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
30         <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
31         <property name="acquireIncrement" value="${jdbc.acquireIncrement}"></property>
32     </bean>
33     
34     <!-- 配置sessionFacory -->
35     <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
36         <property name="dataSource" ref="dataSource"></property>
37         <!-- 如果有多个配置文件,可以使用属性configLocations来配置,多个配置文件之间用逗号“,”来分割,
38             如:classpath:hibernate.cfg.xml,classpath:extension.cfg.xml -->
39         <property name="configLocation" value="classpath:hibernate.cfg.xml"></property>
40         <!-- ORM映射关系配置文件 -->
41         <property name="mappingLocations" value="classpath:com/gzpp123/web/entity/*.hbm.xml"></property>
42     </bean>
43     
44     <!-- 配置spring的声明式事物 -->
45     <!-- 1、配置hibernate的事物管理器 -->
46     <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
47         <property name="sessionFactory" ref="sessionFactory"></property>
48     </bean>
49     
50     <!-- 2、配置事物属性 -->
51     <tx:advice id="txAdvice" transaction-manager="transactionManager">
52         <tx:attributes>
53             <tx:method name="get*" read-only="true"/>
54             <tx:method name="*"/>
55         </tx:attributes>
56     </tx:advice>
57     
58     <!-- 3、配置事物切入点,再把事物属性和事务切入点关联起来 -->
59     <aop:config>
60         <aop:pointcut expression="execution(* com.service.*.*(..))" id="txPointcut"/>
61         <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
62     </aop:config>
63     
64     <aop:config>
65         <aop:pointcut expression="execution (* com.service.CoreService.*(..))" id="txPointcut1"/>
66         <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut1"/>
67     </aop:config>
68 
69 </beans>

再看看类路径下的db.properties文件:

jdbc.user=root
jdbc.password=tiger123
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/gzpp

jdbc.initialPoolSize=2
jdbc.maxPoolSize=10
jdbc.minPoolSize=1
jdbc.acquireIncrement=5
#...
#appSecret: 62e5c0141c2fc9a3bc9d2ae73fb7cd12
#appid: wx15fc2152e1406d02

由于mapping以及dataSource都在spring中配置完成了,所以hibernate.cfg.xml文件则相对简单:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE hibernate-configuration PUBLIC
 3         "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
 4         "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
 5 <hibernate-configuration>
 6     <session-factory>
 7     
 8         <!-- 配置 hibernate 的基本属性 -->
 9     
10         <!-- 方言 -->
11         <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
12         
13         <!-- 是否显示及格式化 SQL -->
14         <property name="hibernate.show_sql">true</property>
15         <property name="hibernate.format_sql">true</property>
16     
17         <!-- 生成数据表的策略 -->
18         <property name="hibernate.hbm2ddl.auto">update</property>
19         
20         <!-- 二级缓存相关 -->
21     
22     </session-factory>
23 </hibernate-configuration>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring和HibernateJava开发中非常常用的两个框架,下面简单介绍一下它们的源码分析。 1. Spring源码分析 Spring是一个轻量级的IoC和AOP框架,其中IoC主要是通过BeanFactory和ApplicationContext来实现的。在Spring的源码中,可以看到很多设计模式的运用,如工厂模式、单例模式、策略模式、代理模式等等,这些设计模式的运用使得Spring的代码更加灵活和易于维护。 其中BeanFactory是Spring的核心接口,它提供了IoC容器的基本实现。ApplicationContext是BeanFactory的一个子类,它提供了更多的企业级特性,如AOP、事件传递等等。在Spring的源码中,可以看到很多关于BeanFactory和ApplicationContext的实现,特别是在Bean的生命周期、依赖注入等方面,Spring的源码展示了很多实现细节。 此外,在Spring的源码中,还有很多关于AOP的实现,如代理模式、动态代理、CGLIB等等。这些实现使得Spring能够很好地支持AOP的实现,从而实现了很多企业级应用的需求。 总之,Spring的源码分析可以让我们更好地了解Spring的实现细节,并且对于我们学习和使用Spring也有很大的帮助。 2. Hibernate源码分析 Hibernate是一个Java持久化框架,它提供了对象/关系映射和查询语言等功能。在Hibernate源码中,可以看到很多关于ORM的实现,如映射文件、Session、Transaction等等。 Hibernate的核心接口是SessionFactory和Session,它们是Hibernate的核心实现。在Hibernate源码中,可以看到很多关于Session和Transaction的实现,如缓存机制、事务管理等等。此外,在Hibernate源码中,还有很多关于查询语言的实现,如HQL、Criteria等等。 总之,Hibernate源码分析可以让我们更好地了解Hibernate的实现细节,并且对于我们学习和使用Hibernate也有很大的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值