自定义一个mybatis持久层框架

x33g5p2x  于2021-06-19 转载在 Spring  
字(19.6k)|赞(0)|评价(0)|浏览(541)

本文用于拉钩训练营笔记

说起持久层框架,大家第一时间想到的应该都是mybatis、hibernate,他们都是非常优秀的持久层框架。那为什么要使用框架而不使用原始的JDBC呢?下面是我们使用JDBC查询数据库的代码:

@Test
public void test() throws ClassNotFoundException{
    String url = \"jdbc:mysql://localhost:3306/lin?characterEncoding=utf-8&serverTimezone=Asia/Shanghai\";
    String user = \"root\";
    String password = \"123456\";
    String driverClass = \"com.mysql.cj.jdbc.Driver\";
    String sql = \"select * from user where id = ?\";
    //加载数据库驱动
    Class.forName(driverClass);
    try (Connection conn = DriverManager.getConnection(url, user, password);
         PreparedStatement preparedStatement = conn.prepareStatement(sql);){
        //设置sql参数,第一个参数为sql中参数的序号(从1开始),第二个参数为参数值
        preparedStatement.setString(1, \"1\");
        ResultSet resultSet = preparedStatement.executeQuery();
        //遍历结果集,封装结果
        while (resultSet.next()) {
            int id = resultSet.getInt(\"id\");
            String username = resultSet.getString(\"username\");
            log.info(\"id: {}, username: {}\",id,username);
        }
    }catch (SQLException e){
        e.printStackTrace();
    }
}

不难看出上面代码的几个问题:

  1. 数据库配置存在硬编码,不够灵活
  2. 频繁创建和释放数据库连接(上面用的try-with-resources)
  3. 如果sql语句和参数设置变化较大,就要经常修改代码
  4. 结果集存在硬编码,如果数据库结构发生改变,还要修改相关sql,同时我们还要手动封装结果集到对象中。

针对以上存在的几个问题提出解决方案:

  1. 将这些配置放入配置文件中
  2. c3p0数据库连接池方式
  3. 配置文件方式,相关sql信息都写入配置文件中
  4. 采用反射自动封装结果集到对应pojo对象中

框架设计

针对之前JDBC存在的问题我们提出了解决方案,现在我们对照解决方案来设计框架:

使用端:

  1. 提供数据库配置,包括数据库地址,用户名密码
  2. 提供sql相关信息,包括sql语句、入参,出参:mapper.xml文件
  3. 引入持久层框架

框架端:

  1. 读取使用端的数据库配置,并连接到数据库
  2. 解析mapper.xml文件,将里面的配置转换为sql语句、入参,出参等等
  3. 实现crud相关操作,并将结果封装为对象返回

接下来针对上面的方案开始实现我们的框架:

首先新建一个名为PersistenceFrameWork的项目:

其中app为使用端,framework为框架端。

引入依赖

先讲讲框架端,框架端要连接数据库,要解析xml,要打印日志,所以引入如下包:

<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</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>
        <!-- 日志处理 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
</dependencies>

对于使用端,只需要引入框架端和打印日志以及测试就够了:

<dependencies>
        <!-- 自定义持久层框架jar包 -->
        <dependency>
            <groupId>org.lin</groupId>
            <artifactId>framework</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- lombok,可自行选择 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
            <scope>provided</scope>
        </dependency>
        <!-- 日志处理 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
        </dependency>
</dependencies>

读取配置信息

我们需要读取使用端的数据库配置信息,需要使用端将配置文件路径传过来,我们再做解析:

对于使用端,在resources下面新建一个sqlConfig.xml文件,提供数据库配置:

<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<configuration>
    <dataSource>
        <property name=\"driverClass\" value=\"com.mysql.cj.jdbc.Driver\" />
        <property name=\"jdbcUrl\" value=\"jdbc:mysql://localhost:3306/lin?characterEncoding=utf-8&serverTimezone=Asia/Shanghai\" />
        <property name=\"user\" value=\"root\" />
        <property name=\"password\" value=\"123456\" />
    </dataSource>
    <mapper resource=\"UserMapper.xml\"/>
</configuration>

其中mapper节点为提供相关sql语句,这个后面再说。

然后就是框架端对配置文件的解析:

public class SqlSessionFactoryBuilder {
    /**
     * 读取配置文件,获取流
     * @param path 文件路径
     */
    private static InputStream getResourceAsSteam(String path) {
        return SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream(path);
    }

    /**
     * 使用dom4j解析配置文件,并返回SqlSessionFactory
     * @param path 配置文件
     * @return SqlSessionFactory
     */
    public SqlSessionFactory build(String path) throws DocumentException, PropertyVetoException {
        // 1.解析配置文件封装configuration
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
        SqlConfiguration configuration = xmlConfigBuilder.parseConfig(getResourceAsSteam(path));
        // 2.创建SqlSessionFactory,并返回
        return new DefaultSqlSessionFactory(configuration);
    }
}

其中,XmlConfigBuilder用于解析数据库配置文件,并且返回一个配置类:

public class XmlConfigBuilder {
    private final SqlConfiguration configuration = new SqlConfiguration();
    /**
     * 使用dom4j解析配置文件,并封装到SqlConfiguration对象
     * @param in 配置文件流
     * @return SqlConfiguration
     */
    @SuppressWarnings(\"unchecked\")
    public SqlConfiguration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
        // 1.解析sqlMapConfig.xml
        // 读取sqlMapConfig.xml配置文件
        Document document = new SAXReader().read(in);
        // 获取根标签:即<configration>标签
        Element rootElement = document.getRootElement();

        // 获取property标签内容:即数据库配置信息
        List<Element> list = rootElement.selectNodes(\"//property\");
        // 封装到properties中
        Properties properties = new Properties();
        for (Element ele : list) {
            String name = ele.attributeValue(\"name\");
            String value = ele.attributeValue(\"value\");
            properties.setProperty(name, value);
        }
        // 将数据库配置信息设置到c3p0的数据库连接池中
        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.setDataSource(comboPooledDataSource);
        // 2.解析mapper.xml
        // 获取mapper.xml的全路径--获取文件流--dom4j解析
        List<Element> mapperList = rootElement.selectNodes(\"//mapper\");
        for (Element ele : mapperList) {
            String mapperPath = ele.attributeValue(\"resource\");
            InputStream inputStream = ResourceFactory.getResourceAsSteam(mapperPath);
            XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
            xmlMapperBuilder.parse(inputStream);
        }
        return configuration;
    }
}

XmlMapperBuilder是用于解析mapper.xml文件,这个也放在后面说,返回的配置类包含数据库连接信息和mapper.xml解析成为sql语句的信息:

@Data
public class SqlConfiguration {
    private DataSource dataSource;
    /**
     * mapper信息:key:statementId:namespace+id;value:MappedStatement
     */
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
}

读取mapper文件,解析为sql语句

使用端已经提供了数据库配置信息,这个时候就可以通过配置信息连接上数据库了。连上数据库之后的操作呢?当然是crud了!所以我们还得要使用端提供sql语句,我们希望的是:

使用端提供sql语句,执行sql的参数,执行完成的返回值,框架端拿到sql之后执行完成,将结果封装为相关对象然后返回给使用端。

这里使用端提供sql,我们统一将其写入到xml文件中,在resources中新建一个UserMapper.xml文件:

<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<Mapper namespace=\"com.lin.mapper.UserMapper\">
    <!-- namespace和id组成statemeId,作为sql语句的唯一标识 -->
    <select id=\"selectAll\"  resultType=\"com.lin.pojo.User\">
        select * from user
    </select>
    <select id=\"selectUserById\" paramterType=\"java.lang.Integer\" resultType=\"com.lin.pojo.User\">
        select * from user where id = #{id}
    </select>
    <insert id=\"insert\" paramterType=\"com.lin.pojo.User\">
        insert into user values(#{id}, #{username})
    </insert>
    <update id=\"update\" paramterType=\"com.lin.pojo.User\">
        update user set username = #{username} where id = #{id}
    </update>
    <delete id=\"deleteById\" paramterType=\"java.lang.Integer\">
        delete from user where id = #{id}
    </delete>
</Mapper>

user类只有两个参数,对应数据库的user表:

@Data
public class User {
    private Integer id;
    private String username;
}

这个xml文件的命名空间为com.lin.mapper.UserMapperUserMapper是一个接口,我们后面会通过动态代理的方式,在调用这个接口方法的时候,执行相关的sql然后封装返回结果:

public interface UserMapper {
    List<User> selectAll();
    User selectUserById(int id);
    int insert(User user);
    int update(User user);
    int deleteById(int id);
}

使用端到这个时候做的事情已经结束了,接下来看框架端的了:

既然要解析mapper文件,那么就要把解析后的结果保存下来,所以需要有这么一个类,用于保存解析结果:

@Data
public class MappedStatement {
    /**
     * id
     */
    private String id;

    /**
     * 返回值类型
     */
    private String resultType;

    /**
     * 参数类型
     */
    private String parameterType;

    /**
     * sql语句
     */
    private String sql;
}

好了,接下来需要一个类来专门解析mapper.xml文件,这个类也就是在上面XmlConfigBuilder中用到的:

public class XmlMapperBuilder {
    private SqlConfiguration configration;
    /**
     * 有参构造函数
     * @param configration
     */
    public XmlMapperBuilder(SqlConfiguration configration) {
        this.configration = configration;
    }

    /**
     * 解析mapper.xml,并将结果封装到configration的mappedStatementMap对象中
     * @param in
     * @throws DocumentException
     */
    @SuppressWarnings(\"unchecked\")
    public void parse(InputStream in) throws DocumentException {
        // 读取mapper.xml配置文件
        Document document = new SAXReader().read(in);
        // 获取根标签:即<mapper>标签
        Element rootElement = document.getRootElement();
        // 获取mapper的namespace属性值
        String namespace = rootElement.attributeValue(\"namespace\");

        // 获取select、update、insert、delete标签内容
        List<Element> list = rootElement.selectNodes(\"//select|//update|//insert|//delete\");

        // 封装MappedStatement对象,并存放到configration中
        for (Element ele : list) {
            String id = ele.attributeValue(\"id\");
            String resultType = ele.attributeValue(\"resultType\");
            String paramterType = ele.attributeValue(\"paramterType\");
            String sql = ele.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParameterType(paramterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sql);
            // key为namepace.id
            String key = namespace + \".\" + id;
            configration.getMappedStatementMap().put(key, mappedStatement);
        }
    }
}

这个时候解析信息已经保存在SqlConfiguration配置类中的map里面了,key为命名空间+方法名,这样我们就可以通过调用接口方法定位到要执行哪个sql操作了。但是注意,这个时候保存的sql还存在占位符的,后续还需要做处理。

执行sql语句,封装返回结果

创建Executor接口和实现,实现jdbc的增删改查:

public interface Executor {
    <T> List<T> query(SqlConfiguration configuration, MappedStatement statement,Object... params) throws Exception;
    int executeUpdate(SqlConfiguration configuration, MappedStatement statement,Object... params) throws Exception;
}

实现:

@Slf4j
public class DefaultExecutor implements Executor{
    @Override
    public <T> List<T> query(SqlConfiguration configuration, MappedStatement statement, Object... params) throws Exception {
        PreparedStatement preparedStatement = preparedStatement(configuration, statement, params);
        ResultSet resultSet = preparedStatement.executeQuery();

        String resultType = statement.getResultType();
        Class<?> classType = getClassType(resultType);
        List<T> list = new ArrayList<>();
        while (resultSet.next()){
            ResultSetMetaData metaData = resultSet.getMetaData();
            Object resultObject = classType.newInstance();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                String columnName = metaData.getColumnName(i);
                Object value = resultSet.getObject(columnName);

                PropertyDescriptor descriptor = new PropertyDescriptor(columnName,classType);
                Method writeMethod = descriptor.getWriteMethod();
                //对带有指定参数的指定对象调用由此方法,此处相当于调用set方法
                writeMethod.invoke(resultObject,value);
            }
            T transform = (T) resultObject;
            list.add(transform);
        }
        return list;
    }

    @Override
    public int executeUpdate(SqlConfiguration configuration, MappedStatement statement, Object... params) throws Exception {
        PreparedStatement preparedStatement = preparedStatement(configuration, statement, params);
        return preparedStatement.executeUpdate();
    }
    /**
     * 转换sql,设置sql参数
     */
    private PreparedStatement preparedStatement(SqlConfiguration configuration, MappedStatement statement, Object... params){
        String sql = statement.getSql();
        BoundSql boundSql = boundSql(sql);
        log.debug(\"执行sql: {}\",boundSql.getSql());
        try {
            Connection connection = configuration.getDataSource().getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
            String parameterType = statement.getParameterType();
            if (StringUtils.isBlank(parameterType)){
                return preparedStatement;
            }
            Class<?> classType = getClassType(parameterType);

            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            StringBuilder parameterStr = new StringBuilder();
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                String content = parameterMapping.getContent();
                if (isCommonDataType(classType) || isWrapClass(classType)){
                    preparedStatement.setObject(i+1,params[0]);
                    parameterStr.append(content.concat(\"=\").concat(params[0].toString()).concat(\",\"));
                }else{
                    Field field = classType.getDeclaredField(content);
                    //获取private的属性
                    field.setAccessible(true);
                    Object o = field.get(params[0]);
                    preparedStatement.setObject(i+1,o);
                    parameterStr.append(content.concat(\"=\").concat(o.toString()).concat(\" \"));
                }
            }
            log.info(\"参数: {}\",parameterStr);
            return preparedStatement;
        }catch (Exception e){
            e.printStackTrace();
            throw new FrameWorkException(\"创建PreparedStatement失败\");
        }
    }

    private BoundSql boundSql(String sql){
        // 主要是存储下面解析出来的参数名
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        // 标记解析器:解析#{}占位符
        GenericTokenParser genericTokenParser = new GenericTokenParser(\"#{\", \"}\", parameterMappingTokenHandler);
        // 解析sql语句,并返回,此时#{***}已经转换成了?
        String parseSql = genericTokenParser.parse(sql);
        // #{***}中解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        return new BoundSql(parseSql, parameterMappings);
    }

    /**
     * 根据全路径获取Class
     */
    private Class<?> getClassType(String parameterType)  {
        try {
            return Class.forName(parameterType);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new FrameWorkException(\"无法获取参数类型,请检查xml文件的参数类型是否正确\");
        }
    }

    /**
     * 判断是否是基础数据类型,即 int,double,long等类似格式
     */
    private boolean isCommonDataType(Class<?> clazz) {
        return clazz.isPrimitive();
    }

    /**
     * 判断是否为基本类型的包装类
     */
    private boolean isWrapClass(Class<?> clazz) {
        try {
            return ((Class<?>) clazz.getField(\"TYPE\").get(null)).isPrimitive();
        } catch (Exception e) {
            return false;
        }
    }
}

这个类里面的代码比较多,我们重点关注preparedStatement方法,主要做了几件事:

  1. 执行了boundSqlboundSql主要作用就是将:select * from user where id = #{id}这样的语句转换为select * from user where id = ?BoundSql定义为:
@Data
public class BoundSql {
 /**
  * 解析后的sql语句:select * from user where username = ?
  */
 private String sql;
 /**
  * 原始sql语句#{***}中的参数名称集合
  */
 private List<ParameterMapping> parameterMappings;
 public BoundSql(String sql, List<ParameterMapping> parameterMappings) {
  this.sql = sql;
  this.parameterMappings = parameterMappings;
 }
}
//ParameterMapping目前只定义了参数名,后面可拓展保存参数类型、数据库类型
@Data
public class ParameterMapping {
    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }
}
  1. 获取预处理对象PreparedStatement
  2. 设置参数。这里我们从mappedStatement中拿到参数的全路径,以及前面处理返回的参数变量名称集合parameterMappings;循环遍历parameterMappings设置参数。
  3. 返回设置完成参数的preparedStatement

最后拿着这个preparedStatement做具体的查询/更新操作。

我们提供给使用端这样的Executor还是比较复杂,所以再做一次封装,封装一个SqlSession

public interface SqlSession {
    <T> List<T> selectList(String statementId, Object... params) throws Exception;
    <T>  T selectOne(String statementId, Object... params) throws Exception;
    int update(String statementId, Object... params) throws Exception;
    <T> T getMapper(Class<?> mapperClass);
}

实现:

public class DefaultSqlSession implements SqlSession{
    private SqlConfiguration configuration;

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

    @Override
    public <T> List<T> selectList(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        DefaultExecutor defaultExecutor = new DefaultExecutor();
        return defaultExecutor.query(configuration,mappedStatement,params);
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        DefaultExecutor defaultExecutor = new DefaultExecutor();
        List<Object> query = defaultExecutor.query(configuration, mappedStatement, params);
        if (query != null && query.size() > 0){
            return (T) query.get(0);
        }
        return null;
    }

    @Override
    public int update(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        DefaultExecutor defaultExecutor = new DefaultExecutor();
        return defaultExecutor.executeUpdate(configuration,mappedStatement,params);
    }

    /**
     * 使用动态代理给mapper接口生成代理类
     * @param mapperClass
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<?> mapperClass){
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //这个地方获取到的是mapper类的全限定类名,我们的statementId为namespace+id。所以必须规定mapper.xml里的命名空间也是这个
                String className = method.getDeclaringClass().getName();
                String methodName = method.getName();
                String statementId = className.concat(\".\").concat(methodName);
                if (StringUtils.startsWith(methodName,\"select\") || StringUtils.startsWith(methodName,\"find\")){
                    //获取调用方法返回类型
                    Type type = method.getGenericReturnType();
                    //是否有泛型参数
                    if (type instanceof ParameterizedType){
                        return selectList(statementId,args);
                    }
                    return selectOne(statementId,args);
                }else {
                    return update(statementId,args);
                }
            }
        });
        return (T) proxyInstance;
    }
}

重点看getMapper方法,这里使用了动态代理,在调用使用端mapper接口的时候,会根据方法名判断需要执行查询还是更新操作。至此,框架端的基本雏形已经有了,现在可以在使用端使用了!

在使用端新建一个测试类:

@Slf4j
public class Main {
    private static SqlSession sqlSession;
    @BeforeAll
    public static void init() throws Exception{
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(\"sqlConfig.xml\");
        sqlSession = factory.openSqlSession();
    }
    
    @Test
    void insertTest() throws Exception{
        User user = new User();
        user.setId(2);
        user.setUsername(\"李四\");
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        int count = mapper.update(user);
        log.info(\"增加单个: {}\",count);
    }

    @Test
    void sqlSessionTest() throws Exception{
        User user = new User();
        user.setId(1);
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User one = mapper.selectUserById(1);
        log.info(\"查询单个: {}\",one.toString());
    }

    @Test
    void selectListTest() throws Exception{
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> list = mapper.selectAll();
        list.forEach(item ->log.info(\"查询: {}\",item));
    }
}

执行结果:

INFO [main] (DefaultExecutor.java:89) - 参数: username=李四 id=2 
INFO [main] (Main.java:36) - 增加单个: 1

INFO [main] (DefaultExecutor.java:89) - 参数: id=1,
INFO [main] (Main.java:45) - 查询单个: User(id=1, username=张三)

INFO [main] (Main.java:52) - 查询: User(id=1, username=张三)
INFO [main] (Main.java:52) - 查询: User(id=2, username=李四)

总结

整个框架的设计中,虽然框架只是初具雏形,但是其中涉及到的知识点挺多的。对后面理解mybatis原理也有一定作用。

用到的知识点有:

  • JDBC基础
  • dom4j解析xml文件
  • 反射
  • jdk动态代理

代码已上传至Gitee:PresistenceFramework

相关文章

微信公众号

最新文章

更多