手撕JDBC框架
开源地址:JDBCToMybatis Git
JDBC封装Mybatis
Select 原生SQL语句
封装原生查询,返回ResultSet
public ResultSet select(String sql, Object... values){
try {
Class.forName(driverPath);
Connection connection = DriverManager.getConnection(sqlUrl,user,password);
PreparedStatement pst = connection.prepareStatement(sql);
for (int i =0;i<values.length;i++){
//jdbc会自动转换类型
pst.setObject(i+1,values[i]);
}
ResultSet set = pst.executeQuery();
connection.close();
pst.close();
return set;
}catch (Exception e){
e.printStackTrace();
return null;
}finally {
System.out.println("执行完毕");
}
}
在调用该方法时,只需传递 select(sql,参数);
("select * from `user` where id = ?",1)
返回一个范型对象,将ResultSet在方法内封装成对象:
public <T> T selectOne(String sql,RowMapper rm,Object... values){
T obj = null; //范型对象
try {
Class.forName(driverPath);
Connection connection = DriverManager.getConnection(sqlUrl,user,password);
PreparedStatement pst = connection.prepareStatement(sql);
for (int i =0;i<values.length;i++){
//jdbc会自动转换类型
pst.setObject(i+1,values[i]);
}
ResultSet set = pst.executeQuery();
while (set.next()){
//对执行结果进行对象封装
obj = rm.rowMapper(set);
}
set.close();
connection.close();
pst.close();
return obj;
}catch (Exception e){
e.printStackTrace();
return null;
}finally {
System.out.println("执行完毕");
}
}
测试类
@Test
public void TestSelectOne() {
RowMapper rowMapper = new RowMapper() {
//RowMapper是一个接口,实例
@Override
public <T> T rowMapper(ResultSet set) throws SQLException{
//策略模式
int id = set.getInt(1); //获取ID
String name = set.getString(2);//获取用户名
int age = set.getInt(3);
User user = new User(id,name,age);
return (T) user;
}
};
User user = SqlSession.getInstance().selectOne("select * from `user` where id = ?",rowMapper,0);
System.out.println(user);
}
同理,如果查询结果包含多条,那么可以创建
Select 简化SQL调用
在进行SQL调用时,我们的代码通常会有很多重复的部分。那么我们该怎么简化SQL语句的执行呢。上面我们通过创建使用原生jdbc获取结果集后,通过RowMapper中的接口来将该结果集转化为实现RowMapper.rowMapper()方法中返回的对象。虽然简化了SQL的调用,但是仍然需要自己实现rowMapper匿名类。
于是我们可以在SqlSession,也就是在Dao层调用sql执行方法的那一层里可以实现对查询结果的封装:
ResultSet set = pst.executeQuery();
while (set.next()){
//对执行结果进行对象封装
obj = handler.mapperClass(set,cls);
}
我们创建了Handler结果集处理层,将实现对SQL执行结果的自动化封装成对象/Map集合/普通类型。
1.我们可以通过Map集合的方式,获取到ResultSet中的列名以及对应的数据,将该数据放入K-V键值对中
2.我们可以传入Class,通过反射的形式获取到ResultSet中的列名对应的属性字段,然后再对其进行赋值即可。也可以通过反射的形式获取set方法来对属性值进行赋值。
//通过反射的方法来获取cls对象的属性列表,在通过set集合的列名来给类设置属性值。
private Object toObject(ResultSet set,Class cls) throws SQLException {
ResultSetMetaData rsm = set.getMetaData();
Object obj = null;
try {
obj = cls.newInstance();
for(int i = 1;i<=rsm.getColumnCount();i++){
// ············································
// 方法一 反射直接修改字段
// //通过反射获取类的字段(属性)信息
// Field field = obj.getClass().getDeclaredField(rsm.getColumnName(i));
// //设置私有属性是可修改的
// field.setAccessible(true);
// //直接设置属性值
// field.set(obj,set.getObject(rsm.getColumnName(i)));
// ·············································
//通过反射获取类中的set方法
String methodName = "set"+rsm.getColumnName(i).substring(0,1).toUpperCase()+rsm.getColumnName(i).substring(1);
System.out.println(methodName);
Field field = cls.getDeclaredField(rsm.getColumnName(i));//根据属性名称获取单个属性
//通过属性的类型来调用类中的Setter方法,如setId(Integer.class);
if (field.getGenericType().toString().equals("int")||field.getGenericType().toString().equals("class java.lang.Integer")){
Method method = obj.getClass().getDeclaredMethod(methodName,Integer.class);
method.invoke(obj,set.getInt(rsm.getColumnName(i)));
}else if(field.getGenericType().toString().equals("string")||field.getGenericType().toString().equals("class java.lang.String")){
Method method = obj.getClass().getDeclaredMethod(methodName,String.class);
method.invoke(obj,set.getString(rsm.getColumnName(i)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
Insert 简化SQL调用
一条插入SQL语句会包含需要传入的参数。一般使用PrepareStatement来对该SQL语句预处理后在对问号部位进行赋值。那么就需要自己传入参数而且参数必须和SQL语句问号位置相对应。
//insert user values(?,?,?)
PreparedStatement pst = getPst(sql);
pst.setInt(1,1001);
pst.setString(2,"刘德华");
pst.setInt(3,19);
如果SQL语句中问号数量太多,如果我们手动设置这些参数非常麻烦。那么我们就需要简化SQL的操作,看怎么才能让程序自动给处理在哪里插入参数。
思路:我们可以构造一个SQL语句如:select * from user where id = #{id},然后再传入一个User对象,该User的对象的id属性存在且不为空,那么可以通过传递sql语句和一个user对象即可完成语句的执行。我们可以通过StringBuilder对传入的SQL进行解析,解析完毕之后再使用反射获取到对象对应属性的值,随后使用原生jdbc对其进行预处理赋值执行即可。
public List<Object> parseSQL(String sql){
List<Object> params = new ArrayList<>();
StringBuilder sb = new StringBuilder(sql);
StringBuilder newSQL = new StringBuilder();
while (true){
int left = sb.indexOf("#{");
int right = sb.indexOf("}");
if(left!=-1 && right!=-1 && left<right){
//取出在#{左侧的所有
newSQL.append(sb.substring(0,left));
//获取参数名 #{name}
String paramName = sb.substring(left+2,right);
//将获取的参数名放入List中
params.add(paramName);
//用问号填充参数
newSQL.append("?");
//从后面的再找
sb = new StringBuilder(sb.substring(right+1));
}else{
//说明本次循环已经找到了,将剩余的字符填充
newSQL.append(sb.substring(right+1));
break;
}
}
params.add(newSQL.toString());
return params;
}
解析完毕SQL之后,那就可以反射传入的对象预处理了。
public boolean insert(String sql,Object obj) throws Exception {
//从SQL语句中获得需要插入的参数。参数名和obj的属性相对应
List<Object> list = handler.parseSQL(sql);
//获取处理后的SQL语句
PreparedStatement pst = getPst((String) list.get(list.size()-1));
//反射对象获取对象的相应属性
for (int i = 0;i<list.size()-1;i++){
//通过反射获取相应字段
Field field = obj.getClass().getDeclaredField((String) list.get(i));
//设置字段可访问
field.setAccessible(true);
//获取字段对应的值
Object value = field.get(obj);
//通过jdbc原生对预处理赋值
pst.setObject(i+1,value);
}
try {
pst.execute();
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
测试代码
@Test
public void testInsert() throws Exception {
String sql = "insert user values (#{id},#{name},#{age})";
User user = new User(1010,"李三",29);
if (SqlSession.getInstance().insert(sql,user)){
System.out.println("insert succeed!");
}else {
System.out.println("insert failed!");
}
}
但是上面的代码有个Bug,就是只能传入对象而不能传入其他基本类型。那么我们可以对obj的类型进行判断再预处理:
//给预处理的SQL语句自动赋值
public void setParams(PreparedStatement pst,Object obj,List<Object> params) throws Exception {
Class<?> cls = obj.getClass();
if(cls == Integer.class || cls == String.class || cls == Float.class || cls == Double.class){
//说明传入的是一个基本参数,直接赋值
pst.setObject(1, obj);
}else if(obj instanceof Map){
//说明传入的是Map对象
Map map = (Map) obj;
for(int i = 0;i<params.size()-1;i++){
//通过参数名来查询Map集合中的属性
Object value = map.get(params.get(i));
pst.setObject(i+1,value);
}
}else {
//传入的是其他对象
//反射对象获取对象的相应属性
for (int i = 0;i<params.size()-1;i++){
//通过反射获取相应字段
Field field = obj.getClass().getDeclaredField((String) params.get(i));
//设置字段可访问
field.setAccessible(true);
//获取字段对应的值
Object value = field.get(obj);
//通过jdbc原生对预处理赋值
pst.setObject(i+1,value);
}
}
}
通过注解执行SQL语句
注解写法
package src.ORM.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
//Retention表示获取注解的段,RunTime表示在java源码、编译、运行都可以调用
@Retention(RetentionPolicy.RUNTIME)
public @interface Delete {
String value();
}
注解默认的方法为 value(); 访问该访问即可获取到注解中value对应的值
调用该方法的语句为 @Delete(value = "delete from user where id = 12")
注解目的
通过注解 我们可以不用重写Dao接口的具体实现方法,通过注解即可实现简单SQL语句的具体实现,如Dao接口的方法为:
void insert(User user);
那么我们可以通过对其进行注解:@Insert("insert users value(#{id},#{name},#{age}) ")
来实现数据的插入。
实现原理
通过代理Proxy类,代理Dao类中的方法来获取被执行的方法返回值和注解类型以及注解值。在通过上面已经封装好的CRUD方法来执行SQL语句。
a.获取需要反射的类,重写invoke方法。
b.通过反射中的参数获得注解名
c.根据注解名来判断执行哪种SQL语句
d.在通过反射注解类获得注解的注释值,即注解中的SQL语句
e.如果执行的SQL语句为select查询语句,带有返回值的。那么需要区分返回的是集合还是普通对象
e.1 通过反射被代理的方法获取该方法的返回值,如果该值为List集合,则需要获取List中的范型类型,再使用封装好的方法执行即可。
e.2 如果为基本类型、自定义类型,直接调用封装的方法即可。
Proxy代理类的方法类似于通过逆向Hook得到类的方法并进行调用。
public <T> T getFuncProxy(Class<?> cls){
//获得类的加载
ClassLoader classLoader = cls.getClassLoader();
Class<?>[] interfaces = new Class[]{cls};
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取返回类型的类
//method -> 代理类被调用的方法
//args -> 代理类被调用方法的参数
Class<?> returnCLass = method.getReturnType();
System.out.println("returnType->"+returnCLass.getTypeName());
System.out.println("MethodName->"+method.getName());
if(String.class.equals(returnCLass)){
return "返回结果已经被代理了";
}else{
return null;
}
}
};
//创建一个代理
Object obj = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
return (T)obj;
}
测试方法
package Proxy;
public interface MyFunction {
String read();
void write();
}
@Test
public void test(){
MyFunction mf = getFuncProxy(MyFunction.class);
System.out.println(mf.read());
mf.write();
}
实现代码
public <T> T getMapper(Class<?> cls){
//此处提供代理Dao对象的功能,实现Dao层接口无需实现即可执行SQL语句的功能
ClassLoader classLoader = cls.getClassLoader();
Class[] interfaces = new Class[]{cls};
InvocationHandler invocationHandler = new InvocationHandler() {
/***
* proxy -> 代理对象
* method ->代理对象的方法
* args -> 代理对象的方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Annotation an = method.getAnnotations()[0]; //通过被代理的方法获取该方法的注解名
Class<? extends Annotation> aClass = an.annotationType();//获取注解类型的类
Method method1 = aClass.getMethod("value"); //通过反射该类调用value方法获取注解值
String sql = (String)method1.invoke(an);
if (Insert.class.equals(aClass)) {
SqlSession. getInstance().insert(sql,args[0]);
} else if (Select.class.equals(aClass)) {
//a.第一步要判断是不是list类型
System.out.println("log1->"+method.getReturnType());
if(method.getReturnType() == List.class){
//查询方法select2的参数为基本类型+Map+自定义类型,
//所以需要获取被代理方法的返回类型才可以进行实体的映射创建
//如果返回类型为List,那么就需要获取List中的范型类用于MapperClass的实体映射。
Type wholeReturnType = method.getGenericReturnType(); //获取返回类型的全部->List<User>
ParameterizedType realReturnTypes = (ParameterizedType)(wholeReturnType);
Type realType = realReturnTypes.getActualTypeArguments()[0];
System.out.println("getReturnType->"+method.getGenericReturnType()
+",getGenericReturnType->"+wholeReturnType+",getActualTypeArguments->"+realType);
if(args == null){
return SqlSession.getInstance().select2(sql,(Class<?>) realType);
}else{
return SqlSession.getInstance().select2(sql,(Class<?>) realType,args[0]);
}
}else {
//此时只需传入方法的返回值类型即可
if(args == null){
return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType()).get(0);
}else{
return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType(),args[0]).get(0);
}
}
} else if (Delete.class.equals(aClass)) {
SqlSession.getInstance().delete(sql,args[0]);
} else if (Update.class.equals(aClass)) {
SqlSession.getInstance().update(sql,args[0]);
}else {
System.err.println("未被注解定义的类型!");
}
return null;
}
};
//开始创建类的代理
Object obj = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
return (T)obj;
}
加入Druid数据库连接线程池
通过静态代码块的方式,让该代码在类被加载时就执行来创建一个Properties对象
{
properties = new Properties();
try {
InputStream in = new BufferedInputStream(new FileInputStream("src/main/druid.properties"));
properties.load(in);
} catch (Exception e) {
e.printStackTrace();
}
}
并由Druid自动创建线程池并获得
public Connection getConnection() throws Exception {
//通过druid工厂创建数据源
dataSource = DruidDataSourceFactory.createDataSource(properties);
return dataSource.getConnection();
}