(六)SpringBoot+SpringCloud —— 集成MyBatis

主要是MyBatis与SpringBoot的继承,此处与SpringCloud无关,作为记录。

依赖与配置

数据库使用的是MySql,所以需要加入以下的依赖:

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.0</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

配置文件application.yml如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 7001
spring:
  application:
    name: SERVICE-USER
  datasource: #数据源
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/owl-bookstore?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: root
    dbcp2: #dbcp2连接池
      max-idle: 10
mybatis:
  #实体类的包
  type-aliases-package: cn.net.bysoft.owl.bookstore.facade.user.entity
  #mapper.xml存放的路径
  mapperLocations: classpath:mapper/*.xml
  #mybatis配置文件存放的路径
  configLocation: classpath:mybatis-config.xml

实现代码

通过上面的依赖和配置,基本可以用自己熟悉的方式使用Mybatis了。

下面来记录一下我自己的实现,实现很简单,没有考虑过多的东西。

可以采用注解的方式使用mybatis,但是此处我使用的是比较原始的方法,配置mapper来使用mybatis。

  1. 定义一个BaseDao接口和BaseDaoImpl实现类,编写一些通用的Dao方法。
  2. 定义一个BaseEntity实体类,同样定义一些常用的方法。
  3. 分页采用了Mybatis的拦截器实现。

直接贴出代码,BaseEntity类:

package cn.net.bysoft.owl.bookstore.common.entity;

import java.io.Serializable;

public abstract class BaseEntity implements Serializable {

	private static final long serialVersionUID = -1105239108084459358L;
	private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

}

分页的实体类:

package cn.net.bysoft.owl.bookstore.common.entity;

import java.io.Serializable;
import java.util.List;

/**
 * 分页组件。<br>
 * 用于前端DataGrid控件初始化。<br>
 * 组件内包含当前页、每页显示条数、总记录数、数据、总页数、页码索引等信息。
 * 
 * @author XuePeng
 */
public class PageBean<T> implements Serializable {

    private static final long serialVersionUID = 8470697978259453214L;

    private int currentPage;
    private int numPerPage;
    private int totalCount;
    private List<T> recordList;

    private int pageCount;
    private int beginPageIndex;
    private int endPageIndex;

    public PageBean(int currentPage, int numPerPage, int totalCount, List<T> recordList) {
        this.currentPage = currentPage;
        this.numPerPage = numPerPage;
        this.totalCount = totalCount;
        this.recordList = recordList;
        // 计算总页码
        pageCount = (totalCount + numPerPage - 1) / numPerPage;
        // 计算 beginPageIndex 和 endPageIndex
        // 总页数不多于10页,则全部显示
        if (pageCount <= 10) {
            beginPageIndex = 1;
            endPageIndex = pageCount;
        }
        // 总页数多于10页,则显示当前页附近的共10个页码
        else {
            // 当前页附近的共10个页码(前4个 + 当前页 + 后5个)
            beginPageIndex = currentPage - 4;
            endPageIndex = currentPage + 5;
            // 当前面的页码不足4个时,则显示前10个页码
            if (beginPageIndex < 1) {
                beginPageIndex = 1;
                endPageIndex = 10;
            }
            // 当后面的页码不足5个时,则显示后10个页码
            if (endPageIndex > pageCount) {
                endPageIndex = pageCount;
                beginPageIndex = pageCount - 10 + 1;
            }
        }
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getNumPerPage() {
        return numPerPage;
    }

    public void setNumPerPage(int numPerPage) {
        this.numPerPage = numPerPage;
    }

    /**
     * @return 获得总记录数。
     */
    public int getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }

    public List<T> getRecordList() {
        return recordList;
    }

    public void setRecordList(List<T> recordList) {
        this.recordList = recordList;
    }

    public int getPageCount() {
        return pageCount;
    }

    public void setPageCount(int pageCount) {
        this.pageCount = pageCount;
    }

    public int getBeginPageIndex() {
        return beginPageIndex;
    }

    public void setBeginPageIndex(int beginPageIndex) {
        this.beginPageIndex = beginPageIndex;
    }

    public int getEndPageIndex() {
        return endPageIndex;
    }

    public void setEndPageIndex(int endPageIndex) {
        this.endPageIndex = endPageIndex;
    }

}
package cn.net.bysoft.owl.bookstore.common.entity;

import java.io.Serializable;

/**
 * 分页参数的实体类。<br>
 * 设置对象的当前页数和每页记录数,进行数据库分页查询。
 * 
 * @author XuePeng
 */
public class PageParam implements Serializable {

    private static final long serialVersionUID = 6297178964005032338L;
    private int pageNum;
    private int numPerPage;

    public PageParam(int pageNum, int numPerPage) {
        this.pageNum = pageNum;
        this.numPerPage = numPerPage;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public int getNumPerPage() {
        return numPerPage;
    }

    public void setNumPerPage(int numPerPage) {
        this.numPerPage = numPerPage;
    }

}

接下来,BaseDao接口,定义了CURD的基本方法,和通过分页查询的方法,具体如下:

package cn.net.bysoft.owl.bookstore.common.core.dao;

import java.util.List;
import java.util.Map;

import cn.net.bysoft.owl.bookstore.common.entity.BaseEntity;
import cn.net.bysoft.owl.bookstore.common.entity.PageBean;
import cn.net.bysoft.owl.bookstore.common.entity.PageParam;

public interface BaseDao<T extends BaseEntity> {

    long insert(T entity);

    int update(T entity);

    int deleteById(long id);

    int deleteByIds(long... id);

    T findById(long id);

    T findByParam(Map<String, Object> paramMap);

    List<T> listByParam(Map<String, Object> paramMap);

    PageBean<T> listByPageAndParam(PageParam pageParam, Map<String, Object> paramMap);

    PageBean<T> listByPageAndParam(PageParam pageParam, Map<String, Object> paramMap, String sqlId);

    long countByParam(Map<String, Object> paramMap);

}

BaseDaoImpl类,实现了BaseDao接口:

package cn.net.bysoft.owl.bookstore.common.core.dao;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import org.springframework.beans.factory.annotation.Autowired;

import cn.net.bysoft.owl.bookstore.common.entity.BaseEntity;
import cn.net.bysoft.owl.bookstore.common.entity.PageBean;
import cn.net.bysoft.owl.bookstore.common.entity.PageParam;

public abstract class BaseDaoImpl<T extends BaseEntity> extends SqlSessionDaoSupport implements BaseDao<T> {

    private static final String SQL_INSERT = "insert";
    private static final String SQL_UPDATE = "update";
    private static final String SQL_DELETE_BY_ID = "deleteById";
    private static final String SQL_DELETE_BY_IDS = "deleteByIds";
    private static final String SQL_FIND_BY_ID = "findById";
    private static final String SQL_FIND_BY_PARAM = "findByParam";
    private static final String SQL_LIST_BY_PARAM = "listByParam";
    private static final String SQL_LIST_BY_PAGE = "listByPageAndParam";
    private static final String SQL_COUNT_BY_PARAM = "countByParam";

    // SqlSessionTemplate实例(要求Spring中进行SqlSessionTemplate的配置)
    // 可以调用sessionTemplate完成数据库操作
    private SqlSessionTemplate sessionTemplate;

    // SqlSessionFactory实例(要求Spring中进行SqlSessionFactory的配置)
    // 可以调用sessionFactory打开一个SqlSession
    private SqlSessionFactory sessionFactory;

    public SqlSessionTemplate getSessionTemplate() {
        return this.sessionTemplate;
    }

    @Autowired
    public void setSessionTemplate(SqlSessionTemplate sessionTemplate) {
        super.setSqlSessionTemplate(sessionTemplate);
        this.sessionTemplate = sessionTemplate;
    }

    public SqlSessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    @Override
    @Autowired
    public void setSqlSessionFactory(SqlSessionFactory sessionFactory) {
        super.setSqlSessionFactory(sessionFactory);
        this.sessionFactory = sessionFactory;
    }

    @Override
    public long insert(T entity) {
        // 对数据库进行insert操作。
        // 执行Mapper配置文件中的insert方法。
        int result = sessionTemplate.insert(getStatement(SQL_INSERT), entity);
        // 判断如果要新建的实体对象不为null,并且成功保存到数据库了,则返回其主键。
        if (entity != null && entity.getId() != null && result > 0) {
            return entity.getId();
        }
        return result;
    }

    @Override
    public int update(T entity) {
        // 对数据库进行update操作,并返回影响行数。
        // 执行Mapper配置文件中的update方法。
        return sessionTemplate.update(getStatement(SQL_UPDATE), entity);
    }

    @Override
    public int deleteById(long id) {
        // 对数据库进行删除操作。
        // 执行Mapper配置文件中的deleteById方法。
        return sessionTemplate.delete(getStatement(SQL_DELETE_BY_ID), id);
    }

    @Override
    public int deleteByIds(long... ids) {
        return sessionTemplate.delete(getStatement(SQL_DELETE_BY_IDS), ids);
    }

    @Override
    public T findById(long id) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的findById方法。
        return sessionTemplate.selectOne(getStatement(SQL_FIND_BY_ID), id);
    }

    @Override
    public T findByParam(Map<String, Object> param) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的findByParam方法。
        if (param == null || param.isEmpty()) {
            return null;
        }
        return sessionTemplate.selectOne(getStatement(SQL_FIND_BY_PARAM), param);
    }

    @Override
    public List<T> listByParam(Map<String, Object> param) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的findListByParam方法。
        return sessionTemplate.selectList(getStatement(SQL_LIST_BY_PARAM), param);
    }

    @Override
    public PageBean<T> listByPageAndParam(PageParam pageParam, Map<String, Object> param) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的findListByPageAndParam方法。
        HashMap<String, Object> params;
        if (param == null) {
            params = new HashMap<>();
        } else {
            params = (HashMap<String, Object>) param;
        }

        // 获取分页数据集 , 注切勿换成 sessionTemplate 对象。
        // 使用RowBounds进行分页。
        List<T> list = getSqlSession().selectList(getStatement(SQL_LIST_BY_PAGE), params,
                new RowBounds((pageParam.getPageNum() - 1) * pageParam.getNumPerPage(), pageParam.getNumPerPage()));

        // 统计总记录数
        Object countObject = (Object) getSqlSession().selectOne(getStatement(SQL_LIST_BY_PAGE),
                new ExecutorInterceptor.CountParameter(params));
        Long count = Long.valueOf(countObject.toString());

        return new PageBean<>(pageParam.getPageNum(), pageParam.getNumPerPage(), count.intValue(), list);
    }

    @Override
    public PageBean<T> listByPageAndParam(PageParam pageParam, Map<String, Object> param, String sqlId) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的自定义的方法。
        HashMap<String, Object> params;
        if (param == null) {
            params = new HashMap<>();
        } else {
            params = (HashMap<String, Object>) param;
        }

        // 获取分页数据集 , 注切勿换成 sessionTemplate 对象
        List<T> list = getSqlSession().selectList(getStatement(sqlId), params,
                new RowBounds((pageParam.getPageNum() - 1) * pageParam.getNumPerPage(), pageParam.getNumPerPage()));

        // 统计总记录数
        Object countObject = (Object) getSqlSession().selectOne(getStatement(sqlId),
                new ExecutorInterceptor.CountParameter(params));
        Long count = Long.valueOf(countObject.toString());

        return new PageBean<>(pageParam.getPageNum(), pageParam.getNumPerPage(), count.intValue(), list);
    }

    @Override
    public long countByParam(Map<String, Object> param) {
        // 对数据库进行查询操作。
        // 执行Mapper配置文件中的findCountByParam方法。
        return sessionTemplate.selectOne(getStatement(SQL_COUNT_BY_PARAM), param);
    }

    protected String getStatement(String sqlId) {
        String name = this.getClass().getName();
        StringBuilder sb = new StringBuilder();
        sb.append(name).append(".").append(sqlId);
        return sb.toString();
    }

}

用于分页的拦截器类:

package cn.net.bysoft.owl.bookstore.common.core.dao;

import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;

public abstract class AbstractInterceptor implements Interceptor {

    protected MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource, boolean isCount) {
        Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
                ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        String[] s = ms.getKeyProperties();
        if (s == null) {
            builder.keyProperty(null);
        } else {
            builder.keyProperty(s[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        if (isCount) {
            List<ResultMap> resultMaps = new ArrayList<>();
            resultMaps.add(new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Integer.class,
                    new ArrayList<ResultMapping>()).build());
            builder.resultMaps(resultMaps);
        } else {
            builder.resultMaps(ms.getResultMaps());
        }
        builder.cache(ms.getCache());
        return builder.build();
    }

    public static class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

}
package cn.net.bysoft.owl.bookstore.common.core.dao;

public interface Dialect {

    default boolean supportsLimit() {
        return false;
    }

    default boolean supportsLimitOffset() {
        return supportsLimit();
    }

    default String getLimitString(String sql, int offset, int limit) {
        return getLimitString(sql, offset, Integer.toString(offset), limit, Integer.toString(limit));
    }

    String getLimitString(String sql, int offset, String offsetPlaceholder, int limit, String limitPlaceholder);
    
}
package cn.net.bysoft.owl.bookstore.common.core.dao;

import java.util.Properties;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.log4j.Logger;

@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class }) })
public class ExecutorInterceptor extends AbstractInterceptor {

    private static final Logger LOGGER = Logger.getLogger(ExecutorInterceptor.class);
    private static final int MAPPED_STATEMENT_INDEX = 0;
    private static final int PARAMETER_INDEX = 1;
    private static final int ROWBOUNDS_INDEX = 2;

    private static final String ORDER_BY = "order by";

    private Dialect dialect;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        processIntercept(invocation.getArgs());
        return invocation.proceed();
    }

    private void processIntercept(final Object[] queryArgs) {
        MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        Object parameter = queryArgs[PARAMETER_INDEX];
        final RowBounds rowBounds = (RowBounds) queryArgs[ROWBOUNDS_INDEX];
        int offset = rowBounds.getOffset();
        int limit = rowBounds.getLimit();
        // 分页
        if (dialect.supportsLimit() && (offset != RowBounds.NO_ROW_OFFSET || limit != RowBounds.NO_ROW_LIMIT)) {
            BoundSql boundSql = ms.getBoundSql(parameter);

            String sql = boundSql.getSql().replaceAll("\\s{2,}", " ").trim();
            if (dialect.supportsLimitOffset()) {
                sql = dialect.getLimitString(sql, offset, limit);
                offset = RowBounds.NO_ROW_OFFSET;
            } else {
                sql = dialect.getLimitString(sql, 0, limit);
            }

            limit = RowBounds.NO_ROW_LIMIT;

            queryArgs[ROWBOUNDS_INDEX] = new RowBounds(offset, limit);
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(),
                    boundSql.getParameterObject());
            MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql), false);
            queryArgs[MAPPED_STATEMENT_INDEX] = newMs;

            LOGGER.debug("==>" + sql);

        } else if (parameter instanceof CountParameter) {
            // 获取总数
            parameter = ((CountParameter) parameter).getParameter();
            BoundSql boundSql = ms.getBoundSql(parameter);

            String sql = boundSql.getSql().replaceAll("\\s{2,}", " ").replace(" FROM", " from")
                    .replace("ORDER BY", ORDER_BY).replace("GROUP BY", ORDER_BY).trim();

            if (sql.split("from").length > 2 || sql.split(ORDER_BY).length > 2 || sql.indexOf(ORDER_BY) > -1) {
                sql = "select count(1) from (" + sql + ") tmp";
            } else {
                int fromIndex = sql.indexOf(" from");
                sql = "select count(1)" + sql.substring(fromIndex);

                int orderByIndex = sql.indexOf(ORDER_BY);
                if (orderByIndex > 0) {
                    sql = sql.substring(0, orderByIndex);
                }
            }

            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(),
                    boundSql.getParameterObject());
            MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql), true);
            queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
            queryArgs[PARAMETER_INDEX] = parameter;

            LOGGER.debug("==>" + sql);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String dialectClass = properties.getProperty("dialectClass");
        try {
            dialect = (Dialect) Class.forName(dialectClass).newInstance();
        } catch (Exception e) {
            throw new RuntimeException("方言加载属性异常");
        }

    }

    public static class CountParameter {
        private Object parameter;

        public CountParameter(Object parameter) {
            this.parameter = parameter;
        }

        public Object getParameter() {
            return parameter;
        }
    }

}
package cn.net.bysoft.owl.bookstore.common.core.dao;

public class MySqlDialect implements Dialect {

    @Override
    public String getLimitString(String sql, int offset, String offsetPlaceholder, int limit, String limitPlaceholder) {
        StringBuilder stringBuilder = new StringBuilder(sql);

        stringBuilder.append(" limit ");
        if (offset > 0) {
            stringBuilder.append(offsetPlaceholder);
            stringBuilder.append(",");
        }
        stringBuilder.append(limitPlaceholder);
        return stringBuilder.toString();
    }
}

最后是Mapper和Mybatis.Config配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.net.bysoft.owl.bookstore.service.user.dao.impl.UserDaoImpl">

	<sql id="condition_sql">
		<if test="email != null and email != ''">
			email = #{email}
		</if>
	</sql>

	<resultMap id="userMap" type="User">
		<id column="id" property="id" />
		<result property="email" column="email" />
		<result property="mobile" column="mobile" />
		<result property="password" column="pwd" />
	</resultMap>

	<insert id="insert" parameterType="User" keyProperty="id"
		useGeneratedKeys="true">
		INSERT INTO obs_user (email, mobile, pwd) VALUES
		(#{email}, #{mobile}, #{password})
	</insert>

	<select id="findById" parameterType="Long" resultMap="userMap">
		SELECT id, email, mobile FROM obs_user
		<where>
		  id = #{id}
		</where>
	</select>

	<select id="countByParam" parameterType="java.util.Map"
		resultType="Long">
		SELECT COUNT(0) FROM obs_user
		<where>
			<include refid="condition_sql" />
		</where>
	</select>

</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- 引用系统全局配置文件 -->
	<settings>
		<!-- 这个配置使全局的映射器启用或禁用 缓存 -->
		<setting name="cacheEnabled" value="true" />
		<!-- 全局启用或禁用延迟加载。当禁用时, 所有关联对象都会即时加载 -->
		<setting name="lazyLoadingEnabled" value="false" />
		<!-- 允许或不允许多种结果集从一个单独 的语句中返回(需要适合的驱动) -->
		<setting name="multipleResultSetsEnabled" value="true" />
		<!-- 使用列标签代替列名。 不同的驱动在这 方便表现不同。 参考驱动文档或充分测 试两种方法来决定所使用的驱动 -->
		<setting name="useColumnLabel" value="true" />
		<!-- 允许 JDBC 支持生成的键。 需要适合的 驱动。 如果设置为 true 则这个设置强制 生成的键被使用, 尽管一些驱动拒绝兼 容但仍然有效(比如 
			Derby) -->
		<setting name="useGeneratedKeys" value="false" />
		<!-- 配置默认的执行器。SIMPLE 执行器没 有什么特别之处。REUSE 执行器重用 预处理语句。BATCH 执行器重用语句 和批量更新 -->
		<setting name="defaultExecutorType" value="SIMPLE" />
		<!-- 设置超时时间, 它决定驱动等待一个数 据库响应的时间 -->
		<setting name="defaultStatementTimeout" value="100" />
		<setting name="safeRowBoundsEnabled" value="false" />
		<setting name="mapUnderscoreToCamelCase" value="false" />
		<setting name="localCacheScope" value="SESSION" />
		<setting name="jdbcTypeForNull" value="OTHER" />
		<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
		<!-- 打印查询语句 -->  
        <setting name="logImpl" value="STDOUT_LOGGING" />
	</settings>
</configuration>

Github

https://github.com/XuePeng87/owl-bookstore

转载于:https://my.oschina.net/u/2450666/blog/1489963

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值