三、持久层框架设计实现及MyBatis源码分析-自定义持久层框架(三)

在前一章节,我们对自定义持久层框架使用端的编写,这一章节我们接着来编写自定义持久层框架本身,在编写之前,我们先来对第一章节中分析出来编写自定义持久层框架的基本思路进行一个回顾:

自定义持久层框架的本质是对JDBC代码进行了封装,所以底层执行的还是JDBC代码,JDBC代码想要正常执行,两部分信息必不可少,一部分是数据库配置信息,还有一部分是SQL配置信息,面这两部份信息,已经被使用端使用两个配置文件进行了提供,所以自定义持久层框架需要做的事情就是使用dom4j来对使用端提供的两个配置文件进行解析,并将解析出来的信息封装到Configuration(核心配置类)MappedStatement(映射配置类)这两个javaBean中,然后这两个javeBean可以作为参数,进行层层传递,传递到SimpleExecutorquery方法当中,而query在JDBC代码执行过程中,会取出Configuration和MappedStatement这两个配置文件所封装的信息进行使用

既然自定义框架,要使用dom4j对配置文件进行解析,那么持久层框架第一步要做的事情把使用端提供的两个配置文件进行加载,将它们加载成字节输入流程存储在内存当中,以便后期我们使用dom4j进行解析,那如何去加载配置文件呢,我们先创建一个Resources类,在这个类中编写一个InputStream getResourceAsStream(String path)方法,根据这个路径我们将两个配置文件加载出来存放到内存当中,同样我们新建一个工程来进行编写

一、解析配置文件

持久层框架中的pom.xml文件

<?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>study.lagou.com</groupId>
    <artifactId>persistence</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>


</project>

Resources类

package study.lagou.com.io;

import java.io.InputStream;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 20:45
 */
public class Resources {

    //根据配置文件路径,将配置文件加载成字节输入流,存放到配置文件当中
    public static InputStream getResourceAsStream(String path){
        //借助类加载器,通过path对配置文件进行加载
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
    
}

编写完成这个类,我们在使用端编写一个测试类,对该方法进行一下测试,因为在使用端调用持久层框架中的方法,所以我们需要先在使用端引入持久层框架中的jar包(注意先安装持久层框架的代码到本地),然后修改使用端使用端配置文件

打包持久层框架到本地maven仓库.png

使用端pom.xml文件中引入持久屋框架依赖

<?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>study.lagou.com</groupId>
    <artifactId>persistence-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>study.lagou.com</groupId>
            <artifactId>persistence</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

接着在使用端项目中编写测试类

package study.lagou.com.test;

import study.lagou.com.io.Resources;

import java.io.InputStream;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 21:19
 */
public class PersistenceTest {

    public void test(){
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    }
}

二、创建两个javaBean容器对象

接下来我们先来定义两个javaBean

Configuration核心配置类:主要用于存储数据库配置文件driverClass、jdbcUrl、username、password信息和mapper.xml的全路径,而数据库连接配置的4个信息,可以直接用javax.sql包中的DataSource类进行存储,mapper.xml文件的全路径我们通过Map<String,MappedStatement>的形式来存储,这个Map中key通过statementId来进行标识,在第一章节中分析的由mapper.xml的namespace.id组成,value是封装好的MappedStatement对象

MappedStatement映射配置文件类:主要用于存储mapper.xml配置文件中每一条SQL语句的id、resultType、paramterType、sql语句信息

以便后面的方法中解析出两个配置文件,并封装到这两个javaBean中

首先我们先来创建MappedStatement

package study.lagou.com.pojo;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 21:58
 */
public class MappedStatement {
    /**
     * mapper.xml文件为SQL对应的id属性
     */
    private String id;
    /**
     * 返回值类型
     */
    private String resultType;
    /**
     * 参数类型
     */
    private String paramterType;
    /**
     * sql语句
     */
    private String sql;

    public String getId() {
        return id;
    }

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

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getParamterType() {
        return paramterType;
    }

    public void setParamterType(String paramterType) {
        this.paramterType = paramterType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }
}

再创建Configuration类

package study.lagou.com.pojo;

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

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 22:03
 */
public class Configuration {

    /**
     * 数据库连接配置信息
     */
    private DataSource dataSource;

    /**
     * key:通过statementId来进行标识,在第一章节中分析的由mapper.xml的namespace.id组成
     * value:封装好的MappedStatement对象
     */
    private Map<String,MappedStatement> mappedStatementMap = new HashMap<>();

    public DataSource getDataSource() {
        return dataSource;
    }

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

    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }

    public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
        this.mappedStatementMap = mappedStatementMap;
    }
}

三、通过dom4j解析配置文件

1、解析sqlMapConfig.xml配置文件

(1)、由于解析配置文件,需要使用到dom4j,将数据库连接解析到数据库连接池并放到java.sql包的DataSource对象中,我们先在持久层框架中引入相应的依赖包

<?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>study.lagou.com</groupId>
    <artifactId>persistence</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>

        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
    </dependencies>
</project>

(2)、创建SqlSessionFactoryBuilder对象

package study.lagou.com.sqlSession;

import study.lagou.com.config.XMLConfigBuilder;

import java.io.InputStream;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 22:49
 */
public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream inputStream){
        //1、解析配置文件,将配置文件封装到Configuration对象当中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();


        //2、创建sqlSessionFactory
        return null;
    }
}

SqlSessionFactory接口

package study.lagou.com.sqlSession;

public interface SqlSessionFactory {
}

XMLConfigBuilder类

package study.lagou.com.config;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import study.lagou.com.io.Resources;
import study.lagou.com.pojo.Configuration;

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

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 22:56
 */
public class XMLConfigBuilder {

    private Configuration configuration;

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

    /**
     * 该方法就是使用dom4j对配置文件进行解析,然后将配置文件信息封装到Configuration类中
     * @param inputStream
     * @return
     * @throws DocumentException
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        //通过dom4j包中的SAXReader类对读取出来字节输入流进行解析
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        //通过//这种表示方法,不论property属性在xml文件中的哪一些都可以直接读取到,通过rootElement.selectNodes方法
        //可以获取到对应配置文件中属性为property的所有Element
        List<Element> list = rootElement.selectNodes("//property");
        //这里我们定义一个Properties对象来存储配置文件中读取出来的元素,可以存储key和value的对象还可以是Map,但是我们
        //这里使用Properties来存储,因为后面创建数据库连接池会使用到
        Properties properties = new Properties();
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.put(name,value);
        }

        //创建数据库连接池对象
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));

        //设置Configuration对象中的DataSource属性
        configuration.setDataSource(comboPooledDataSource);

        //解析mapper.xml文件,首先获取到路径,然后通过路径将配置文件加载成字节输入流再用dom4j进行解析
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
            xmlMapperBuilder.parse(resourceAsStream);
        }

        return configuration;
    }
}

XMLMapperBuilder类

package study.lagou.com.config;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;

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

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 23:32
 */
public class XMLMapperBuilder {

    private Configuration configuration;

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

    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");

        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramterType = element.attributeValue("paramterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParamterType(paramterType);
            mappedStatement.setSql(sqlText);
            String statementId = namespace+"."+id;
            configuration.getMappedStatementMap().put(statementId,mappedStatement);
        }
    }
}

四、创建SqlSessionFactory及SqlSessionFactory实现类,生产SqlSession

前面部分代码我们将解析sqlMapConfig.xml文本解析封装到Configuration对象的工作我处理完成,接下我们开始第二步操作,创建SqlSessionFactory及实现类DefaultSqlSessionFactory,生产SqlSession,在编写SqlSessionFactory接口的openSession方法之前,我们先定义一个空的SqlSession接口及SqlSession的实现类DefaultSqlSession,因为openSession方法需要返回一个SqlSession对象

SqlSession接口

package study.lagou.com.sqlSession;

public interface SqlSession {

}

SqlSession接口实现类

package study.lagou.com.sqlSession;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 21:24
 */
public class DefaultSqlSession implements SqlSession {
}

接着编写SqlSessionFactory接口的openSession方法
SqlSessionFactory接口

package study.lagou.com.sqlSession;
public interface SqlSessionFactory {
    SqlSession openSession();
}

再下一步编写SqlSessonFactory实现类,DefaultSqlSessionFactory

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 21:23
 */
public class DefaultSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

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

SqlSessionFactory接口方法编写完成后,注意将SqlSessionFactoryBuilder类进行完善

package study.lagou.com.sqlSession;

import org.dom4j.DocumentException;
import study.lagou.com.config.XMLConfigBuilder;
import study.lagou.com.pojo.Configuration;

import java.beans.PropertyVetoException;
import java.io.InputStream;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 22:49
 */
public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
        //1、解析配置文件,将配置文件封装到Configuration对象当中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        //2、创建sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new DefaultSessionFactory(configuration);
        return sqlSessionFactory;
    }
}


五、在SqlSession接口中定义对数据库的CRUD操作方法selectList、selectOne、update、delete

1、先在SqlSession接口中定义对应的方法

package study.lagou.com.sqlSession;

import java.util.List;

public interface SqlSession {

    /**
     * 查询列表信息
     * @param statementId
     * @param params
     * @param <E>
     * @return
     */
    <E> List<E> selectList(String statementId, Object... params);

    /**
     * 查询单个数据信息
     * @param statementId
     * @param params
     * @param <T>
     * @return
     */
    <T> T selectOne(String statementId,Object... params);

    /**
     * 删除信息
     * @param statementId
     * @param params
     */
    void delete(String statementId,Object... params);

    /**
     * 更新数据信息
     * @param statementId
     * @param params
     */
    void update(String statementId,Object... params);
}

2、在对应的DefaultSqlSession中实现对应的方法,在DefaultSqlSession中可以直接通过JDBC代码进行数据操作,但是我们此处先不这么做,我们将操作JDBC的代码封装到下层级中进行处理,在Executor接口的实现类SimpleExecutor中进行处理

package study.lagou.com.sqlSession;

import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 21:24
 */
public class DefaultSqlSession implements SqlSession {
    @Override
    public <E> List<E> selectList(String statementId, Object... params) {
        return null;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) {
        //selectOne方法和selectList方法操作数据库的方式一致,所以我们此处直接调用selectList方法即可
        List<Object> objects = selectList(statementId, params);
        if(objects.size() == 1){
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("查询到的结果集为空或返回的结果集较多!");
        }
    }

    @Override
    public void delete(String statementId, Object... params) {

    }

    @Override
    public void update(String statementId, Object... params) {

    }
}

3、编写Executor接口及实现类SimpleExecutor,在Executor中定义一个query方法,执行的就是JDBC代码
Executor接口

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;

import java.util.List;

public interface Executor {

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params);

}

SimpleExecutor接口实现类,此处先不对query方法进行具体实现,定义出来即可

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;

import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) {
        return null;
    }
    
}

接下来我们开始完善DefaultSqlSession中的selectList方法,因为在selectList方法在调用Executor中的query方法,需要传递Configuration参数,所以我们先对DefaultSqlSession进行一个调整,将Configuration在构造方法中传递进来

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;

import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 21:24
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

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

    @Override
    public <E> List<E> selectList(String statementId, Object... params) {
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> query = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) query;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) {
        //selectOne方法和selectList方法操作数据库的方式一致,所以我们此处直接调用selectList方法即可
        List<Object> objects = selectList(statementId, params);
        if(objects.size() == 1){
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("查询到的结果集为空或返回的结果集较多!");
        }
    }

    @Override
    public void delete(String statementId, Object... params) {

    }

    @Override
    public void update(String statementId, Object... params) {

    }
}

DefaultSqlSessionFactory中,生产SqlSession的openSession方法也需要跟着调整

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 21:23
 */
public class DefaultSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

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

六、Executor中query方法的具体编写

简单的思路分析,持久层框架本质其实就是对JDBC代码进行封装,所以在此query方法中其实也就是执行JDBC代码操作,针对JDBC代码操作方式还是比较固定的

1、注册驱动,获取连接

package study.lagou.com.sqlSession;

import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;

import java.sql.Connection;
import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1、注册驱动、获取连接
        Connection connection = configuration.getDataSource().getConnection();
        return null;
    }
}

2、获取SQL语句,并转换SQL,在转换的过程当中需要对#{}里面的值进行解析存储,在解析的过程当中我们会使用到MyBatis源码中的标记解析器,标记解析器中的代码我直接复制到项目中即可

2.1、TokenHandler标记解析器接口
/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package study.lagou.com.utils;

/**
 * @author Clinton Begin
 */
public interface TokenHandler {
  String handleToken(String content);
}
2.2、ParameterMapping类,主要用于将#{}中的每一个属性都解析出来,直接封装成一个对象
package study.lagou.com.utils;

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}
2.3、ParameterMappingTokenHandler标记处理类
package study.lagou.com.utils;

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




public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context是参数名称 #{id} #{username}

    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

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

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

    public void setParameterMappings(List<ParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }

}

2.4、GenericTokenParser标记解析器,该标记解析器只提供一个有参数构造
/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package study.lagou.com.utils;

/**
 * @author Clinton Begin
 */
public class GenericTokenParser {

  private final String openToken; //开始标记
  private final String closeToken; //结束标记
  private final TokenHandler handler; //标记处理器

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
   * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
   */
  public String parse(String text) {
    // 验证参数问题,如果是null,就返回空字符串。
    if (text == null || text.isEmpty()) {
      return "";
    }

    // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
    // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression变量,避免空指针或者老数据干扰。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {////存在结束标记时
          if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在转义字符,即需要作为参数进行处理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根据参数的key(即expression)进行参数处理,返回?作为占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

2.5、定义一个BoundSql类,该类主要用于存储由标记解析器解析处理完成后的SQL和在原始SQL中从#{}解析出来的属性值
package study.lagou.com.config;

import study.lagou.com.utils.ParameterMapping;

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

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 23:11
 */
public class BoundSql {
    /**
     * 解析替换过#{}为?的SQL语句信息
     */
    private String sqlText;

    /**
     * 解析SQL过程中在#{}解析出来的属性值
     */
    private List<ParameterMapping> parameterMappingList = new ArrayList<>();

    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }

    public String getSqlText() {
        return sqlText;
    }

    public void setSqlText(String sqlText) {
        this.sqlText = sqlText;
    }

    public List<ParameterMapping> getParameterMappingList() {
        return parameterMappingList;
    }

    public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
        this.parameterMappingList = parameterMappingList;
    }
}

2.6、通过标记解析器对sql进行解析,对SQL中的#{}用?进行替换,并将#{}中的属性值解析出来存储到BoundSql当中
package study.lagou.com.sqlSession;

import study.lagou.com.config.BoundSql;
import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;
import study.lagou.com.utils.GenericTokenParser;
import study.lagou.com.utils.ParameterMapping;
import study.lagou.com.utils.ParameterMappingTokenHandler;

import java.sql.Connection;
import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1、注册驱动、获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2、获取SQL信息,并对SQL语句进行转换,在转换过程中需要对#{}里面的值进行解析和存储
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        return null;
    }

    /**
     * 完成对#{}的解析工作:1、将#{}使用?进行代替 2、解析出#{}里面的值进行存储
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配合标记处理器完成对占位符的处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //标记处理器中的三个参数分别是开始标记、结束标记和标记处理类信息
        GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //通过标记处理器的parse方法解析出来的SQL
        String parseSql = tokenParser.parse(sql);
        //由标记处理类从#{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappingList = parameterMappingTokenHandler.getParameterMappings();
        //将解析出来的SQL和#{}中的参数封装到BoundSql对象中
        BoundSql boundSql = new BoundSql(parseSql, parameterMappingList);
        return boundSql;
    }

}

3、获取预处理对象:preparedStatement

package study.lagou.com.sqlSession;

import study.lagou.com.config.BoundSql;
import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;
import study.lagou.com.utils.GenericTokenParser;
import study.lagou.com.utils.ParameterMapping;
import study.lagou.com.utils.ParameterMappingTokenHandler;

import java.sql.Connection;
import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1、注册驱动、获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2、获取SQL信息,并对SQL语句进行转换,在转换过程中需要对#{}里面的值进行解析和存储
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3、获取预处理对象preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        return null;
    }

    /**
     * 完成对#{}的解析工作:1、将#{}使用?进行代替 2、解析出#{}里面的值进行存储
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配合标记处理器完成对占位符的处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //标记处理器中的三个参数分别是开始标记、结束标记和标记处理类信息
        GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //通过标记处理器的parse方法解析出来的SQL
        String parseSql = tokenParser.parse(sql);
        //由标记处理类从#{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappingList = parameterMappingTokenHandler.getParameterMappings();
        //将解析出来的SQL和#{}中的参数封装到BoundSql对象中
        BoundSql boundSql = new BoundSql(parseSql, parameterMappingList);
        return boundSql;
    }

}

4、设置参数

package study.lagou.com.sqlSession;

import study.lagou.com.config.BoundSql;
import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;
import study.lagou.com.utils.GenericTokenParser;
import study.lagou.com.utils.ParameterMapping;
import study.lagou.com.utils.ParameterMappingTokenHandler;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1、注册驱动、获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2、获取SQL信息,并对SQL语句进行转换,在转换过程中需要对#{}里面的值进行解析和存储
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3、获取预处理对象preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        //4、设置参数信息
        String paramterType = mappedStatement.getParamterType();
        //4.1、通过参数全路径获取到参数对象
        Class<?> paramterClass = getClassType(paramterType);
        //4.2、通过反射设置参数信息,先获取到标记解析器解析出来的#{}中的属性信息,然后循环遍历进行参数设置
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            //通过反射根据属性名称获取到具体实体类对应的属性
            Field declaredField = paramterClass.getDeclaredField(parameterMapping.getContent());
            //设置对属性值进行暴力访问
            declaredField.setAccessible(true);
            //设置从哪个对象中获取对应的属性值
            Object o = declaredField.get(params[0]);
            //设置preparedStatement对应的参数信息,注意下标从1开始
            preparedStatement.setObject(i+1,o);
        }
        return null;
    }

    /**
     * 通过类路径获取到对应的实体类信息
     * @param paramterType
     * @return
     * @throws ClassNotFoundException
     */
    private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
        if(paramterType != null){
            Class<?> aClass = Class.forName(paramterType);
            return aClass;
        }
        return null;
    }

    /**
     * 完成对#{}的解析工作:1、将#{}使用?进行代替 2、解析出#{}里面的值进行存储
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配合标记处理器完成对占位符的处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //标记处理器中的三个参数分别是开始标记、结束标记和标记处理类信息
        GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //通过标记处理器的parse方法解析出来的SQL
        String parseSql = tokenParser.parse(sql);
        //由标记处理类从#{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappingList = parameterMappingTokenHandler.getParameterMappings();
        //将解析出来的SQL和#{}中的参数封装到BoundSql对象中
        BoundSql boundSql = new BoundSql(parseSql, parameterMappingList);
        return boundSql;
    }

}

5、执行SQL
6、封装返回结果集

package study.lagou.com.sqlSession;

import study.lagou.com.config.BoundSql;
import study.lagou.com.pojo.Configuration;
import study.lagou.com.pojo.MappedStatement;
import study.lagou.com.utils.GenericTokenParser;
import study.lagou.com.utils.ParameterMapping;
import study.lagou.com.utils.ParameterMappingTokenHandler;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-2-22 22:10
 */
public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1、注册驱动、获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2、获取SQL信息,并对SQL语句进行转换,在转换过程中需要对#{}里面的值进行解析和存储
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3、获取预处理对象preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        //4、设置参数信息
        String paramterType = mappedStatement.getParamterType();
        //4.1、通过参数全路径获取到参数对象
        Class<?> paramterTypeClass = getClassType(paramterType);
        //4.2、通过反射设置参数信息,先获取到标记解析器解析出来的#{}中的属性信息,然后循环遍历进行参数设置
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            //通过反射根据属性名称获取到具体实体类对应的属性
            Field declaredField = paramterTypeClass.getDeclaredField(parameterMapping.getContent());
            //设置对属性值进行暴力访问
            declaredField.setAccessible(true);
            //设置从哪个对象中获取对应的属性值
            Object o = declaredField.get(params[0]);
            //设置preparedStatement对应的参数信息,注意下标从1开始
            preparedStatement.setObject(i+1,o);
        }
        //5、执行SQL
        ResultSet resultSet = preparedStatement.executeQuery();
        //6、封装返回结果集
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        ArrayList<Object> objectArrayList = new ArrayList<>();
        while (resultSet.next()){
            Object resultObj = resultTypeClass.newInstance();
            //获取到resultSet的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //获取元数据中的字段名,注意获取字段名的时候下标需要从1开始,所以我们这里的for循环遍历下标是从1开始的
                String columnName = metaData.getColumnName(i);
                //获取字段值
                Object value = resultSet.getObject(columnName);
                //使用反射或内省,根据数据库表和实体的对应关系,完成封装
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                //获取到PropertyDescriptor对象的写方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                //通过写方法将值写入到对象中
                writeMethod.invoke(resultObj,value);
            }
            objectArrayList.add(resultObj);
        }
        return (List<E>) objectArrayList;
    }

    /**
     * 通过类路径获取到对应的实体类信息
     * @param paramterType
     * @return
     * @throws ClassNotFoundException
     */
    private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
        if(paramterType != null){
            Class<?> aClass = Class.forName(paramterType);
            return aClass;
        }
        return null;
    }

    /**
     * 完成对#{}的解析工作:1、将#{}使用?进行代替 2、解析出#{}里面的值进行存储
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配合标记处理器完成对占位符的处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //标记处理器中的三个参数分别是开始标记、结束标记和标记处理类信息
        GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //通过标记处理器的parse方法解析出来的SQL
        String parseSql = tokenParser.parse(sql);
        //由标记处理类从#{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappingList = parameterMappingTokenHandler.getParameterMappings();
        //将解析出来的SQL和#{}中的参数封装到BoundSql对象中
        BoundSql boundSql = new BoundSql(parseSql, parameterMappingList);
        return boundSql;
    }

}

接下来我们在客户端程序中编写测试方法

package study.lagou.com.test;

import org.junit.Test;
import study.lagou.com.io.Resources;
import study.lagou.com.persistence.test.pojo.User;
import study.lagou.com.sqlSession.SqlSession;
import study.lagou.com.sqlSession.SqlSessionFactory;
import study.lagou.com.sqlSession.SqlSessionFactoryBuilder;

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

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-28 21:19
 */
public class PersistenceTest {

    @Test
    public void testOne() throws Exception {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = build.openSession();


        User user = new User();
        user.setId(1);
        user.setUsername("zhangsan");
        User u = sqlSession.selectOne("user.selectOne", user);
        System.out.println(u);
    }

    @Test
    public void testList() throws Exception {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = build.openSession();


        List<User> userList = sqlSession.selectList("user.selectList", null);
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

运行测试得到如下效果

Mybatis自定义框架测试结果.png

附:数据库脚本信息

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名称',
  `password` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
  `nickname` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'zhangsan', '111111', '张三');
INSERT INTO `user` VALUES ('2', 'lisi', '111111', '李四');
INSERT INTO `user` VALUES ('3', 'wangwu', '111111', '王五');

具体代码对应下载地址:https://gitee.com/happymima/mybatis.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容