我们不重复制造轮子,这里主要写的是如何封装JDBC,实现将数据库查询直接映射成javaBean,实现数据与对象的查询与映射。进阶可以思考开源框架hibernate,mybatis、JPA的底层是如何实现的。
直接正题,需要具备的基础知识如下:
这里复习下JDBC的流程
那么我们开始封装JDBC流程,将可变结果集转换行为使用策略模式进行抽象,可以创建接口:
public interface ResultSetHandler {
/**结果集转换*/
T doHandler(ResultSet rs) throws SQLException;
}
封装JDBC查询方法如下(没有具体实现的方法,在ide中可能会有红色底线,不要紧,我们需要的是抽象编程,虽然该实现使用的是Java语言,学会抽象该ORM框架换个语言也可以实现):
/**
* 基础查询,需要处理结果集映射
* @param
* @param conn
* @param sql
* @param params
* @param resultSetHandler
* @return
* @throws SQLException
*/
public static T find(Connection conn, String sql, Map
我们开始实现processSQL解析方法sql:select * from user where user_name= #{userName} ,因为解析器是复杂的,需要逐个切割字符串将 #{userName} 替换为 '?' 号,然后在将params对应key为 userName的值记录后面设置查询参数的位置(第几个问号),可以自行切割字符串然后解析#{}符号切割解析,或者直接参考hibernate的SQLQueryParser解析器,或参考mybatis的源码,这里我们就使用简单正则表达式进行解析(可能存在bug,仅写此博客使用过),一个我们的目的不是为了真正可以运行在生产环境,而是这ORM框架sql解析的一种抽象策略,我们的目的还是为了手写框架(不限制语言),解析器代码简单如下:
/**
* 解析sql的一种策略抽象
* @param sql 类似这样的HQL:select * from user where user_name= #{userName} and password=#{password}
* @param result 解析每个参数的位子,key参数名,value位置
* @return 最终可执行的预编译sql带问号
*/
private static String processSQL(String sql, Map result) {
Matcher m = Pattern.compile("\\#\\{.*?\\}").matcher(sql);
StringBuffer sb = new StringBuffer();
int i=0;
while (m.find()) {
String param = m.group();
String key = param.replaceAll("\\#|\\{|\\}", "").trim();
result.put(key, i++);//记录每个参数的位置
m.appendReplacement(sb, "?");//将每个参数 替换为 问号
}
m.appendTail(sb);
return sb.toString();
}
实现setPreparedStatementParams,为每个‘?’问号的参数设置值,逻辑就是前者解析sql得到每个问号的位置与每个参数的字段名称,给相应设值,这里简单扩展了下查询支持时间数据类型,兴趣的同学可以基于此自行扩展支持IN参数等复杂参数查询
/**
* @param pst 查询对象
* @param result 参数的位置
* @param params 查询参数
* @throws SQLException
*/
private static void setPreparedStatementParams(PreparedStatement pst, Map result, Map
此时我们已经实现了基础的sql查询得到结果集,现在开始增强该查询,支持JavaBean的参数与,返回List
/**
* 高级查询支持javaBean自动映射,支持复杂参数
*/
public static List findForList(Connection conn, String sql, Object bean, final Class clazz) throws SQLException {
//解析Bean参数转换为前者基础查询的Map参数
Map
开始实现paramConvert,这里我偷懒了直接使用org.apache.commons.beanutils.PropertyUtils的工具类了,其实使用原本的反射也是可以的,因为反射需要些很多代码,我偷懒了,这里简单说话实现逻辑,利用类反射,遍历obj的所有属性,将属性名作为hashMap的key,属性值put到hashMap中。
/**
* 将参数全部转换为Map参数
*/
@SuppressWarnings("unchecked")
private static Map
接下来实现getObjectFromResultSet对单条的结果集进行映射转换,这里写死小驼峰转换,其中我又偷懒了,使用了org.apache.commons.lang.StringUtils的工具类,但是这不要紧,重要的是我们将每个过程都策略抽象了
private static T getObjectFromResultSet(ResultSet rs, Class clazz) throws SQLException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
Object resultBean = null;
if (clazz == String.class) {
return (T) rs.getString(1);
} else if (clazz == Integer.class || clazz == int.class) {
return (T) new Integer(rs.getInt(1));
} else if (clazz == Boolean.class || clazz == boolean.class) {
return (T) new Boolean(rs.getBoolean(1));
} else if (clazz == Float.class || clazz == float.class) {
return (T) new Float(rs.getFloat(1));
} else if (clazz == short.class || clazz == Short.class) {
return (T) new Short(rs.getShort(1));
} else if (clazz == Double.class || clazz == double.class) {
return (T) new Double(rs.getDouble(1));
} else if (clazz == Long.class || clazz == long.class) {
return (T) new Long(rs.getLong(1));
} else {
resultBean = clazz.newInstance();
ResultSetMetaData rmd = rs.getMetaData();
for (int i = 1; i <= rmd.getColumnCount(); i++) {
String colName = rmd.getColumnLabel(i).toLowerCase();
// 转换属性名称
String colNameConverted = convertColName(colName);
String setName = "set" + colNameConverted;
Method method = getMethod(setName, clazz);
if (method == null) {
continue;
}
Class>[] paramtypes = method.getParameterTypes();
Class> paramType = paramtypes[0];
if (paramType.getName().equals("java.lang.String")) {
String val = "";
val = rs.getString(i);
method.invoke(resultBean, new Object[] { val });
} else if (paramType.getName().equals(Integer.class.getName()) || paramType.getName().equals("int")) {
method.invoke(resultBean, new Object[] { rs.getInt(i) });
} else if (paramType.getName().equals(Long.class.getName()) || paramType.getName().equals("long")) {
method.invoke(resultBean, new Object[] { rs.getLong(i) });
} else if (paramType.getName().equals(Long.class.getName()) || paramType.getName().equals("float")) {
method.invoke(resultBean, new Object[] { rs.getFloat(i) });
} else if (paramType.getName().equals(Double.class.getName()) || paramType.getName().equals("double")) {
method.invoke(resultBean, new Object[] { rs.getDouble(i) });
} else if (paramType.getName().equals(BigDecimal.class.getName())) {
method.invoke(resultBean, new Object[] { rs.getBigDecimal(i) });
}else if(paramType.getName().equals(Date.class.getName())){
Timestamp timestamp = rs.getTimestamp(i);
if(timestamp!=null){
Date jDate=new Date(timestamp.getTime());
method.invoke(resultBean, new Object[] {jDate});
}
}
}
}
return (T) resultBean;
}
/**小驼峰*/
private static String convertColName(String colName) {
String[] tmp = colName.toLowerCase().split("_");
StringBuilder sb = new StringBuilder();
for (String col : tmp) {
//这里又偷懒了,使用org.apache.commons.lang.StringUtils
sb.append(StringUtils.capitalize(col));
}
return sb.toString();
}
/**
* 从类中获取某方法对象
*
* @param name
* @param cla
* @return
*/
private static Method getMethod(String name, Class> cla) {
Method[] mtds = cla.getMethods();
for (int i = 0; i < mtds.length; i++) {
if (name.equals(mtds[i].getName())) {
return mtds[i];
}
}
return null;
}
到这里我们就实现了基础的查询功能了
public static void main(String[] args) throws SQLException, ClassNotFoundException {
String sql="select * from (select '姓名1' user_name ,'123' password from dual UNION select '姓名2' user_name ,'123' password from dual ) where user_name = #{userName} and password=#{password} ";
Connection conn=getConnection();//该方法请自行实现
Map bean=new HashMap();
bean.put("userName", "姓名1");
bean.put("password", "123");
List findForList = findForList(conn, sql, bean, String.class);
conn.close();
System.out.println();
}
到这里我们已经实现了基础查询, 更新修改sql就留给同学们了, 接下来我们来思考hibernate,mybatis、JPA他们是如何实现的
hibernate使用配置xml文件或注解绑定对象进行查询与映射,使用HQL,其实就是在我们解析processSQL的方法抽象的另外一种实现,在结果集映射getObjectFromResultSet也换成另外一种映射实现,那么就是将这些方法都进行策略抽象即可。
mybatis使用xml或注解与hibernate同理,只是mybatis的xml配置文件可以使用OGNL(对象导航图语言(Object Graph Navigation Language))这个有兴趣的同学可以关注留言,改天也可以带大家手写实现这个OGNL(简单实现),在接口方法名与配置的sql进行绑定,使用类方射调用。
同理JPA使用的是接口方法名,按照一定的规则生成sql执行
回到正题,接下来开始手写mybatis的注解查询 ,新建注解Select与Param
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
String[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
String value();
}
新建测试查询相关类
interface UserDao {
@Select("select * from (select '姓名1' user_name ,'123' password from dual UNION select '姓名2' user_name ,'123' password from dual ) where user_name = #{userName} and password=#{password} ")
User user(@Param("userName")String userName,@Param("password") String password);
@Select("select * from (select '姓名1' user_name ,'123' password from dual UNION select '姓名2' user_name ,'123' password from dual ) ")
List users();
}
public static class User{
String userName;
String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
这里的类容都是利用类反射加泛型加注解的灵活运用,外加class扫描(此文不实现扫描),感觉有点手写SpringMVC框架时候controller的方法解析的味道
解析接口,将接口的所有方法查询解析返回得到Dao接口类
/**
* 解析接口生成可以调用的接口实现
* @param
* @param inClazz 接口类型
* @return
*/
@SuppressWarnings("unchecked")
public static T processDaoInterface(Class inClazz,final DataSource dataSource){
//用来存储接口每个方法的sql,缓存高效性能,所以mybatis修改xml或者注解需要重新启动工程加载(或自定义实现插件)
final Map metaData=new HashMap();
Method[] declaredMethods = inClazz.getDeclaredMethods();
for (Method method : declaredMethods) {
String mName = method.getName();
//这里仅仅实现Select注解,其他注解 就留给同学们了
Select annotation = method.getAnnotation(Select.class);
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Map paramIndexMap=new HashMap();//记录方法参数的位置与名称
int paramIndex=0;
for (Annotation[] pA : parameterAnnotations) {
//这里偷懒默认就只认为参数只要Param注解
if(pA[0].annotationType()==Param.class) {
String paramName = ((Param)pA[0]).value();//参数名
paramIndexMap.put(paramIndex++,paramName);
}
}
metaData.put(mName, new Object[] {annotation.value()[0],paramIndexMap});
}
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class>[] {inClazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
Object[] metaDataA = metaData.get(name);
String sql=(String) metaDataA[0];//根据方法名称拿到SQL
Map paramIndexMap =(Map) metaDataA[1];//参数的位置
//组长基查询参数
Map findParam=new HashMap();
for (int i = 0; args!=null && i < args.length; i++) {
findParam.put(paramIndexMap.get(i), args[i]);
}
//返回值类型
Type returnType = method.getGenericReturnType();
Type rawType=returnType;
if (returnType instanceof ParameterizedType) {
//根据接口方法返回值获取 返回值类型 与 列表的泛型
ParameterizedType returnTypeP= ((ParameterizedType) returnType);
Type[] actualTypeArguments = returnTypeP.getActualTypeArguments();//泛型
returnType=actualTypeArguments[0];
rawType = returnTypeP.getRawType();//List类型
}
//策略抽象获取连接,后面可扩展事务管理,
Connection conn = dataSource.getConnection();
List> findForList = findForList(conn, sql, findParam, (Class)returnType);
if(rawType==List.class) {
return findForList;
}else {
return findForList.isEmpty()? null:findForList.get(0);
}
}
});
}
接下来就是主函数测试调用,processDaoInterface方法能根据接口类型与数据源得到一个接口实现对象(是一个动态代理类),这里可以我们思考下mybatis在与spring整合时候需要配置指向一个数据源(数据库连接池)其实就是这个,然后将所有Dao保存到IOC容器中,这里仅是一个主函数进行测试,如下:
public static void main(String[] args) throws SQLException, ClassNotFoundException {
final Connection conn = getConnection();// 该方法请自行实现
String sql = "select * from (select '姓名1' user_name ,'123' password from dual UNION select '姓名2' user_name ,'123' password from dual ) where user_name = #{userName} and password=#{password} ";
Map bean = new HashMap();
bean.put("userName", "姓名1");
bean.put("password", "123");
List findForList = findForList(conn, sql, bean, String.class);
UserDao processDaoInterface = processDaoInterface(UserDao.class, new DataSource() {
@Override
public T unwrap(Class iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return false;
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public Connection getConnection() throws SQLException {
return conn;
}
});
List findUser = processDaoInterface.users();
User user = processDaoInterface.user("姓名1", "123");
System.out.println(findUser);
System.out.println(user);
}
执行测试结果如下:
到这里就已经实现了mybatis使用接口上的注解进行数据库Dao查询操作,同理其他原理一致,本文中的代码并不与原开源框架相同,只是框架功能的简单实现,框架只是对API的封装,使用了设计模式可对原本的框架进行插件扩展,多个层切面可随意替换,但总体流程是缺少不了的。
下篇将介绍数据库查询跨Dao的事务管理器是什么,spring的事务管理器,事务传播是如何实现的,进阶分布式事务管理器要如何实现。
最后,其实这些代码就是我现场手写一行,然后写一行博客内容(大家只要将代码拷贝到一个类文件中就可以执行的),其实花时间的(我偷懒了,也并无经过健壮性测试,是存在bug的,仅限于学习使用),至于前面提到的OGNL(对象导航图语言(Object Graph Navigation Language))这块相当于一门解释行语言,这块的实现还是有点意思的,提示下,例如需要数据结构"栈"的灵活运用,解析逻辑标签等,这块的原理相当于实现一门解释语言的味道。
看了该文章,如果喜欢的,请给我关注加评论支持下,谢谢!
另外该文的源码文件下载地址 https://download.csdn.net/download/qq_18497293/13755283