为什么用反射封装
在上篇文章的封装中我没有用到反射去给实体类赋值,那么在这篇文章我将介绍一下java的反射机制,以及对DAO层(Data Access Object 数据访问层,也叫持久层,意思就是对数据永久化存储到硬盘(数据库)的封装)数据访问的封装。
如何用反射封装
当我们需要在程序运行中对动态生成的对象进行操作时(对属性赋值,调用方法,创建实例等),以往的做法通常是直接利用该对象引用本身的属性或者方法,或者是利用该类构造实例化,像这样直接引用就不能叫动态,因为这个时候这个对象并没有在内存中。
首先我们来看对对象操作的几个前提:
- 我们必须知道对象的类型(就是那个类的实例)
- 必须知道该类的属性名称和方法名称
- 该类继承了或者实现了哪些类或接口
我们现在知道了这几个前提后,会发现如果这个对象都是我们未知的,那我们该如何知道该对象的类型、该对象的属性名称和方法、该类继承了或实现了哪些接口呢?在上篇文章中,我是利用了函数式编程(lambda表达式)提前把实体类的set方法存储到接口类型的数组中模仿了指向函数的指针,但是那样还是得把实体类的设值方法提前写好(我自己把他勉强叫做伪动态),能有什么办法把这步省掉吗?当然办法还是有的。那么我现在就来介绍一下如何用反射封装BaseDao中的统一查询方法。
首先我们必须知道哪个类是要我们在运行时要对其属性、方法、构造进行信息获取的,其次就是我们要获取该类哪些必要属性或者是方法等,观察如下代码:
1 public List<Object> executeQuery(String strSql, Object[] param) {
2 conn = getConnection();
3 List<Object> pList = new ArrayList<Object>();
4
5 try {
6 pstmt = conn.prepareStatement(strSql);
7 if(pstmt != null) {
8 for(int i = 0; i < (param == null ? 0 : param.length); i++) {
9 pstmt.setObject(i+1, param[i]);
10 }
11 rs = pstmt.executeQuery();
12 while(rs.next()) {
13 for(int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
14 pList.add(rs.getObject(i+1));
15 }
16 }
17 pList.add(rs.getMetaData().getColumnCount());
18 return pList;
19 }
20 } catch (SQLException e) {
21 e.printStackTrace();
22 } finally {
23 closeAll(this.rs, this.pstmt, this.conn);
24 }
25 return null;
26 }
现在我们侧重观察12、13、14、15、16、17行,可以看到,这里是取出数据库的每行每列数据并存放到一个集合中,思考一下之前我为什么把所有数据都存储到集合中,为什么不直接在这里把数据存储到实体类中呢?因为在没有用反射之前,我们必须在知道实体类的属性个数,属性名称才能对实体类属性设值,所以我只能把所有数据都统一放在Object类型的集合中,等到要调用时再把返回的集合拿出来对每个对应的对象一一赋值。
如果想要在该方法中就完成所有操作必定要利用到反射在方法中获取的所有类型的实体类:
代码如下:
/**
* 通过反射填充实体类
* @param t 实体类对象
* @param attr 实体类属性名
* @param v 要给属性设的值
*/
1 public void setEntity(T t, String attr, Object v) {
2 try {
3 //获取t类(实体类)的属性,根据attr属性名
4 Field f = t.getClass().getDeclaredField(attr);
5 //实体类中的属性是私有的,故做此操作
6 f.setAccessible(true);
7 //对该字段赋值v
8 f.set(t,v);
9 } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
10 e.printStackTrace();
11 }
12 }
上面代码可以实现对一个未知类型类的对象属性赋值,可以看到3-8行之间的三行主要代码,都是由反射实现的,其中第4行根据属性名获取了泛型对象t的Field对象,属性名是数据库中的字段名,然后第5行就是将要对t对象的私有属性进行操作的设置,最后对该属性f进行赋值操作,其实此处可以利用反射调用set方法来对私有属性赋值的,但是也没有必要这样的做了。到现在可以发现,我们现在需要从外部获取的三个参数中,只有一个要利用到反射,就是t对象,我们要利用反射对t对象实例化,也就是为泛型t进行实例化。
1 while(rs.next()) {
2 //对t进行实例化
3 t = clazz.newInstance();
4 //将一行中数据的列遍历完成
5 for(int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
6 //获取每一列的名称
7 String colName = rs.getMetaData().getColumnName(i+1);
8 //获取每一列的名称对应的值
9 Object value = rs.getObject(colName);
10 //把当前的值利用反射添加到实体类中
11 this.setEntity(t,colName,value);
12 }
13 将设好值的t对象存储到集合中
14 pList.add(t);
15 }
然后将查询方法改为如下形式
public List<T> executeQuery(String strSql, Object[] param, Class<T> clazz)
多了一个参数,第三个参数是为了获取实体类的类信息,所以15行可以对泛型为T的对象t进行实例化
到这里所有封装已完毕,现在在外部调用该方法时是这样的
BaseDao<User> basedao = new BaseDao<User>();
List<User> list = basedao.executeQuery("select * from userinfo", null, User.class);
for(User user : list) {
System.out.println(user.toString());
}
和之前我第一个版本调用方式几乎一样,不过这次利用了反射,不用在外部传入实体类示例,只要在外部传入实体类的类信息就ok了,所以并不需要知道User类有几个属性几个方法,只需将实体类的属性名与数据库字段名一一对应即可,所以也不要像我之前要提前设好每个实体类的封装方法,然后传入实体类对象一一获取数据表行的数据。
这次的封装可以说是对反射的一个应用,虽然没有用到反射的方方面面但是充分的体现用反射来封装是很不错的一个选择,但是在我们学习的过程中还是要考虑多种方法去封装,这样我们能在对整个功能的封装过程中能过拓展我们的思维,能够对程序把握的更加准确。
下面是整个executeQuery方法的源码:
package com.property.dao;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* 数据库连接的封装
* 数据库增删改查的封装
* @author BurNIng
*
*/
public class BaseDao<T> {
private static String driver; //加载jdbc驱动信息字符串
private static String url; //连接数据库信息字符串
private static String user; //用户名
private static String password; //数据库用户密码
public Connection conn = null; //连接对象
public PreparedStatement pstmt = null; //执行sql语句对象
public ResultSet rs = null; //sql结果集对象
static {
//类加载时初始化连接信息
init();
}
/**
* 初始化连接信息
* 连接信息从database.properties文件中提取
*/
private static final void init() {
//读取.properties(键值形式编码)文件的对象
Properties pro = new Properties();
//配置文件名
String configFile = "database.properties";
//加载到流中
InputStream is = BaseDao.class.getClassLoader().getResourceAsStream(configFile);
try {
//加载到properties对象中
pro.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//通过文件中的driver键值获取值
driver = pro.getProperty("driver");
//通过文件中的url键值获取值
url = pro.getProperty("url");
//通过文件中的username键值获取值
user = pro.getProperty("username");
//通过文件中的password键值获取值
password = pro.getProperty("password");
}
/**
* 获取连接对象的封装
* @return Connection
*/
public Connection getConnection() {
try {
Class.forName(driver);//加载驱动
conn = DriverManager.getConnection(url,user,password);//获取连接对象
} catch (SQLException | ClassNotFoundException e) {//处理异常
e.printStackTrace();
}
return conn;//返回connection对象
}
/**
* 对资源释放的封装
* @param rs
* @param pstmt
* @param conn
*/
public void closeAll(ResultSet rs, PreparedStatement pstmt, Connection conn) {
if(rs != null) {
try {
//此处为释放资源
this.rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(pstmt != null) {
try {
//此处为释放资源
this.pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
//此处为释放资源
this.conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 对增删改的封装
* @param strSql
* @param param
* @return 返回受影响的行数
*/
public int executeUpdate(String strSql, Object[] param) {
//受影响的行数
int resultLine = 0;
//获取连接对象
conn = getConnection();
try {
//获取执行语句的对象
pstmt = conn.prepareStatement(strSql);
//判断是否有带条件参数
if(param != null) {
//如果有点条件参数循环设值
for(int i = 0; i < (param == null ? 0 : param.length); i++) {
//对第i个参数设值
pstmt.setObject(i+1, param[i]);
}
//执行并返回受影响的行数
resultLine = pstmt.executeUpdate();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(null, pstmt, conn);
}
//返回受影响的行数
return resultLine;
}
/**
* 查询的封装
* 将查询到表的的所有数据存储在Object集合中
* @param strSql
* @param param
* @return 返回数据集合
*/
public List<T> executeQuery(String strSql, Object[] param, Class<T> clazz) {
conn = getConnection();
//该集合存储数据表数据
List<T> pList = new ArrayList<T>();
//实体对象
T t = null;
try {
//获取执行Sql对象
pstmt = conn.prepareStatement(strSql);
if(pstmt != null) {
for(int i = 0; i < (param == null ? 0 : param.length); i++) {
pstmt.setObject(i+1, param[i]);
}
//获取数据集合
rs = pstmt.executeQuery();
//将数据表数据填充到集合中
while(rs.next()) {
t = clazz.newInstance();
//将一行中数据的列遍历完成
for(int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
//获取每一列的名称
String colName = rs.getMetaData().getColumnName(i+1);
//获取每一列的名称对应的值
Object value = rs.getObject(colName);
//把当前的值利用反射添加到实体类中
this.setEntity(t,colName,value);
}
pList.add(t);
}
//为了方便读取,在集合的最后对改表的列数进行存储
return pList;
}
} catch (SQLException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
} finally {
closeAll(this.rs, this.pstmt, this.conn);
}
return null;
}
/**
* 通过反射填充实体类
* @param t 实体类对象
* @param attr 实体类属性名
* @param v 要给属性设的值
*/
public void setEntity(T t, String attr, Object v) {
try {
//获取t类(实体类)的属性,根据attr属性名
Field f = t.getClass().getDeclaredField(attr);
//实体类中的属性是私有的,故做此操作
f.setAccessible(true);
//对改字段赋值v
f.set(t,v);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
希望大家能够从中获取自己的理解与灵感,今后能够写出更加优秀的代码,也希望大家能够给出宝贵的建议。