JDBC工具类——JdbcUtils(2)
前言
本系列文章介绍JDBC工具类——JdbcUtils
的封装,部分实现参考了Spring框架的JdbcTemplate
。
完整项目地址:https://github.com/byx2000/JdbcUtils
回顾
在上一篇文章中,我们已经通过提取公共方法将获取连接、语句创建、释放资源这些重复操作抽象成了工具类中的方法。但是,在客户代码中还存在着一些结构上的重复。这些重复,可以通过模板方法模式来进一步封装。
由于查询操作比较常用,因此我们先考虑对查询操作进行封装。
抽象ResultSetMapper<T>
接口
在JDBC的查询操作中,需要对结果集进行处理,那么该如何处理呢?这需要让用户程序自己决定。所以,我们抽象出一个ResultSetMapper<T>
接口,用于封装用户程序对结果集的处理:
/**
* 结果集转换器接口
* @param <T> 转换类型
*/
public interface ResultSetMapper<T>
{
T map(ResultSet rs) throws Exception;
}
其中,泛型参数T
表示要将结果集转换成什么类型,而接口中的map
方法就是具体的转换函数。
比如说,如果我们想把结果集转换成一个User
类的列表,则可以写如下实现类:
public class UserListResultSetMapper implements ResultSetMapper<List<User>>
{
@Override
public List<User> map(ResultSet rs) throws Exception
{
List<User> users = new ArrayList<>();
while (rs.next())
{
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
users.add(user);
}
return users;
}
}
封装query
函数
有了ResultSetMapper<T>
接口,整个JDBC的查询操作就可以被封装成一个query
函数:
public class JdbcUtils
{
...
public static <T> T query(String sql, ResultSetMapper<T> resultSetMapper, Object... params)
{
ResultSet rs = null;
PreparedStatement stmt = null;
Connection conn = null;
try
{
conn = getConnection();
stmt = createPreparedStatement(conn, sql, params);
rs = stmt.executeQuery();
return resultSetMapper.map(rs);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
close(rs, stmt, conn);
}
}
...
}
其中,sql
和params
是用户程序传入的sql语句和参数,resultSetMapper
是用户程序自定义的结果集处理函数。
query
函数的返回值类型是一个泛型T
,该泛型参数就是ResultSetMapper
的泛型参数,即ResultSet
的转换结果类型。
在query
方法内部,调用了之前封装的getConnection
方法、close
方法和createPreparedStatement
方法。
下面两行代码获取结果集,然后调用用户自定义的结果集转换器转换结果集,最后返回转换结果:
rs = stmt.executeQuery();
return resultSetMapper.map(rs);
有了query
函数,假设用户需要实现一个“查询所有id大于5的用户”的需求,那么需要写如下代码:
// UserListResultSetMapper.java
public class UserListResultSetMapper implements ResultSetMapper<List<User>>
{
@Override
public List<User> map(ResultSet rs) throws Exception
{
List<User> users = new ArrayList<>();
while (rs.next())
{
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
users.add(user);
}
return users;
}
}
// 查询
List<User> users = JdbcUtils.query("SELECT * FROM users WHERE id > ?",
new UserListResultSetMapper(),
5);
如果类UserListResultSetMapper
是一次性的,也可以写成匿名内部类的形式:
List<User> users = JdbcUtils.query("SELECT * FROM users WHERE id > ?", new ResultSetMapper<List<User>>()
{
@Override
public List<User> map(ResultSet rs) throws Exception
{
List<User> us = new ArrayList<>();
while (rs.next())
{
User u = new User();
u.setId(rs.getInt("id"));
u.setUsername(rs.getString("username"));
u.setPassword(rs.getString("password"));
us.add(u);
}
return us;
}
}, 5);
如果使用Java8新增的lambda表达式,还可以进一步简化:
List<User> users = JdbcUtils.query("SELECT * FROM users WHERE id > ?", rs ->
{
List<User> us = new ArrayList<>();
while (rs.next())
{
User u = new User();
u.setId(rs.getInt("id"));
u.setUsername(rs.getString("username"));
u.setPassword(rs.getString("password"));
us.add(u);
}
return us;
}, 5);
query
函数的异常处理
query
函数不仅封装了查询操作的流程,同时也封装了异常处理。在query
函数的实现中,如果JDBC API抛出了异常,则将异常封装成RuntimeException
,然后直接向外抛出:
catch (Exception e)
{
throw new RuntimeException(e);
}
为什么要这么做呢?
首先,为了增强工具类的可用性,我们既要允许用户就近处理工具类抛出的异常,也不希望强制用户每次在调用工具类的方法时都加上try{...}catch{...}
结构。结合以上两点,就采取了上述“封装成RuntimeException
再重新抛出”的方法。采用这种方法,既可以保证异常传递出去,也不会强制用户捕获异常。
如果用户想就近处理异常,只需写如下代码:
try
{
// 调用JdbcUtils.query
}
catch (Exception e)
{
// 异常处理
}
如果用户使用了更高级的异常处理技术(如Spring框架的AOP),不想在数据访问层处理异常,而是在更高的层次对异常进行统一处理,则只需把try{...}catch{...}
结构去掉,让JdbcUtils
抛出的异常继续向上层抛出。
总结
到目前为止,我们把JDBC的查询操作封装成了一个query
函数,用户在调用函数的时候只需传入sql语句、sql参数和结果集转换器,就能直接获取查询结果,再也不用写繁琐冗长的JDBC原生API了!