1. 程式人生 > 實用技巧 >5、手寫程式碼實現MyBatis的查詢所有功能

5、手寫程式碼實現MyBatis的查詢所有功能

專案準備

1、新增相關依賴

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.3</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

2、resources根目錄下建立全域性配置檔案 SqlMapConfiguration.xml

<?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>
    <!--環境配置-->
    <environments default="mysql">
        <!--配置資料庫的環境-->
        <environment id="mysql">
            <!--配置事務型別-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置資料來源(連線池)-->
            <dataSource type="POOLED">
                <!--配置連線資料庫的四個基本資訊-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--url中的mybatis就是資料庫名稱,serverTimezone是時區,沒有配置時可能會丟擲異常-->
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="12345"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/example/dao/UserMapper.xml"/>
    </mappers>
</configuration>

3、建立對映檔案 UserMapper.xml

<?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">
<!--namespace填寫的是Mapper介面的全限定名-->
<mapper namespace="com.example.dao.UserMapper">
    <!--
        id必須與介面的方法名一致
        select語句會返回查詢結果,所以需要配置一個resultType將查詢結果賦值給物件的屬性
    -->
    <select id="findAll" resultType="com.example.pojo.User">
        select * FROM user
    </select>
</mapper>

4、建立UserMapper介面(介面檔案和對映檔案的包結構保證相同)

public interface UserMapper {
    List<User> findAll();
}

5、建立POJO類接收返回的資料 User

package com.example.pojo;

import java.util.Date;

public class User {
    private Integer id;
    private String username;
    private Date birthday;
    private String gender;
    private String address;
      //省略了getter、setter和toString方法
 }

6、編寫測試類是否可以執行

public class UserMapperTest {
    @Test
    public void testFindAll() throws IOException {
        //    1、讀取配置檔案(從類路徑下開始讀取)
        InputStream in = Resources.getResourceAsStream("SqlMapConfiguration.xml");
        //    2、建立SqlSessionFactory工廠
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(in);
        //    3、使用工廠生產SqlSession物件
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //    4、使用SQLSession建立Dao介面的代理物件
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //    5、使用代理物件執行方法
        List<User> users = userMapper.findAll();
        for (User user: users
                ) {
            System.out.println(user);
        }
        //     6、釋放資源
        sqlSession.close();
        in.close();

    }
}

編寫MyBatis實現的查詢方法的程式碼

1、去除mybatis的jar包

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

去除相關依賴後,測試程式碼報紅,如下:

這說明缺乏MyBatis提供的類物件

還有,因為沒有提供MyBatis的jar支援,所以全域性配置檔案和對映檔案上的MyBatis約束不能用了,直接刪掉即可

public class Resources
public class SqlSessionFactoryBuilder
public interface SqlSessionFactory
public interface SqlSession

2、建立Resources類

/**
 * 使用類載入器讀取配置檔案的類
 */
public class Resources {
    /**
     * 根據傳入的引數,獲取一個位元組輸入流
     * 通過當前類的位元組碼獲取類載入器,並根據類載入器讀取配置檔案從而獲得一個流
     * @param globalXmlFileName
     * @return
     */
    public static InputStream getResourceAsStream(String globalXmlFileName){
        return Resources.class.getClassLoader().getResourceAsStream(globalXmlFileName);

    }
}

匯入自己建立的Resources類,效果如下:

3、建立SqlSessionFactoryBuilder類,
通過該物件的build方法傳入全域性配置檔案的輸入流,建立SqlSessionFactory物件

/**
 * 用於常見一個sqlSessionFactory物件
 */
public class SqlSessionFactoryBuilder {
    /**
     * 根據引數的位元組輸入流來構建一個SQLSessionFactory工廠
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream){
        return null;
    };
}

此時沒有SqlSessionFactory類,開發工具提示無法解析

導包

4、建立SqlSessionFactory介面

public interface SqlSessionFactory {
    /**
     * 用於開啟一個新的sqlSession物件
     */
    SqlSession openSession();

}

此時沒有 SqlSession,開發工具提示無法解析

導包

5、建立 SqlSession介面
SqlSession需要提供getMapper方法來建立代理物件
提供close方法接收與資料庫的會話

/**
 * 自定義MyBatis中和資料庫互動的核心類
 * 它裡面可以建立dao介面的代理物件
 */

public interface SqlSession {
    /**
     * 根據引數建立一個代理物件
     * @param daoInterfaceClass dao的介面位元組碼
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);

    /**
     * 釋放資源
     */
    void close();
}

導包

全部所需的類都準備完成,還需要實現每個類的功能和職責

3、實現類的功能

1、建立解析xml讀取配置資訊的類,

public class XMLConfigBuilder {

    /**
     * 解析主配置檔案,把裡面的內容填充到DefaultSqlSession所需要的地方
     * 使用的技術:
     *      dom4j+xpath
     */
    public static Configuration loadConfiguration(InputStream config){
        try{
            //定義封裝連線資訊的配置物件(mybatis的配置物件)
            Configuration cfg = new Configuration();

            //1.獲取SAXReader物件
            SAXReader reader = new SAXReader();
            //2.根據位元組輸入流獲取Document物件
            Document document = reader.read(config);
            //3.獲取根節點
            Element root = document.getRootElement();
            //4.使用xpath中選擇指定節點的方式,獲取所有property節點
            List<Element> propertyElements = root.selectNodes("//property");
            //5.遍歷節點
            for(Element propertyElement : propertyElements){
                //判斷節點是連線資料庫的哪部分資訊
                //取出name屬性的值
                String name = propertyElement.attributeValue("name");
                if("driver".equals(name)){
                    //表示驅動
                    //獲取property標籤value屬性的值
                    String driver = propertyElement.attributeValue("value");
                    cfg.setDriver(driver);
                }
                if("url".equals(name)){
                    //表示連線字串
                    //獲取property標籤value屬性的值
                    String url = propertyElement.attributeValue("value");
                    cfg.setUrl(url);
                }
                if("username".equals(name)){
                    //表示使用者名稱
                    //獲取property標籤value屬性的值
                    String username = propertyElement.attributeValue("value");
                    cfg.setUsername(username);
                }
                if("password".equals(name)){
                    //表示密碼
                    //獲取property標籤value屬性的值
                    String password = propertyElement.attributeValue("value");
                    cfg.setPassword(password);
                }
            }
            //取出mappers中的所有mapper標籤,判斷他們使用了resource還是class屬性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            //遍歷集合
            for(Element mapperElement : mapperElements){
                //判斷mapperElement使用的是哪個屬性
                Attribute attribute = mapperElement.attribute("resource");
                if(attribute != null){
                    System.out.println("使用的是XML");
                    //表示有resource屬性,用的是XML
                    //取出屬性的值
                    String mapperPath = attribute.getValue();//獲取屬性的值
                    //把對映配置檔案的內容獲取出來,封裝成一個map
                    Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath);
                    //給configuration中的mappers賦值
                    cfg.setMappers(mappers);
                }
            }
            //返回Configuration
            return cfg;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            try {
                config.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }

    }

    /**
     * 根據傳入的引數,解析XML,並且封裝到Map中
     * @param mapperPath    對映配置檔案的位置
     * @return  map中包含了獲取的唯一標識(key是由dao的全限定類名和方法名組成)
     *          以及執行所需的必要資訊(value是一個Mapper物件,裡面存放的是執行的SQL語句和要封裝的實體類全限定類名)
     */
    private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
        InputStream in = null;
        try{
            //定義返回值物件
            Map<String,Mapper> mappers = new HashMap<String,Mapper>();
            //1.根據路徑獲取位元組輸入流
            in = Resources.getResourceAsStream(mapperPath);
            //2.根據位元組輸入流獲取Document物件
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //3.獲取根節點
            Element root = document.getRootElement();
            //4.獲取根節點的namespace屬性取值
            String namespace = root.attributeValue("namespace");//是組成map中key的部分
            //5.獲取所有的select節點
            List<Element> selectElements = root.selectNodes("//select");
            //6.遍歷select節點集合
            for(Element selectElement : selectElements){
                //取出id屬性的值      組成map中key的部分
                String id = selectElement.attributeValue("id");
                //取出resultType屬性的值  組成map中value的部分
                String resultType = selectElement.attributeValue("resultType");
                //取出文字內容            組成map中value的部分
                String queryString = selectElement.getText();
                //建立Key
                String key = namespace+"."+id;
                //建立Value
                Mapper mapper = new Mapper();
                mapper.setQueryString(queryString);
                mapper.setResultType(resultType);
                //把key和value存入mappers中
                mappers.put(key,mapper);
            }
            return mappers;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            in.close();
        }
    }
}

該類需要提供以下依賴

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.1</version>
</dependency>
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

該類需要提供Configuration類

通過

獲取全域性配置檔案中的properties配置,並設定其資料來源

2、建立Configuration類

public class Configuration {

    private String driver;
    private String url;
    private String username;
    private String password;

    private Map<String, Mapper> mappers = new HashMap<>();

    public Map<String, Mapper> getMappers() {
        return mappers;
    }

    public void setMappers(Map<String, Mapper> mappers) {
        this.mappers.putAll(mappers);
    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

此時解析xml的配置類還需要提供mapper類

3、建立Mapper類

這時的Configuration類還缺少setMappers方法

新增方法

 private Map<String, Mapper> mappers;

  public Map<String, Mapper> getMappers() {
      return mappers;
  }

  public void setMappers(Map<String, Mapper> mappers) {
      this.mappers.putAll(mappers);
  }

SqlSessionFactoryBuilder通過該解析類建立SqlSessionFactory物件

/**
 * 用於常見一個sqlSessionFactory物件
 */
public class SqlSessionFactoryBuilder {
    /**
     * 根據引數的位元組輸入流來構建一個SQLSessionFactory工廠
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream){
        Configuration cfg = XMLConfigBuilder.loadConfiguration(inputStream);
        return new DefaultSqlSessionFactory(cfg);
    };
}

DefaultSqlSessionFactory是SqlSessionFactory的實現類

4、建立SqlSessionFactory實現類DefaultSqlSessionFactory

/**
 * SqlSessionFactory介面的實現類
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory{

    private Configuration configuration;

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

    /**用於獲取一個新的操作資料庫的物件
     * 該方法需要提供資料庫連線
     * @return
     */
    @Override
    public SqlSession openSession() {
        return new DefaultSession(configuration);
    }
}

此時需要建立SqlSession的實現類

建立SqlSession實現類

public class DefaultSession implements SqlSession{

    private Configuration configuration;
    private Connection connection;

    public DefaultSession(Configuration configuration) {
        this.configuration = configuration;
        connection = DataSourceUtils.getConnection(configuration);
    }

    /**
     * 建立代理物件
     * @param daoInterfaceClass dao的介面位元組碼
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        //建立代理物件

        return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(), new Class[]{daoInterfaceClass}, new MapperProxy(configuration.getMappers(),connection));
    }

    /**
     * 釋放資源
     */
    @Override
    public void close() {
        try {
            if(connection!=null){
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

通過DataSourceUtils獲取連線

public class DataSourceUtils {

    public static Connection getConnection(Configuration configuration)  {
        try {
            Class.forName(configuration.getDriver());
            return DriverManager.getConnection(configuration.getUrl(),configuration.getUsername(),configuration.getPassword());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

建立MapperProxy類該類實現的InvocationHandler,GetMapping方法的實現與Spring的AOP實現是原理是相似的

public class MapperProxy implements InvocationHandler{

    private Map<String, Mapper> mappers;
    private Connection connection;

    public MapperProxy(Map<String, Mapper> mappers, Connection connection) {
        this.mappers = mappers;
        this.connection = connection;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.獲取方法名
        String methodName = method.getName();
        System.out.println(methodName);
        //2.獲取方法所在類的名稱
        String className = method.getDeclaringClass().getName();
        System.out.println(className);
        //3.組合key
        String key = className+"."+methodName;
        //4.獲取mappers中的Mapper物件
        Mapper mapper = mappers.get(key);
        System.out.println(mapper);
        if(mapper==null){
            throw new IllegalArgumentException("傳入引數有誤");
        }
        //呼叫工具類查詢所有
        return new Executor().selectList(mapper,connection);
    }
}
public class Executor {

    public <E> List<E> selectList(Mapper mapper, Connection conn) {
        PreparedStatement pstm = null;
        ResultSet rs = null;
        try {
            //1.取出mapper中的資料
            String queryString = mapper.getQueryString();//select * from user
            String resultType = mapper.getResultType();
            Class domainClass = Class.forName(resultType);
            //2.獲取PreparedStatement物件
            pstm = conn.prepareStatement(queryString);
            //3.執行SQL語句,獲取結果集
            rs = pstm.executeQuery();
            //4.封裝結果集
            List<E> list = new ArrayList<E>();//定義返回值
            while(rs.next()) {
                //例項化要封裝的實體類物件
                E obj = (E)domainClass.newInstance();

                //取出結果集的元資訊:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                //取出總列數
                int columnCount = rsmd.getColumnCount();
                //遍歷總列數
                for (int i = 1; i <= columnCount; i++) {
                    //獲取每列的名稱,列名的序號是從1開始的
                    String columnName = rsmd.getColumnName(i);
                    //根據得到列名,獲取每列的值
                    Object columnValue = rs.getObject(columnName);
                    //給obj賦值:使用Java內省機制(藉助PropertyDescriptor實現屬性的封裝)
                    PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:實體類的屬性和資料庫表的列名保持一種
                    //獲取它的寫入方法
                    Method writeMethod = pd.getWriteMethod();
                    //把獲取的列的值,給物件賦值
                    writeMethod.invoke(obj,columnValue);
                }
                //把賦好值的物件加入到集合中
                list.add(obj);
            }
            return list;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            release(pstm,rs);
        }
    }


    private void release(PreparedStatement pstm,ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }

        if(pstm != null){
            try {
                pstm.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}