JDBC连接数据库基础(三)

JDBC连接数据库基础(三)

四、JDBC对Sql注入漏洞的处理

  • 使用PreparedStatement替换Statement

上一节,我们预先根据用户输入的参数生成完整的sql语句,然后使用Statement对象执行sql语句,实现了增删改查。但我们发现,部分sql是用户来输入的,而且我们也没有做任何的校验处理,这就无法避免用户通过恶意输入来破坏我们的数据库。
比如说下面这种情况,用户输入了' or 1 = '1 ,拼接出了一段查询student表所有数据的sql语句,这会将某些我们不希望被看到的数据呈现给用户。如果这段sql执行的是删除、修改,那后果将是十分恐怖的。
JDBC连接数据库基础(三)_第1张图片

JDBC针对这种情况,给出了另外一种拼接sql的方法,即使用占位符?来替代变量,预先生成一段sql,然后使用PreparedStatement这个对象代替Statement,并把占位符替换成变量。这个过程中,会对引号等做过滤处理,有效的避免了sql注入。
JDBC连接数据库基础(三)_第2张图片

五、使用反射实现自动注入

  • 反射

在前面,我们是把查询到结果集根据字段名,一个个地手动匹配注入到对象中。如果采用这种方式,那是很繁琐的,这意味着有多少个实体类,我们就要写多少个针对该实体类的注入方法。
好在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这个数组来获得传入的参数列表。特别的,如果只传入一个参数,且这个参数类型为数组,数组会被拆分成参数列表,有兴趣的可以试一下。
T 表示是泛型,java1.5以后增加的类型,可以表示任意类型

这样我们就能完成每一行的映射了

	//自定义查询方法
	/**
	 * @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包,但旧版本也可以用):
JDBC连接数据库基础(三)_第3张图片
最后,再附上其他增删改的方法,注意,删除和修改通常是需要根据主键来删除,视情况而定,所以需要用户手动书写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;
	}

你可能感兴趣的:(java)