1. 传统的JDBC模式 在没有使用ORM框架时,我们基本都是通过JDBC进行数据库的操作,一般的逻辑代码如下:
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 public static void main (String[] args) { Connection connection = null ; PreparedStatement preparedStatement = null ; ResultSet resultSet = null ; try { Class.forName("com.mysql.jdbc.Driver" ); connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8" , "root" , "root" ); String sql = "select * from user where username = ?" ; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1 , "tom" ); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { int id = resultSet.getInt("id" ); String username = resultSet.getString("username" ); user.setId(id); user.setUsername(username); } 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(); } } } }
通过对JDBC的代码进行分析,可以发现存在以下问题:
数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能;
Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码;
使用preparedStatement向占位符传参存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护;
对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便;
由于硬编码可以通过配置文件解决,针对上述的问题,所以我们可以想到如下的解决思路:
使用数据库连接池初始化连接资源;
将sql语句抽取到xml配置文件中;
使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射;
根据上述的思路进行自定义的Mybatis框架编写。
2. 客户端开发 首先客户端要提供数据库的连接信息以及SQL的信息。 根据设计思路,这些信息通过配置文件来解决,所以在resources目录下创建sqlMapConfig.xml文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 <configuration > <dataSource > <property name ="driver" value ="com.mysql.jdbc.Driver" > </property > <property name ="jdbcUrl" value ="jdbc:mysql:///test" > </property > <property name ="username" value ="root" > </property > <property name ="password" value ="123456" > </property > </dataSource > <mappers > <mapper resource ="UserMapper.xml" > </mapper > </mappers > </configuration >
在这个文件中配置了要加载那些mapper.xml,这样只需要加载一次就完成了所有的加载。 在同个目录下创建映射配置文件UserMapper.xml,并定义了简单的增删改查的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 <mapper namespace ="com.ormtest.mapper.UserMapper" > <select id ="selectAll" resultType ="com.ormtest.pojo.User" > select * from user </select > <select id ="selectOne" resultType ="com.ormtest.pojo.User" parameterType ="com.ormtest.pojo.User" > select * from user where id = #{id} and username = #{username} </select > <insert id ="insertUser" parameterType ="com.ormtest.pojo.User" > insert into user values(#{id},#{username},#{password}) </insert > <update id ="updateUser" parameterType ="com.ormtest.pojo.User" > update user set username = #{username} where id = #{id} </update > <delete id ="deleteUser" parameterType ="java.lang.Integer" > delete from user where id = #{id} </delete > </mapper >
同时也要生成对应的POJO对象,并定义Dao层接口,代码简单,此处就不粘贴了。 到此为止,客户的代码就编写完成了,等完成框架的编写之后,就可以进行测试了。
3. 自定义框架开发 开发的时候,我们根据逻辑流程一步步进行。
首先我们需要一个类来接受和加载核心配置文件:
1 2 3 4 5 6 7 8 public class Resources { public static InputStream getResourceAsSteam (String path) { InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
核心配置文件加载完成之后,就需要对字节流进行解析,所以声明一个解析xml的类,最终这个类要返回一个SqlSessionFactory的对象:
1 2 3 4 5 6 7 8 9 10 11 12 public class SqlSessionFactoryBuilder { public SqlSessionFactory build (InputStream in) throws Exception { XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(); Configuration configuration = xmlConfigBuilder.parseConfig(in); SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; } }
在这个解析的过程中,声明了一些解析类,比如XMLConfigBuilder进行核心配置文件的解析,里面还嵌套调用了mapper.xml的解析类,代码如下:
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 public class XMLConfigBuilder { private Configuration configuration; private static final String DRIVER = "driver" ; private static final String JDBCURL = "jdbcUrl" ; private static final String USERNAME = "username" ; private static final String PASSWORD = "password" ; public XMLConfigBuilder () { this .configuration = new Configuration(); } public Configuration parseConfig (InputStream inputStream) throws DocumentException, PropertyVetoException { Document document = new SAXReader().read(inputStream); Element rootElement = document.getRootElement(); List<Element> elementList = rootElement.selectNodes("//property" ); Properties properties = new Properties(); for (Element element : elementList) { String name = element.attributeValue("name" ); String value = element.attributeValue("value" ); properties.setProperty(name, value); } ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); comboPooledDataSource.setDriverClass(properties.getProperty(DRIVER)); comboPooledDataSource.setJdbcUrl(properties.getProperty(JDBCURL)); comboPooledDataSource.setUser(properties.getProperty(USERNAME)); comboPooledDataSource.setPassword(properties.getProperty(PASSWORD)); configuration.setDataSource(comboPooledDataSource); List<Element> mappers = rootElement.selectNodes("//mapper" ); for (Element element : mappers) { String mapperPath = element.attributeValue("resource" ); InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); xmlMapperBuilder.parseMapper(resourceAsSteam); } return this .configuration; } }
通过解析mapper.xml,生成对应的mappedStatement对象,存入Configuration对象中,因为全局只有一个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 public class XMLMapperBuilder { private Configuration configuration; public XMLMapperBuilder (Configuration configuration) { this .configuration = configuration; } public void parseMapper (InputStream inputStream) throws DocumentException { Document document = new SAXReader().read(inputStream); Element rootElement = document.getRootElement(); String namespace = rootElement.attributeValue("namespace" ); List<Element> elementList = rootElement.elements(); for (Element element : elementList) { String id = element.attributeValue("id" ); String resultType = element.attributeValue("resultType" ); String parameterType = element.attributeValue("parameterType" ); String sqlText = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setId(id); mappedStatement.setResultType(resultType); mappedStatement.setParameterType(parameterType); mappedStatement.setSql(sqlText); mappedStatement.setSqlCommandType(element.getName()); String key = namespace+"." +id; configuration.getMappedStatementMap().put(key,mappedStatement); } } }
到此,所有的配置文件都加载完成,最终根据生成的Configuration对象,返回对应的SqlSessionFactory对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory (Configuration configuration) { this .configuration = configuration; } @Override public SqlSession openSession () { return new DefaultSqlSession(configuration); } }
当客户端调用openSession方法时,就会返回一个SqlSession对象,该对象里面封装了增删改查的方法,供客户端调用:
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 public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession (Configuration configuration) { this .configuration = configuration; } @Override public Object doQuery (String statementId, boolean resultTypeFlag, Object... params) throws Exception { MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); switch (mappedStatement.getSqlCommandType()) { case "select" : { if (resultTypeFlag) { return selectList(statementId, params); } else { return selectOne(statementId, params); } } case "insert" :{ return insert(statementId, params); } case "update" :{ return update(statementId, params); } case "delete" :{ return delete(statementId, params); } } return null ; } @Override public <E> List<E> selectList (String statementId, Object... params) throws Exception { MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); Executor executor = new SimpleExecutor(); List<Object> result = executor.query(configuration, mappedStatement, params); return (List<E>) result; } @Override public <T> T selectOne (String statementId, Object... params) throws Exception { List<Object> objects = this .selectList(statementId, params); if (1 == objects.size()) { return (T) objects.get(0 ); } else { throw new RuntimeException("查询结果为空或查询结果过多" ); } } @Override public int insert (String statementId, Object... params) throws Exception { MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); Executor executor = new SimpleExecutor(); executor.updateDatabase(configuration, mappedStatement, params); return 1 ; } @Override public int update (String statementId, Object... params) throws Exception { MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); Executor executor = new SimpleExecutor(); executor.updateDatabase(configuration, mappedStatement, params); return 1 ; } @Override public int delete (String statementId, Object... params) throws Exception { MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); Executor executor = new SimpleExecutor(); executor.updateDatabase(configuration, mappedStatement, params); return 1 ; } @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){ return doQuery(statementId, true , args); } return doQuery(statementId, false , args); } }); return (T) proxyInstance; } }
最终的增删改查操作实际上还是通过JDBC来实现,所以声明一个Executor类来专门执行具体操作:
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 public class SimpleExecutor implements Executor { @Override public <E> List<E> query (Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception { PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params); ResultSet resultSet = preparedStatement.executeQuery(); List<Object> resultList = new ArrayList<>(); String resultType = mappedStatement.getResultType(); Class<?> resultTypeClass = getClassType(resultType); 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); } resultList.add(o); } return (List<E>) resultList; } @Override public boolean updateDatabase (Configuration configuration, MappedStatement mappedStatement, Object... params) throws ClassNotFoundException, SQLException, IllegalAccessException, NoSuchFieldException { PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params[0 ]); boolean execute = preparedStatement.execute(); return execute; } private PreparedStatement getPreparedStatement (Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Connection connection = configuration.getDataSource().getConnection(); String sql = mappedStatement.getSql(); BoundSql boundSql = getBoundSql(sql); PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText()); String parameterType = mappedStatement.getParameterType(); Class<?> parameterTypeClass = getClassType(parameterType); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0 ; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); Field declaredField = null ; if (isCommonDataType(parameterTypeClass) || isWrapClass(parameterTypeClass)) { declaredField = parameterTypeClass.getDeclaredField("value" ); } else { declaredField = parameterTypeClass.getDeclaredField(content); } declaredField.setAccessible(true ); Object o = declaredField.get(params[0 ]); preparedStatement.setObject(i + 1 , o); } return preparedStatement; } 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 ; } } private Class<?> getClassType(String type) throws ClassNotFoundException { if (type != null ){ Class<?> clazz = Class.forName(type); return clazz; } return null ; } private BoundSql getBoundSql (String sql) { ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{" , "}" , parameterMappingTokenHandler); String formatSql = genericTokenParser.parse(sql); List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(formatSql, parameterMappings); return boundSql; } }
还有一些基础的POJO这里就不再粘贴了,到此,框架的代码就基本开发完毕,下面进行测试。
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 30 31 32 33 34 public class MyTest { private SqlSession sqlSession; @Before public void prepare () throws Exception { InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml" ); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam); sqlSession = sqlSessionFactory.openSession(); } @Test public void test () throws Exception { List<User> userList = sqlSession.selectList("com.ormtest.mapper.UserMapper.selectAll" ); for (User user : userList) { System.out.println(user); } } @Test public void test2 () throws Exception { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> userList = userMapper.selectAll(); for (User user : userList) { System.out.println(user); } } }
最终测试通过,查询结果正确。
至此,自定义Mybatis的简单实现就完成了~~