本文可能是全網(wǎng)最全面最詳細(xì)的 JDBC 驅(qū)動開發(fā)教程雷酪,轉(zhuǎn)載請注明出處
要自己實現(xiàn)一個 JDBC 驅(qū)動無疑是較為困難的淑仆,在此之前我查閱了很多資料,也查到了許多正在提問的帖子哥力,最后都是無疾而終蔗怠,提問者沒有得到想要的回答。而我由于剛好需要做這方面的開發(fā)省骂,也只能硬啃 JDBC 驅(qū)動的文檔了蟀淮,寫這篇算是一個資料的整理,同時我也已經(jīng)完成了一個通用的 JDBC 生成工具钞澳,文末可以下載怠惶。
首先還是來讀文檔,JDBC 的文檔可謂是非常詳細(xì)了轧粟,直接翻到 6.3 JDBC 4.0 API Compliance
策治,它很明確的告訴了你應(yīng)該做什么:
1. 支持自動加載的,繼承自 java.sql.Driver 的類
2. 支持“僅向前”的結(jié)果集
3. 支持“只讀的”結(jié)果集并發(fā)類型
4. 支持批量更新
5. 實現(xiàn)以下接口
1) java.sql.Driver
2) java.sql.DatabaseMetaData
3) java.sql.ParameterMetaData
4) java.sql.ResultSetMetaData
5) java.sql.Wrapper
6) javax.sql.DataSource
7) java.sql.Connection
8) java.sql.Statement
9) java.sql.PreparedStatement
10) java.sql.CallableStatement
11) java.sql.ResultSet
好的兰吟,看到這里我們會遇到官方文檔挖的第一個坑通惫,其實并非這么多東西都要實現(xiàn)的,實際開發(fā)中混蔼,只實現(xiàn)以下內(nèi)容也可以:
1) java.sql.Driver
2) java.sql.DatabaseMetaData
3) java.sql.ResultSetMetaData
4) java.sql.Connection
5) java.sql.Statement
6) java.sql.PreparedStatement
7) java.sql.ResultSet
只要有這些東西履腋,就足以支撐起一個完整的 JDBC 驅(qū)動。
那么接下來就是實現(xiàn)了,不得不說遵湖,整個 JDBC 的協(xié)議真的又臭又長悔政,所有的類加起來一共要實現(xiàn) 584 個接口函數(shù),而且里面有一大半是不起任何作用的延旧。
限于篇幅谋国,在這里我不可能把所有的函數(shù)予以列出,只看最重要的那些迁沫,其他的請大家自行發(fā)揮芦瘾。
那先來看 Driver
:
class MyDriver: Driver {
companion object {
init {
try {
DriverManager.registerDriver(MyDriver())
} catch (e: Exception) {
throw RuntimeException("Can't register $DRIVER_NAME", e)
}
}
}
override fun acceptsURL(url: String) = url.toLowerCase().startsWith(JDBC_URL)
override fun connect(url: String, info: Properties?): Connection {
if (!acceptsURL(url)) throw SQLException("Invalid URL: $url")
val props = MyDriverUtil.parseMergeProperties(url.replace("jdbc:", ""), info)
return MyConnection(props)
}
... ...
}
使用 companion object
的 init
塊來完成驅(qū)動的自動加載,需要特別注意的是集畅,千萬不要不帶 companion object
近弟,如果不帶的話,init
塊實質(zhì)上是類的構(gòu)造函數(shù)挺智,而不是靜態(tài)初始化塊藐吮。
acceptsURL
方法指出了什么樣的 URL 可以被驅(qū)動接受,比如說我們經(jīng)常在使用 mysql 驅(qū)動時看到其 JDBC URL 為 jdbc:mysql://
逃贝,就是在 acceptsURL
里接受了這樣的前綴谣辞。
connect
方法用于獲取一個連接,它是在 DriverManager.getConnection
時自動被調(diào)用的沐扳,返回一個非空的 Connection
對象泥从。
這里有個函數(shù),MyDriverUtil.parseMergeProperties
用于將 url 的參數(shù)和 info: Properties
所攜帶的參數(shù)進(jìn)行拼接沪摄,該函數(shù)實現(xiàn)如下:
fun parseMergeProperties(url: String, prop: Properties?) = mutableMapOf<String, String>().apply {
val uri = URI(url)
this[PROP_HOST] = uri.host
this[PROP_PORT] = (if (uri.port == -1) DEFAULT_PORT else uri.port).toString()
this[PROP_PATH] = uri.path.replaceFirst("/", "")
if (uri.query != null) {
this += uri.query.split("&").map { p -> p.split("=").let { i -> Pair(i[0], i[1]) } }.toMap()
}
if (prop != null) {
this += prop.map { e -> Pair(e.key.toString(), e.value.toString()) }.toMap()
}
}.toMap()
在這段代碼中出現(xiàn)了 MyConnection
這個對象躯嫉,我們下面就來看看如何實現(xiàn)它。
實現(xiàn) Connection
:
class MyConnection(props: Map<String, String>) : Connection {
val io = MyIO(props)
private var isClosed = false
private val autoCommit = true
override fun prepareStatement(sql: String) = MyPreparedStatement(sql, this)
override fun prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int) = MyPreparedStatement(sql, this)
override fun prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int) = MyPreparedStatement(sql, this)
override fun prepareStatement(sql: String, autoGeneratedKeys: Int) = MyPreparedStatement(sql, this)
override fun prepareStatement(sql: String, columnIndexes: IntArray?) = MyPreparedStatement(sql, this)
override fun prepareStatement(sql: String, columnNames: Array<out String>?) = MyPreparedStatement(sql, this)
override fun getAutoCommit() = autoCommit
override fun getWarnings(): SQLWarning? = null
override fun getCatalog(): String? {
checkConnection()
return null
}
override fun isValid(timeout: Int) = isClosed
override fun close() {
isClosed = true
}
override fun isClosed() = isClosed
override fun isReadOnly() = false
override fun createStatement() = MyStatement(this)
override fun createStatement(resultSetType: Int, resultSetConcurrency: Int) = MyStatement(this)
override fun createStatement(resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int) = MyStatement(this)
override fun getMetaData() = MyDatabaseMetaData()
override fun getTransactionIsolation() = Connection.TRANSACTION_NONE
private fun checkConnection() {
if (isClosed()) throw SQLException("Connection is closed")
}
... ...
}
這里需要實現(xiàn)的東西就比較多了杨拐,需要實現(xiàn)所有返回 Statement
或 PreparedStatement
的函數(shù)祈餐,也需要實現(xiàn) isClosed
和 close
這類改變或檢查狀態(tài)的函數(shù)。
這里有一個 MyIO
的對象哄陶,是一個自定義的類帆阳,用于完成真正的數(shù)據(jù)獲取工作,在一會的代碼中就會用到它了屋吨。
在實現(xiàn) Statement 之前蜒谤,要先做一點準(zhǔn)備工作,有一些公用的東西需要被抽象出來至扰。
abstract class MyAbsStatement {
internal var isClosed = false
internal var connection: MyConnection
internal lateinit var resultSet: ResultSet
protected lateinit var sql: String
constructor(sql: String, conn: MyConnection) {
this.sql = sql
this.connection = conn
}
constructor(conn: MyConnection) {
this.connection = conn
}
open fun executeForResultSet(sql: String): Boolean {
if (isClosed) throw SQLException("This statement is closed.")
try {
resultSet = connection.io.internalExecuteQuery(sql)
return true
} catch (th: Throwable) {
throw SQLException(th)
}
}
open fun executeForResult(sql: String): Int {
if (isClosed) throw SQLException("This statement is closed.")
try {
return connection.io.internalExecuteUpdate(sql)
} catch (th: Throwable) {
throw SQLException(th)
}
}
}
有了這個類之后鳍徽,我們可以繼承它,并且實現(xiàn) Statement
接口:
class MyStatement(conn: MyConnection) : MyAbsStatement(conn), Statement {
private val batchOps = mutableListOf<String>()
override fun execute(sql: String, autoGeneratedKeys: Int) = execute(sql)
override fun execute(sql: String, columnIndexes: IntArray?) = execute(sql)
override fun execute(sql: String, columnNames: Array<out String>?) = execute(sql)
override fun clearBatch() {
batchOps.clear()
}
override fun getResultSetType() = ResultSet.TYPE_FORWARD_ONLY
override fun isCloseOnCompletion() = false
override fun <T : Any> unwrap(iface: Class<T>): T? = null
override fun getMaxRows() = 0
override fun getConnection() = this.connection
override fun getWarnings(): SQLWarning? = null
override fun executeQuery(sql: String): ResultSet {
this.execute(sql)
return this.getResultSet()
}
override fun close() {
isClosed = true
}
override fun isClosed() = this.isClosed
override fun getMaxFieldSize() = 0
override fun isWrapperFor(iface: Class<*>) = false
override fun getUpdateCount() = -1
override fun getFetchSize() = 0
override fun executeBatch() = IntArray(batchOps.size).apply {
this@MyStatement.batchOps.forEachIndexed { index, sql ->
try {
this@MyStatement.execute(sql)
this[index] = SUCCESS_NO_INFO
} catch (th: Throwable) {
throw BatchUpdateException(th)
}
}
}
override fun getQueryTimeout() = 0
override fun isPoolable() = false
override fun addBatch(sql: String) {
batchOps.add(sql)
}
override fun getGeneratedKeys(): ResultSet? = null
override fun getResultSetConcurrency() = ResultSet.CONCUR_READ_ONLY
override fun getResultSet() = this.resultSet
override fun execute(sql: String) = executeForResultSet(sql)
override fun executeUpdate(sql: String) = executeForResult(sql)
override fun executeUpdate(sql: String, autoGeneratedKeys: Int) = executeUpdate(sql)
override fun executeUpdate(sql: String, columnIndexes: IntArray?) = executeUpdate(sql)
override fun executeUpdate(sql: String, columnNames: Array<out String>?) = executeUpdate(sql)
override fun getFetchDirection() = 0
override fun getResultSetHoldability() = ResultSet.CLOSE_CURSORS_AT_COMMIT
override fun getMoreResults() = false
override fun getMoreResults(current: Int) = false
... ...
}
這里主要實現(xiàn) execute
相關(guān)的方法敢课,這個時候定義在抽象類里的 executeForResult
和 executeForResultSet
就有了用武之地阶祭,它們可以將所有的請求一并接管起來绷杜。
同樣需要注意的,是在 JDBC 文檔內(nèi)所述的濒募,必須支持批量更新接剩,在 Statement
內(nèi)需要予以支持。
與 Statement
類似的萨咳,下面來實現(xiàn) PreparedStatement
,與 Statement
不同的地方在于疫稿,PreparedStatement
需要用戶自己處理替換問號占位符的操作培他。
先給出這個操作的代碼:
private fun replaceSQL() {
var idx = 1
while (sql.indexOf("?") > 1) {
try {
val p = parameters[idx]
sql = sql.replaceFirst("?", if (p == null) "null" else "'$p'")
} catch (e: IndexOutOfBoundsException) {
throw SQLException("Can't find defined parameter for position: $idx")
}
idx++
}
}
然后來看看實現(xiàn) PreparedStatement
需要做些什么:
class MyPreparedStatement(sql: String, conn: MyConnection) : MyAbsStatement(sql, conn), PreparedStatement {
private val parameters = mutableMapOf<Int, String?>()
override fun execute(): Boolean {
replaceSQL()
return super.executeForResultSet(sql)
}
override fun execute(sql: String): Boolean {
this.sql = sql
return this.execute()
}
override fun execute(sql: String, autoGeneratedKeys: Int) = execute(sql)
override fun execute(sql: String, columnIndexes: IntArray?) = execute(sql)
override fun execute(sql: String, columnNames: Array<out String>?) = execute(sql)
override fun getResultSetType() = ResultSet.TYPE_FORWARD_ONLY
override fun clearParameters() {
parameters.clear()
}
override fun getConnection() = this.connection
override fun getWarnings(): SQLWarning? = null
override fun getParameterMetaData(): ParameterMetaData? = null
override fun executeQuery(): ResultSet {
this.execute()
return this.resultSet
}
override fun executeQuery(sql: String): ResultSet {
execute(sql)
return this.resultSet
}
override fun executeUpdate(): Int {
replaceSQL()
return executeForResult(sql)
}
override fun executeUpdate(sql: String): Int {
replaceSQL()
return executeForResult(sql)
}
override fun executeUpdate(sql: String, autoGeneratedKeys: Int) = executeUpdate(sql)
override fun executeUpdate(sql: String, columnIndexes: IntArray?) = executeUpdate(sql)
override fun executeUpdate(sql: String, columnNames: Array<out String>?) = executeUpdate(sql)
override fun close() {
isClosed = true
}
override fun isCloseOnCompletion() = false
override fun getMaxRows() = 0
override fun isClosed() = isClosed
override fun getMaxFieldSize() = 0
override fun getUpdateCount() = 0
override fun getFetchSize() = 0
override fun executeBatch(): IntArray? = null
override fun getQueryTimeout() = 0
override fun isPoolable() = false
override fun getGeneratedKeys(): ResultSet? = null
override fun getResultSetConcurrency() = ResultSet.CONCUR_READ_ONLY
override fun getResultSet() = this.resultSet
override fun getMetaData() = MyResultSetMetaData()
override fun getFetchDirection() = ResultSet.FETCH_FORWARD
override fun getResultSetHoldability() = ResultSet.CLOSE_CURSORS_AT_COMMIT
override fun getMoreResults() = false
override fun getMoreResults(current: Int) = false
override fun setFloat(parameterIndex: Int, x: Float) {
pushIntoParameters(parameterIndex, x.toString())
}
override fun setLong(parameterIndex: Int, x: Long) {
pushIntoParameters(parameterIndex, x.toString())
}
override fun setDouble(parameterIndex: Int, x: Double) {
pushIntoParameters(parameterIndex, x.toString())
}
override fun setInt(parameterIndex: Int, x: Int) {
pushIntoParameters(parameterIndex, x.toString())
}
override fun setString(parameterIndex: Int, x: String?) {
pushIntoParameters(parameterIndex, x)
}
override fun setTimestamp(parameterIndex: Int, x: Timestamp?) {
pushIntoParameters(parameterIndex, x.toString())
}
private fun pushIntoParameters(index: Int, value: String?) {
if (index <= 0) throw SQLException("Invalid position for parameter ($index)")
this.parameters[index] = value
}
... ...
}
可以清楚的看到,在這里主要是用 Map 來保存需要替換的值遗座,然后在執(zhí)行的時候?qū)⒄鎸嵉膮?shù)替換進(jìn)問號中舀凛。然后對于執(zhí)行 SQL 的地方,依然調(diào)用了抽象類里的 executeForResult
和 executeForResultSet
方法途蒋。
好了猛遍,現(xiàn)在我們已經(jīng)完成了 Statement
和 PreparedStatement
,你可能要問了号坡,能不能跑起代碼看看效果呀懊烤?答案是否定的,因為還沒有做好完整的準(zhǔn)備宽堆,我們還需要一些東西腌紧,下面這個也很關(guān)鍵,是 ResultSet
畜隶。
其實這也是 JDBC 坑的一個地方壁肋,通常情況下我們可能會希望寫一點代碼就運行起來看看效果,但是寫 JDBC 驅(qū)動時偏偏不能籽慢,還是要先完整實現(xiàn)才可以浸遗。
一個標(biāo)準(zhǔn)的 ResultSet
實現(xiàn)如下:
class MyResultSet : ResultSet {
private var isClosed = false
private var position = -1
private lateinit var fields: List<String>
private lateinit var result: List<List<String>>
constructor(jsonString: String) {
MyResultSetUtil.jsonToResultData(jsonString) { f, l ->
fields = f
result = l
}
}
constructor(fields: List<String>, list: List<List<String>>) {
this.fields = fields
this.result = list
}
override fun findColumn(columnLabel: String) = fields.indexOf(columnLabel)
override fun getStatement(): Statement? = null
override fun getWarnings(): SQLWarning? = null
override fun beforeFirst() {
checkIfClosed()
position = -1
}
override fun close() {
isClosed = true
}
override fun isFirst(): Boolean {
checkIfClosed()
return position == 0
}
override fun isLast(): Boolean {
checkIfClosed()
return position == result.size - 1
}
override fun last(): Boolean {
position = result.size - 1
return result.isNotEmpty()
}
override fun isAfterLast(): Boolean {
checkIfClosed()
return position >= result.size
}
override fun relative(rows: Int): Boolean {
checkIfClosed()
return if (rows + position in 1 until result.size) {
position += rows
true
} else {
false
}
}
override fun absolute(row: Int): Boolean {
checkIfClosed()
return if (row in 1 until result.size) {
position = row
true
} else {
false
}
}
override fun next(): Boolean {
checkIfClosed()
return if (position < result.size - 1) {
position++
true
} else {
false
}
}
override fun first(): Boolean {
checkIfClosed()
position = 0
return result.isNotEmpty()
}
override fun afterLast() {
checkIfClosed()
position = result.size
}
override fun previous(): Boolean {
checkIfClosed()
return if (position > 1) {
position--
true
} else {
false
}
}
override fun isBeforeFirst(): Boolean {
checkIfClosed()
return position < 0
}
override fun getFloat(columnIndex: Int) = result[position][columnIndex].toFloat()
override fun getFloat(columnLabel: String) = result[position][findColumn(columnLabel)].toFloat()
override fun wasNull() = false
override fun getRow() = position + 1
override fun getType() = ResultSet.TYPE_SCROLL_INSENSITIVE
override fun getString(columnIndex: Int) = result[position][columnIndex]
override fun getString(columnLabel: String) = result[position][findColumn(columnLabel)]
override fun getLong(columnIndex: Int) = result[position][columnIndex].toLong()
override fun getLong(columnLabel: String) = result[position][findColumn(columnLabel)].toLong()
override fun getTimestamp(columnIndex: Int): Timestamp = Timestamp.valueOf(result[position][columnIndex])
override fun getTimestamp(columnLabel: String): Timestamp = Timestamp.valueOf(result[position][findColumn(columnLabel)])
override fun getDouble(columnIndex: Int) = result[position][columnIndex].toDouble()
override fun getDouble(columnLabel: String) = result[position][findColumn(columnLabel)].toDouble()
override fun getInt(columnIndex: Int) = result[position][columnIndex].toInt()
override fun getInt(columnLabel: String) = result[position][findColumn(columnLabel)].toInt()
override fun isClosed() = isClosed
override fun getFetchSize() = result.size
override fun getConcurrency() = ResultSet.CONCUR_READ_ONLY
override fun clearWarnings() {
checkIfClosed()
}
override fun getFetchDirection() = ResultSet.TYPE_SCROLL_INSENSITIVE
private fun checkIfClosed() {
if (isClosed()) throw SQLException()
}
... ...
}
這個看起來就有點復(fù)雜了,主要是對游標(biāo)的移動和獲取值的操作箱亿,同樣的跛锌,這里也有一個自定義的函數(shù) MyResultSetUtil.jsonToResultData
,用于將 json 字符串轉(zhuǎn)換為二維數(shù)組届惋。這也就意味著我們在這里已經(jīng)決定了數(shù)據(jù)的傳遞方式察净,以是 json 作為媒介的。
轉(zhuǎn)換函數(shù)的實現(xiàn)如下:
fun jsonToResultData(jsonString: String, callback:(fields: List<String>, data: List<List<String>>) -> Unit) {
val fields = getFields(jsonString)
val data = mutableListOf<List<String>>()
JSONArray(jsonString).forEach { _, obj -> data.add(fields.map { obj.get(it).toString() }) }
callback(fields, data)
}
private fun getFields(jsonString: String) = try {
JSONArray(jsonString).run { if (length() > 0) getJSONObject(0).keySet().toList() else listOf() }
} catch (th: Throwable) {
throw SQLException("Cannot get result fields.")
}
最后是補全驅(qū)動所需的另兩個文件盼樟,分別是 DatabaseMetaData
和 ResultSetMetaData
其實這兩個 MetaData 都可以什么都不填氢卡,因為基本上用不到,只是 JDBC 標(biāo)準(zhǔn)說必須實現(xiàn)晨缴,所以才予以實現(xiàn)译秦,通常的處理方法是將其中所有的方法都標(biāo)記為“不支持”:
throw SQLFeatureNotSupportedException()
像這樣就可以了。
好了,是不是現(xiàn)在就想跑起代碼來看看效果筑悴?我們還有最后一步们拙,還記得上面提到的 IO
對象不,現(xiàn)在來實現(xiàn)這個對象阁吝,以完成對數(shù)據(jù)的請求砚婆。當(dāng)然了,在這里我們使用的是寫死的假數(shù)據(jù):
object MyTestRequset {
var LOCAL_TEST = false
private val SAMPLEDATA = """[{"id":1, "name":"test1", "age":10},{"id":2, "name":"test2", "age":20},{"id":3, "name":"test3", "age":30},{"id":4, "name":"test4", "age":40},{"id":5, "name":"test5", "age":50}]"""
@TestOnly
fun localTestInternalRequest(sql: String) = if (sql.contains("select ")) SAMPLEDATA else "1"
}
class MyIO(private val prop: Map<String, String>) {
fun internalExecuteQuery(sql: String) = try {
MyResultSet(internalRequest(sql))
} catch (th: Throwable) {
println("internalExecuteQuery error: $th")
null
} ?: throw SQLException("cannot parse ResultSet")
fun internalExecuteUpdate(sql: String) = try {
internalRequest(sql).toInt()
} catch (th: Throwable) {
println("internalExecuteUpdate error: $th")
-1
}
private fun internalRequest(sql: String): String {
if (MyTestRequset.LOCAL_TEST) return MyTestRequset.localTestInternalRequest(sql)
TODO("獲取數(shù)據(jù)的真實代碼寫在此處")
}
}
好了突勇,現(xiàn)在我們的代碼已經(jīng)完整了装盯,可以運行看看效果,在此寫一個 Testcase 來跑一下:
class Test {
@Test
fun doTest() {
MyTestRequset.LOCAL_TEST = true
Class.forName("com.sample.MyDriver")
DriverManager.getConnection("jdbc:myurl://0.0.0.0/sampledb", Properties().apply { setProperty(PROP_SCHEMA, "http") }).use { conn ->
conn.prepareStatement("select * from Data").use { stmt ->
stmt.executeQuery().use { result ->
while (result.next()) {
println(result.getString("name"))
}
}
}
conn.prepareStatement("insert into Data(name) values (?)").use { stmt ->
stmt.setString(1, "23333")
println(stmt.executeUpdate())
}
}
}
}
能順利跑通就說明我們的驅(qū)動已經(jīng)正常工作了甲馋。同樣的埂奈,符合 JDBC 標(biāo)準(zhǔn)的驅(qū)動也可以被 myBatis 等框架加載并使用。
好了定躏,下面是大招账磺,還記得上面的 MyIO
里有一個 TODO
嗎?我們完全可以把對數(shù)據(jù)庫的請求代理掉痊远,讓它成為一個遠(yuǎn)程的數(shù)據(jù)請求垮抗,代碼如下:
private fun internalRequest(sql: String): String {
var ret: String? = null
http {
url = "${if (prop.containsKey(PROP_SCHEMA)) prop[PROP_SCHEMA] else "http"}://${prop[PROP_HOST]}:${prop[PROP_PORT]}/${prop[PROP_PATH]}"
method = HttpMethod.POST
if (prop.containsKey(PROP_USER)) authenticatorUser = prop[PROP_USER]
if (prop.containsKey(PROP_PASSWORD)) authenticatorPassword = prop[PROP_PASSWORD]
postParam = mutableMapOf("sql" to sql)
onSuccess { code, text, _ ->
if (code != 200) throw SQLException("Remote execute SQL failed: $code")
ret = text
}
}
return ret ?: throw SQLException("Remote SQL result is null.")
}
同時,只需要使用 Ktor 寫幾行代碼碧聪,跑起服務(wù)器借宵,這一切都順理成章了(還不會 Ktor 的小伙伴可以看我的 Ktor 從入門到放棄 系列)。
服務(wù)端代碼:
fun Routing.ISCRouting() {
post("/sampledb") {
val sql = call.requestParameters()["sql"] ?: ""
call.respondText { doRequestDb(sql) }
}
}
在 doRequestDb
的過程中矾削,就可以做各種騷操作了壤玫,如分庫分表,權(quán)限控制等哼凯,在此就不贅述了欲间,大家可以發(fā)揮自己的想象力。
最后断部,最上面提到的那個生成 JDBC 驅(qū)動代碼的工具猎贴,可以從我的 Github 下載 EasyJDBC 并編譯,然后愉快的開發(fā)吧蝴光。