1.自定义持久层框架
1.1 分析JDBC操作问题
# 1.引入相关依赖
- mysql-connector-java
- druid
# 2.编写工具类
# 3.编写测试类
传统方式
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcUtils {
private static Properties properties;
private static String url;
private static String username;
private static String password;
private static String driver;
private static Boolean flag = true;
static{
if(flag){
properties = new Properties();
try {
properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties"));
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
driver = properties.getProperty("driver");
} catch (IOException e) {
e.printStackTrace();
}finally {
properties.clear();
}
flag = false;
}
}
//每一次的连接对象都是不同的
public static Connection getConnection() throws ClassNotFoundException, SQLException {
//1.注册驱动
Class.forName(driver);
//2.获得连接对象
return DriverManager.getConnection(url, username, password);
}
}
连接池方式
import com.alibaba.druid.pool.DruidDataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DruidUtils {
private static DruidDataSource dataSource;
private static Properties properties;
private static Boolean flag = true;
static {
if(flag){
properties = new Properties();
try {
properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties"));
dataSource = new DruidDataSource();
dataSource.setDriverClassName(properties.getProperty("driver"));
dataSource.setUrl(properties.getProperty("url"));
dataSource.setUsername(properties.getProperty("username"));
dataSource.setPassword(properties.getProperty("password"));
dataSource.setMaxActive(3);//连接池的最大数量
}catch (IOException e){
e.printStackTrace();
}finally {
properties.clear();
}
flag = false;
}
}
//每一次的连接对象都是不同的,但是当连接关闭后会返回到池子中
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
测试类
Connection conn = null;
try {
//1.获取连接
conn = JdbcUtils.getConnection();
//2.编写SQL语句
String sql = "select * from user where name = ?;";
//3.获取执行SQL的对象
PreparedStatement statement = conn.prepareStatement(sql);
//4.填充
statement.setString(1,"灰二");
//5.执行
ResultSet resultSet = statement.executeQuery();
//6.遍历
while (resultSet.next()){
System.out.print(resultSet.getInt(1)+"+");
System.out.print(resultSet.getString(2)+"+");
System.out.println(resultSet.getInt(3));
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
if(conn == null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
JDBC问题总结
# 原始JDBC开发存在的问题
- 1.数据库配置信息存在硬编码问题,频繁创建释放数据库连接。
- 2.sql语句、设置参数、获取结果集均存在硬编码问题。
- 3.手动封装返回结果集,步骤繁琐。
1.2 问题解决思路
# 1.配置文件 2.连接池 3.反射、内省
1.3 自定义框架设计
使用端
# 提供核心配置文件
- sqlMapConfig.xml:存放数据库配置信息,存放mapper.xml的全路径
- mapper.xml:存放sql配置信息
框架段
# 加载配置文件:根据配置文件的路径,加载配置文件成输入字节流,存储在内存中
- 创建Resources类 方法:InputStream getResourceAsStream(String path);
# 创建两个JavaBean:存放对配置文件解析出来的内容
- Configuration:核心配置类,存放sqlMapConfig.xml解析出来的内容
- MappedStatement:映射配置类,存放mapper.xml解析出来的内容
# 解析配置文件:dom4j
- 创建类:SqlSessionFactoryBuild 方法:build(InpuStream in)
- 使用dom4j解析配置文件,将解析出来的内容封装到JavaBean中
- 创建SqlSessionFactory对象:生产SqlSession对象[会话对象]
# 创建SqlSessionFactory接口和实现类DefaultSqlSessionFactory
- 方法:openSession(),生产SqlSession
# 创建SqlSession接口和实现类DefaultSession
- 定义对象数据库的crud操作
- selectList
- selectOne
# 创建Executor接口和实现类SimpleExecutor
- 方法:query(Configuration,MappedStatement,Object...params)。对JDBC方法的封装
涉及到的设计模式
Builder构建者设计模式、工厂模式、代理模式
1.4 自定义框架实现
使用端
1.数据源的配置文件
2.sql语句的配置文件
3.POJO类
4.导入框架端的依赖
sqlMapConfig
<configuration>
<dataSource>
<property name="driverName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/huier_zdy_mybatis?serverTimezone=Asia/Shanghai"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<mapper resource="userMapper.xml"></mapper>
</configuration>
mapper.xml
<mapper namespace="user">
<!--唯一标识:namespace.id-->
<select id="selectList" resultType="com.huier.pojo.User">
select * from user;
</select>
<!-- <select id="selectOne">-->
<!-- select * from user where id = ?;-->
<!-- </select>-->
<select id="selectOne" paramterType="com.huier.pojo.User" resultType="com.huier.pojo.User">
select * from user where id = #{id} and username = #{username};
</select>
</mapper>
实体类
public class User {
private Long id;
private String username;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
框架端
1.将配置文件以流的形式存在内存中
2.将配置文件的信息封装到JavaBean中,生成工厂
3.通过工厂生成会话对象
jar包依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>
</dependencies>
Resources
public class Resources {
//作用:将文件加载到内存中以流的形式存在
public static InputStream getResourceAsStream(String path){
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
SqlSessionFactoryBuild
public class SqlSessionFactoryBuild {
//加载配置文件到JavaBean中,生成工厂
public SqlSessionFactory build(InputStream in) throws DocumentException {
//1.使用dom4j解析xml,封装javaBean
XMLConfigBuildr xmlConfigBuildr = new XMLConfigBuildr();
Configuration configuration = xmlConfigBuildr.parseConfig(in);
//2.创建SqlSessionFactory对象
return new DefaultSqlSessionFactory(configuration);
}
}
JavaBean
Configuration
public class Configuration {
//1.数据源
private DataSource dataSource;
//2.唯一映射
private Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MappedStatement> getMappedStatementMap() {
return mappedStatementMap;
}
public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
this.mappedStatementMap = mappedStatementMap;
}
}
MappedStatement
//封装sql配置信息
public class MappedStatement {
private String id;//标识
private String resultType;
private String paramterType;
private String sql;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getParamterType() {
return paramterType;
}
public void setParamterType(String paramterType) {
this.paramterType = paramterType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
XML解析,封装JavaBean
XMLConfigBuilder
//解析Configuration.xml
public class XMLConfigBuildr {
private Configuration configuration;
public XMLConfigBuildr(){
this.configuration = new Configuration();
}
public Configuration parseConfig(InputStream in) throws DocumentException {
Document document = new SAXReader().read(in);
Element root = document.getRootElement();
List<Element> list = root.selectNodes("//property");
Properties properties = new Properties();
for (Element element : list) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.put(name,value);
}
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getProperty("driverName"));
druidDataSource.setUrl(properties.getProperty("url"));
druidDataSource.setUsername(properties.getProperty("username"));
druidDataSource.setPassword(properties.getProperty("password"));
configuration.setDataSource(druidDataSource);
List<Element> mappers = root.selectNodes("//mapper");
for (Element mapper : mappers) {
String mapperPath = mapper.attributeValue("resource");
InputStream stream = Resources.getResourceAsStream(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(stream);
}
return configuration;
}
}
XMLMapperBuilder
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration){
this.configuration = configuration;
}
public void parse(InputStream in) throws DocumentException {
Document document = new SAXReader().read(in);
Element root = document.getRootElement();
List<Element> list = root.selectNodes("//select");
String namespace = root.attributeValue("namespace");
Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
for (Element element : list) {
String paramterType = element.attributeValue("paramterType");
String resultType = element.attributeValue("resultType");
String id = element.attributeValue("id");
String sql = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParamterType(paramterType);
mappedStatement.setResultType(resultType);
mappedStatement.setSql(sql);
mappedStatementMap.put(namespace+"."+id,mappedStatement);
}
configuration.setMappedStatementMap(mappedStatementMap);
}
}
SqlSessionFactory接口
//用来生产SqlSession对象
public interface SqlSessionFactory {
public SqlSession openSession();
}
DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
SqlSession接口
public interface SqlSession {
//可能模糊查询
public <E> List<E> selectList(String statementId,Object... params) throws Exception;
public <E> E selectOne(String statementId,Object... params) throws Exception;
}
DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
Executor executor = new DefaultExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
return executor.query(configuration,mappedStatement,params);
}
public <E> E selectOne(String statementId, Object... params) throws Exception {
List<Object> list = selectList(statementId, params);
if(list.size() == 1){
return (E) list.get(0);
}else{
throw new RuntimeException("结果过多");
}
}
}
Executor:封装底层具体的jdbc代码
Executor接口
public interface Executor {
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement,Object... params) throws SQLException, Exception;
}
DefaultExecutor
public class DefaultExecutor implements Executor {
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
Connection connection = configuration.getDataSource().getConnection();
//select * from user where id = #{id} and username = #{username}
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);//将原始sql转换成可预编译的sql语句
PreparedStatement statement = connection.prepareStatement(boundSql.getSqlTest());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();//参数
Class clz = getParamterClass(mappedStatement.getParamterType());//获得参数类
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
String canshu = parameterMapping.getContent();//#{id} 值
Field declaredField = clz.getDeclaredField(canshu);
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
statement.setObject(i+1,o);
}
ResultSet resultSet = statement.executeQuery();
List<E> result = new ArrayList<E>();
Class resultClz = getParamterClass(mappedStatement.getResultType());
while (resultSet.next()){
ResultSetMetaData metaData = resultSet.getMetaData();//每一行的数据
Object o = resultClz.newInstance();//创建实体对象
for(int i = 1;i <= metaData.getColumnCount();i++){
String filedName = metaData.getColumnName(i);
Object filedValue = resultSet.getObject(filedName);
//反射
Field declaredField = resultClz.getDeclaredField(filedName);
declaredField.setAccessible(true);
declaredField.set(o,filedValue);//设置值
// //内省
// PropertyDescriptor propertyDescriptor = new PropertyDescriptor(filedName,resultClz);
// Method writeMethod = propertyDescriptor.getWriteMethod();
// writeMethod.invoke(o,filedValue);
}
result.add((E) o);
}
return result;
}
private Class getParamterClass(String paramterType) throws ClassNotFoundException {
if(paramterType == null || "".equals(paramterType))return null;
return Class.forName(paramterType);
}
private BoundSql getBoundSql(String sql) {
//标记处理类:配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler();
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String parseSql = parser.parse(sql);
//#{}中的参数名称
List<ParameterMapping> parameterMappings = handler.getParameterMappings();
return new BoundSql(parseSql,parameterMappings);
}
}
BoundSql:解析后的sql语句**{#{id} -> ?}**
public class BoundSql {
private String sqlTest;
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
public BoundSql(String sqlTest, List<ParameterMapping> parameterMappings) {
this.sqlTest = sqlTest;
this.parameterMappings = parameterMappings;
}
public String getSqlTest() {
return sqlTest;
}
public void setSqlTest(String sqlTest) {
this.sqlTest = sqlTest;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
工具:将原始的sql语句转变成可以预编译处理的sql
GenericTokenParser
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
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;
}
}
ParameterMappingTokenHandler
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
public interface TokenHandler {
String handleToken(String content);
}
测试
@Test
public void t() throws Exception {
InputStream in = Resources.getResourceAsStream("sqlMapConfig.xml");//加载配置文件
SqlSessionFactory factory = new SqlSessionFactoryBuild().build(in);//填充JavaBean,生成工厂
SqlSession sqlSession = factory.openSession();//工厂生产SqlSession
//测试selectOne
User user = new User();
user.setId(1L);
user.setUsername("灰二");
User user1 = sqlSession.selectOne("user.selectOne", user);
System.out.println(user1);
//测试selectList
List<Object> objects = sqlSession.selectList("user.selectList");
objects.forEach(System.out::println);
}
1.5 自定义框架的优化
# 原始自定义框架存在的问题
-- 1.代码重复度高
-- 加载配置文件
-- 创建工厂
-- 生成SqlSession
-- 2.存在硬编码问题
-- sql的唯一标识需要手动书写
# 解决
-- 1.不要具体的实现类,使用代理模式生成Dao层接口的代理实现类
使用端
1.增加dao层接口,里面定义方法
2.修改mapper.xml配置文件内容
namspace:使用接口的全类名
id:使用接口中的方法名
UserMapper
public interface UserMapper {
List<User> selectList();
User selectOne(User user);
}
mapper.xml
<mapper namespace="com.huier.mapper.UserMapper">
<!--唯一标识:namespace.id-->
<select id="selectList" resultType="com.huier.pojo.User">
select * from user;
</select>
<!-- <select id="selectOne">-->
<!-- select * from user where id = ?;-->
<!-- </select>-->
<select id="selectOne" paramterType="com.huier.pojo.User" resultType="com.huier.pojo.User">
select * from user where id = #{id} and username = #{username};
</select>
</mapper>
框架端
1.在SqlSession中增添获得代理对象的方法
2.通过被代理的对象的全类名+方法名获sql语句的唯一标识
SqlSession接口
public interface SqlSession {
//可能模糊查询
public <E> List<E> selectList(String statementId,Object... params) throws Exception;
public <E> E selectOne(String statementId,Object... params) throws Exception;
//生成代理对象
public <E> E getMapper(Class<?> mapperClass);
}
DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
Executor executor = new DefaultExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
return executor.query(configuration,mappedStatement,params);
}
public <E> E selectOne(String statementId, Object... params) throws Exception {
List<Object> list = selectList(statementId, params);
if(list.size() == 1){
return (E) list.get(0);
}else{
throw new RuntimeException("结果过多");
}
}
public <E> E getMapper(Class<?> mapperClass) {
//使用jdk动态代理
/*
三个参数
1.类加载器
2.class数组
3.必须要实现的接口
*/
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
/*
proxy:当前代理对象的引用
method:当前被调用方法的引用
args:传递的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//底层还是执行jdbc代码
//准备参数 1.namespace.id sql语句的唯一标识
//接口全限定名+方法名
String methodName = method.getName();//方法名
String interfaceName = method.getDeclaringClass().getName();//接口全限定名
String statementId = interfaceName+"."+methodName;
//准备参数 2.参数
Type genericReturnType = method.getGenericReturnType();//获得方法返回值类型
if(genericReturnType instanceof ParameterizedType){//判断是否有泛型
return selectList(statementId,args);
}else{
return selectOne(statementId,args);
}
}
});
return (E) proxyInstance;
}
}
测试
@Test
public void t2() throws Exception {
InputStream in = Resources.getResourceAsStream("sqlMapConfig.xml");//加载配置文件
SqlSessionFactory factory = new SqlSessionFactoryBuild().build(in);//填充JavaBean,生成工厂
SqlSession sqlSession = factory.openSession();//工厂生产SqlSession
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = userMapper.selectList();
users.forEach(System.out::println);
}
@Test
public void t3() throws Exception {
InputStream in = Resources.getResourceAsStream("sqlMapConfig.xml");//加载配置文件
SqlSessionFactory factory = new SqlSessionFactoryBuild().build(in);//填充JavaBean,生成工厂
SqlSession sqlSession = factory.openSession();//工厂生产SqlSession
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User findUser = new User();
findUser.setId(1L);
findUser.setUsername("灰二");
System.out.println(userMapper.selectOne(findUser));
}
# 总结
# 1.使用端创建配置文件,框架端加载并存储配置信息,配置信息包含了数据源+sql信息。
# 2.sql配置文件的namespace.id是sql的唯一标识。
# 3.不使用动态代理:手动写namespace.id找到唯一sql语句然后执行。
# 4.使用动态代理:namespace必须是接口的全类名,id必须是方法名,通过被代理对象的方法获得信息,然后执行sql。