自定义持久层框架思路分析
JDBC问题分析
- 数据库配置信息存在硬编码问题。
- 频繁创建释放数据库链接
- sql语句、设置参数、获取结果集,存在硬编码问题
-
解决方法
利用配置文件解决数据库硬编码问题。
- 利用连接池解决频繁创建释放问题。
- 利用配置文件解决sql语句、设置参数、获取结果集硬编码问题。
-
思路
引入自定义持久层框架的jar包
- 提供两部分配置信息:数据库配置信息、sql配置信息(SQL语句,参数类型,返回值类型)
- 使用配置文件来提供这两部分配置信息:
(1)sqlMapConfig.xml : 存放数据库配置信息
(2)mapper.xml :存放sql配置信息
自定义持久层框架:
本质就是对JDBC代码进行了封装。 - 加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。创建Resources类,方法:InputSteam getResourceAsSteam(String path)
- 创建两个javaBean:存放的就是对配置文件解析出来的内容。
Configuration:核心配置类,存放sqlMapConfig.xml解析出来的内容。
MappedStatement:映射配置类,存放mapper.xml解析出来的内容。 - 解析配置文件(dom4j):
创建类:SqlSessionFactoryBuilder,方法:build(InputSteam in)
(1)使用dom4j解析配置文件,将解析出来的内容封装到容器对象中。
(2)创建SqlSessionFactory对象;生产sqlSession(会话对象[工厂模式]) - 创建sqlSessionFactory接口及实现DefaultSqlSessionFactory。
第一:openSession():生产sqlSession - 创建SqlSession接口及实现DefaultSqlSession。定义对数据库CRUD的操作。
- 创建Executer接口及实现类SimpleExecuter实现类。
Query(Configuration,MappedStatement,Object… params):执行的就是JDBC代码
编写测试类
- 引入自定义持久层框架的依赖
<!--引入自定义持久层框架的依赖--><dependencies><dependency><groupId>com.learn</groupId><artifactId>IPersistence</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
- 创建sqlMapConfig.xml
<configuration><!-- 数据库配置信息--><dataSource><propewrty name="driverClass" value="com.mysql.jdbc.Driver"></propewrty><propewrty name="jdbcUrl" value="jdbc:mysql:///blog"></propewrty><propewrty name="username" value="root"></propewrty><propewrty name="password" value="root"></propewrty></dataSource><!-- 存放mapper.xml的全路径--><mapper resource="UserMapper.xml"></mapper></configuration>
- 创建UserMapper.xml
注意:namespace.id作为映射xml的唯一标识。例如:user.selectUserList
<mapper namespace="user"><select id="selectUserList" resultType="com.learn.pojo.User">select * from t_user</select><select id="selectUserInfo" resultType="com.learn.pojo.User" paramterType="com.learn.pojo.User">select * from t_user where PK_ID= #{id} AND USER_NAME = #{userName}</select></mapper>
Resources类定义
新建一个模块,封装JDBC的代码。
创建一个Resources类
public class Resources {// 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中public static InputStream getResourceAsSteam(String path){InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);return resourceAsStream;}}
容器对象定义
- 定义Configuration容器对象
public class Configuration {private DataSource dataSource;/** key: statementid value:封装好的mappedStatement对象* */Map<String,MappedStatement> mappedStatementMap = new HashMap<>();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映射对象
public class Configuration {private DataSource dataSource;/** key: statementid value:封装好的mappedStatement对象* */Map<String,MappedStatement> mappedStatementMap = new HashMap<>();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;}}
解析配置文件mapper.xml 和 数据库连接配置文件sqlMapConfig.xml
- 实现DefaultSqlSessionFactory的build()方法,返回一个SqlSessionFactory接口对象
public class SqlSessionFactoryBuilder {public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {// 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();Configuration configuration = xmlConfigBuilder.parseConfig(in);// 第二:创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);return defaultSqlSessionFactory;}}
- 创建类XMLConfigBuilder,对sqlMapConfig.xml配置文件进行解析
public class XMLConfigBuilder {private Configuration configuration;public XMLConfigBuilder() {this.configuration = new Configuration();}/*** 该方法就是使用dom4j对配置文件进行解析,封装Configuration*/public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {Document document = new SAXReader().read(inputStream);//<configuration>Element rootElement = document.getRootElement();List<Element> list = rootElement.selectNodes("//property");Properties properties = new Properties();for (Element element : list) {String name = element.attributeValue("name");String value = element.attributeValue("value");properties.setProperty(name,value);}ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));comboPooledDataSource.setUser(properties.getProperty("username"));comboPooledDataSource.setPassword(properties.getProperty("password"));configuration.setDataSource(comboPooledDataSource);//mapper.xml解析: 拿到路径--字节输入流---dom4j进行解析List<Element> mapperList = rootElement.selectNodes("//mapper");for (Element element : mapperList) {String mapperPath = element.attributeValue("resource");InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);xmlMapperBuilder.parse(resourceAsSteam);}return configuration;}}
- 创建类XMLMapperBuilder,对mapper.xml映射配置文件进行解析
public class XMLMapperBuilder {private Configuration configuration;public XMLMapperBuilder(Configuration configuration) {this.configuration =configuration;}public void parse(InputStream inputStream) throws DocumentException {Document document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();String namespace = rootElement.attributeValue("namespace");List<Element> list = rootElement.selectNodes("//select");for (Element element : list) {String id = element.attributeValue("id");String resultType = element.attributeValue("resultType");String paramterType = element.attributeValue("paramterType");String sqlText = element.getTextTrim();MappedStatement mappedStatement = new MappedStatement();mappedStatement.setId(id);mappedStatement.setResultType(resultType);mappedStatement.setParamterType(paramterType);mappedStatement.setSql(sqlText);String key = namespace+"."+id;configuration.getMappedStatementMap().put(key,mappedStatement);}}}
- 创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象,返回一个SqlSession的对象
public interface SqlSessionFactory {public SqlSession openSession();}
public class DefaultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}@Overridepublic SqlSession openSession() {return new DefaultSqlSession(configuration);}}
会话对象sqlSession类定义
定义SqlSession接口并且实现接口DefaultSqlSession类
实现查询所有数据、根据条件查询耽搁数据、为Dao接口生成代理实现类
public interface SqlSession {//查询所有public <E> List<E> selectList(String statementid,Object... params) throws Exception;//根据条件查询单个public <T> T selectOne(String statementid,Object... params) throws Exception;//为Dao接口生成代理实现类public <T> T getMapper(Class<?> mapperClass);}
public class DefaultSqlSession implements SqlSession {private Configuration configuration;public DefaultSqlSession(Configuration configuration) {this.configuration = configuration;}@Overridepublic <E> List<E> selectList(String statementid, Object... params) throws Exception {//将要去完成对simpleExecutor里的query方法的调用simpleExecutor simpleExecutor = new simpleExecutor();MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementid);List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);return (List<E>) list;}@Overridepublic <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("查询结果为空或者返回结果过多");}}@Overridepublic <T> T getMapper(Class<?> mapperClass) {// 使用JDK动态代理来为Dao接口生成代理对象,并返回Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne// 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名// 方法名:findAllString methodName = method.getName();String className = method.getDeclaringClass().getName();String statementId = className+"."+methodName;// 准备参数2:params:args// 获取被调用方法的返回值类型Type genericReturnType = method.getGenericReturnType();// 判断是否进行了 泛型类型参数化if(genericReturnType instanceof ParameterizedType){List<Object> objects = selectList(statementId, args);return objects;}return selectOne(statementId,args);}});return (T) proxyInstance;}}
最后调用jdbc底层执行sql,然后封装结果集,将数据进行返回。
创建执行器接口
public interface Executor {public <E> List<E> query(Configuration configuration,MappedStatement mappedStatement,Object... params) throws Exception;}
实现query方法
public class simpleExecutor implements Executor {@Override //userpublic <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {// 1. 注册驱动,获取连接Connection connection = configuration.getDataSource().getConnection();// 2. 获取sql语句 : select * from user where id = #{id} and username = #{username}//转换sql语句: select * from user where id = ? and username = ? ,转换的过程中,还需要对#{}里面的值进行解析存储String sql = mappedStatement.getSql();BoundSql boundSql = getBoundSql(sql);// 3.获取预处理对象:preparedStatementPreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());// 4. 设置参数//获取到了参数的全路径String paramterType = mappedStatement.getParamterType();Class<?> paramtertypeClass = getClassType(paramterType);List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();for (int i = 0; i < parameterMappingList.size(); i++) {ParameterMapping parameterMapping = parameterMappingList.get(i);String content = parameterMapping.getContent();//反射Field declaredField = paramtertypeClass.getDeclaredField(content);//暴力访问declaredField.setAccessible(true);Object o = declaredField.get(params[0]);preparedStatement.setObject(i+1,o);}// 5. 执行sqlResultSet resultSet = preparedStatement.executeQuery();String resultType = mappedStatement.getResultType();Class<?> resultTypeClass = getClassType(resultType);ArrayList<Object> objects = new ArrayList<>();// 6. 封装返回结果集while (resultSet.next()){Object o =resultTypeClass.newInstance();//元数据ResultSetMetaData metaData = resultSet.getMetaData();for (int i = 1; i <= metaData.getColumnCount(); 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);}objects.add(o);}return (List<E>) objects;}private Class<?> getClassType(String paramterType) throws ClassNotFoundException {if(paramterType!=null){Class<?> aClass = Class.forName(paramterType);return aClass;}return null;}/*** 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储* @param sql* @return*/private BoundSql getBoundSql(String sql) {//标记处理类:配置标记解析器来完成对占位符的解析处理工作ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);//解析出来的sqlString parseSql = genericTokenParser.parse(sql);//#{}里面解析出来的参数名称List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();BoundSql boundSql = new BoundSql(parseSql,parameterMappings);return boundSql;}}
解析sql的工具类
- 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;}}
- GenericTokenParser :这里的通用标记解析器处理的是SQL脚本中#{parameter}、${parameter}参数,根据给定TokenHandler(标记处理器)来进行处理,TokenHandler是标记真正的处理器,而本篇的解析器只是处理器处理的前提工序——解析,本类重在解析,而非处理,具体的处理会调用具体的TokenHandler的handleToken()方法来完成。
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)解析传进来的content内容,看是否有变量,如果有从对应参数类中获取这个属性的类型,
(2)解析其存在他其他如javaType等参数设置到ParameterMapping内部的Builder方法中
(3)通过Builder构建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;}}
- TokenHandler接口
public interface TokenHandler {String handleToken(String content);}
BoundSql工具类,存储xml中解析过后的sql字符串
public class BoundSql {private String sqlText; //解析过后的sqlprivate List<ParameterMapping> parameterMappingList = new ArrayList<>();public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {this.sqlText = sqlText;this.parameterMappingList = parameterMappingList;}public String getSqlText() {return sqlText;}public void setSqlText(String sqlText) {this.sqlText = sqlText;}public List<ParameterMapping> getParameterMappingList() {return parameterMappingList;}public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {this.parameterMappingList = parameterMappingList;}}
Client端运行测试
- 实体类User
public class User {private Integer id;private String username;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +'}';}}
- IUserDao类
public interface IUserDao {//查询所有用户public List<User> findAll() throws Exception;//根据条件进行用户查询public User findByCondition(User user) throws Exception;}
- 测试运行
public class IPersistenceTest {@Testpublic void test() throws Exception {InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);SqlSession sqlSession = sqlSessionFactory.openSession();//调用User user = new User();user.setId(1);user.setUsername("张三");/* User user2 = sqlSession.selectOne("user.selectOne", user);System.out.println(user2);*//* List<User> users = sqlSession.selectList("user.selectList");for (User user1 : users) {System.out.println(user1);}*/IUserDao userDao = sqlSession.getMapper(IUserDao.class);List<User> all = userDao.findAll();for (User user1 : all) {System.out.println(user1);}}}
