自定义持久层框架

环境准备

数据库准备

CREATE DATABASE IF NOT EXISTS `persistent`;
USE `persistent`;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `age` int NULL DEFAULT NULL,
  `sex` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三', 22, '男', '广东省广州市');
INSERT INTO `user` VALUES (2, '李四', 23, '女', '湖北省武汉市');
INSERT INTO `user` VALUES (3, '王五', 30, '男', '广州市深圳市');

SET FOREIGN_KEY_CHECKS = 1;

创建实体

package com.xs.persistent.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author xs
 * @Description User实体类
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    /**
     * 用户id
     */
    private Integer id;

    /**
     * 用户姓名
     */
    private String name;

    /**
     * 用户年龄
     */
    private Integer age;

    /**
     * 用户性别
     */
    private Character sex;

    /**
     * 用户住址
     */
    private String address;

}

导入pom依赖

最好创建父工程:mybatis-study 导入到父工程pom中 其他模块需要导入对应的依赖就好

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.xs</groupId>
    <artifactId>mybatis-study</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>jdbc-study</module>
    </modules>
    <packaging>pom</packaging>
    <description>Mybatis study project!</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <mysql.version>8.0.30</mysql.version>
        <c3p0.version>0.9.1.2</c3p0.version>
        <junit.version>4.13.2</junit.version>
        <dom4j.version>2.1.3</dom4j.version>
        <jaxen.version>2.0.0</jaxen.version>
        <slf4j.version>2.0.6</slf4j.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.18.24</lombok.version>
    </properties>

    <!-- 依赖声明 -->
    <dependencyManagement>
        <dependencies>
            <!-- 数据库驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <!-- 数据库连接池 -->
            <dependency>
                <groupId>c3p0</groupId>
                <artifactId>c3p0</artifactId>
                <version>${c3p0.version}</version>
            </dependency>

            <!-- Junit单元测试 -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>

            <!-- Dom4j -->
            <dependency>
                <groupId>org.dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>${dom4j.version}</version>
            </dependency>

            <!-- jaxen -->
            <dependency>
                <groupId>jaxen</groupId>
                <artifactId>jaxen</artifactId>
                <version>${jaxen.version}</version>
            </dependency>

            <!-- slf4j日志门面 -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>${slf4j.version}</version>
                <type>pom</type>
                <scope>test</scope>
            </dependency>

            <!-- log4j日志实现 -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>

            <!-- slf4j-api -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>${slf4j.version}</version>
            </dependency>

            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.9</version>
            </dependency>

            <!-- lombok -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <scope>provided</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

</project>

重温JDBC

JDBC是我们学习java操作数据库的必回技能, 虽然项目很少使用,但是我们学习的持久层框架底层都是通过JDBC来实现的, 只用掌握JDBC后才能知道框架演变过来时解决了哪些问题方便我们程序员使用,那我们就开始吧!

创建JDBC子模块

创建子模块:jdbc-study 并导入pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mybatis-study</artifactId>
        <groupId>org.xs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>jdbc-study</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

手敲JDBC

jdbc案例 分析jdbc并思考怎么封装持久化框架

package com.xs.jdbc;


import com.xs.jdbc.entity.User;
import java.sql.*;

/**
 * @Author xs
 * @Description jdbc案例 分析jdbc并思考怎么封装持久化框架
 *              总结: 1.连接数据库的时候参数写死 修改需要重新编译 后期不好维护 解决办法: 提取到配置文件 运行时读取配置文件获取数据源
 *                   2. 每一次查询都要获取和关闭资源 数据库多次网络IO 降低性能  解决办法: 使用数据库连接池
 *                   3. 获取结果集后需要手动封装结果集 如果查询很多每次自己封装很费时费力 效率低 解决办法: 通过反射技术拿到要封装的对象class 自动给对象设置属性
 **/
public class JdbcDemo {
    public static void main(String[] args) {

        Connection connection;

        PreparedStatement preparedStatement;

        ResultSet resultSet;

        try {
            //加载驱动  META-INF\services\java.sql.Driver文件下已经指定默认驱动 8.0.0及以后版本可以不写
            Class.forName("com.mysql.cj.jdbc.Driver");
            String url = "jdbc:mysql://127.0.0.1:3306/persistent?useSSL=false&serverTimezone=UTC";
            String user ="root";
            String password = "123456";
            //获取连接
            connection = DriverManager.getConnection(url, user, password);
            //获取预处理对象
            preparedStatement = connection.prepareStatement("select u.id,u.name,u.age,u.sex,u.address from user u where name = ?");
            //设置参数
            preparedStatement.setString(1,"xs");
            //执行sql
            resultSet = preparedStatement.executeQuery();
            //创建要封装的实体
            User u = null;
            //遍历结果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");
                String sex = resultSet.getString("sex");
                String address = resultSet.getString("address");
                u = new User(id,name,age,sex.charAt(0),address);
            }
            //输出结果
            System.out.println(u);
            //关闭资源
            close(connection,preparedStatement,resultSet);
        } catch (Exception e) {
            e.printStackTrace();
            if(e instanceof ClassNotFoundException){
                throw new RuntimeException("获取数据库驱动失败!");
            }
        }
    }

    private static void close(Connection connection,PreparedStatement statement,ResultSet resultSet){
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new RuntimeException("关闭结果集对象异常!");
            }
        }
        
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new RuntimeException("关闭预处理对象异常!");
            }
        }
        
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new RuntimeException("关闭数据库连接异常!");
            }
        }
        
    }
}

总结:
1.连接数据库的时候参数写死 修改需要重新编译 后期不好维护;
解决办法: 提取到配置文件 运行时读取配置文件获取数据源
2. 每一次查询都要获取和关闭资源 数据库多次网络IO 降低性能
解决办法: 使用数据库连接池
3. 获取结果集后需要手动封装结果集 如果查询很多每次自己封装很费时费力 效率低
解决办法: 通过反射技术拿到要封装的对象class 自动给对象设置属性

进入正题: 自定义持久层框架

兄弟们!开始我们自定义框架吧!

敲代码前的思考

我们先整理梳理一下思路 这样写起来就不会毫无头绪,小伙伴们工作的时候最好先理一下思路再开始写会比较顺哦! (推荐使用思维导图这样更加清晰明了)
在这里插入图片描述

创建persistent模块

引入模块所需依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mybatis-study</artifactId>
        <groupId>org.xs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>persistent-study</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!-- 数据库连接池 -->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
        </dependency>

        <!-- Junit单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Dom4j -->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
        </dependency>

        <!-- jaxen -->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
        </dependency>

        <!-- slf4j日志门面 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>

        <!-- log4j日志实现 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </dependency>

        <!-- slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>

        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

User对象记得从JDBC模块copy过来

创建persistentConfig.xml文件

注意mapper会有多个,后边会通过namespace定位到是那一个mapper 这也是面试常问的

<configuration>
   <!--数据库连接属性-->
    <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
    <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1/persistent?useSSL=false&amp;serverTimezone=UTC"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>

    <!--映射mapper文件集合-->
    <mappers>
        <mapper resource="user/UserMapper.xml"/>
    </mappers>
</configuration>


创建UserMapper.xml文件

这里可以通过namespace.id知道具体是哪个sql语句,后边看代码会有更深刻的理解namespace的作用

<mapper namespace="User">
    <select id="selectOne" resultType="com.xs.persistent.entity.User" paramType="com.xs.persistent.entity.User">
        select u.id,u.name,u.age,u.sex,u.address from user u where name = #{name}
    </select>

    <select id="select" resultType="com.xs.persistent.entity.User">
        select u.id,u.name,u.age,u.sex,u.address from user u
    </select>
</mapper>

目录结构
在这里插入图片描述

创建Configuration主配置类

package com.xs.persistent.config;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author xs
 * @Description 主配置类
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Configuration {
    /**
     * 数据源 封装连接数据
     */
    private DataSource dataSource;

    /**
     * 封装多个mapper 装在map里 通过namespace.id获取到对应的执行语句等 key: namespace.id value:MapperStatement
     */
    private Map<String,MapperStatement> mapperStatements = new HashMap<>();
}

}

创建MapperStatement实体

封装mapper.xml中一个执行标签的数据 比如select insert update delete

package com.xs.persistent.config;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author xs
 * @Description 封装mapper.xml中一个执行标签的数据 比如select insert update delete
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperStatement {
    /**
     * 命名空间 后期通过动态代理就必需是Mapper接口的全限定类名
     */
    private String namespace;

    /**
     * id 对应方法名 后期通过动态代理就必需和方法名相同这里先不用纠结
     */
    private String id;

    /**
     * 参数类型
     */
    private Class<?> paramsType;

    /**
     * 结果类型
     */
    private Class<?> resultType;

    /**
     * sql语句 这里是带#{param}的
     */
    private String sql;
}

创建Resource类读取配置文件流

package com.xs.persistent.utils;

import java.io.InputStream;

/**
 * @Author xs
 * @Description 读取配置文件资源类
 **/
public class Resource {
    public static InputStream getResourceAsStream(String path){
        return Resource.class.getClassLoader().getResourceAsStream(path);
    }
}

创建工具类ConvertClass

package com.xs.persistent.utils;


/**
 * @Author xs
 * @Description 通过全限定类名获取class对象
 **/
public class ConvertClass {

    public static Class<?> getClass(String className){
        if (className == null){
            return null;
        }
        Class<?> aClass = null;
        try {
            aClass = Class.forName(className);
            return aClass;
        } catch (Exception e) {
            if(e instanceof ClassNotFoundException){
                throw new RuntimeException("获取class对象失败!");
            }
            e.printStackTrace();
        }
        return aClass;
    }
}

创建SqlSessionFacotryBuilder构建者

package com.xs.persistent.session;

import com.xs.persistent.builder.XMLConfigBuilder;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.session.defaults.DefaultSqlSessionFactory;
import java.io.InputStream;

/**
 * @Author xs
 * @Description SqlSessionFactory构建者类 这里用到了构建者模式 构建的是Configuration类
 **/
public class SqlSessionFactoryBuilder {
    private Configuration configuration;

    /**
     * 初始化的时候创建Configuration对象
     */
    public SqlSessionFactoryBuilder(){
        this.configuration = new Configuration();
    }

    /**
     * 构建SqlSessionFactory对象的方法
     */
    public SqlSessionFactory build(InputStream inputStream){
        XMLConfigBuilder configBuilder = new XMLConfigBuilder(configuration);
        configuration = configBuilder.parse(inputStream);
        return new DefaultSqlSessionFactory(configuration);
    }
}

创建XMLConfigBuilder类

解析persistent主配置文件并把数据封装到Configuration对象中

package com.xs.persistent.builder;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.utils.Resource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;
import java.util.Properties;

/**
 * @Author xs
 * @Description
 **/
public class XMLConfigBuilder {
    private Configuration configuration;

    public XMLConfigBuilder(Configuration configuration){
        this.configuration = configuration;
    }

    /**
     * 解析配置文件 封装数据到Configuration对象中
     * @param resourceAsStream
     * @return configuration
     */
    public Configuration parse(InputStream resourceAsStream){
        //使用dom4j的SAXReader读取文件流
        try {
            //获取配置文件文档对象
            Document document = new SAXReader().read(resourceAsStream);
            //通过xpath表示式获取property节点
            List<Node> nodes = document.selectNodes("//property");
            //创建properties对象
            Properties properties = new Properties();
            //遍历节点
            for (Node node : nodes) {
                Element element = (Element)node;
                //根据属性名获取属性值
                String name = element.attributeValue("name");
                String value = element.attributeValue("value");
                //封装数据到properties中
                properties.setProperty(name,value);
            }
            //创c3p0数据源
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(properties.getProperty("driverClass"));
            dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
            dataSource.setUser(properties.getProperty("username"));
            dataSource.setPassword(properties.getProperty("password"));
            //设置数据源
            configuration.setDataSource(dataSource);
            //获取mapper文件地址 会有多个
            List<Node> mapperNodes = document.selectNodes("//mapper");
            //遍历mapper.xml地址
            for (Node mapperNode : mapperNodes) {
                Element element = (Element) mapperNode;
                String mapperPath = element.attributeValue("resource");
                XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(configuration);
                InputStream in = Resource.getResourceAsStream(mapperPath);
                configuration = mapperBuilder.parse(in);
            }


        } catch (Exception e) {
            e.printStackTrace();
            if(e instanceof DocumentException){
                throw new RuntimeException("读取文件流失败!");
            }
        }
        return configuration;
    }
}


创建XMLMapperBuilder

解析XXXMpper.xml文件中的增删改查标签并封装到MapperStatement对象中
这里只实现了查询 增删改少了结果集封装其实很简单,有兴趣的小伙伴可以去试试!

package com.xs.persistent.builder;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.utils.ConvertClass;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;

/**
 * @Author xs
 * @Description 构建 XXXMapper.xml文件 目前只解析select标签 有兴趣可以自己写一下其他标签的解析 应为少了封装结果集会简单很多
 **/
public class XMLMapperBuilder {
    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration){
        this.configuration = configuration;
    }

    public Configuration parse(InputStream inputStream){
        try {
            Document document = new SAXReader().read(inputStream);
            //获取根节点
            Element rootElement = document.getRootElement();
            //获取命名空间
            String namespace = rootElement.attributeValue("namespace");
            //获取所有select节点
            List<Node> nodes = rootElement.selectNodes("//select");
            for (Node node : nodes) {
                MapperStatement mapperStatement = new MapperStatement();
                Element element = (Element) node;
                //获取类型
                String paramType = element.attributeValue("paramType");
                //获取结果集类型
                String resultType = element.attributeValue("resultType");
                //获取id
                String id = element.attributeValue("id");
                //获取sql 这时候的sql是带#{}的
                String textTrim = element.getTextTrim();
                //设置id
                mapperStatement.setId(id);
                //设置命名空间
                mapperStatement.setNamespace(namespace);
                //设置参数类型
                mapperStatement.setParamsType(ConvertClass.getClass(paramType));
                //设置结果集
                mapperStatement.setResultType(ConvertClass.getClass(resultType));
                //设置sql
                mapperStatement.setSql(textTrim);
                //创建key 唯一定位到mapper中的增删改查标签 这里是定位到具体哪一个select
                String key = namespace+"."+id;
                //设置map map中存了MapperStatement key是namespace.id
                configuration.getMapperStatements().put(key,mapperStatement);
            }
        } catch (Exception e) {
            if(e instanceof DocumentException){
                throw new RuntimeException("读取XXXMapper.xml文件失败");
            }
            e.printStackTrace();
        }
        return configuration;
    }
}


创建SqlSessionFactory工厂类

package com.xs.persistent.session;

/**
 * @Author xs
 * @Description sqlSession工厂接口
 **/
public interface SqlSessionFactory {

    /**
     * 获取sqlSession
     */
    SqlSession openSession();

}

创建DefaultSqlSessionFactory工厂实现类

package com.xs.persistent.session.defaults;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;

/**
 * @Author xs
 * @Description SqlSession工厂接口实现类
 **/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration){
        this.configuration = configuration;
    }


    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}


创建SqlSession接口

封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了

package com.xs.persistent.session;

import java.util.List;

/**
 * @Author xs
 * @Description sqlSession 封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了
 **/
public interface SqlSession {

    /**
     * 查询所有的方法
     */
    <T> List<T> select(String statement, Object... param);

    /**
     * 查询一个的方法
     */
    <T> T selectOne(String statement,Object... param);
}


创建DefaultSqlSession

package com.xs.persistent.session.defaults;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.executor.impl.SimpleExecutor;
import com.xs.persistent.session.SqlSession;
import java.util.List;

/**
 * @Author xs
 * @Description sqlSession接口实现类
 **/
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration){
        this.configuration = configuration;
        this.executor = new SimpleExecutor();
    }

    @Override
    public <T> List<T> select(String statement, Object... param) {
        //获取MapperStatement
        MapperStatement mapperStatement = configuration.getMapperStatements().get(statement);
        List<T> query = executor.query(configuration, mapperStatement, param);
        if (query.size() == 0){
            throw  new RuntimeException("未查到匹配的数据!");
        }
        return query;
    }

    @Override
    public <T> T selectOne(String statement,Object... param) {
        List<Object> select = select(statement, param);
        if(select.size() > 1){
            throw new RuntimeException("查询出现多个值!");
        }
        return (T)select.get(0);
    }
}

创建Executor执行器接口

package com.xs.persistent.executor;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import java.util.List;

/**
 * @Author xs
 * @Description 执行器接口
 **/
public interface Executor {

    <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... param);

}


}

创建SimpleExecutor

在这里执行了JDBC 并拿到Configuration里的数据再通过反射设置参数,封装结果集 理解了这里 整个持久层框架的就很好理解了
其中解析sql的参考解析sql 我也是参考mybatis源码做了精简
代码里try catch有点多是为了方便输出错误 也可以直接方法抛出去 代码看着就不会看着那么乱了

package com.xs.persistent.executor.impl;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.mapping.BoundSql;
import com.xs.persistent.mapping.ParameterMapping;
import com.xs.persistent.parsing.GenericTokenParser;
import com.xs.persistent.parsing.ParameterMappingTokenHandler;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author xs
 * @Description 执行器接口实现类 这里通过执行JDBC获取结果并封装 是最关键的地方
 **/
public class SimpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... param) {
        //获取数据库连接
        Connection connection = getConnection(configuration.getDataSource());
        //获取BoundSql 里边封装了解析好的sql及参数名
        BoundSql boundSql = getBoundSql(mapperStatement.getSql());
        //获取预处理对象
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(boundSql.getSql());
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("创建预处理对象失败!");
        }
        //获取ParameterMapping集合
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //创建结果集合
        List<E> lists = new ArrayList<>();
        //遍历
        for (int i = 0; i < parameterMappings.size(); i++) {
            //获取参数名
            String parameterName = parameterMappings.get(i).getProperty();
            //获取mapper里参数class对象
            Class<?> paramsType = mapperStatement.getParamsType();
            //获取参数名的字段对象
            Field field = null;
            try {
                field = paramsType.getDeclaredField(parameterName);
                //开启暴力反射
                field.setAccessible(true);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                throw new RuntimeException("根据字段名获取字段对象!");
            }
            //获取方法传进来的参数值
            Object o = null;
            try {
                o = field.get(param[0]);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                throw new RuntimeException("获取实体参数值异常!");
            }
            //设置参数
            try {
                statement.setObject(i+1,o);
            } catch (SQLException e) {
                e.printStackTrace();
                throw new RuntimeException("设置参数异常!");
            }
        }

        try {
            //执行sql 获取结果集
            ResultSet resultSet = statement.executeQuery();
            //遍历结果集
            while (resultSet.next()){
                //获取元数据
                ResultSetMetaData metaData = resultSet.getMetaData();
                //获取字段名集合统计数
                int columnCount = metaData.getColumnCount();
                //获取结果class
                Class<?> resultType = mapperStatement.getResultType();
                //创建结果集类型对象
                Object resultObject = resultType.newInstance();
                //遍历
                for (int i = 1; i <= columnCount; i++) {
                    //获取字段名
                    String columnName = metaData.getColumnName(i);
                    //获取字段值
                    Object object = resultSet.getObject(columnName);
                    //获取字段对象
                    Field field = resultType.getDeclaredField(columnName);
                    //开启暴力反射
                    field.setAccessible(true);
                    //获取字段名称
                    String typeName = field.getType().getName();
                    if(typeName.equals("java.lang.Character")){
                        //给char类型的字段赋值 这里获取到的为String 所以要转成char
                        field.set(resultObject,object.toString().charAt(0));
                    }else{
                        //给对象的字段设置值
                        field.set(resultObject,object);
                    }
                }
                //把封装结果放到集合里
                lists.add((E)resultObject);
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("获取结果集异常!");
        } catch (InstantiationException e) {
            e.printStackTrace();
            throw new RuntimeException("创建结果集类型对象异常!");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException("获取字段对象异常!");
        }
        return lists;
    }

    /**
     * 获取连接
     * @param dataSource
     * @return Connection
     */
    private Connection getConnection(DataSource dataSource){
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("获取数据库连接失败!");
        }
        return connection;
    }

    private static BoundSql getBoundSql(String sql){
        //创建BoundSql
        BoundSql boundSql = new BoundSql();
        //创建参数标记处理器
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        //创建通用标记处理器 解析sql 配合参数标记处理器获取解析后的sql和参数名集合
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",tokenHandler);
        //解析
        String parse = genericTokenParser.parse(sql);
        //获取参数集合
        List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
        //设置参数集合
        boundSql.setParameterMappings(parameterMappings);
        //设置解析后的sql 这里就是带#{}替换成?的sql
        boundSql.setSql(parse);
        return boundSql;
    }

}

测试框架

package com.xs.persistent;
import com.xs.persistent.entity.User;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;

/**
 * @Author xs
 * @Description
 **/
public class test {
    private static final SqlSession sqlSession;

    static {
        InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = build.openSession();
    }

    /**
     * 查询单个User
     */
    @Test
    public void test01(){
        User user = new User();
        user.setName("张三");
        Object o = sqlSession.selectOne("User.selectOne",user);
        System.out.println(o);
    }

    /**
     * 查询全部User
     */
    @Test
    public void test02(){
        List<Object> select = sqlSession.select("User.select");
        for (Object o : select) {
            System.out.println(o);
        }
    }
}

输出结果
selectOne
在这里插入图片描述select
在这里插入图片描述

解析sql

解析sql这部分代码都是参考源码精简的 源码更加复杂 有兴趣的朋友可以去研究一下

创建BoundSql

部分源码
在这里插入图片描述

package com.xs.persistent.mapping;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * @Author xs
 * @Description 封装解析后的sql及#{}中参数名
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoundSql {
    /**
     * 解析后的sql
     */
    private String sql;

    /**
     * 解析后的参数集合
     */
    private List<ParameterMapping> parameterMappings;
}

创建ParameterMapping

部分源码
在这里插入图片描述
封装了解析后的sql语句

package com.xs.persistent.mapping;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author xs
 * @Description 封装解析好的sql
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParameterMapping {
    /**
     * 解析好的sql语句
     */
    private String property;
}

创建TokenHandler

部分源码
在这里插入图片描述

package com.xs.persistent.parsing;

/**
 * @Author xs
 * @Description 标记处理接口
 **/
public interface TokenHandler {
    /**
     * 处理方法
     * @param var1
     * @return
     */
    String handleToken(String var1);
}

创建ParameterMappingTokenHandler

部分源码 这里是一个内部类 我们只需要用到ParameterMappingTokenHandler
在这里插入图片描述

package com.xs.persistent.parsing;

import com.xs.persistent.mapping.ParameterMapping;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author xs
 * @Description 标记处理接口实现类
 **/
public class ParameterMappingTokenHandler implements TokenHandler{
    private List<ParameterMapping> parameterMappings;

    public ParameterMappingTokenHandler(){
        parameterMappings = new ArrayList<>();
    }

    @Override
    public String handleToken(String content) {
        //content是参数名 封装到parameterMapping里再添加到集合
        this.parameterMappings.add(this.buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content){
        ParameterMapping parameterMapping = new ParameterMapping();
        parameterMapping.setProperty(content);
        return parameterMapping;
    }

    public List<ParameterMapping> getParameterMappings(){
        return parameterMappings;
    }
}

创建GenericTokenParser

部分源码 主要解析sql的通用标记解析类 解析出sql和参数名集合
在这里插入图片描述

package com.xs.persistent.parsing;

/**
 * @Author xs
 * @Description 通用标记解析器 主要解析sql 把#{}替换成? 把#{}里的参数封装到ParameterMapping里
 *              分别为openToken (开始标记)、closeToken (结束标记)、handler (标记处理器)
 **/
public class GenericTokenParser {
    /**
     * 开始标记
     */
    private final String openToken;
    /**
     * 结束标记
     */
    private final String closeToken;
    /**
     * 标记处理器
     */
    private final TokenHandler handler;

    /**
     * 构造方法 初始化GenericTokenParser
     * @param openToken
     * @param closeToken
     * @param handler
     */
    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
    }

    /**
     * 解析方法 我们可以带着一个单个查询sql来走一遍 比较好理解一点 text: select * from user where name = #{name}
     * @param text XXXMapper.xml里的sql 带#{}
     * @return
     */
    public String parse(String text) {
        //判断处理的sql是否为空
        if (text != null && !text.isEmpty()) {
            //拿到第一个{的下标
            int start = text.indexOf(this.openToken);
            //判断是否包含{
            if (start == -1) {
                //不包含直接返回原sql
                return text;
            } else {
                //包含标记继续处理
                //把sql转为字符数组
                char[] src = text.toCharArray();
                //截取开始下标
                int offset = 0;
                //new一个StringBuilder可变字符串
                StringBuilder builder = new StringBuilder();
                //用来存放#{}的内容
                StringBuilder expression = null;

                do {
                    //判断是否被注释
                    if (start > 0 && src[start - 1] == '\\') {
                        //this open token is escaped.  remove the backslash and continue.
                        builder.append(src, offset, start - offset - 1).append(this.openToken);
                        offset = start + this.openToken.length();
                    } else {
                        if (expression == null) {
                            //new 一个StringBuilder
                            expression = new StringBuilder();
                        } else {
                            //不为空设置长度为0
                            expression.setLength(0);
                        }
                        //获取src字符数组中offset 到start-offset的值包含offset
                        //第一次返回#{之前的字符串
                        builder.append(src, offset, start - offset);
                        //这里返回#{之后的字符的下标
                        offset = start + this.openToken.length();

                        int end;
                        //offset开始拿到结束标记}的下标 等于-1就是不包含
                        for(end = text.indexOf(this.closeToken, offset); end > -1; end = text.indexOf(this.closeToken, offset)) {
                            if (end <= offset || src[end - 1] != '\\') {
                                //返回到#{}中的参数名 这里offset就是#{的后一个字符小标 end是}的下标
                                expression.append(src, offset, end - offset);
                                break;
                            }

                            expression.append(src, offset, end - offset - 1).append(this.closeToken);
                            offset = end + this.closeToken.length();
                        }

                        if (end == -1) {
                            //不包含结束标记 连接上开始标记之后的字符串
                            builder.append(src, start, src.length - start);
                            offset = src.length;
                        } else {
                            //把参数名传递过去 返回?替换掉#{}
                            builder.append(this.handler.handleToken(expression.toString()));
                            //当前结束标记的下标加上当前结束下标的位置 src会通过返回的offset拿到下一个开始标记
                            offset = end + this.closeToken.length();
                        }
                    }

                    start = text.indexOf(this.openToken, offset);
                } while(start > -1);

                if (offset < src.length) {
                    //如果最后截取小标小于sql字符数组长度就加上结束标签后边的字符串
                    builder.append(src, offset, src.length - offset);
                }

                //返回处理好的sql
                return builder.toString();
            }
        } else {
            return "";
        }
    }
}


使用动态代理实现查询

工作中我们都有一个mapper接口来操作数据库 但是我们会看到并有实现类 可能大家会很困惑 mybaits里其实使用的动态代理来创建我们的mapper接口实现类 具体就是从sqlSession的getMapper()方法获取到的

getMapper()

在SqlSession接口创建getMapper()方法并在DefaultSqlSession中实现

SqlSession (动态代理)

package com.xs.persistent.session;

import java.util.List;

/**
 * @Author xs
 * @Description sqlSession 封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了
 **/
public interface SqlSession {

    /**
     * 查询所有的方法
     */
    <T> List<T> select(String statement, Object... param);

    /**
     * 查询一个的方法
     */
    <T> T selectOne(String statement,Object... param);

    /**
     * 获取mapper接口实现类
     */
    <T> T getMapper(Class<T> aClass);
}

DefaultSqlSession (动态代理)

package com.xs.persistent.session.defaults;

import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.executor.impl.SimpleExecutor;
import com.xs.persistent.session.SqlSession;

import java.lang.reflect.*;
import java.util.List;
import java.util.Queue;

/**
 * @Author xs
 * @Description sqlSession接口实现类
 **/
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration){
        this.configuration = configuration;
        this.executor = new SimpleExecutor();
    }

    @Override
    public <T> List<T> select(String statement, Object... param) {
        //获取MapperStatement
        MapperStatement mapperStatement = configuration.getMapperStatements().get(statement);
        List<T> query = executor.query(configuration, mapperStatement, param);
        if (query.size() == 0){
            throw  new RuntimeException("未查到匹配的数据!");
        }
        return query;
    }

    @Override
    public <T> T selectOne(String statement,Object... param) {
        List<Object> select = select(statement, param);
        if(select.size() > 1){
            throw new RuntimeException("查询出现多个值!");
        }
        return (T)select.get(0);
    }

    @Override
    public <T> T getMapper(Class<T> aClass) {
        //创建接口实现类
        T t = (T)Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * 获取到namesapce.id  namespace设置为接口全限定类名 id设置为方法名
                 * 这样方法被调用时我们就可以通过namespace.id 拿到MapperStatement 执行具体方法
                 */
                //获取接口权限定类名
                String namespace = aClass.getName();
                //获取方法名
                String id = method.getName();
                String key = namespace+"."+id;
                //拿到方法的返回值 这里为了判断是调用select还是selectOne 源码不是这样写的哦 这里为了方便
                Type genericReturnType = method.getGenericReturnType();
                if(genericReturnType instanceof ParameterizedType){
                    //有泛型
                    //调用select
                    return select(key, args);
                }else{
                    return selectOne(key,args);
                }
            }
        });
        return t;
    }
}

创建UserMapper

package com.xs.persistent.mapper;

import com.sun.org.apache.bcel.internal.generic.Select;
import com.xs.persistent.entity.User;

import java.util.List;

/**
 * @Author xs
 * @Description
 **/
public interface UserMapper {
    List<User> select();

    User selectOne(User user);
}

修改UserMapper.xml中的namespace为接口权限定类名

<mapper namespace="com.xs.persistent.mapper.UserMapper">
    <select id="selectOne" resultType="com.xs.persistent.entity.User" paramType="com.xs.persistent.entity.User">
        select u.id,u.name,u.age,u.sex,u.address from user u where name = #{name}
    </select>

    <select id="select" resultType="com.xs.persistent.entity.User">
        select u.id,u.name,u.age,u.sex,u.address from user u
    </select>
</mapper>

测试

package com.xs.persistent;
import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;

/**
 * @Author xs
 * @Description
 **/
public class test {
    private static final SqlSession sqlSession;

    static {
        InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = build.openSession();
    }

    /**
     * 查询单个User
     */
    @Test
    public void test01(){
        User user = new User();
        user.setName("张三");
        Object o = sqlSession.selectOne("User.selectOne",user);
        System.out.println(o);
    }

    /**
     * 查询全部User
     */
    @Test
    public void test02(){
        List<Object> select = sqlSession.select("User.select");
        for (Object o : select) {
            System.out.println(o);
        }
    }

    /**
     * 动态代理创建接口实现类
     */
    @Test
    public void test03(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = new User();
        user.setName("张三");
        User user01 = mapper.selectOne(user);
        System.out.println("user:"+user);
        List<User> select = mapper.select();
        System.out.println("select:"+select);
    }

}

运行结果
在这里插入图片描述

探究动态代理原理

提出问题 为什么创建的代理类执行方法的时候就能帮我执行getMapper()呢
看源码 当我Proxy.newInstance的时候穿了三个参数 第一个类加载器,通过累加加载器我们可以在java运行时动态的创建类,第二个参数接口class数组,我们这里是new了一个Object数组把UserMpper.class放了进去,Proxy内部会帮我们让生成的代理对象都是实现这些接口的方法 第三个参数InvocationHandler处理程序 源码里这样解释: 处理代理实例方法调用并返回,其实就是代理实例调用时候就会调用我们实现InvocationHandle.invoke()方法,这里大家可能不好理解, 我们来看看生成的代理对象就知道了;

*全局搜索ProxyGenerator
在这里插入图片描述找到方框里的内容复制在这里插入图片描述sun.misc.ProxyGenerator.saveGeneratedFiles

创建GetProxy

import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;

import java.io.InputStream;
import java.util.List;

/**
 * @Author xs
 * @Description
 **/
public class GetProxy {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = build.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = new User();
        user.setName("张三");
        User user01 = mapper.selectOne(user);
        System.out.println("user:"+user);
        List<User> select = mapper.select();
        System.out.println("select:"+select);
    }
}

查看代理对象

会生成在项目根目录
在这里插入图片描述

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.proxy;

import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;

public final class $Proxy2 extends Proxy implements UserMapper {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m4;
    private static Method m0;

    public $Proxy2(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final User selectOne(User var1) throws  {
        try {
            return (User)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final List select() throws  {
        try {
            return (List)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.xs.persistent.mapper.UserMapper").getMethod("selectOne", Class.forName("com.xs.persistent.entity.User"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.xs.persistent.mapper.UserMapper").getMethod("select");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

分析原理

可以看到这个代理类实现了UserMapper接口并实现了接口方法,
继承了Proxy对象, 还记得我们创建代理对象传的第三个参数吗 处理调用接口实现类 他会被赋值到proxy的Invocationhandler属性上
在这里插入图片描述然后代理对象的每个方法里都都用了我们写InvocationHandler的invoke方法
在这里插入图片描述然后传入了三个参数 this 就是当前代理类的引用,第二个参数是当前方法的Method对象(见下方图片),第三个参数就是方法的参数 这时候我们是不是就豁然开朗了,当我们处理invoke方法时也能游刃有余,不会感觉迷迷糊糊了
在这里插入图片描述在这里插入图片描述
框架源码更加复杂,有兴趣可以研究一下哦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值