Statement使用的注意事项
statement的作用是执行一段静态的sql语句,并返回处理的结果集对象。但是statement存在一定的弊端:
①sql语句需要传递参数时,我们需要对sql语句进行拼接,这样会很麻烦,影响我们的开发效率。
②使用statement执行sql语句时会存在sql注入问题,简单来说就是利用没有对用户输入的数据进行检查,利用非法的sql语句完成恶意行为的做法
下面写了一个简单的登录例子,用来测试statement存在的sql注入问题。
正常访问数据库时:
@Test
public void queryDataByStatement() {
/*
简单的登录模块测试statement的弊端
*/
Scanner scanner = new Scanner(System.in);
System.out.print( "请输入用户账号:" );
String userNum = scanner.nextLine();
System.out.print( "请输入用户密码:" );
String password = scanner.nextLine();
Connection connection = null ;
Statement statement = null ;
ResultSet resultSet = null ;
try {
// 1.获取数据库的连接:使用自定义工具类
connection = MyJDBCUtils.getConnection();
// 2.创建一个statement实例
statement = connection.createStatement();
// 3.创建sql语句:此处需要对sql语句进行拼串操作,略微麻烦
String sql="select user,password from user_table where user=‘"+userNum+"‘ and password=‘"+password+"‘" ;
// 4.执行sql语句
resultSet = statement.executeQuery(sql);
// 5.对返回结果进行简单处理
if (resultSet.next())
System.out.println( "登录成功!!!" );
else
System.out.println( "登录失败!!!" );
// 6.关闭数据库的连接,此时statement和结果集也需要被关闭:使用自定义工具类
} catch (Exception e) {
e.printStackTrace();
} finally {
MyJDBCUtils.closeConnection(connection,statement,resultSet);
}
}
返回的结果是正常的:
当恶意访问数据库时:
@Test
public void queryDataByStatement() {
/*
简单的登录模块测试 statement 的弊端
*/
Scanner scanner = new Scanner(System. in) ;
System. out.print( " 请输入用户账号: ") ;
String userNum = scanner.nextLine() ;
System. out.print( " 请输入用户密码: ") ;
String password = scanner.nextLine() ;
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
//1. 获取数据库的连接 : 使用自定义工具类
connection = MyJDBCUtils. getConnection() ;
//2. 创建一个 statement 实例
statement = connection.createStatement() ;
//3. 创建 sql 语句:此处需要对 sql 语句进行拼串操作,略微麻烦
String sql= "select user ,password from user_table where user = ‘"+userNum+ " ‘and password = ‘"+password+ "‘" ;
//4. 执行 sql 语句
resultSet = statement.executeQuery(sql) ;
//5. 对返回结果进行简单处理
if (resultSet.next())
System. out.println( " 登录成功!!! ") ;
else
System. out.println( " 登录失败!!! ") ;
//6. 关闭数据库的连接 , 此时 statement 和结果集也需要被关闭:使用自定义工具类
} catch (Exception e) {
e.printStackTrace() ;
} finally {
MyJDBCUtils. closeConnection(connection ,statement ,resultSet) ;
}
执行结果为:此时账号和密码明显不对,但是却能登陆成功。
之所以出现这种情况,是因为statement没有对sql语句进行事先的编译,我们传入什么,它就会向数据库发送什么数据,当账号和密码是上图中的情况时,sql语句实际为下图的情况,这个就叫做sql注入
P reparedStatement使用注意事项
为了解决statement中sql注入的问题,我们需要使用preparedStatement来替换原有的statement。
preparedStatement是statement的一个子接口,它的好处是可以对sql语句进行预编译,在创建preparedStatement实例时已经知道了自己要执行的sql语句是什么
使用preparedStatement完成对数据库的增删改查操作1.使用prepareStatement向user表中添加一条数据
@Test
public void addUserByPre() {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
try {
// 1.获取数据库的连接
connection = MyJDBCUtils.getConnection();
// 2.创建sql语句:此时数据库中设计的主键id是自增的,我们可以不用主动添加
// ?此时代表占位符,表明你将要传递的参数,有几个?代表需要传递几个参数
String sql="insert into `user`(name,password,address,phone) values(?,?,?,?)" ;
// 3.创建preparedStatement对象
preparedStatement = connection.prepareStatement(sql);
// 4.注入占位符(
// 两个参数,第一个为需要注入的占位符的下标,第二个参数为具体注入的内容.这里需要注意的是下标是从1开始的而不是0)。
preparedStatement.setString(1,"王宝强" );
preparedStatement.setString( 2,"123456" );
preparedStatement.setString( 3,"河北省秦皇岛市" );
preparedStatement.setString( 4,"12345678910" );
// 5.执行相关操作
preparedStatement.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.关闭相应连接
MyJDBCUtils.closeConnection(connection,preparedStatement);
}
}
2. 使用preparedStatement修改user表中的某条数据
@Test
public void updateUserByPre() {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
try {
// 1.获取数据库连接
connection = MyJDBCUtils.getConnection();
// 2.创建sql语句
String sql="update user set name = ? where id = ?" ;
// 3.创建preparedStatement对象
preparedStatement = connection.prepareStatement(sql);
// 4.填充占位符
preparedStatement.setString(1,"许三多" );
preparedStatement.setInt( 2,8 );
// 5.执行操作
int i = preparedStatement.executeUpdate();
if (i != 0 )
System.out.println( "修改成功" );
else
System.out.println( "修改失败" );
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.关闭资源
MyJDBCUtils.closeConnection(connection,preparedStatement);
}
}
3.使用preparedStatement删除user表中的一条数据
@Test
public void deleteUserByPre(){
Connection connection = null ;
PreparedStatement preparedStatement = null ;
try {
// 1.获取数据库连接
connection = MyJDBCUtils.getConnection();
// 2.创建sql语句
String sql="delete from user where id = ?" ;
// 3.创建preparedStatement对象
preparedStatement = connection.prepareStatement(sql);
// 4.填充占位符
preparedStatement.setInt(1,6 );
// 5.执行操作
int i = preparedStatement.executeUpdate();
if (i != 0 )
System.out.println( "删除成功" );
else
System.out.println( "删除失败" );
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.关闭资源
MyJDBCUtils.closeConnection(connection,preparedStatement);
}
}
4.观察代码可以看出来,增删改三种方法的代码是具有一定的重复性的,唯一的区别无非就是sql语句和占位符的不同,因此我们可以考虑将三种方法封装为同一个方法,调用的时候只需要传递sql语句和占位符即可。代码如下(可自行测试,这里就不再写测试代码了)
public static void updateDataBase(String sql,Object ...args) {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
try {
// 1.获取数据库的连接
connection = getConnection();
// 2.创建sql语句,此步骤可直接使用传递进来的sql语句
// 3.创建preparedStatement对象
preparedStatement = connection.prepareStatement(sql);
// 4.填充占位符
/*
1.首先要获取占位符的个数,因为可变形参的个数就是占位符的个数,所以只需要获取args的长度即可
2.填充占位符,使用for循环来做,需要注意的是下标的问题
*/
for ( int i = 0; i < args.length; i++ ) {
preparedStatement.setObject(i +1 ,args[i]);
}
// 5.执行操作
int i = preparedStatement.executeUpdate();
if (i != 0 )
System.out.println( "此次操作成功!" );
else
System.out.println( "此次操作失败!" );
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.关闭资源
closeConnection(connection,preparedStatement);
}
}
5.使用preparedStatement查询user表中的一条记录(查询和增删改是不同的,因为查询需要有返回的结果集)
public User queryUser(String sql,Object ...args) {
Connection connection = null ;
PreparedStatement prepareStatement = null ;
ResultSet resultSet = null ;
try {
// 1.获取数据库连接
connection = MyJDBCUtils.getConnection();
// 2.创建sql语句
// 3.创建preparedStatement对象
prepareStatement = connection.prepareStatement(sql);
// 4.填充占位符
for ( int i = 0; i < args.length; i++ ) {
prepareStatement.setObject(i+1 ,args[i]);
}
// 5.执行操作
resultSet = prepareStatement.executeQuery();
// 6.将查询出来的数据封装成为一个对象
// 1.获取一个元数据对象
ResultSetMetaData metaData = resultSet.getMetaData();
// 2.通过元数据对象来获取该条数据中一共有多少列
int columnCount = metaData.getColumnCount();
if (resultSet.next()){
/*
resultSet.next()有些类似与迭代器中的hashNext()和next()的结合体
在迭代器中,hasNext()的作用是判断下一个位置是否为空,next()如果下一个位置不为空,指针下移并且返回当前对象,如果为空,则结束操作
而resultSet.next()的作用是判断下一个位置是否为空,并且指针下移,返回的是Boolean值
*/
// 3.创建一个对象实体
User user = new User();
// 如何将数据封装进一个JavaBean中呢?此时并不知道取出的元素具体是什么类型的!
// 在resultSet中提供了一个方法用来获取查询到的元数据(元数据:修饰查询出来数据的数据,可以参考元注解的概念),
// 4.使用元数据来获取当前这一条数据的每一列的列名和对应的列值
for ( int i = 0; i < columnCount; i++ ) {
String columnName = metaData.getColumnName(i + 1 );
Object columnValue = resultSet.getObject(i + 1 );
// 5.使用反射技术动态的为bean对象中的属性赋值
Field declaredField = User. class .getDeclaredField(columnName);
declaredField.setAccessible( true );
declaredField.set(user,columnValue);
}
// System.out.println(user);
return user;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.关闭资源
MyJDBCUtils.closeConnection(connection,prepareStatement,resultSet);
}
return null ;
}
这个时候需要提供一个对应的JavaBean实例
6.同样的,我们也可以封装一个函数用来获取不同的表中的单条数据
public static <T> T getBeanByPre(Class<T> clazz,String sql,Object ...args) {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
ResultSet resultSet = null ;
try {
// 1.获取到数据库的连接
connection = getConnection();
// 2.创建一个preparedStatement实例
preparedStatement = connection.prepareStatement(sql);
// 3.填充占位符
for ( int i = 0; i < args.length; i++ ) {
preparedStatement.setObject(i +1 ,args[i]);
}
// 4.执行操作
resultSet = preparedStatement.executeQuery();
// 5.获取查询记录的元数据
ResultSetMetaData metaData = resultSet.getMetaData();
// 6.获取查询记录中的列数
int columnCount = metaData.getColumnCount();
if (resultSet.next()){
// 7.使用反射创建一个bean是咧
T t = clazz.newInstance();
for ( int i = 0; i < columnCount; i++ ) {
// 8.获取到每一列的别名
String columnLabel = metaData.getColumnLabel(i + 1 );
// 9.获取到每一列的值
Object columnValue = resultSet.getObject(i + 1 );
// 10.使用反射为bean中的属性赋值
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible( true );
field.set(t,columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 11.关闭资源
closeConnection(connection,preparedStatement,resultSet);
}
return null ;
}
7. 我们也可以封装一个函数用来获取不同的表中的多条数据
public static <T>List<T> getBeanListByPre(Class<T> clazz, String sql, Object ...args) {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
ResultSet resultSet = null ;
try {
// 1.获取到数据库的连接
connection = getConnection();
// 2.创建一个preparedStatement实例
preparedStatement = connection.prepareStatement(sql);
// 3.填充占位符
for ( int i = 0; i < args.length; i++ ) {
preparedStatement.setObject(i +1 ,args[i]);
}
// 4.执行操作
resultSet = preparedStatement.executeQuery();
// 5.获取查询记录的元数据
ResultSetMetaData metaData = resultSet.getMetaData();
// 6.获取查询记录中的列数
int columnCount = metaData.getColumnCount();
// 7.创建list集合
ArrayList<T> list = new ArrayList<> ();
while (resultSet.next()){
// 7.使用反射创建一个bean实例
T t = clazz.newInstance();
for ( int i = 0; i < columnCount; i++ ) {
// 8.获取到每一列的别名
String columnLabel = metaData.getColumnLabel(i + 1 );
// 9.获取到每一列的值
Object columnValue = resultSet.getObject(i + 1 );
// 10.使用反射为bean中的属性赋值
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible( true );
field.set(t,columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 11.关闭资源
closeConnection(connection,preparedStatement,resultSet);
}
return null ;
}
之所以preparedStatement可以解决sql注入问题,是因为它的预编译sql语句的功能,在我们生成preparedStatement对象时,已经使用过了sql语句。在占位符还没有填充之前,它就已经对sql语句进行了解析。对于刚开始的登录测试来说,创建 preparedStatement对象 时,sql语句表示的就是user = ?and password = ?,无论传入什么值,它都会认为是user和password的值。以此避免了sql注入问题
preparedStatement同时还可以操作Blob类型的数据,可以更高效的实现批量操作
JDBC第二部分—statment和preparedStatement
标签:void 并且 png 内容 and image 注意 hash sele
查看更多关于JDBC第二部分—statment和preparedStatement的详细内容...