自定义持久层框架

一、JDBC 的使用与存在的问题

1. 编码流程

加载驱动 -> 获取连接 -> 定义 sql -> 获取预处理 statement并设置参数 -> 执行 sql -> 封装结果集 -> 释放资源

2. 引入 mysql 驱动依赖

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package server;

import pojo.User;

import java.sql.*;

public class JDBCUtil {

public static void main(String[] args) {

// 数据库连接
Connection connection = null;

// 预处理 statement
PreparedStatement preparedStatement = null;

// 结果集
ResultSet resultSet = null;

try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&characterEncoding=UTF-8";
String username = "root";
String password = "123456";
connection = DriverManager.getConnection(url, username, password);

// 获取预处理 statement
String sql = "select * from user where id = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);

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

// 遍历结果集封装到 User
User user = null;
while (resultSet.next()) {
int id = resultSet.getInt("id");
String uname = resultSet.getString("username");
user = User.builder()
.id(id)
.username(uname).build();
}
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}finally {
// 释放资源
if (resultSet !=null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement !=null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}

Tips: mysql5 以上的 Driver 使用 com.mysql.cj.jdbc.Driver 而非 com.mysql.jdbc.Driver

4. JDBC 方式存在的问题

  1. 硬编码问题:(解决思路:配置文件)

    • 数据库配置信息
    • sql 语句、设置参数、获取结果集
  2. 多次操作会频繁创建、释放数据库连接(解决思路:连接池)

  3. 手动封装结果集较为繁琐(解决思路:反射、内省)

二、 自定义持久层框架

框架端 + 使用端

1. 思路分析

使用端(项目)的工作:

1. 引入自定义持久层框架的 jar 包

2. 编写两部分配置文件:

    - sqlMapConfig.xml: 设置数据源并引入 mapper.xml 文件
  
    - mapper.xml: sql 语句的编写

框架端的工作:

本质就是对 JDBC 代码进行了封装:

    1. 加载配置文件:根据配置文件的路径,加载配置文件成字节输入流保存储在内存中

      创建 Resources 类,方法:InputStrem getResourceAsStream(String path)

    2. 创建两个 JavaBean(容器对象),存放的就是配置文件解析出来的内容:

        Configuration: 核心配置类,存放 sqlMapConfig.xml 解析出来的内容
        MappedStatement: 映射配置类,存放 mapper.xml 解析出来的内容

    3. 解析配置文件:dom4j 类

        创建类:SqlSessionFactoryBuilder 方法:build(InputStream in)
        第一:使用 dom4j 解析配置文件,将解析出来的内容封装到容器对象
        第二:创建 SqlSessionFactory 对象 -> 生产 SqlSession(会话对象):工厂模式

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

        第一:openSession():生产 SqlSession
    
    5. 创建 SqlSession 接口及 DefaultSqlSession

        定义数据库的 crud 操作:selectList() selectOne() update() delete()

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

        query(Configuration conf, MappedStatement ms, Object... params):执行的就是 JDBC 代码

2. 新建使用端项目

2.1 创建 sqlMapConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>

<!--配置数据源-->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&amp;serverTimezone=GMT%2B8"/>
<property name="user" value="root"/>
<property name="password" value="123456"/>


<!--引入mapper文件-->
<mapper resource="mapper.xml"/>

</configuration>

2.2 创建 mapper.xml

1
2
3
4
5
6
7
8
9
10
<mapper namespace="User">

<select id="selectOne" parameterType="pro.fengjian.pojo.User" resultType="pro.fengjian.pojo.User">
select * from user where id = #{id} and username=#{username}
</select>

<select id="selectList" resultType="pro.fengjian.pojo.User">
select * from user
</select>
</mapper>

3. 新建框架端项目

3.1 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<dependencies>

<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>

<!-- 数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.1</version>
</dependency>

<!-- 日志相关 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<!-- 解析 xml -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>

<!-- xpath 语法 -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>

<!-- 代码简略 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>

3.2 新建相关类来保存从使用端读取的配置文件信息

配置信息类:Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package config;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

@Data
public class Configuration {

// 数据源
private DataSource dataSource;

// map 集合
private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();

}

sql 映射类:MappedStatement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package config;

import lombok.Data;

@Data
public class MappedStatement {

// id
private Integer id;

// sql 语句
private String sql;

// 参数类型
private Class<?> parameterType;

// 返回值类型
private Class<?> resultType;


}

3.3 读取配置文件到流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package config;

import java.io.InputStream;

public class Resources {

public static InputStream getResourcesAsStream(String path) {

InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);

return resourceAsStream;
}
}

3.4 解析 sqlMapConfig.xml 文件封装到 Configuration 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package config;

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

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

public class XMLConfigureBuilder {

private Configuration configuration;

public XMLConfigureBuilder(Configuration configuration) {
this.configuration = configuration;

}

public Configuration parseConfiguration(InputStream inputStream) throws DocumentException, PropertyVetoException {

Document document = new SAXReader().read(inputStream);

// <configuration>
Element rootElement = document.getRootElement();
List<Element> propertyElements = rootElement.selectNodes("//property");

Properties properties = new Properties();
for (Element propertyElement : propertyElements) {
String name = propertyElement.attributeValue("name");
String value = propertyElement.attributeValue("value");
properties.put(name, 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 部分
List<Element> mapperElements = rootElement.selectNodes("//mapper");
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
for (Element mapperElement : mapperElements) {
String mapperPath = mapperElement.attributeValue("resource");
InputStream resourcesAsStream = Resources.getResourcesAsStream(mapperPath);
xmlMapperBuilder.parse(resourcesAsStream);
}
return configuration;

}
}

3.5 解析 mapper.xml 文件 封装到 MappedStatement 和 Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package config;

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

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

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> selectNodes = document.selectNodes("//select");
for (Element element : selectNodes) {
String id = element.attributeValue("id");// id 值
String parameterType = element.attributeValue("parameterType");// 参数类型
String resultType = element.attributeValue("resultType");// 返回值类型

// 获取参数和返回值类型
Class<?> parameterTypeClass = this.getClassType(parameterType);
Class<?> resultTypeClass = this.getClassType(resultType);

// statementId
String statementId = namespace + "." + id;
// sql
String sql = element.getTextTrim();
// 封装到 MappedStatement
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParameterType(parameterTypeClass);
mappedStatement.setResultType(resultTypeClass);
mappedStatement.setSql(sql);
// 填充 Configuration
configuration.getMappedStatementMap().put(statementId,mappedStatement);
}

}

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

3.6 构建者模式构建 SqlSessionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package sqlSession;

import config.XMLConfigureBuilder;
import org.dom4j.DocumentException;
import pojo.Configuration;

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

public class SqlSessionFactoryBuilder {

private Configuration configuration;

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

public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {

// 1. 解析配置文件,封装到 Configuration
XMLConfigureBuilder xmlConfigureBuilder = new XMLConfigureBuilder(configuration);
Configuration configuration = xmlConfigureBuilder.parseConfiguration(inputStream);

// 2. 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return sqlSessionFactory;
}
}


3.7 sqlSessionFactory 开启 sqlSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

package sqlSession;

import pojo.Configuration;

public class DefaultSqlSessionFactory implements SqlSessionFactory{

private Configuration configuration;

public DefaultSqlSessionFactory(Configuration configuration) {
}


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

3.8 sqlSession 编写 sql 方法

1
2
3
4
5
6
7
8
9
10
11
package sqlSession;

import java.util.List;

public interface SqlSession {

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

<T> T selectOne(String statementId, Object... param);
}

3.9 defaultSqlSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package sqlSession;

import pojo.Configuration;
import pojo.MappedStatement;

import java.util.List;

public class DefaultSqlSession implements SqlSession {

private Configuration configuration;

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

// 执行器对象
private Executor simpleExecutor = new SimpleExecutor();


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

@Override
public <T> T selectOne(String statementId, Object... param) {
// 调用 selectList
List<Object> objects = this.selectList(statementId, param);
if (objects.size() == 1) {
return (T) objects.get(0);
} else {
throw new RuntimeException("期望查询条数 1 条,但返回多条!");
}
}
}

3.10 Executor 执行器,实际 sql 操作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package sqlSession;

import pojo.BoundSql;
import pojo.Configuration;
import pojo.MappedStatement;
import utils.GenericTokenParser;
import utils.ParameterMapping;
import utils.ParameterMappingTokenHandler;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class SimpleExecutor implements Executor {

private Connection connection;

@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] param) throws SQLException, NoSuchFieldException, IllegalAccessException, InstantiationException, IntrospectionException, InvocationTargetException {

// 获取连接
connection = configuration.getDataSource().getConnection();

// 对 mapper 中的 sql 进行解析
// 1. 将 #{xx} -> ?
// 2. 封装参数列表 #{id} #{username} 中的 id username 等
BoundSql boundSql = this.getBoundSql(mappedStatement.getSql());
String finalSql = boundSql.getSqlText();

// 设置 sql 参数,获取 preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(finalSql);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
// 参数类型
Class<?> parameterType = mappedStatement.getParameterType();
for (int i = 0; i < parameterMappingList.size(); i++) {
String name = parameterMappingList.get(i).getContent();
Field declaredField = parameterType.getDeclaredField(name);
declaredField.setAccessible(true);
Object o = declaredField.get(param[0]);// 参数 user
preparedStatement.setObject(i + 1, o);
}

// 执行 sql,封装结果集
ResultSet resultSet = preparedStatement.executeQuery();
Class<?> resultType = mappedStatement.getResultType();
List<E> result = new ArrayList<>();

while (resultSet.next()) {
E e = (E) resultType.newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
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, resultType);
// 获取写方法
Method writeMethod = propertyDescriptor.getWriteMethod();
// 写入值
writeMethod.invoke(e, value);
}
result.add(e);
}
return result;
}

@Override
public void close() {

}

private BoundSql getBoundSql(String sql) {
// 标记处理类:主要配合通用标记解析器 GenericTokenParser 类完成对配置文件等的
// 解析工作,其中 TokenHandler 主要完成处理
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();

// GenericTokenParser:通用的标记解析器,完成了代码片段中占位符的解析,然后再根据
// 给定的标记解析器(TokenHandler)进行表达式的处理
// openToken closeToken handler
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
String parse = genericTokenParser.parse(sql);

List<utils.ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
return new BoundSql(parse,parameterMappings);
}
}

Tips:
getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
getField 仅能获取类(及其父类可以自己测试) public属性成员

3.11 解析类

GenericTokenParser:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils;

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

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

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

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

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

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

ParameterMapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package utils;

public class ParameterMapping {

private String content;

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

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

```java
package utils;

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




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

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

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

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

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

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

}


TokenHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils;

/**
* @author Clinton Begin
*/
public interface TokenHandler {
String handleToken(String content);
}

4. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import io.Resources;
import org.dom4j.DocumentException;
import org.junit.Test;
import pro.fengjian.User;
import sqlSession.SqlSession;
import sqlSession.SqlSessionFactory;
import sqlSession.SqlSessionFactoryBuilder;

import java.beans.IntrospectionException;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;

public class IPersistenceTest {

@Test
public void testIPersistence() throws DocumentException, PropertyVetoException, ClassNotFoundException, IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {

InputStream inputStream = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

User user = User.builder().id(1).username("jack").build();
user = sqlSession.selectOne("UserMapper.selectOne",user);
System.out.println(user);
}
}

5. 使用代理模式进行优化

自定义持久层框架存在的问题:

1. Dao 层使用自定义持久层框架,代码重复,整个操作过程模板重复(加载配置文件、创建 SqlSessionFactory、生产 sqlSession)

2. satementId 存在硬编码,每次查询时还需要重复编写 statementId,比如 `UserMapper.selectOne`

解决思路:

1. 使用代理模式生成 Dao 层接口的代理实现类,代理对象调用接口中的任意方法,都会执行 invoke 方法

2. 约定 statementId = 接口全路径 + 方法名

SqlSession 接口

1
public <T> T getMappper(Class<?> mapperClass);

DefaultSqlSession 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public <T> T getMapper(Class<?> mapperClass) {
// 使用 jdk 动态代理为 Dao 接口生成代理对象并返回,使用代理对象调用接口中的任意方法,都是执行 invoke 方法
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 底层都是执行 jdbc 代码 // 根据不同情况调用 selectOne 或 selectList 方法
// 准备参数 1: statementId:sql 语句唯一标识:namespace.id
// 约定:namespace 为接口全路径,id 为方法名
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String statementId = className + "." + methodName;

// 准备参数 2: params : args
// 根据方法返回值判断是调用 selectList 还是 selectOne,selectList 返回值包含泛型化符号:<>
// 获取被调用方法返回值类型
Type genericReturnType = method.getGenericReturnType();
// 判断是否进行了泛型类型参数化
if (genericReturnType instanceof ParameterizedType) {
List<Object> objects = selectList(statementId, args);
return objects;
}
return selectOne(statementId, args);
}
});


return (T) proxyInstance;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testIPersistence() throws DocumentException, PropertyVetoException, ClassNotFoundException, IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {

InputStream inputStream = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

User user = User.builder()
.id(1)
.username("jack")
.build();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
user = userMapper.selectOne(user);
System.out.println(user);
}

Tips: 本节源码地址 ipersistence-test | ipersistence


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!