Java基础:JDBC

文章发布时间:

最后更新时间:

文章总字数:
3.4k

预计阅读时间:
14 分钟

JDBC(Java Database Connectivity)是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。

JDBC Connection

Connection流程

Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册对应的驱动类,然后调用getConnection方法才能连接上数据库。

JDBC定义了一个叫做java.sql.Driver的接口类负责对数据库的连接,数据库驱动包必须自己去实现这个接口才能完成数据库的连接操作,java.sql.DriverManagergetConnection方法实际上就是调用了java.sql.Driver的connect方法实现连接,连接成功后返回一个java.sql.Connection的数据库连接对象,以后一切的数据库操作都依赖这个对象。

image-20230902144416882

可以看到getConnection方法经历了很多重载方法的调用,主要就是要把user,password包进info,另外从这个函数需要的参数可以知道我们需要传入url,user和password来实现数据库连接。

image-20230902144901560

在最后调用的getConnection中可以看到先检测了驱动是否注册,后尝试调用connect方法来连接数据库。

总的来说:JDBC连接数据库的一般步骤:

  1. 注册驱动,Class.forName("数据库驱动的类名")
  2. 获取连接,DriverManager.getConnection(xxx)

JDBC连接数据库示例代码如下:

1
2
3
4
5
6
7
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/mysql"
String USERNAME = "root";
String PASSWORD = "root";

Class.forName(CLASS_NAME);// 注册JDBC驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

数据库配置信息

传统的Web应用的数据库配置信息一般都是存放在WEB-INF目录下的*.properties*.yml*.xml中的,如果是Spring Boot项目的话一般都会存储在jar包中的src/main/resources/目录下。常见的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xmlWEB-INF/hibernate.cfg.xmlWEB-INF/jdbc/jdbc.properties,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"

为何需要forName

为了给出这个问题的答案,我们可以先去找一个数据库的驱动类来分析:

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>

搭好之后找到com.mysql.jdbc.Driver,这个就是mysql数据库的驱动类:

image-20230902150342564

可以看到这里是有一块静态代码块的,而结合我们之前学过的类加载知识,当Class.forName(CLASS_NAME)实际上会触发显式类加载,这个类会被初始化,静态代码块会自动执行,再仔细看看这个代码,就是往DriverManager里面注册了这个驱动包。

也就是说实际上forName这一步是利用了Java反射+类加载机制往DriverManager中注册了驱动包。

另外说一句,如果只是想反射而不想初始化,需要使用Class.forName(“xxxx”, false, loader)方法,将第二个参数传入false,或者ClassLoader.load(“xxxx”);

为何forName可以省去

刚刚说了forName这么重要,注册驱动包,现在又说可以省去??别急,是因为Java的SPI机制帮你自动完成了forName,所以我们写代码的时候可以省略不写。

因为DriverManager在初始化的时候会调用java.util.ServiceLoader类提供的SPI机制,Java会自动扫描jar包中的META-INF/services目录下的文件,并且还会自动进行Class.forName(文件中定义的类)

image-20230902152009378

JDBC DataSource

配置数据源,就是建立与数据库的连接,常见的数据源有hikariCP、druid、tomcat-jdbc、dbcp、c3p0等,他们都实现了javax.sql.DataSource提供的接口,那么为什么要用数据源去建立与数据库的连接,而不是用我们上面提到的jdbc呢,原因之一就是使用jdbc建立连接和销毁太过于浪费资源。

一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource对象即可获取数据库连接了。

下面的spring框架相关这里先做记录,以后再详细分析。

Spring MVC 数据源

在Spring MVC中我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象了。

在基于XML配置的SpringMVC中配置数据源:

1
2
3
4
5
6
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
....
/>

SpringBoot配置数据源:

在SpringBoot中只需要在application.propertiesapplication.yml中定义spring.datasource.xxx即可完成DataSource配置。

1
2
3
4
5
spring.datasource.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Spring 数据源Hack

这里给出一个脚本,通过在webshell中使用注入数据源的方式来获取数据库连接对象,进而获取数据库信息或者执行SQL语句:

spring-datasource.jsp获取数据源/执行SQL语句示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>
th, td {
border: 1px solid #C1DAD7;
font-size: 12px;
padding: 6px;
color: #4f6b72;
}
</style>
<%!
// C3PO数据源类
private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";

// DBCP数据源类
private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";

//Druid数据源类
private static final String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";

/**
* 获取所有Spring管理的数据源
* @param ctx Spring上下文
* @return 数据源数组
*/
List<DataSource> getDataSources(ApplicationContext ctx) {
List<DataSource> dataSourceList = new ArrayList<DataSource>();
String[] beanNames = ctx.getBeanDefinitionNames();

for (String beanName : beanNames) {
Object object = ctx.getBean(beanName);

if (object instanceof DataSource) {
dataSourceList.add((DataSource) object);
}
}

return dataSourceList;
}

/**
* 打印Spring的数据源配置信息,当前只支持DBCP/C3P0/Druid数据源类
* @param ctx Spring上下文对象
* @return 数据源配置字符串
* @throws ClassNotFoundException 数据源类未找到异常
* @throws NoSuchMethodException 反射调用时方法没找到异常
* @throws InvocationTargetException 反射调用异常
* @throws IllegalAccessException 反射调用时不正确的访问异常
*/
String printDataSourceConfig(ApplicationContext ctx) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException {

List<DataSource> dataSourceList = getDataSources(ctx);

for (DataSource dataSource : dataSourceList) {
String className = dataSource.getClass().getName();
String url = null;
String UserName = null;
String PassWord = null;

if (C3P0_CLASS_NAME.equals(className)) {
Class clazz = Class.forName(C3P0_CLASS_NAME);
url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);
UserName = (String) clazz.getMethod("getUser").invoke(dataSource);
PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
} else if (DBCP_CLASS_NAME.equals(className)) {
Class clazz = Class.forName(DBCP_CLASS_NAME);
url = (String) clazz.getMethod("getUrl").invoke(dataSource);
UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
} else if (DRUID_CLASS_NAME.equals(className)) {
Class clazz = Class.forName(DRUID_CLASS_NAME);
url = (String) clazz.getMethod("getUrl").invoke(dataSource);
UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
}

return "URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";
}

return null;
}
%>
<%
String sql = request.getParameter("sql");// 定义需要执行的SQL语句

// 获取Spring的ApplicationContext对象
ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());

// 获取Spring中所有的数据源对象
List<DataSource> dataSourceList = getDataSources(ctx);

// 检查是否获取到了数据源
if (dataSourceList == null) {
out.println("未找到任何数据源配置信息!");
return;
}

out.println("<hr/>");
out.println("Spring DataSource配置信息获取测试:");
out.println("<hr/>");
out.print(printDataSourceConfig(ctx));
out.println("<hr/>");

// 定义需要查询的SQL语句
sql = sql != null ? sql : "select version()";

for (DataSource dataSource : dataSourceList) {
out.println("<hr/>");
out.println("SQL语句:<font color='red'>" + sql + "</font>");
out.println("<hr/>");

//从数据源中获取数据库连接对象
Connection connection = dataSource.getConnection();

// 创建预编译查询对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 执行查询并获取查询结果对象
ResultSet rs = pstt.executeQuery();

out.println("<table><tr>");

// 获取查询结果的元数据对象
ResultSetMetaData metaData = rs.getMetaData();

// 从元数据中获取字段信息
for (int i = 1; i <= metaData.getColumnCount(); i++) {
out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")\t" + "</th>");
}

out.println("<tr/>");

// 获取JDBC查询结果
while (rs.next()) {
out.println("<tr>");

for (int i = 1; i <= metaData.getColumnCount(); i++) {
out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");
}

out.println("<tr/>");
}

rs.close();
pstt.close();
}
%>

上面的代码不需要手动去配置文件中寻找任何信息就可以直接读取出数据库配置信息甚至是执行SQL语句,其实是利用了Spring的ApplicationContext遍历了当前Web应用中Spring管理的所有的Bean,然后找出所有DataSource的对象,通过反射读取出C3P0DBCPDruid这三类数据源的数据库配置信息,最后还利用了DataSource获取了Connection对象实现了数据库查询功能。

Java Web Server 数据源

除了第三方数据源库实现,标准web容器也提供了数据源服务,通常会在容器中配置DataSource信息并注册到JNDI(Java Naming and Directory Interface)中,在Web应用中我们可以通过JNDI的接口lookup(定义的JNDI路径)来获取到DataSource对象。

Tomcat JNDI DataSource

Tomcat配置JNDI数据源需要手动修改Tomcat目录/conf/context.xml文件

1
2
3
4
5
6
7
8
<Context>

<Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mysql"/>

</Context>

Resin JNDI DataSource

Resin需要修改resin.xml,添加database配置

1
2
3
4
5
6
7
<database jndi-name='jdbc/test'>
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://localhost:3306/mysql</url>
<user>root</user>
<password>root</password>
</driver>
</database>

JDBC SQL注入

sql注入可以说是大家都不陌生,各种注入方式也是层出不穷。今天主要想分析一下java代码中如何对sql注入进行防御。

通常情况下有以下方式来防御sql注入攻击:

  • 加黑名单过滤(事实证明,此法一般还是会被突破)
  • 限制用户传入的数据类型,如果预期传入的是数字,就使用Integer.parseInt()/Long.parseLong强转
  • 使用PreparedStatement对象提供的SQL语句预编译(转义单引号)。

PreparedStatement SQL预编译查询

将存在注入的Java代码改为?(问号)占位的方式即可实现SQL预编译查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取用户传入的用户ID
String id = request.getParameter("id");

// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =? ";

// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 设置预编译查询的第一个参数值
pstt.setObject(1, id);

// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery()

将用户传入的变量先用问号表示,然后对sql语句进行预编译,再把变量设置成参数值,这样就可以防御sql注入。

其实由最终执行的 SQL 可以看出,PreparedStatement 防止 SQL 注入的原理就是把用户非法输入的单引号进行转义,最终传入参数作为一个整体执行,这样就不会让用户输入的数据影响问号后面的语句。

JDBC预编译

在jdbc使用PreparedStatement对象的sql语句实现预编译有两种方式,一种是客户端预编译,另一种是服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmtsfalse时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmtstrue时使用数据库服务器端预编译。

当使用数据库服务器预编译的时候,本地抓查询数据包会发现没有发生编译,而使用客户端预编译的时候,可以看到数据包中已经对sql语句进行了转义。

image-20230903091621224

客户端预编译调用的是PreparedStatement里面的setString方法。

Mysql预编译

Mysql默认提供了预编译命令:prepare,使用prepare命令可以在Mysql数据库服务端实现预编译查询。

prepare查询示例:

1
2
3
prepare stmt from 'select host,user from mysql.user where user = ?';
set @username='root';
execute stmt using @username;

总结:本文主要研究了java如何连接数据库,以及sql注入的防御。