Struts2+spring+hibernate整合

第四章 搭建SSH框架

本章目标

  • 掌握Spring与Struts 2的集成重点(难点)
  • 掌握Spring与Hibernate的集成重点(难点)

SSH系统程序架构

SSH架构

  • Struts 2 + Spring + Hibernate
  • 以Spring作为核心框架,数据持久化使用Hibernate完成,表现层使用Struts 2
  • Spring提供对象管理、面向切面编程等实用功能
  • 通过Spring提供的服务简化编码、降低开发难度、提高开发效率

image

使用SSH架构开发登录功能

用户登录

  • 登录成功,保存当前用户到Session,根据角色跳转
  • 登录失败,转发回登录页面,并提示错误信息

用户注册

  • 注册成功,跳转到登录页面
  • 注册失败,返回注册页面,并提示错误信息。

开发准备:添加所需的jar文件

SSH整合体验最不好的就是添加jar包,由于三个框架所依赖的jar包非常多,其中有一些jar包可能冲突,我们应该将冲突的jar包,保留高级版本的,删掉低级版本的。

image

Spring与Struts 2集成

实现步骤

  • 添加struts2-spring-plugin-xxx.jar
  • 按照名称匹配的原则定义业务Bean和Action中的setter方法
  • 在struts.xml正常配置Action

分析

  • Action实例如何创建?
  • Spring创建的业务Bean如何注入给Action?

配置web.xml文件

<!-- Struts2的配置 -->
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring的配置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- Spring配置文件的名称和路径存放位置,如果文件放置在/WEB-INF/目录下且名称为applicationContext.xml,可以不配置此项,多个文件使用“,”分割。 -->
    <param-value>classpath:spring.xml,classpath:springDao.xml</param-value>
</context-param>
<listener>
    <!-- spring 加载监听器  该监听器用于在web环境中启动Spring容器 -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Spring整合Struts2自动装配

  • Spring和Struts2整合的关键是如何让Spring来管理Struts2的Action。
  • Spring的自动装配:
    • 导入struts2-spring-plugin-2.3.24.jar包后,Struts2会将Action对象的管理交由Spring来管理。
    • Spring默认按照byname的方式进行自动装配,会自动查找Spring容器中是否有同名的bean。如果没有找到,就不执行注入。
  • 如何使用自动装配:设置<bean>元素的autowire属性
    • autowire:自动装配类型(byName,byType),默认为byName。
    • autowire-candidate:设值是否支持自动注入,(true,false),默认为true

Struts 2和Spring集成的第二种方式

spring.xml配置

<!-- 在Spring配置文件中配置Action Bean,注意scope="prototype"属性 -->
<bean id="userAction" class="com.znsd.ssh.action.UserAction" scope="prototype">
    <property name="userService" ref="userService"></property>
</bean>

struts.xml配置

<package name="default" namespace="/" extends="struts-default">
    <!-- 在Struts.xml中配置Action,class属性的值不再是Action类的全限定名,
  而是Spring配置文件中相应的Action Bean的名称-->
    <action name="login" class="userAction" method="login">
        <result>success.jsp</result>
        <result name="input">login.jsp</result>
    </action>
</package>

提示:

  1. 采用此种配置方式时,Action的实例由Spring创建,Struts 2插件的工厂类只需根据Action Bean的id查找该组件使用即可
  2. 此种方式可以为Action进行更灵活的配置,但代价是在Spring配置文件中需要定义很多Action Bean,增加了配置工作量,如非必需并非首选
  3. 采用此种配置方式,同样需要添加struts2-spring-plugin-xxx.jar文件

添加编码格式过滤器

Spring为了避免Struts2中的乱码问题,给我们提供了一个默认的编码过滤器。

<!-- 开启编码过滤器 -->
<filter>
    <description>字符集过滤器</description>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <description>字符集编码</description>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

小结

  • Spring和Struts2进行整合。
  • Spring开启Struts2编码过滤器。

Spring整合Hibernate的优势

  • 通用的资源管理,直接管理Hibernate底层的DataSource,SessionFactory。
  • 有效的Session管理。
  • 声明式的事务管理。
  • 异常包装。
  • 工具类封装。

Spring管理Hibernate配置文件

  • 在Spring中Hibernate的配置基本可以交由Spring来进行管理。
  • 由于Spring的Dao层文件较为独立,所以推荐单独创建一个springdao.xml文件用来存放数据层的配置信息。

SpringDao.xml文件

SpringDao中用到许多spring的特性,比如aop,tx,context等。所以需要导入schema头文件。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">
</beans>

c3p0连接池配置

  • 一般项目中使用连接池进行数据库连接,这里我们选用c3p0连接池。首先添加c3p0的连接池信息。
  • 首先需要导入c3p0连接池对应的驱动包。
  • 当然也可以选用其他连接池信息,只需要修改对应的驱动类和参数即可。
  • 连接池信息有两种配置方案:
    • 在springDao文件中配置。
    • 在hibernate.cfg.xml文件配置。

Springdao.xml配置方案

  • 在springDao文件中配置。
<!-- 配置c3p0连接池属性 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="com.mysql.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
    <property name="user" value="root" />
    <property name="password" value="root" />
    <!-- 队列中的最小连接数 -->
    <property name="minPoolSize" value="15"></property>
    <!-- 队列中的最大连接数 -->
    <property name="maxPoolSize" value="25"></property>
    <!-- 当连接耗尽时创建的连接数 -->
    <property name="acquireIncrement" value="15"></property>
    <!-- 等待时间 -->
    <property name="checkoutTimeout" value="10000"></property>
    <!-- 初始化连接数 -->
    <property name="initialPoolSize" value="20"></property>
    <!-- 最大空闲时间,超出时间连接将被丢弃 -->
    <property name="maxIdleTime" value="20"></property>
    <!-- 每隔60秒检测空闲连接 -->
    <property name="idleConnectionTestPeriod" value="60000"></property>
</bean>

Hibernate连接池配置方案

  • 在hibernate.cfg.xml文件配置
<session-factory>
    <!-- 数据库链接配置 -->
    <property name="hibernate.connection.driver_class">org.gjt.mm.mysql.Driver</property>
    <property name="hibernate.connection.password">root</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
    <!-- c3p0连接池配置 -->
    <!-- 最大连接数 -->
    <property name="hibernate.c3p0.max_size">20</property>
    <!-- 最小连接数 -->
    <property name="hibernate.c3p0.min_size">5</property>
    <!-- 获得连接的超时时间,如果超过这个时间,会抛出异常,单位毫秒 -->
    <property name="hibernate.c3p0.timeout">120</property>
    <!-- 最大的PreparedStatement的数量 -->
    <property name="hibernate.c3p0.max_statements">100</property>
    <!-- 每隔120秒检查连接池里的空闲连接 ,单位是秒-->
    <property name="hibernate.c3p0.idle_test_period">120</property>
    <!-- 当连接池里面的连接用完的时候,C3P0一下获取的新的连接数 -->
    <property name="hibernate.c3p0.acquire_increment">2</property>
    <!-- 每次都验证连接是否可用 -->
    <property name="hibernate.c3p0.validate">true</property>
</session-factory>

sessionFactory工厂配置

  • Hibernate中的sessionFactory工厂,在Spring中可以交由Spring来进行维护和管理。
<!-- session工厂由spring来管理 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean" >
    <!-- 数据源配置 -->
    <property name="dataSource" ref="dataSource"></property>
    <!-- 读取hibernate配置信息 -->
    <property name="configLocation" value="classpath:hibernate.cfg.xml" />
    <!-- 自动加载映射文件 *表示匹配该文件下的所有映射文件 -->
    <property name="mappingLocations">
        <list>
            <!-- Spring可以自动加载映射文件,这样非常方便。 -->
            <value>classpath:com/lxit/ssh/entities/mapper/*.hbm.xml</value>
        </list>
    </property>
</bean>
  • hibernate.cfg.xml
<hibernate-configuration>
    <session-factory>
        <!-- 数据库方言 -->
        <property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
        <!-- 在控制台输出sql语句 -->
        <property name="show_sql">true</property>
        <!-- 格式化sql语句 -->
        <property name="format_sql">true</property>
         <!--整合spring时因为session交给spring管理了,所以不需要一下配置-->
        <!--<property name="hibernate.current_session_context_class">thread</property>-->
        <!-- 数据库生成策略 -->
        <property name="hibernate.hbm2ddl.auto">update</property>
    </session-factory>
</hibernate-configuration>

丢弃hibernate.cfg.xml配置文件的sessionFactory配置

<!-- session工厂由spring来管理 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <!-- 数据源配置 -->
    <property name="dataSource" ref="dataSource"></property>
    <!-- 自动加载映射文件 *表示匹配该文件下的所有映射文件 -->
    <property name="mappingLocations">
        <list>
            <!-- Spring可以自动加载映射文件,这样非常方便。 -->
            <value>classpath:com/znsd/ssh/bean/*.hbm.xml</value>
        </list>
    </property>
    <!--各种hibernate属性 -->
    <property name="hibernateProperties">
        <props>
            <!-- 方言 -->
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
            <prop key="hibernate.hbm2ddl.auto">update</prop>
            <prop key="hibernate.current_session_context_class">org.springframework.orm.hibernate4.SpringSessionContext</prop>
        </props>
    </property>
    <!-- 引入单个hibernate映射文件 -->
    <!-- <property name="mappingResources" value="com/znsd/ssh/bean/User.hbm.xml"></property> -->
</bean>

dataSource配置文件的提取

  • 添加jdbc.properties配置文件
  ## jdbc 连接配置
  jdbc.driver=com.mysql.jdbc.Driver
  jdbc.url=jdbc:mysql://192.168.41.10:3306/test
  jdbc.user=znsd_test
  jdbc.password=123456
  ## c3p0 数据源配置
  c3p0.minPoolSize=15
  c3p0.maxPoolSize=25
  c3p0.acquireIncrement=15
  c3p0.checkoutTimeout=10000
  c3p0.initialPoolSize=20
  c3p0.maxIdleTime=20
  c3p0.idleConnectionTestPeriod=60000
  • spring加载配置文件
<!-- 通过context:property-placeholder加载配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!-- 通过${}获取配置文件中的值 -->
<!-- 配置c3p0连接池属性 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="${jdbc.driver}" />
    <property name="jdbcUrl" value="${jdbc.url}" />
    <property name="user" value="${jdbc.user}" />
    <property name="password" value="${jdbc.password}" />
    <!-- 队列中的最小连接数 -->
    <property name="minPoolSize" value="${c3p0.minPoolSize}"></property>
    <!-- 队列中的最大连接数 -->
    <property name="maxPoolSize" value="${c3p0.maxPoolSize}"></property>
    <!-- 当连接耗尽时创建的连接数 -->
    <property name="acquireIncrement" value="${c3p0.acquireIncrement}"></property>
    <!-- 等待时间 -->
    <property name="checkoutTimeout" value="${c3p0.checkoutTimeout}"></property>
    <!-- 初始化连接数 -->
    <property name="initialPoolSize" value="${c3p0.initialPoolSize}"></property>
    <!-- 最大空闲时间,超出时间连接将被丢弃 -->
    <property name="maxIdleTime" value="${c3p0.maxIdleTime}"></property>
    <!-- 每隔60秒检测空闲连接 -->
    <property name="idleConnectionTestPeriod" value="${c3p0.idleConnectionTestPeriod}"></property>
</bean>

配置事务

  • 在Hibernate中,如果实现增加、修改、删除必须使用事务处理。
<!-- 声明式事务管理 -->
<bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"></property>
</bean>
<!-- 事务的通知方式 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
    <!-- 事务规则,当切面中的方法满足规则时,所使用怎样的事务进行处理。-->
    <tx:attributes>
        <tx:method name="find*" propagation="REQUIRED" read-only="true" />
        <tx:method name="search*" propagation="REQUIRED" read-only="true" />
        <tx:method name="query*" propagation="REQUIRED" read-only="true" />

        <tx:method name="add*" propagation="REQUIRED" />
        <tx:method name="submit*" propagation="REQUIRED" />
        <tx:method name="save*" propagation="REQUIRED" />
        <tx:method name="insert*" propagation="REQUIRED" />

        <tx:method name="del*" propagation="REQUIRED" />
        <tx:method name="remove*" propagation="REQUIRED" />

        <tx:method name="update*" propagation="REQUIRED" />
        <tx:method name="modify*" propagation="REQUIRED" />

        <tx:method name="*" propagation="REQUIRED" read-only="true" />
    </tx:attributes>
</tx:advice>

属性介绍

  • 是Spring中事务通知类标签,这里用来定义Hibernate中的事务。
  • 常用属性如下。
属性默认值描述
name(必选) 必选,与事务关联的方法名,通配符()可以用来指定一批关联到相同的事务属性的方法,如"add"、"get*"、"select*"等。
propagationREQUIRED事务传播行为
isolationDEFAULT事务隔离级别
timeout-1超时事件,单位为秒
read-onlyfalse是否只读

事务传播行为

  • Spring事务传播行为有以下类型:
事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是 最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

事务隔离级别

数据库并发操作存在的异常情况:

  • 更新丢失
  • 脏读取
  • 不可重复读取
  • 两次更新问题
  • 幻读

数据库隔离级别:

  • 未授权读
  • 授权读取
  • 可重复读
  • 串行

 Spring可以简化Hibernate编码

  • 使用HibernateTemplate来简化Hibernate编程
public void addEmployee(Employee employee) {
    Transaction tx = null;
    try {
        tx = HibernateUtil.getSession().beginTransaction();
        HibernateUtil.getSession().save(employee);
        tx.commit();
    } catch(RuntimeException re) {
        tx.rollback();
        throw re;
    } finally {
        HibernateUtil.closeSession();
    }
}

上面的流程化的代码仍显烦琐,可以改为一下代码,Spring的目标是使现有的Java EE技术更易用

public void addEmployee(Employee employee) {
    this.getHibernateTemplate().save(employee);
}

HibernateTemplate类

  • HibernateTemplate类似于前面我们定义的HibernateUtil工具类,只不过这个HibernateTemplate是基于Spring管理的,其实本身就是session的一个代理。功能更加强大。
  • 常用方法:

    • get/load存取单条数据:
    (Teacher)this.hibernateTemplate.get(Teacher.class, id);  
    
    • find/iterate查询操作:
    (List)this.hibernateTemplate().find("from Teacher t where t.age>?", new Integer(age));  
    
    • save/update/saveOrUpdate/delete 保存/更新/删除操作
    this.hibernateTemplate.save(teacher); 
    

使用HibernateDaoSupport基类

实现步骤

  • DAO类继承HibernateDaoSupport
  • 使用getHibernateTemplate()方法获取HibernateTemplate实例完成持久化操作

问题

  • 如何将SessionFactory注入DAO ?
  • 如何创建HibernateTemplate实例?

分析

  • HibernateDaoSupport基类的setSessionFactory()方法

实现数据层的方法

  • 使用Spring+Hibernate实现数据层的用户登录、用户注册、用户查询的方法。
  • public int login(User user);
  • public String addUser(User user);
  • public List<User> getList();

OpenSessionView模式

  • Spring为我们解决Hibernate的Session的关闭与开启问题。
  • Hibernate 允许对关联对象、属性进行延迟加载,但是必须保证延迟加载的操作限于同一个 Hibernate Session 范围之内进行。如果 Service 层返回一个启用了延迟加载功能的领域对象给 Web 层,当 Web 层访问到那些需要延迟加载的数据时,由于加载领域对象的 Hibernate Session 已经关闭,这些导致延迟加载数据的访问异常
<!-- Spring开启Hibernate的OpenSessionView模式,一定要放在struts过滤器前面,否则不会生效 -->
<filter>
    <filter-name>OpenSessionInViewFilter</filter-name>
    <filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>OpenSessionInViewFilter</filter-name>
    <url-pattern>*</url-pattern>
</filter-mapping>

其他配置

  • 防止内存泄漏
  <!-- 防止spring内存溢出监听器 -->
  <listener>
    <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
  </listener>
  • 设置Session过期时间
  <!-- 配置session超时时间,单位分钟 -->
  <session-config>
      <session-timeout>15</session-timeout>
  </session-config>

Write operations are not allowed in read-only mode问题解决

错误原因:  这是因为Spring默认将一个名为"readOnly"的属性设置成为true,导致只能对数据库进行“读”操作,不允许进行“写”操作。

解决: 可以使用hibernateTemplate来解决次问题,配置一个HibernateTemplate将检查读的操作属性(checkWriteOperations)设置为false

<bean id="hibernateTemplate" class="org.springframework.orm.hibernate4.HibernateTemplate">
    <property name="sessionFactory" ref="sessionFactory" />
    <property name="checkWriteOperations" value="false" />
</bean>
<bean id="userDao" class="com.znsd.ssh.dao.impl.UserDaoImpl">
    <property name="hibernateTemplate" ref="hibernateTemplate"></property>
</bean>

小结

  • 使用Spring整合Hibernate。
    • 数据库连接池
    • sessionFactory
    • HibernateTemplate
  • Spring中的事务管理
  • OpenSessionView模式
  • 防止内存泄漏
  • 设置session有效事件

分页处理

  • 虽然HibernateTemplate帮我们集成了非常强大的功能,但是灵活性确降低了,比如要实现分页,因为没有提供setFirstResult()方法,所以无法进行分页,那么我们怎么做呢?
  • Hibernate还支持一种回调机制。
  • execute(hibernateCallback);

模板与回调机制

  • 通过HibernateCallback实现分页功能
public List<Employee> find(final int page, final int size) {
    List result = getHibernateTemplate().execute(
        new HibernateCallback() {
            public Object doInHibernate(Session session)
                        throws HibernateException, SQLException {
                Query query = session.createQuery("from Employee");
                query.setFirstResult((page - 1) * size);
                query.setMaxResults(size);
                return query.list();
            }
        }
    );
    return result;
}

抽取BaseDao

  • 抽取常用功能接口BaseDao。
  public interface BaseDao<T> {

    public void add(T t);

    public void delete(Serializable id);

    public void uodate(T t);

    public T load(Serializable id);

    public T get(Serializable id);

    public List<T> list();

    public List<T> list(String hql, Object[] args);
  }
  • 提供实现类BaseDaoImpl实现BaseDao接口。
  public class BaseDaoImpl<T> extends HibernateDaoSupport implements BaseDao<T> {

    /**
     * 实体类类型(由构造方法自动赋值)
     */
    private Class<?> entityClass = null;

    /**
     * 构造方法,根据实例类自动获取实体类类型
     */
    public BaseDaoImpl() {
        entityClass = this.getSuperClassType(getClass());
    }

    /**
     * 通过反射获取T的类型信息实例
     */
    protected Class<?> getSuperClassType(Class<?> c) {
        Type type = c.getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            Type[] params = ((ParameterizedType) type).getActualTypeArguments();
            if (!(params[0] instanceof Class)) {
                return Object.class;
            }
            return (Class<?>) params[0];
        }
        return null;
    }

    @Override
    public void add(T t) {
        this.getHibernateTemplate().save(t);
    }

    @Override
    public void delete(Serializable id) {
        this.getHibernateTemplate().delete(this.load(id));
    }

    @Override
    public void uodate(T t) {
        this.getHibernateTemplate().update(t);
    }

    @Override
    public T load(Serializable id) {
        return (T) this.getHibernateTemplate().load(entityClass, id);
    }

    @Override
    public List<T> list() {
        return createCriteria().list();
    }

    @Override
    public List<T> list(String hql, Object[] args) {
        return (List<T>) this.getHibernateTemplate().find(hql, args);
    }

    @Override
    public T get(Serializable id) {
        return (T) this.getHibernateTemplate().get(entityClass, id);
    }

    /**
     * 创建与会话绑定的检索标准对象
     */
    public Criteria createCriteria(Criterion... criterions) {
        return this.createDetachedCriteria(criterions).getExecutableCriteria(this.currentSession());
    }

    /**
     * 创建与会话无关的检索标准对象
     */
    public DetachedCriteria createDetachedCriteria(Criterion... criterions) {
        DetachedCriteria dc = DetachedCriteria.forClass(this.entityClass);
        for (Criterion c : criterions) {
            dc.add(c);
        }
        return dc;
    }
  }

小结

  • 使用Spring集成Hibernate的步骤是什么?
  • 实现Spring和Hibernate集成中的分页。
  • 抽取BaseDao类

总结

  • 实现Spring和Struts 2集成
  • SSH框架整合的系统架构,Action、Service、Dao、SessionFactory
  • 通过HibernateTemplate简化Hibernate DAO的编码
  • 在Dao中使用HibernateCallback接口
  • 1
    点赞
  • 9
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

Jason.Yang_Tom

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值