前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >长文干货 | 手写自定义持久层框架!

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

作者头像
田维常
发布2021-01-13 10:50:08
7590
发布2021-01-13 10:50:08
举报

文章来源于公众号:PoXing

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

  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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端技术全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为何要手写自定义持久层框架?
  • 解决问题的思路
  • 编码前思路整理
  • 用到的设计模式
  • 进入编码
    • 1.创建、读取配置文件
      • 2.解析配置文件(使用dom4j)
        • 3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory
          • 4. 创建 SqlSession接口及实现类DefaultSqlSession
            • 5.创建Executor接口及实现类SimpleExecutor实现类
              • 6.测试代码
                • 7.补充
                • 继续优化自定义框架
                  • 1.添加getMapper方法
                    • 2. 实现类实现方法
                      • 3.调整mapper.xml配置文件
                        • 4. 进入测试
                        • 目录结构调整
                          • 将代码分为两个模块
                            • 项目目录结构最终为
                              • 源码地址
                              相关产品与服务
                              数据库
                              云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档