背景
前两天一个小伙伴面试的时候,被问JDBC底层是如何连接数据库的?
他顿时一脸懵逼,因为大部分人只知道JDBC的几个步骤,至于底层到底是怎么连接数据库的,还真不知道。
由于小伙伴是面试高级开发,问这种问题倒也不能说面试官过分,如果是初级或者中级,那问着问题就确实有些过分了。
但是如果你在初级或者中级的阶段,就知道了答案,岂不是爽歪歪么?
估计大部分人都不知道这个问题该怎么回答,稍微发散一下思维,倒是可以猜测一下,今天我们就来搞清楚JDBC底层到底是如何连接数据库的。往后别再猜了。
反过来,如果面试官问你JDBC的时候,你能知道底层是怎么连接数据库的,估计,很多相对较水的面试官也会一脸懵逼。
何为 JDBC ?
JDBC(Java DataBase Connectivity)是Java和数据库之间的一个桥梁,是一个「规范」而不是一个实现,能够执行SQL语句。JDBC由一组用Java语言编写的类和接口组成。各种不同类型的数据库都有相应的实现,注意:本文中的代码都是针对MySQL数据库实现的。
JDBC 架构
分为双层架构和三层架构。
双层
作用:此架构中,Java Applet 或应用直接访问数据源。
条件:要求 Driver 能与访问的数据库交互。
机制:用户命令传给数据库或其他数据源,随之结果被返回。
部署:数据源可以在另一台机器上,用户通过网络连接,称为 C/S配置(可以是内联网或互联网)。
三层
侧架构特殊之处在于,引入中间层服务。
流程:命令和结构都会经过该层。
吸引:可以增加企业数据的访问控制,以及多种类型的更新;另外,也可简化应用的部署,并在多数情况下有性能优势。
历史趋势:以往,因性能问题,中间层都用 C 或 C++ 编写,随着优化编译器(将 Java 字节码 转为 高效的 特定机器码)和技术的发展,如EJB,Java 开始用于中间层的开发这也让 Java 的优势突显出现出来,使用 Java 作为服务器代码语言,JDBC随之被重视。
入门案例
下面给出一个JDBC入门级案例:
publicclassJdbcDemo{publicstaticfinalStringURL="jdbc:mysql://localhost:3306/mblog";publicstaticfinalStringUSER="root";publicstaticfinalStringPASSWORD="123456";publicstaticvoidmain(String[]args)throwsException{Class.forName("com.mysql.jdbc.Driver");Connectionconn=DriverManager.getConnection(URL,USER,PASSWORD);Statementstmt=conn.createStatement();ResultSetrs=stmt.executeQuery("SELECTid,name,ageFROMm_userwhereid=1");while(rs.next()){System.out.println("name:"+rs.getString("name")+"年龄:"+rs.getInt("age"));}}}JDBC 步骤
数据库驱动:
Class.forName("com.mysql.jdbc.Driver");获取连接:
Connectionconn=DriverManager.getConnection(URL,USER,PASSWORD);创建Statement或者PreparedStatement对象:
Statementstmt=conn.createStatement();执行sql数据库查询:
ResultSetrs=stmt.executeQuery("SELECTid,name,ageFROMm_userwhereid=1");解析结果集:
System.out.println("name:"+rs.getString("name")+"年龄:"+rs.getInt("age"));最后就是各种资源的关闭。
数据库驱动
加载MySql的驱动类 :
Class.forName("com.mysql.jdbc.Driver");我们安装好数据库之后,我们的应用程序也是不能直接使用数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。其实也就是数据库厂商的JDBC接口实现,即对Connection等接口的实现类的jar文件。
Driver接口
java.sql.Driver此接口是提供给数据库厂商实现的。比如说MySQL的,需要依赖对应的jar包。
<depency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.16</version></depency>MySQL数据库对应的实现驱动实现类:
packagecom.mysql.cj.jdbc;importjava.sql.SQLException;publicclassDriverextsNonRegisteringDriverimplementsjava.sql.Driver{static{try{//注册驱动java.sql.DriverManager.registerDriver(newDriver());}catch(SQLExceptionE){thrownewRuntimeException("Can'tregisterdriver!");}}publicDriver()throwsSQLException{}}DriverManager是rt.jar包下的类,(rt=runtime),把我们需要驱动类注册进去。
//DriverManager类中的方法publicstaticsynchronizedvoidregisterDriver(java.sql.Driverdriver,DriverActionda)throwsSQLException{/*Registerthedriverifithasnotalreadybeenaddedtoourlist*/if(driver!=null){registeredDrivers.addIfAbsent(newDriverInfo(driver,da));}else{//ThisisforcompatibilitywiththeoriginalDriverManagerthrownewNullPointerException();}println("registerDriver:"+driver);}相应装载Oracle驱动:
Class.forName("oracle.jdbc.driver.OracleDriver");Sql Server驱动:
Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");获取链接
给我们看起来就这一行代码:
Connectionconn=DriverManager.getConnection(URL,USER,PASSWORD);下面我们进行深入聊聊这行代码,到底底层是怎么连接数据库的?
getConnection方法三个参数:链接地址,用户名和密码。
publicstaticConnectiongetConnection(Stringurl,Stringuser,Stringpassword)throwsSQLException{java.util.Propertiesinfo=newjava.util.Properties();if(user!=null){info.put("user",user);}if(password!=null){info.put("password",password);}return(getConnection(url,info,Reflection.getCallerClass()));}创建一个Properties对象,Properties是HashTable的子类。
publicclassPropertiesextsHashtable<Object,Object>{//.....}再看getConnection方法:
//WorkermethodcalledbythepublicgetConnection()methods.privatestaticConnectiongetConnection(Stringurl,java.util.Propertiesinfo,Class<?>caller)throwsSQLException{ClassLoadercallerCL=caller!=null?caller.getClassLoader():null;SQLExceptionreason=null;//遍历气门注册的数据库驱动for(DriverInfoaDriver:registeredDrivers){try{//获取连接Connectioncon=aDriver.driver.connect(url,info);if(con!=null){//Success!println("getConnectionreturning"+aDriver.driver.getClass().getName());return(con);}}catch(SQLExceptionex){if(reason==null){reason=ex;}}}}这段代码的关键是这一句代码:
Connectioncon=aDriver.driver.connect(url,info);connet()方法是每个数据库驱动自己的实现的。
packagecom.mysql.cj.jdbc;publicclassNonRegisteringDriverimplementsjava.sql.Driver{@Overridepublicjava.sql.Connectionconnect(Stringurl,Propertiesinfo)throwsSQLException{//部分无关键要的代码省略//下面是重点ConnectionUrlconStr=ConnectionUrl.getConnectionUrlInstance(url,info);switch(conStr.getType()){//SINGLE_CONNECTION("jdbc:mysql:",HostsCardinality.SINGLE),//caseSINGLE_CONNECTION:returncom.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());caseLOADBALANCE_CONNECTION:returnLoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl)conStr);caseFAILOVER_CONNECTION:returnFailoverConnectionProxy.createProxyInstance(conStr);caseREPLICATION_CONNECTION:returnReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl)conStr);default:returnnull;}}}ConnectionUrl从这个类名应该能猜到还不到真正连接的,只是创建一个连接Url相关信息封装。
publicabstractclassConnectionUrlimplementsDatabaseUrlContainer{privatestaticfinalStringDEFAULT_HOST="localhost";privatestaticfinalintDEFAULT_PORT=3306;//...}熟悉的身影,MySQL数据库默认端口。我们继续看下一行重要的代码:
ConnectionImpl.getInstance(conStr.getMainHost());
这里就是获取一个实例,不出意外,连接就在这里面产生的。继续:
//ConnectionImplpublicstaticJdbcConnectiongetInstance(HostInfohostInfo)throwsSQLException{returnnewConnectionImpl(hostInfo);}ConnectionImpl构造方法里有调用createNewIO方法:
@OverridepublicvoidcreateNewIO(booleanisForReconnect){synchronized(getConnectionMutex()){try{if(!this.autoReconnect.getValue()){connectOneTryOnly(isForReconnect);return;}connectWithRetries(isForReconnect);}catch(SQLExceptionex){}}}privatevoidconnectOneTryOnly(booleanisForReconnect)throwsSQLException{ExceptionconnectionNotEstablishedBecause=null;JdbcConnectionc=getProxy();//又看到熟悉的connet方法,this.session.connect(this.origHostInfo,this.user,this.password,this.database,DriverManager.getLoginTimeout()*1000,c);this.session.setQueryInterceptors(this.queryInterceptors);}其中,这里的session是NativeSession。
publicvoidconnect(HostInfohi,Stringuser,Stringpassword,Stringdatabase,intloginTimeout,TransactionEventHandlertransactionManager)throwsIOException{SocketConnectionsocketConnection=newNativeSocketConnection();socketConnection.connect(this.hostInfo.getHost(),this.hostInfo.getPort(),this.propertySet,getExceptionInterceptor(),this.log,loginTimeout);this.protocol.connect(user,password,database);this.protocol.getServerSession().setErrorMessageEncoding(this.protocol.getAuthenticationProvider().getEncodingForHandshake());}在这个方法里,我们看到了Socket的命名开头的类,哈哈,是不是就是使用Socket进行通信的呢?
精彩继续:
socketConnection.connect(this.hostInfo.getHost(),this.hostInfo.getPort(),...);来到NativeSocketConnection类中方法:
//com.mysql.cj.protocol.a.NativeSocketConnection@Overridepublicvoidconnect(StringhostName,intportNumber,PropertySetpropSet,ExceptionInterceptorexcInterceptor,Loglog,intloginTimeout){this.mysqlSocket=this.socketFactory.connect(this.host,this.port,propSet,loginTimeout);//...}这里的socketFactory是StandardSocketFactory。所以也就是调用的是StandardSocketFactory的connect方法:
//StandardSocketFactorypublic<TextsCloseable>Tconnect(Stringhostname,intportNumber,PropertySetpset,intloginTimeout)throwsIOException{this.rawSocket=createSocket(pset);this.rawSocket.connect(sockAddr,getRealTimeout(connectTimeout));}protectedSocketcreateSocket(PropertySetprops){returnnewSocket();}这里就算到底了,说白JDBC的底层就是使用「Socket」进行连接数据库的。
常用方法
方法描述createStatement()创建向数据库发送sql的statement对象。prepareStatement(sql)创建向数据库发送预编译sql的PrepareSatement对象。prepareCall(sql)创建执行存储过程的callableStatement对象。setAutoCommit(boolean autoCommit)设置事务是否自动提交。commit()在链接上提交事务。rollback()在此链接上回滚事务。
获取Statement
三种类型
要执行SQL语句,必须获得java.sql.Statement实例,Statement实例分为以下3 种类型:
执行静态SQL语句。通常通过Statement实例实现。执行动态SQL语句。通常通过PreparedStatement实例实现。执行数据库存储过程。通常通过CallableStatement实例实现。具体获取方式
Statementstmt=con.createStatement();PreparedStatementpstmt=con.prepareStatement(sql);CallableStatementcstmt=con.prepareCall("{CALLdemoSp(?,?)}");常用方法
方法含义executeQuery(String sql)用于向数据发送查询语句。executeUpdate(String sql)用于向数据库发送insert、update或delete语句execute(String sql)用于向数据库发送任意sql语句addBatch(String sql)把多条sql语句放到一个批处理中。executeBatch()向数据库发送一批sql语句执行。
Statement和PreparedStatement的异同及优缺点
同:两者都是用来执SQL语句的
异:PreparedStatement需要根据SQL语句来创建,它能够通过设置参数,指定相应的值,不是像Statement那样使用字符串拼接的方式。
PreparedStatement的优点:
1、其使用参数设置,可读性好,不易记错。在statement中使用字符串拼接,可读性和维护性比较差。
2、其具有预编译机制,性能比statement更快。
3、其能够有效防止SQL注入攻击。
execute和executeUpdate的区别
相同点:二者都能够执行增加、删除、修改等操作。
不同点:
1、execute可以执行查询语句,然后通过getResult把结果取出来。executeUpdate不能执行查询语句。
2、execute返回Boolean类型,true表示执行的是查询语句,false表示执行的insert、delete、update等。executeUpdate的返回值是int,表示有多少条数据受到了影响。