Spring DAO

PS: 有了Spring Boot ,看这些感觉要疯呀,感觉文档可能几年没更新过

事务

这里写图片描述

@Transactional注解

这里写图片描述

事务传播行为

这里写图片描述

事务监听器

@Component
   public class MyComponent {

           @TransactionalEventListener
           public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
                     ...
           }
   }

DAO support

@Repository
public class JpaMovieFinder implements MovieFinder {
        //JPA工厂
        @PersistenceContext
        private EntityManager entityManager;

        // ...

}

@Repository
public class HibernateMovieFinder implements MovieFinder {
        //Hibernate工厂
        private SessionFactory sessionFactory;
        @Autowired
        public void setSessionFactory(SessionFactory sessionFactory) {
                this.sessionFactory = sessionFactory;
        }

        // ...

}

支持ORM框架

  • 易于测试。Spring IoC的模式使得开发者可以轻易的替换Hibernate的SessionFactory实例,JDBC的DataSource实例,事务管理器,以及映射对象(如果有必要)的配置和实现。这一特点十分利于开发者对每个模块进行独立的测试。

  • 泛化数据访问异常。Spring可以将ORM工具的异常封装起来,将所有异常(可以是受检异常)封装成运行时的DataAccessException体系。这一特性可以令开发者在合适的逻辑层上处理绝大多数不可修复的持久化异常,避免了大量的catch,throw和异常的声明。开发者还可以按需来处理这些异常。其中,JDBC异常(包括一些特定DB语言)都会被封装为相同的体系,意味着开发者即使使用不同的JDBC操作,基于不同的DB,也可以保证一致的编程模型。

  • 通用的资源管理。Spring的应用上下文可以通过处理配置源的位置来灵活配置Hibernate的SessionFactory实例,JPA的EntityManagerFactory实例,JDBC的DataSource实例以及其他类似的资源。Spring的这一特性使得这些实例的配置十分易于管理和修改。同时,Spring还为处理持久化资源的配置提供了高效,易用和安全的处理方式。举个例子,有些代码使用了Hibernate需要使用相同的Session来确保高效性和正确的事务处理。Spring通过Hibernate的SessionFactory来获取当前的Session,来透明的将Session绑定到当前的线程。Srping为任何本地或者JTA事务环境解决了在使用Hibernate时碰到的一些常见问题。

  • 集成事务管理。开发者可以通过@Transactional注解或在XML配置文件中显式配置事务AOP Advise拦截,将ORM代码封装在声明式的AOP方法拦截器中。事务的语义和异常处理(回滚等)都可以根据开发者自己的需求来定制。在后面的章节中,资源和事务管理中,开发者可以在不影响ORM相关代码的情况下替换使用不同的事务管理器。例如,开发者可以在本地事务和JTA之间进行交换,并在两种情况下具有相同的完整服务(如声明式事务)。而且,JDBC相关的代码在事务上完全和处理ORM部分的代码集成。这对于不适用于ORM的数据访问非常有用,例如批处理和BLOB流式传输,仍然需要与ORM操作共享常见事务。

JPA

Spring JPA支持提供了三种配置JPAEntityManagerFactory的方法,之后通过EntityManagerFactory来获取对应的实体管理器。
LocalEntityManagerFactoryBean(通常只有在简单的部署环境中使用此选项,例如在独立应用程序或者进行集成测试时,才会使用这种方式。):LocalEntityManagerFactoryBean创建一个适用于应用程序且仅使用JPA进行数据访问的简单部署环境的EntityManagerFactory。工厂bean会使用JPAPersistenceProvider自动检测机制,并且在大多数情况下,仅要求开发者指定持久化单元的名称:
<beans>
        <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
                <property name="persistenceUnitName" value="myPersistenceUnit"/>
        </bean>
</beans>
从JNDI中获取EntityManagerFactory(在部署到J2EE服务器时可以使用此选项。检查服务器的文档来了解如何将自定义JPA提供程序部署到服务器中,从而对服务器进行比默认更多的个性化定制。):
<beans>
    <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>
LocalContainerEntityManagerFactoryBean(在基于Spring的应用程序环境中使用此选项来实现完整的JPA功能。这包括诸如Tomcat的Web容器,以及具有复杂持久性要求的独立应用程序和集成测试。):会基于persistence.xml文件,dataSourceLookup策略和指定的loadTimeWeaver来创建一个PersistenceUnitInfo实例。因此,可以在JNDI之外使用自定义数据源并控制织入(weaving)过程。以下示例显示LocalContainerEntityManagerFactoryBean的典型Bean定义:
<beans>
        <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
                <property name="dataSource" ref="someDataSource"/>
                <property name="loadTimeWeaver">
                        <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
                </property>
        </bean>
</beans>

//persistence.xml


<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
        <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
                <mapping-file>META-INF/orm.xml</mapping-file>
                <exclude-unlisted-classes/>
        </persistence-unit>
</persistence>


Hibernate

//设置SessionFactory


<beans>

        <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
                <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
                <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
                <property name="username" value="sa"/>
                <property name="password" value=""/>
        </bean>

        <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
                <property name="dataSource" ref="myDataSource"/>
                <property name="mappingResources">
                        <list>
                                <value>product.hbm.xml</value>
                        </list>
                </property>
                <property name="hibernateProperties">
                        <value>
                                hibernate.dialect=org.hibernate.dialect.HSQLDialect
                        </value>
                </property>
        </bean>

</beans>


//或者jndi
<beans>
        <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>


//基于Hibernate API来实现DAO
@Component
public class ProductDaoImpl implements ProductDao {

        private SessionFactory sessionFactory;
        @Autowired
        public void setSessionFactory(SessionFactory sessionFactory) {
                this.sessionFactory = sessionFactory;
        }

        public Collection loadProductsByCategory(String category) {
                return this.sessionFactory.getCurrentSession()
                                .createQuery("from test.Product product where product.category=?")
                                .setParameter(0, category)
                                .list();
        }
}

//声明式事务划分
public class ProductServiceImpl implements ProductService {

        private ProductDao productDao;

        public void setProductDao(ProductDao productDao) {
                this.productDao = productDao;
        }

        @Transactional
        public void increasePriceOfAllProductsInCategory(final String category) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // ...
        }

        @Transactional(readOnly = true)
        public List<Product> findAllProducts() {
                return this.productDao.findAllProducts();
        }

}

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <tx:annotation-driven/>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>

内嵌数据库

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .generateUniqueName(true)
            .setType(H2)
            .setScriptEncoding("UTF-8")
            .ignoreFailedDrops(true)
            .addScript("schema.sql")
            .addScripts("user_data.sql", "country_data.sql")
            .build();
    }
}

Data access with JDBC

  • JdbcTemplate 是经典的Spring JDBC访问方式,也是最常用的。这是“最基础”的方式、其他所有方式都是在 JdbcTemplate的基础之上封装的。
  • NamedParameterJdbcTemplate 在原有JdbcTemplate的基础上做了一层包装支持命名参数特性、用于替代传统的JDBC“?”占位符。当SQL语句中包含多个参数时使用这种方式能有更好的可读性和易用性
  • SimpleJdbcInsert和SimpleJdbcCall操作类主要利用JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。这种方式简化了编码、你只需要提供表或者存储过程的名字、以及和列名相匹配的参数Map。但前提是数据库需要提供足够的元数据。如果数据库没有提供这些元数据,需要开发者显式配置参数的映射关系。
  • RDBMS对象的方式包含MappingSqlQuery, SqlUpdate和StoredProcedure,需要你在初始化应用数据访问层时创建可重用和线程安全的对象。这种方式设计上类似于JDO查询、你可以定义查询字符串,声明参数及编译查询语句。一旦完成这些工作之后,执行方法可以根据不同的传入参数被多次调用。

JdbcTemplate

Querying (SELECT)
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
                "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

String lastName = this.jdbcTemplate.queryForObject(
                "select last_name from t_actor where id = ?",
                new Object[]{1212L}, String.class);

Actor actor = this.jdbcTemplate.queryForObject(
                "select first_name, last_name from t_actor where id = ?",
                new Object[]{1212L},
                new RowMapper<Actor>() {
                        public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                                Actor actor = new Actor();
                                actor.setFirstName(rs.getString("first_name"));
                                actor.setLastName(rs.getString("last_name"));
                                return actor;
                        }
                });

List<Actor> actors = this.jdbcTemplate.query(
                "select first_name, last_name from t_actor",
                new RowMapper<Actor>() {
                        public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                                Actor actor = new Actor();
                                actor.setFirstName(rs.getString("first_name"));
                                actor.setLastName(rs.getString("last_name"));
                                return actor;
                        }
                });

Updating (INSERT/UPDATE/DELETE)
this.jdbcTemplate.update(
                "insert into t_actor (first_name, last_name) values (?, ?)",
                "Leonor", "Watling");

this.jdbcTemplate.update(
                "update t_actor set last_name = ? where id = ?",
                "Banjo", 5276L);

this.jdbcTemplate.update(
                "delete from actor where id = ?",
                Long.valueOf(actorId));
Other jdbcTemplate operations
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

NamedParameterJdbcTemplate

private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

        String sql = "select count(*) from T_ACTOR where first_name = :first_name";

        SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

        return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

public int countOfActorsByFirstName(String firstName) {

        String sql = "select count(*) from T_ACTOR where first_name = :first_name";

        Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

        return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}

public int countOfActors(Actor exampleActor) {

        // notice how the named parameters match the properties of the above 'Actor' class
        String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

        SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

        return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

数据源连接

DataSource

//DriverManagerDataSource
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

//DBCP configuration
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

//C3P0 configuration
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

其他常用类

  • DataSourceUtils类是一个方便有用的工具类,提供了从JNDI获取和关闭连接等有用的静态方法。它支持线程绑定的连接、例如:使用DataSourceTransactionManager的时候,将把数据库连接绑定到当前的线程上。
  • AbstractDataSource是Spring DataSource实现的基础抽象类,封装了DataSource的基础通用功能。你可以继承AbstractDataSource自定义DataSource 实现。
  • SingleConnectionDataSource实现了SmartDataSource接口、内部封装了一个在每次使用后都不会关闭的单一连接。显然,这种场景下无法支持多线程。这个类主要用于测试目的。例如,他使得测试代码能够脱离应用服务器,很方便的在单一的JNDI环境下调试。和DriverManagerDataSource相反,它总是重用相同的连接,这是为了避免在测试过程中创建过多的物理连接。
  • DriverManagerDataSource类实现了标准的DataSource接口,可以通过Java Bean属性来配置原生的JDBC驱动,并且每次都返回一个新的连接。这个实现对于测试和JavaEE容器以外的独立环境比较有用,无论是作为一个在Spring IOC容器内的DataSource Bean,或是在单一的JNDI环境中。由于Connection.close()仅仅只是简单的关闭数据库连接,因此任何能够操作DataSource的持久层代码都能很好的工作。但是,使用JavaBean类型的连接池,比如commons-dbcp往往更简单、即使是在测试环境下也是如此,因此更推荐commons-dbcp。
  • TransactionAwareDataSourceProxy会创建一个目标DataSource的代理,内部包装了DataSource,在此基础上添加了Spring事务管理功能。有点类似于JavaEE服务器中提供的JNDI事务数据源。
  • DataSourceTransactionManager
DataSourceTransactionManager类实现了PlatformTransactionManager接口。它将JDBC连接从指定的数据源绑定到当前执行的线程中,
允许一个线程连接对应一个数据源。

应用代码需要通过DataSourceUtils.getConnection(DataSource) 来获取JDBC连接,而不是通过JavaEE标准的DataSource.getConnection来获取。它会抛出org.springframework.dao的运行时异常而不是编译时SQL异常。所有框架类像JdbcTemplate都默认使用这个策略。如果不需要和这个 DataSourceTransactionManager类一起使用,DataSourceUtils 提供的功能跟一般的数据库连接策略没有什么两样,因此它可以在任何场景下使用。

DataSourceTransactionManager支持自定义隔离级别,以及JDBC查询超时机制。为了支持后者,应用代码必须在每个创建的语句中使用JdbcTemplate或是调用DataSourceUtils.applyTransactionTimeout(..)方法

在单一的资源使用场景下它可以替代JtaTransactionManager,不需要要求容器去支持JTA。如果你严格遵循连接查找的模式的话、可以通过配置来做彼此切换。JTA本身不支持自定义隔离级别!

JDBC批处理

public class JdbcActorDao implements ActorDao {
        private JdbcTemplate jdbcTemplate;

        public void setDataSource(DataSource dataSource) {
                this.jdbcTemplate = new JdbcTemplate(dataSource);
        }

        public int[] batchUpdate(final List<Actor> actors) {
                int[] updateCounts = jdbcTemplate.batchUpdate("update t_actor set first_name = ?, " +
                                "last_name = ? where id = ?",
                        new BatchPreparedStatementSetter() {
                                public void setValues(PreparedStatement ps, int i) throws SQLException {
                                                ps.setString(1, actors.get(i).getFirstName());
                                                ps.setString(2, actors.get(i).getLastName());
                                                ps.setLong(3, actors.get(i).getId().longValue());
                                        }

                                        public int getBatchSize() {
                                                return actors.size();
                                        }
                                });
                return updateCounts;
        }

        // ... additional methods
}

public class JdbcActorDao implements ActorDao {
        private NamedParameterTemplate namedParameterJdbcTemplate;

        public void setDataSource(DataSource dataSource) {
                this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
        }

        public int[] batchUpdate(final List<Actor> actors) {
                SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
                int[] updateCounts = namedParameterJdbcTemplate.batchUpdate(
                                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                                batch);
                return updateCounts;
        }

        // ... additional methods
}

public class JdbcActorDao implements ActorDao {

        private JdbcTemplate jdbcTemplate;

        public void setDataSource(DataSource dataSource) {
                this.jdbcTemplate = new JdbcTemplate(dataSource);
        }

        public int[] batchUpdate(final List<Actor> actors) {
                List<Object[]> batch = new ArrayList<Object[]>();
                for (Actor actor : actors) {
                        Object[] values = new Object[] {
                                        actor.getFirstName(),
                                        actor.getLastName(),
                                        actor.getId()};
                        batch.add(values);
                }
                int[] updateCounts = jdbcTemplate.batchUpdate(
                                "update t_actor set first_name = ?, last_name = ? where id = ?",
                                batch);
                return updateCounts;
        }

        // ... additional methods

}

利用SimpleJdbc类简化JDBC操作

SimpleJdbcInsert类和SimpleJdbcCall类主要利用了JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。这意味着可以在前端减少配置,当然你也可以覆盖或是关闭底层的元数据处理,在代码里面指定所有的细节。
public class JdbcActorDao implements ActorDao {

        private JdbcTemplate jdbcTemplate;
        private SimpleJdbcInsert insertActor;

        public void setDataSource(DataSource dataSource) {
                this.jdbcTemplate = new JdbcTemplate(dataSource);
                this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
        }

        public void add(Actor actor) {
                Map<String, Object> parameters = new HashMap<String, Object>(3);
                parameters.put("id", actor.getId());
                parameters.put("first_name", actor.getFirstName());
                parameters.put("last_name", actor.getLastName());
                insertActor.execute(parameters);
        }

        // ... additional methods
}

//自动生成主键
public class JdbcActorDao implements ActorDao {

        private JdbcTemplate jdbcTemplate;
        private SimpleJdbcInsert insertActor;

        public void setDataSource(DataSource dataSource) {
                this.jdbcTemplate = new JdbcTemplate(dataSource);
                this.insertActor = new SimpleJdbcInsert(dataSource)
                                .withTableName("t_actor")
                                .usingGeneratedKeyColumns("id");
        }

        public void add(Actor actor) {
                Map<String, Object> parameters = new HashMap<String, Object>(2);
                parameters.put("first_name", actor.getFirstName());
                parameters.put("last_name", actor.getLastName());
                Number newId = insertActor.executeAndReturnKey(parameters);
                actor.setId(newId.longValue());
        }

        // ... additional methods
}
<think>好的,我现在需要回答用户关于Spring DAO实现最佳实践的问题。首先,我得回想一下Spring DAO的核心概念。记得Spring DAO主要是为了简化数据访问层的开发,提供了统一的异常处理数据访问模板。比如,JdbcTemplate就是常用的工具之一,能减少很多样板代码。 接下来要分步骤解释。首先应该介绍Spring DAO的模块,比如属于Spring Framework的哪个部分,支持哪些技术,比如JDBC、Hibernate、JPA等。然后需要提到异常处理,Spring将特定于持久化技术的异常转换成统一的DataAccessException,这样业务层就不需要处理各种不同的异常了。 然后是数据访问模板,比如JdbcTemplate的使用方法,举一个例子,比如查询数据或者插入数据。这部分要确保代码示例正确,符合用户的格式要求,使用代码块包裹,并且语法正确。 接下来是事务管理,说明如何通过@Transactional注解来声明事务,以及配置事务管理器的重要性。可能需要提到如何在Spring Boot中自动配置事务管理器,比如使用PlatformTransactionManager的不同实现。 最佳实践部分要包括几个方面:使用JdbcTemplate简化操作,避免原生JDBC的繁琐;异常处理的最佳方式,比如捕获DataAccessException;事务管理的建议,比如在服务层使用事务;还有分层架构的重要性,保持DAO层独立。 然后生成相关问题,需要结合用户之前提供的引用内容。比如用户提到了水平分库的配置,引用[2]里提到根据业务ID分库,避免跨库关联。所以相关问题可以涉及分库分表时如何结合Spring DAO,或者如何优化Spring DAO的性能,事务在分库场景下的处理等。 另外,用户提供的引用[1]提到SpringSource认证的代码配置,可能需要在最佳实践中提到遵循官方推荐配置,确保代码的可维护性性能。 需要检查是否符合用户的所有格式要求,比如行内数学表达式用$...$,独立公式用$$...$$,但这个问题可能不涉及数学公式,所以可能不需要。但如果有提到版本号或类似的地方,可能需要用正确的格式。 确保回答结构清晰,分点说明,每个部分有明确的标题,比如核心概念、实现方式、最佳实践。代码示例要正确,使用正确的语法,比如Java代码而不是之前的Python示例。同时,在引用用户提供的引用时,要在适当的位置添加引用标识,比如在提到分库分表时引用[^2]。 最后,生成的相关问题需要与Spring DAO相关,同时结合用户提供的引用内容,比如分库分表的实现、事务管理优化、异常处理等。确保问题有针对性,能够引导用户进一步学习。</think>### Spring DAO实现与最佳实践 #### 1. Spring DAO核心概念 Spring DAO(Data Access Object)是Spring框架中用于简化数据访问层(DAL)开发的模块,核心目标是**解耦业务逻辑与数据访问逻辑**,并提供统一的异常处理机制。例如: - **统一异常体系**:将JDBC、Hibernate等技术的特定异常(如`SQLException`)转换为`DataAccessException`,开发者无需处理底层差异[^1]。 - **模板模式**:通过`JdbcTemplate`、`HibernateTemplate`等工具类封装重复操作(如连接管理、异常处理)。 #### 2. 实现方式 ##### 2.1 使用JdbcTemplate ```java @Repository public class UserDao { @Autowired private JdbcTemplate jdbcTemplate; public User getUserById(Long id) { return jdbcTemplate.queryForObject( "SELECT * FROM users WHERE id = ?", new Object[]{id}, (rs, rowNum) -> new User(rs.getString("name"), rs.getInt("age")) ); } } ``` 此代码通过`JdbcTemplate`简化了JDBC查询,自动处理资源释放与异常转换。 ##### 2.2 事务管理 通过声明式事务(`@Transactional`)确保操作原子性: ```java @Service public class UserService { @Autowired private UserDao userDao; @Transactional public void updateUser(User user) { userDao.update(user); } } ``` 需配置事务管理器(如`DataSourceTransactionManager`)[^1]。 #### 3. 最佳实践 1. **优先使用JdbcTemplate**:避免原生JDBC的冗余代码,减少资源泄漏风险。 2. **异常处理策略**:捕获`DataAccessException`并记录日志,而非直接暴露底层异常。 3. **事务边界控制**:事务注解应作用于服务层方法,而非DAO层。 4. **分库分表兼容性**:在水平分库场景中,通过业务ID路由数据源,保持DAO层无感知。 5. **依赖注入**:通过`@Repository`标注DAO类,利用Spring的组件扫描与依赖注入。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值