一、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 ; 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); String sql = "select * from user where id = ?" ; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1 , 1 ); resultSet = preparedStatement.executeQuery(); 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 方式存在的问题
硬编码问题:(解决思路:配置文件)
数据库配置信息
sql 语句、设置参数、获取结果集
多次操作会频繁创建、释放数据库连接(解决思路:连接池)
手动封装结果集较为繁琐(解决思路:反射、内省)
二、 自定义持久层框架
框架端 + 使用端
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& serverTimezone=GMT%2B8" /> <property name ="user" value ="root" /> <property name ="password" value ="123456" /> <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 > <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 > <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 > 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; 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 { private Integer id; 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); 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.setDataSource(comboPooledDataSource); 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); Element rootElement = document.getRootElement(); String namespace = rootElement.attributeValue("namespace" ); List<Element> selectNodes = document.selectNodes("//select" ); for (Element element : selectNodes) { String id = element.attributeValue("id" ); String parameterType = element.attributeValue("parameterType" ); String resultType = element.attributeValue("resultType" ); Class<?> parameterTypeClass = this .getClassType(parameterType); Class<?> resultTypeClass = this .getClassType(resultType); String statementId = namespace + "." + id; String sql = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setId(id); mappedStatement.setParameterType(parameterTypeClass); mappedStatement.setResultType(resultTypeClass); mappedStatement.setSql(sql); 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 { XMLConfigureBuilder xmlConfigureBuilder = new XMLConfigureBuilder(configuration); Configuration configuration = xmlConfigureBuilder.parseConfiguration(inputStream); 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) { 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(); BoundSql boundSql = this .getBoundSql(mappedStatement.getSql()); String finalSql = boundSql.getSqlText(); 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 ]); preparedStatement.setObject(i + 1 , o); } 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) { ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); 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 package utils;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; } public String parse (String text) { if (text == null || text.isEmpty()) { return "" ; } int start = text.indexOf(openToken, 0 ); if (start == -1 ) { return text; } char [] src = text.toCharArray(); int offset = 0 ; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null ; while (start > -1 ) { if (start > 0 && src[start - 1 ] == '\\' ) { builder.append(src, offset, start - offset - 1 ).append(openToken); offset = start + openToken.length(); } else { 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 ] == '\\' ) { 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 ) { builder.append(src, start, src.length - start); offset = src.length; } else { 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; } } ```javapackage utils;import java.util.ArrayList;import java.util.List;public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); 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 package utils;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) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String statementId = className + "." + methodName; 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