长文干货 | 手写自定义持久层框架!

2021-01-19 12:06:49 浏览数 (1)

为何要手写自定义持久层框架?

  1. JDBC 编码的弊端
  • 会造成硬编码问题(无法灵活切换数据库驱动) 频繁创建和释放数据库连接造成系统资源浪费 影响系统性能
  • sql 语句存在硬编码,造成代码不易维护,实际应用中 sql 变化可能较大,变动 sql 需要改 Java 代码
  • 使用 preparedStatement 向占有位符号传参数存在硬编码, 因 sql 语句的 where 条件不确定甚至没有where条件,修改 sql 还要修改代码 系统不易维护
  • 对结果集解析也存在硬编码, sql变化导致解析代码变化
  1. 更有助于读 mybatis 持久层框架源码

JDBC代码

代码语言:javascript复制
public class jdbcConnection {
    private static Connection connection = null;
    private static PreparedStatement preparedStatement = null;
    private static ResultSet resultSet = null;

    public static void main(String[] args) {
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 通过驱动管理类获取数据库连接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/huodd", "root", "1234");
            // 定义sql语句 ? 表示占位符
            String sql = "select id,username from user where id = ?";
            // 获取预处理对象 statement
            PreparedStatement preparedStatement = (PreparedStatement) connection.prepareStatement(sql);
            // 设置参数 第一个参数为 sql 语句中参数的序号(从1开始) 第二个参数为 设置的参数值
            preparedStatement.setInt(1, 1);
            // 向数据库发出sql执行查询 查询出结果集
            resultSet = preparedStatement.executeQuery();
            // 遍历查询结果集
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                // 封装对象
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                System.out.println(user);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            try {
                // 释放资源
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

解决问题的思路

  1. 数据库频繁创建连接、释放资源 -> 连接池
  2. sql语句及参数硬编码 -> 配置文件
  3. 手动解析封装结果集 -> 反射、内省

编码前思路整理

  1. 创建、读取配置文件
  • sqlMapConfig.xml 存放数据库配置信息
  • userMapper.xml :存放sql配置信息
  • 根据配置文件的路径,加载配置文件成字节输入流,存储在内存中Resources#getResourceAsStream(String path)
  • 创建两个JavaBean存储配置文件解析出来的内容
    1. Configuration :核心配置类 ,存放 sqlMapConfig.xml解析出来的内容
    2. MappedStatement:映射配置类:存放mapper.xml解析出来的内容

  1. 解析配置文件(使用dom4j)
  • 创建类:SqlSessionFactoryBuilder#build(InputStream in) -> 设计模式之构建者模式
  • 使用dom4j解析配置文件,将解析出来的内容封装到容器对象(JavaBean)中

  1. 创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory
  • SqlSessionFactory对象,生产sqlSession会话对象 -> 设计模式之工厂模式

  1. 创建 SqlSession接口及实现类DefaultSqlSession
  • 定义对数据库的CRUD操作
    1. selectList()
    2. selectOne()
    3. update()
    4. delete()

  1. 创建Executor接口及实现类SimpleExecutor实现类
  • query(Configuration configuration, MappedStatement mapStatement, Object... orgs) 执行的就是JDBC代码

  1. 测试代码

用到的设计模式

  • 构建者模式
  • 工厂模式
  • 代理模式

进入编码

1.创建、读取配置文件

sqlMapConfig.xml 存放数据库配置信息

代码语言:javascript复制
<configuration>
    <dataSource>
        <!-- 引入数据库连接信息 -->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///huodd"></property>
        <property name="user" value="root"></property>
        <property name="password" value="1234"></property>
    </dataSource>

    <!-- 引入sql配置文件 -->
    <mapper resource="userMapper.xml"></mapper>

</configuration>

userMapper.xml 存放sql配置信息

代码语言:javascript复制
<mapper namespace="user">

    <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 user.selectList -->
    <select id="selectList" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User">
        select * from user
    </select>

    <select id="selectOne" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User">
        select * from user where id = #{id} and username =#{username}
    </select>

</mapper>

User.java

代码语言:javascript复制
public class User {
    private Integer id;
    private String username;

    ... 省略getter setter 方法
    ... 省略 toString 方法
}

pom.xml 中引入依赖

代码语言:javascript复制
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.17</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>

创建两个JavaBean对象 用于存储解析的配置文件的内容(Configuration.java、MappedStatement.java)

代码语言:javascript复制
public class Configuration {

    // 数据源
    private DataSource dataSource;
    //map集合 key:statementId value:MappedStatement
    private Map<String,MappedStatement> mappedStatementMap = new HashMap<>();

    ... 省略getter setter 方法
}
代码语言:javascript复制
public class MappedStatement {

    // id
    private String id;
    // sql 语句
    private String sql;
    // 参数值类型
    private Class<?> paramterType;
    // 返回值类型
    private Class<?> resultType;

   ... 省略getter setter 方法
}

创建Resources工具类 并编写静态方法getResourceAsSteam(String path)

代码语言:javascript复制
public class Resources {

    /**
     * 根据配置文件的路径 将配置文件加载成字节输入流 存储在内存中
     * @param path
     * @return InputStream
     */
    public static InputStream getResourceAsStream(String path) {
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

2.解析配置文件(使用dom4j)

创建 SqlSessionFactoryBuilder类 并添加 build 方法

代码语言:javascript复制
public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
        // 1. 使用 dom4j 解析配置文件 将解析出来的内容封装到Configuration中
        XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder();

        // configuration 是已经封装好了sql信息和数据库信息的对象
        Configuration configuration = xmlConfigerBuilder.parseConfig(in);

        // 2. 创建 SqlSessionFactory 对象  工厂类 主要是生产sqlSession会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }
}
代码语言:javascript复制
public class XMLConfigerBuilder {

    private Configuration configuration;

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

    /**
     * 该方法 使用dom4j对配置文件进行解析 封装Configuration
     * @param in
     * @return
     */
     public Configuration parseConfig (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
         Document document = new SAXReader().read(in);
         // <configuation>
         Element rootElement = document.getRootElement();
         List<Element> propertyElements = rootElement.selectNodes("//property");
         Properties properties = new Properties();
         for (Element propertyElement : propertyElements) {
             properties.setProperty(propertyElement.attributeValue("name"), propertyElement.attributeValue("value"));
         }
         // 连接池
         ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
         comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
         comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
         comboPooledDataSource.setUser(properties.getProperty("user"));
         comboPooledDataSource.setPassword(properties.getProperty("password"));

         // 填充 configuration
         configuration.setDataSource(comboPooledDataSource);

         // mapper 部分  拿到路径 -> 字节输入流 -> dom4j进行解析
         List<Element> mapperElements = rootElement.selectNodes("//mapper");
         XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
         for (Element mapperElement : mapperElements) {
             String mapperPath = mapperElement.attributeValue("resource");
             InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
             xmlMapperBuilder.parse(resourceAsStream);
         }

         return configuration;
     }
}
代码语言:javascript复制
public class XMLMapperBuilder {

    private Configuration configuration;

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

    public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException {
        Document document = new SAXReader().read(inputStream);
        // <mapper>
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List<Element> select = rootElement.selectNodes("//select");
        for (Element element : select) {
            // 获取 id 的值
            String id = element.attributeValue("id");
            String paramterType = element.attributeValue("paramterType");
            String resultType = element.attributeValue("resultType");
            // 输入参数 class
            Class<?> paramterTypeClass = getClassType(paramterType);
            // 返回结果 class
            Class<?> resultTypeClass = getClassType(resultType);
            // sql 语句
            String sqlStr = element.getTextTrim();

            // 封装 mappedStatement
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParamterType(paramterTypeClass);
            mappedStatement.setResultType(resultTypeClass);
            mappedStatement.setSql(sqlStr);

            // statementId
            String key = namespace   "."   id;
            // 填充 configuration
            configuration.getMappedStatementMap().put(key, mappedStatement);
        }

    }

    private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
        Class<?> aClass = Class.forName(paramterType);
        return aClass;
    }
}

3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory

代码语言:javascript复制
public interface SqlSessionFactory {
    SqlSession openSession();
}
代码语言:javascript复制
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

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

4. 创建 SqlSession接口及实现类DefaultSqlSession

代码语言:javascript复制
public interface SqlSession {

    <E> List<E> selectList(String statementId, Object... param) throws Exception;

    <T> T selectOne(String statementId, Object... params) throws Exception;

    void close() throws SQLException;
   
}
代码语言:javascript复制
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    // 处理器对象
    private Executor simpleExcutor = new SimpleExecutor();

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

    @Override
    public <E> List<E> selectList(String statementId, Object... param) throws Exception {
        // 完成对 simpleExcutor里的query方法的调用
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<E> list = simpleExcutor.query(configuration, mappedStatement, param);
        return list;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> objects = selectList(statementId, params);
        if (objects.size() == 1) {
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("返回结果过多");
        }
    }

    @Override
    public void close() throws SQLException {
        simpleExcutor.close();
    }

}

5.创建Executor接口及实现类SimpleExecutor实现类

代码语言:javascript复制
public interface Executor {

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception;

    void close() throws SQLException;
}
代码语言:javascript复制
public class SimpleExecutor implements Executor {


    private Connection connection = null;

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

        // select * from user where id = #{id} and username = #{username}
        String sql = mappedStatement.getSql();

        // 对 sql 进行处理
        BoundSql boundSql = getBoundSql(sql);

        // select * from where id = ? and username = ?
        String finalSql = boundSql.getSqlText();

        // 获取传入参数类对象
        Class<?> paramterTypeClass = mappedStatement.getParamterType();

        // 获取预处理 preparedStatement 对象
        PreparedStatement preparedStatement = connection.prepareStatement(finalSql);

        // 设置参数
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i  ) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String name = parameterMapping.getContent();

            // 反射  获取某一个属性对象
            Field declaredField = paramterTypeClass.getDeclaredField(name);
            // 设置暴力访问
            declaredField.setAccessible(true);

            // 参数传递的值
            Object o = declaredField.get(param[0]);
            // 给占位符赋值
            preparedStatement.setObject(i   1, o);

        }

        // 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();

        // 封装返回结果集
        // 获取返回参数类对象
        Class<?> resultTypeClass = mappedStatement.getResultType();
        ArrayList<E> results = new ArrayList<>();
        while (resultSet.next()) {
            // 取出 resultSet的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            E o = (E) resultTypeClass.newInstance();
            int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i  ) {
                // 属性名/字段名
                String columnName = metaData.getColumnName(i);
                // 属性值/字段值
                Object value = resultSet.getObject(columnName);

                // 使用反射或者内省 根据数据库表和实体的对应关系 完成封装
                // 创建属性描述器 为属性生成读写方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                // 获取写方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                // 向类中写入值
                writeMethod.invoke(o, value);
            }
            results.add(o);
        }
        return results;
    }

    /**
     * 转换sql语句 完成对 #{} 的解析工作
     * 1. 将 #{} 使用?进行代替
     * 2. 解析出 #{} 里面的值进行存储
     *
     * @param sql 转换前的原sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        // 标记处理类: 主要是配合通用解析器 GenericTokenParser 类完成对配置文件等的解析工作 其中TokenHandler 主要完成处理
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();

        // GenericTokenParser: 通用的标记解析器 完成了代码片段中的占位符的解析 然后根据给定的标记处理器( TokenHandler ) 来进行表达式的处理

        // 三个参数: 分别为 openToken (开始标记)、 closeToken (结束标记)、 handler (标记处理器)
        GenericTokenParser genericTokenParse = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        // 解析出来的sql
        String parseSql = genericTokenParse.parse(sql);
        // #{} 里面解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();

        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);

        return boundSql;

    }

    @Override
    public void close() throws SQLException {
        connection.close();
    }
}
代码语言:javascript复制
public class BoundSql {
    // 解析过后的 sql 语句
    private String sqlText;

    // 解析出来的参数
    private List<ParameterMapping> parameterMappingList = new ArrayList<>();
    
    // 有参构造方便创建时赋值
    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }

   ... 省略getter setter 方法

}

6.测试代码

代码语言:javascript复制
public class IPersistenceTest {

    @Test
    public void test () throws Exception {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sessionFactory.openSession();

        User user = new User();
        user.setId(1);
        user.setUsername("bd2star");
        User res = sqlSession.selectOne("user.selectOne", user);
        System.out.println(res);
        
        // 关闭资源
       sqlSession.close()
    }
}

运行结果如下

代码语言:javascript复制
User{id=1, username='bd2star'}

测试通过 调整代码

创建 接口 Dao及实现类

代码语言:javascript复制
public interface IUserDao {

    // 查询所有用户
    public List<User> selectList() throws Exception;


    // 根据条件进行用户查询
    public User selectOne(User user) throws Exception;
}
代码语言:javascript复制
public class UserDaoImpl implements IUserDao {
    @Override
    public List<User> findAll() throws Exception {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sessionFactory.openSession();
        List<User> res = sqlSession.selectList("user.selectList");
        sqlSession.close();
        return res;
    }

    @Override
    public User findByCondition(User user) throws Exception {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sessionFactory.openSession();
        User res = sqlSession.selectOne("user.selectOne", user);
        sqlSession.close();
        return res;

    }
}

调整测试方法

代码语言:javascript复制
public class IPersistenceTest {

    @Test
    public void test () throws Exception {
        User user = new User();
        user.setId(1);
        user.setUsername("bd2star");
        IUserDao userDao = new UserDaoImpl();
        User res = userDao.findByCondition(user);
        System.out.println(res);
    }
}

运行结果如下

代码语言:javascript复制
User{id=1, username='bd2star'}

测试通过

7.补充

huodd.sql

代码语言:javascript复制
--新建数据库
CREATE DATABASE huodd;
--使用数据库
use huodd;
--创建表
CREATE TABLE `user`  (
  `id` int(11) NOT NULL,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- 插入测试数据
INSERT INTO `user` VALUES (1, 'bd2star');
INSERT INTO `user` VALUES (2, 'bd3star');

用到的工具类

GenericTokenParser.java

代码语言:javascript复制
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();
  }
}

ParameterMapping.java

代码语言:javascript复制
public class ParameterMapping {

    private String content;

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

    ... 省略getter setter 方法
}

ParameterMappingTokenHandler.java

代码语言:javascript复制
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;
   }

}

TokenHandler.java

代码语言:javascript复制
public interface TokenHandler {
  String handleToken(String content);
}

继续优化自定义框架

通过上述自定义框架,我们解决了JDBC操作数据库带来的一些问题,例如频繁创建释放数据库连接,硬编码,手动封装返回结果等问题

但从测试类可以发现新的问题

  • dao 的实现类存在重复代码 整个操作的过程模板重复 (如创建 SqlSession 调用 SqlSession方法 关闭 SqlSession)
  • dao 的实现类中存在硬编码,如调用 sqlSession 方法时 参数 statementId 的硬编码

解决方案

  • 通过代码模式来创建接口的代理对象

1.添加getMapper方法

删除dao的实现类 UserDaoImpl.java 我们通过代码来实现原来由实现类执行的逻辑

在 SqlSession 中添加 getMapper 方法

代码语言:javascript复制
public interface SqlSession {
   <T> T getMapper(Class<?> mapperClass);
}

2. 实现类实现方法

DefaultSqlSession 类中实现 getMapper 方法

代码语言:javascript复制
@Override
public <T> T getMapper(Class<?> mapperClass) {
    // 使用 JDK 动态代理 来为 Dao 接口生成代理对象 并返回
    Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
        /**
         *
         * @param proxy 当前代理对象的引用
         * @param method 当前被调用方法的引用
         * @param args 传递的参数
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 底层都还是去执行 JDBC 代码  -> 根据不同情况 调用 selectList() 或者 selectOne()
            // 准备参数  1. statmentId sql语句的唯一标识  namespace.id = 接口全限定名.方法名
            //          2. params -> args
            
            // 拿到的是方法名 findAll
            String methodName = method.getName();
            // 拿到该类的全限定类名 com.huodd.dao.IUserDao
            String className = method.getDeclaringClass().getName();

            String statmentId = className   "."   methodName;

            // 获取被调用方法的返回值类型
            Type genericReturnType = method.getGenericReturnType();
            // 判断是否进行了 泛型类型参数化
            if (genericReturnType instanceof ParameterizedType) {
                List<Object> list = selectList(statmentId, args);
                return list;
            }
            return selectOne(statmentId, args);
        }
    });

    return (T) proxyInstance;
}

3.调整mapper.xml配置文件

这里要注意两点

  1. namespace 与 dao 接口的全限定类名保持一致
  2. id 与 dao 接口中定义的方法名保持一致
代码语言:javascript复制
<mapper namespace="com.huodd.dao.IUserDao">

    <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 Userselect.List -->
    <select id="findAll" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User">
        select * from user
    </select>

    <select id="findByCondition" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User">
        select * from user where id = #{id} and username =#{username}
    </select>

</mapper>

4. 进入测试

代码语言:javascript复制
public class IPersistenceTest {

    @Test
    public void test () throws Exception {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sessionFactory.openSession();
        User user = new User();
        user.setId(1);
        user.setUsername("bd2star");
  // 此时返回的 userDao 就是代理对象 所以它的类型就是 Proxy
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        // userDao 是代理对象  调用了接口中的 findAll()  代理对象调用接口中任意方法 都会执行 invoke()
        List<User> users = userDao.findAll();
        System.out.println(users);
        User res = userDao.findByCondition(user);
        System.out.println(res);

    }
}

运行结果如下

代码语言:javascript复制
[User{id=1, username='bd2star'}, User{id=2, username='bd3star'}]
User{id=1, username='bd2star'}

目录结构调整

将代码分为两个模块

  • 提供端(自定义持久层框架-本质就是对JDBC代码的封装)
  • 使用端 (引用持久层框架的jar )
    • 包含数据库配置信息
    • 包含sql配置信息
    • 包含sql语句
    • 参数类型
    • 返回值类型

项目目录结构最终为

提供端

使用端

源码地址

https://gitee.com/bx2star/mybatis-learning.git

0 人点赞