上一节,我们预先根据用户输入的参数生成完整的sql语句,然后使用Statement对象执行sql语句,实现了增删改查。但我们发现,部分sql是用户来输入的,而且我们也没有做任何的校验处理,这就无法避免用户通过恶意输入来破坏我们的数据库。
比如说下面这种情况,用户输入了' or 1 = '1
,拼接出了一段查询student表所有数据的sql语句,这会将某些我们不希望被看到的数据呈现给用户。如果这段sql执行的是删除、修改,那后果将是十分恐怖的。
JDBC针对这种情况,给出了另外一种拼接sql的方法,即使用占位符?来替代变量,预先生成一段sql,然后使用PreparedStatement这个对象代替Statement,并把占位符替换成变量。这个过程中,会对引号等做过滤处理,有效的避免了sql注入。
在前面,我们是把查询到结果集根据字段名,一个个地手动匹配注入到对象中。如果采用这种方式,那是很繁琐的,这意味着有多少个实体类,我们就要写多少个针对该实体类的注入方法。
好在Java是强大的,它存在着反射机制,通过反射,我们只要知道类名,就能够去得到这个类里面的方法、字段、字段类型等,甚至可以根据方法名去调用这个方法。
我们来讲讲一点基础的反射知识:
首先是根据一个对象获得它的类:
Class clazz = 对象.getClass();
根据类获得它的方法、字段等:
Fields[] fileds = clazz.getDeclaredFileds(); //获取所有字段
Fields[] fileds = clazz.getFileds(); //获取所有public的字段
Method[] methods = clazz.getDeclaredFileds(); //获取所有方法
Method[] fileds = clazz.getFileds(); //获取所有public的方法
String name = Filed.getName(); //获得字段名称
Type type = Filed.getType(); //获得字段的数据类型
Method method = clazz.getDeclaredMethod(String 方法名,Type 参数类型)
........................
还有个最重要的,根据方法对象,和参数列表,调用方法
method.invoke(对象,params[]) //反射调用,调用成功会返回方法返回值
在注入之前,考虑到数据库和实体类字段书写格式不同,我们先来写两个工具方法,实现驼峰命名和下划线命名互相转换。简单的字符串操作,不需要看懂,当作工具来使用就可以。
/**
* 转为驼峰命名
* @param str
* @return string
*/
public String underlineTocamel(String str) {
if (null != str && !"".equals(str)) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0, len = str.length(); i < len; i++) {
if (str.charAt(i) == '_') {
while (str.charAt(i + 1) == '_') {
i++;
}
stringBuilder.append(("" + str.charAt(++i)).toUpperCase());
} else {
stringBuilder.append(str.charAt(i));
}
}
return stringBuilder.toString();
}
return str;
}
//驼峰命名转下划线命名
public String camelToUnderline(String para){
StringBuilder sb=new StringBuilder(para);
int temp=0;//定位
for(int i=0;i<para.length();i++){
if(Character.isUpperCase(para.charAt(i))){
sb.insert(i+temp, "_");
temp+=1;
}
}
return sb.toString().toUpperCase();
}
在我们开发过程中,遇到像数据库连接参数这种常量时,通常会将它们放在配置文件中,然后通过io流去读取文件来获得参数。
为什么要这样做呢,因为我们的java文件都是编译后成为class文件再部署到服务器上的。如果我们把这些参数写在java文件中,那么在需要修改的时候,就必须在代码环境下修改并编译,然后使用一个新的class文件来替换服务器上面的文件。而写在配置文件中,我们可以直接在服务器上面修改文本。
这里我们先不升级那么快,姑且单独用一个类来存放常量。
package com.zx.util;
/**
* 存放数据库连接常量
* @author BMC
*
*/
public class DataBaseUtil {
//着你应该看得懂,按照你的数据库修改
public static final String URL = "jdbc:mysql://localhost:3306/demo";
public static final String USER = "root";
public static final String PASSWORD = "root";
public static final String DRIVERNAME = "com.mysql.jdbc.Driver";
}
考虑到每次都要创建连接和语句对象,我们把它们提取出来(不用跟着我敲,看看就行了,后面会把完整代码发出来)
//获得连接的方法
public Connection getConnection() throws SQLException {
//加载连接驱动
//Mysql数据库加载这个类 mysql8以后的jar包是加载 com.mysql.cj.jdbc.Driver
try {
Class.forName(DataBaseUtil.DRIVERNAME);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//Orcale数据库加载这个类 其他部分都差不多
//Class.forName("oracle.jdbc.driver.OracleDriver");
//调用驱动根据url、用户名、密码 创建一个连接
Connection conn = DriverManager.getConnection(DataBaseUtil.URL,
DataBaseUtil.USER, DataBaseUtil.PASSWORD);
return conn;
}
//根据sql和参数列表获得语句的方法
public PreparedStatement getPreparedStatement(String sql,Connection conn,Object... objs) throws SQLException {
PreparedStatement pstmt = conn.prepareStatement(sql);
if(null != objs) {
for(int i = 0;i<objs.length;i++) {
//从1开始
pstmt.setObject(i+1, objs[i]);
}
}
return pstmt;
}
我们在查询的时候会从数据库获得一个ResultSet,对于每一行我们都要对各个字段进行映射。每一行的字段都是相同的,所以可以把每一行注入的方法抽象出来。通过反射知识,很容易做到。
//把数据库的一行数据 映射成 对象集合
/**
* @param rs 结果集
* @param clazz 实体类类型
* @param objs 参数列表
* @return
*/
public <T> T invoke(ResultSet rs,Class<T> clazz, Object... objs) {
try {
//实例化对象
T object = clazz.getConstructor().newInstance();
//获取结果集的数据,方便获得每一行
ResultSetMetaData rsData = rs.getMetaData();
for(int i = 0 ; i< rsData.getColumnCount();i++) {
//获得当前这一列的列名 以及当前这一列的值
String columName = rsData.getColumnName(i+1);
Object columValue = rs.getObject(i+1);
//接下来我们需要使用反射来调用set方法来进行赋值
//我们需要:方法名 方法对象 参数类型 参数值
//转换成驼峰命名
String fieldName = underlineTocamel(columName);
//拼接set方法名 注意单词首字母大写
String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
//根据字段名 和 传入对象 获得这个字段对象
Field field = clazz.getDeclaredField(fieldName);
//根据字段对象获得字段类型
Class type = field.getType();
//根据方法名 和 方法参数类型 获得方法
Method method = clazz.getDeclaredMethod(methodName,type);
method.invoke(object,columValue);
}
return object;
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException | SQLException | NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
Object... objs 这种使用...的参数类型,表示可传入任意多个Object类型的参数,可以为空。我们可以通过操作objs这个数组来获得传入的参数列表。特别的,如果只传入一个参数,且这个参数类型为数组,数组会被拆分成参数列表,有兴趣的可以试一下。
这样我们就能完成每一行的映射了
//自定义查询方法
/**
* @param sql sql语句
* @param clazz 映射实体类的Class
* @param objs 参数列表
* @return
* @throws SQLException
*/
public <T> List myQuery(String sql,Class<T> clazz,Object... objs){
Connection conn = null;
List list = new ArrayList();
//工具方法中就捕获异常,避免使用时要处理
try {
conn = getConnection();
PreparedStatement pstmt = getPreparedStatement(sql,conn, objs);
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
T t = invoke(rs, clazz, objs);
list.add(t);
}
//查询完毕关闭资源
closeAll(conn, pstmt, rs);
}catch(SQLException sqlException) {
sqlException.printStackTrace();
}
return list;
}
编写个测试类测试一下
/**
* 测试类
* @author BMC
*
*/
package com.zx.test;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.zx.dao.MyDAO;
import com.zx.domain.Student;
/**
* 测试类
* @author BMC
*
*/
public class Test {
public static void main(String[] args) {
MyDAO myDAO = new MyDAO();
String sql = "select * from student";
List<Student> list = new ArrayList<Student>();
//返回的是一个list 如果只有一条结果,就(Student)list.get(0),注意强转类型
list = myDAO.myQuery(sql, Student.class, null);
for(Student stu : list) {
System.out.println(stu.getName());
}
// Student stu = new Student();
// stu.setName("海绵宝宝");
// stu.setClassId(3);
// stu.setUsername("haimianbaobao");
// stu.setPassword("123456");
// stu.setAge(22);
// //Orcale主键一般需要根据序列来获取,但是mysql有自增,不需要赋值
// stu.setId(9);
// stu.setTest(null);
//
// new MyDAO().SaveObject(stu);
//
}
}
查询结果如下(报错是因为我驱动导的不对,建议最新版本jar包,但旧版本也可以用
):
最后,再附上其他增删改的方法,注意,删除和修改通常是需要根据主键来删除,视情况而定,所以需要用户手动书写sql来执行。
/**
* 根据传入的object 插入一条数据
* @param obj
* @throws SQLException
* @throws SecurityException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public <T> int SaveObject(Object obj) {
Connection conn = null;
int result = 0;
try {
conn = getConnection();
List list = new ArrayList<>();
//获得表名和字段以生成sql
String tableName = obj.getClass().getSimpleName();
StringBuffer sql = new StringBuffer();
sql.append("insert into ").append(tableName).append("(");
//拼接sql
Field[] fileds = obj.getClass().getDeclaredFields();
if(null != fileds && fileds.length != 0) {
for(int i = 0;i<fileds.length;i++) {
String columName = camelToUnderline(fileds[i].getName());
sql.append(columName);
if(i < fileds.length - 1) sql.append(",");
String methodName = "get" + fileds[i].getName().substring(0, 1).toUpperCase() + fileds[i].getName().substring(1);
Method method = obj.getClass().getDeclaredMethod(methodName);
list.add(method.invoke(obj));
}
sql.append(") values(");
for(int i = 0;i<fileds.length;i++) {
sql.append("?");
if(i < fileds.length - 1) sql.append(",");
}
sql.append(")");
PreparedStatement pstmt = getPreparedStatement(sql.toString(),conn, list.toArray());
result = pstmt.executeUpdate();
closeAll(conn, pstmt, null);
}
}catch(SQLException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException sqlException){
}
return result;
}
//删除 根据具体sql删除
public int Delete(String sql,Object...objs) {
int result = 0;
Connection conn;
try {
conn = getConnection();
PreparedStatement pstmt = getPreparedStatement(sql,conn,objs);
result = pstmt.executeUpdate();
closeAll(conn, pstmt, null);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
//更新 根据具体sql更新
public int Update(String sql,Object...objs) {
int result = 0;
Connection conn;
try {
conn = getConnection();
PreparedStatement pstmt = getPreparedStatement(sql,conn,objs);
result = pstmt.executeUpdate();
closeAll(conn, pstmt, null);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}