Java Review(三十四、JDBC)

public static void main(String[] args)throws Exception

{

ExecuteDML ed = new ExecuteDML();

ed.initParam(“mysql.ini”);

int result = ed.insertData(“insert into jdbc_test(jdbc_name,jdbc_desc)”

  • "select s.student_name , t.teacher_name "

  • "from student_table s , teacher_table t "

  • “where s.java_teacher = t.teacher_id;”);

System.out.println(“–系统中共有” + result + “条记录受影响–”);

}

}

4.2、使用 execute 方法执行 SQL 语句


Statement 的 execute()方法几乎可以执行任何 SQL 语句, 通常没有必要使用 execute()方法来执行 SQL 语句, 使用 executeQuery()或 executeUpdate()方法更简单。 但如果不清楚 SQL 语句的类型, 则只能使用 executeO方法来执行该 SQL 语句。

ExecuteSQL.java

import java.util.*;

import java.io.*;

import java.sql.*;

public class ExecuteSQL

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void executeSql(String sql)throws Exception

{

// 加载驱动

Class.forName(driver);

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url

, user , pass);

// 使用Connection来创建一个Statement对象

Statement stmt = conn.createStatement())

{

// 执行SQL,返回boolean值表示是否包含ResultSet

boolean hasResultSet = stmt.execute(sql);

// 如果执行后有ResultSet结果集

if (hasResultSet)

{

try(

// 获取结果集

ResultSet rs = stmt.getResultSet())

{

// ResultSetMetaData是用于分析结果集的元数据接口

ResultSetMetaData rsmd = rs.getMetaData();

int columnCount = rsmd.getColumnCount();

// 迭代输出ResultSet对象

while (rs.next())

{

// 依次输出每列的值

for (int i = 0 ; i < columnCount ; i++ )

{

System.out.print(rs.getString(i + 1) + “\t”);

}

System.out.print(“\n”);

}

}

}

else

{

System.out.println(“该SQL语句影响的记录有”

  • stmt.getUpdateCount() + “条”);

}

}

}

public static void main(String[] args) throws Exception

{

ExecuteSQL es = new ExecuteSQL();

es.initParam(“mysql.ini”);

System.out.println(“------执行删除表的DDL语句-----”);

es.executeSql(“drop table if exists my_test”);

System.out.println(“------执行建表的DDL语句-----”);

es.executeSql(“create table my_test”

  • "(test_id int auto_increment primary key, "

  • “test_name varchar(255))”);

System.out.println(“------执行插入数据的DML语句-----”);

es.executeSql("insert into my_test(test_name) "

  • “select student_name from student_table”);

System.out.println(“------执行查询数据的查询语句-----”);

es.executeSql(“select * from my_test”);

}

}

4.3、使用 PreparedStatement 执行 SQL 语句


如果经常需要反复执行一条结构相似的 SQL 语句, 例如如下两条 SQL 语句:

insert into student_table values(null,'张三、 1);

insert into student_table values(null,'李四、 2);

对于这两条 SQL 语句而言, 它们的结构基本相似, 只是执行插入时插入的值不同而己。 对于这种情况, 可以使用带占位符(?) 参数的 SQL 语句来代替它:

insert into student table values(null,?,?);

但 Statement 执行 SQL 语句时不允许使用问号占位符参数, 而且这个问号占位符参数必须获得值后才可以执行。 为了满足这种功能, JDBC 提供PreparedStatement 接口, 它是 Statement 接口的子接口,它可以预编译 SQL 语句, 预编译后的 SQL 语句被存储在 PreparedStatement 对象中, 然后可以使用该对象多次高效地执行该语句。 简而言之, 使用 PreparedStatement 比使用 Statement 的效率要高。

创建 PreparedStatement 对象使用 Connection 的 prepareStatement()方法, 该方法需要传入一个 SQL字符串, 该 SQL 字符串可以包含占位符参数。 如下代码所示:

/ / 仓ij建一个 PreparedStatement 对象

pstmt = conn.prepareStatement(“insert into student_table values(null,?,1)”);

PreparedStatement 也提供了 execute()、 executeUpdate()、 executeQuery()三个方法来执行 SQL 语句,不过这三个方法无须参数, 因为 PreparedStatement 己存储了预编译的 SQL 语句。

使用 PreparedStatement 预编译 SQL 语句时, 该 SQL 语句可以带占位符参数, 因此在执行 SQL 语句之前必须为这些参数传入参数值,PreparedStatement 提供了一系列的 setXxx(int index , Xxx value)方法来传入参数值。

如果程序很清楚 PreparedStatement 预编译 SQL 语句中各参数的类型, 则使用相应的setXxx()方法来传入参数即可; 如果程序不清楚预编译 SQL 语句中各参数的类型 , 则可以使用 setObject()方法来传入参数, 由 PreparedStatement 来负责类型转换。

下面程序示范了使用 Statement 和 PreparedStatement 分别插入 100 条记录的对比。 使用 Statement需要传入 100 条 SQL 语句, 但使用 PreparedStatement 则只需要传入 1 条预编译的 SQL 语句, 然后 100次为该 PreparedStatement 的参数设值即可。

PreparedStatementTest.java

import java.util.*;

import java.io.*;

import java.sql.*;

public class PreparedStatementTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

// 加载驱动

Class.forName(driver);

}

public void insertUseStatement()throws Exception

{

long start = System.currentTimeMillis();

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url

, user , pass);

// 使用Connection来创建一个Statment对象

Statement stmt = conn.createStatement())

{

// 需要使用100条SQL语句来插入100条记录

for (int i = 0; i < 100 ; i++ )

{

stmt.executeUpdate(“insert into student_table values(”

  • " null ,‘姓名" + i + "’ , 1)");

}

System.out.println(“使用Statement费时:”

  • (System.currentTimeMillis() - start));

}

}

public void insertUsePrepare()throws Exception

{

long start = System.currentTimeMillis();

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url

, user , pass);

// 使用Connection来创建一个PreparedStatement对象

PreparedStatement pstmt = conn.prepareStatement(

“insert into student_table values(null,?,1)”))

{

// 100次为PreparedStatement的参数设值,就可以插入100条记录

for (int i = 0; i < 100 ; i++ )

{

pstmt.setString(1 , “姓名” + i);

pstmt.executeUpdate();

}

System.out.println(“使用PreparedStatement费时:”

  • (System.currentTimeMillis() - start));

}

}

public static void main(String[] args) throws Exception

{

PreparedStatementTest pt = new PreparedStatementTest();

pt.initParam(“mysql.ini”);

pt.insertUseStatement();

pt.insertUsePrepare();

}

}

总体来看, 使用 PreparedStatement 比使用 Statement 多 / 如下三个好处。

  • PreparedStatement 预编译 SQL 语句, 性能更好。

  • PreparedStatement 无须“拼接” SQL 语句, 编程更简单。

  • PreparedStatement 可以防止 SQL 注入, 安全性更好。

基于以上三点, 应尽量使用 Statement 来执行 SQL 语句, 改为使用 PreparedStatement 执行SQL 语句。

API:java.sql.PreparedStatement

4.4、使用 CallableStatement 调用存储过程


下面的 SQL 语句可以在 MySQL 数据库中创建一个简单的存储过程:

delimiter / /

create procedure add_pro(a int , b int, out sum int)

begin

set sum = a + b;

end;

//

上面程序创建了名为 add_pm 的存储过程, 该存储过程包含三个参数: a、 b 是传入参数, 而 sum 使用 out 修饰, 是传出参数。

调 用 存 储 过 程 使 用 CallableStatement , 可 以 通 过 Connection 的 prepareCall()方 法 来 创 建CallableStatement 对象, 创建该对象时需要传入调用存储过程的 SQL 语句。 调用存储过程的 SQL 语句总是这种格式: {call 过程名(?,?,?..)}, 其中的问号作为存储过程参数的占位符。 例如, 如下代码就创

建了调用上面存储过程的 CallableStatement 对象:

// 使用Connection创建一个 CallableStatement 对象

cstmt = conn.prepareCall(“{call add_pro(?,?,?)}”);

存储过程的参数既有传入参数, 也有传出参数。 所谓传入参数就是 Java 程序必须为这些参数传入值, 可以通过 CallableStatement 的 setXxx()方法为传入参数设置值; 所谓传出参数就是 Java 程序可以通过该参数获取存储过程里的值, CallableStatement 需要调用 registerOutParameter()方法来注册该参数。

如下代码所示:

// 注册 CallableStatement 的第三个参数是 int 类型

cstmt.registerOutParameter(3, Types.INTEGER);

经过上面步骤之后, 就可以调用 CallableStatement 的 execute()方法来执行存储过程了, 执行结束后通过 CallableStatement 对象的 getXxx(int index)方法来获取指定传出参数的值。 下面程序示范了如何来调用该存储过程:

CallableStatementTest.java

import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

import java.util.*;

import java.io.*;

import java.sql.*;

public class CallableStatementTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void callProcedure()throws Exception

{

// 加载驱动

Class.forName(driver);

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url

, user , pass);

// 使用Connection来创建一个CallableStatment对象

CallableStatement cstmt = conn.prepareCall(

“{call add_pro(?,?,?)}”))

{

cstmt.setInt(1, 4);

cstmt.setInt(2, 5);

// 注册CallableStatement的第三个参数是int类型

cstmt.registerOutParameter(3, Types.INTEGER);

// 执行存储过程

cstmt.execute();

// 获取,并输出存储过程传出参数的值。

System.out.println("执行结果是: " + cstmt.getInt(3));

}

}

public static void main(String[] args) throws Exception

{

CallableStatementTest ct = new CallableStatementTest();

ct.initParam(“mysql.ini”);

ct.callProcedure();

}

}

API:java.sql.CallableStatement

5、管理结果集

=========================================================================

JDBC 使用 ResultSet 来封装执行查询得到的查询结果, 然后通过移动ResultSet 的记录指针来取出结果集的内容。

5.1、可滚动、 可更新的结果集


可以使用 absolute()、 previous()、 afterLast()等方法自由移动记录指针的 ResultSet 被称为可滚动的结果集。

以默认方式打开的 ResultSet 是不可更新的, 如果希望创建可更新的 ResultSet, 则 必 须 在 创 建Statement 或 PreparedStatement 时传入额外的参数。

Connection 在创建 Statement 或 PreparedStatement时还可额外传入如下两个参数。

  • resultSetType: 控制 ResultSet 的类型, 该参数可以取如下三个值。

• ResultSet.TYPE_FORWARD_ONLY: 该常量控制记录指针只能向前移动。 这是 JDK 1.4 以前的默认值。

• ResultSet.TYPE_SCROLL_INSENSITIVE: 该常量控制记录指针可以自由移动( 可滚动结果集), 但底层数据的改变不会影响 ResultSet 的内容。

• ResultSet.TYPE_SCROLL_SENSITIVE: 该常量控制记录指针可以自由移动( 可滚动结果集),而且底层数据的改变会影响 ResultSet 的内容。

  • resultSetConcurrency: 控制 ResultSet 的并发类型, 该参数可以接收如下两个值。

• ResultSet.CONCUR_READ_ONLY: 该常量指示 ResultSet 是只读的并发模式( 默认)。

• ResultSet.CONCUR_UPDATABLE: 该常量指示 ResultSet 是可更新的并发模式。

下面代码通过这两个参数创建了一个 PreparedStatement 对象, 由该对象生成的 ResultSet 对象将是可滚动、 可更新的结果集。

// 使用 Connection 创建一个 PreparedStatement 对象

// 传入控制结果集可滚动、 可更新的参数

pstmt = conn.prepareStatement(sql , ResultSet•TYPE_SCROLL_INSENSITIVE

, ResultSet.CONCUR_

需要指出的是, 可更新的结果集还需要满足如下两个条件。

  • 所有数据都应该来自一个表。

  • 选出的数据集必须包含主键列。

Java 8 为 ResultSet 添加了 updateObject(String columnLabel, Object x, SQLType targetSqlType)和updateObject(int columnlndex, Object x, SQLType targetSqlType)两个默认方法, 这两个方法可以直接用Object 来修改记录指针所指记录、 特定列的值, 其中 SQLType 用于指定该数据列的类型。 但目前最新

的 MySQL 驱动暂不支持该方法。

下面程序示范了这种创建可滚动、 可更新的结果集的方法:

ResultSetTest.java

import java.util.*;

import java.io.*;

import java.sql.*;

public class ResultSetTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void query(String sql)throws Exception

{

// 加载驱动

Class.forName(driver);

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url , user , pass);

// 使用Connection来创建一个PreparedStatement对象

// 传入控制结果集可滚动,可更新的参数。

PreparedStatement pstmt = conn.prepareStatement(sql

, ResultSet.TYPE_SCROLL_INSENSITIVE

, ResultSet.CONCUR_UPDATABLE);

ResultSet rs = pstmt.executeQuery())

{

rs.last();

int rowCount = rs.getRow();

for (int i = rowCount; i > 0 ; i-- )

{

rs.absolute(i);

System.out.println(rs.getString(1) + “\t”

  • rs.getString(2) + “\t” + rs.getString(3));

// 修改记录指针所有记录、第2列的值

rs.updateString(2 , “学生名” + i);

// 提交修改

rs.updateRow();

}

}

}

public static void main(String[] args) throws Exception

{

ResultSetTest rt = new ResultSetTest();

rt.initParam(“mysql.ini”);

rt.query(“select * from student_table”);

}

}

5.2、处理 Blob 类型数据


Blob ( Binary Long Object ) 是二进制长对象的意思, Blob 列通常用于存储大文件, 典型的 Blob 内容是一张图片或一个声音文件, 由于它们的特殊性, 必须使用特殊的方式来存储。 使用 Blob 列可以把图片、 声音等文件的二进制数据保存在数据库里, 并可以从数据库里恢复指定文件。

如果需要将图片插入数据库, 显然不能直接通过普通的 SQL 语句来完成, 因为有一个关键的问题Blob 常量无法表示。 所以将 Blob 数据插入数据库需要使用 PreparedStatement,该对象有一个方法:

setBinaryStream(int parameterlndex,InputStream x), 该方法可以为指定参数传入二进制输入流, 从而可以实现将 Blob 数据保存到数据库的功能。

当需要从 ResultSet 里取出 Blob 数据时, 可以调用 ResultSet 的 getBlob(int columnlndex)方法, 该方法将返回一个 Blob 对象, Blob 对象提供了getBinaryStream()方法来获取该 Blob 数据的输入流, 也可以使用 Blob 对象提供的 getBytes()方法直接取出该 Blob 对象封装的二进制数据。

创建一个表:

create table userblob

(

id int(4) primary key not null auto_increment,

name varchar(30),

photo longblob

);

向数据库中插入blob数据:

BlobDemo01.java

import java.sql.Connection ;

import java.sql.DriverManager ;

import java.sql.SQLException ;

import java.sql.PreparedStatement ;

import java.io.File ;

import java.io.FileInputStream ;

import java.io.InputStream ;

public class BlobDemo01{

// 定义MySQL的数据库驱动程序

public static final String DBDRIVER = “org.gjt.mm.mysql.Driver” ;

// 定义MySQL数据库的连接地址

public static final String DBURL = “jdbc:mysql://localhost:3306/sys” ;

// MySQL数据库的连接用户名

public static final String DBUSER = “root” ;

// MySQL数据库的连接密码

public static final String DBPASS = “aaaaaa” ;

public static void main(String args[]) throws Exception{ // 所有异常抛出

Connection conn = null ; // 数据库连接

PreparedStatement pstmt = null ;

String name = “小华” ;

String sql = "INSERT INTO userblob(name,photo) VALUES (?,?) " ;

Class.forName(DBDRIVER) ; // 加载驱动程序

conn = DriverManager.getConnection(DBURL,DBUSER,DBPASS) ;

pstmt = conn.prepareStatement(sql) ;

File f = new File(“d:” + File.separator + “图片1.JPG”) ; // 图片文件

InputStream input = null ;

input = new FileInputStream(f) ;

pstmt.setString(1,name) ; // 设置第一个“?”的内容

pstmt.setBinaryStream(2,input,(int)f.length()) ; // 设置输入流

pstmt.executeUpdate() ; // 更新数据库

pstmt.close() ;

conn.close() ; // 数据库关闭

}

};

从数据库中查询blob数据:

import java.sql.Connection ;

import java.sql.DriverManager ;

import java.sql.PreparedStatement ;

import java.io.File ;

import java.io.FileOutputStream ;

import java.sql.ResultSet ;

import java.io.InputStream ;

import java.io.OutputStream ;

public class BlobDemo01{

// 定义MySQL的数据库驱动程序

public static final String DBDRIVER = “org.gjt.mm.mysql.Driver” ;

// 定义MySQL数据库的连接地址

public static final String DBURL = “jdbc:mysql://localhost:3306/sys” ;

// MySQL数据库的连接用户名

public static final String DBUSER = “root” ;

// MySQL数据库的连接密码

public static final String DBPASS = “aaaaaa” ;

public static void main(String args[]) throws Exception{ // 所有异常抛出

Connection conn = null ; // 数据库连接

PreparedStatement pstmt = null ;

ResultSet rs = null ;

int id = 1 ;

String sql = “SELECT name,photo FROM userblob WHERE id=?” ;

Class.forName(DBDRIVER) ; // 加载驱动程序

conn = DriverManager.getConnection(DBURL,DBUSER,DBPASS) ;

pstmt = conn.prepareStatement(sql) ;

pstmt.setInt(1,id) ;

rs = pstmt.executeQuery() ; // 执行查询

if(rs.next()){

String name = rs.getString(1) ;

System.out.println(“姓名:” + name) ;

InputStream input = rs.getBinaryStream(2) ;

File f = new File(“d:” + File.separator + “load图片1.gif”) ; // 图片文件

OutputStream out = null ;

out = new FileOutputStream(f) ;

int temp = 0 ;

while((temp=input.read())!=-1){ // 边读边写

out.write(temp) ;

}

input.close() ;

out.close() ;

}

pstmt.close() ;

conn.close() ; // 数据库关闭

}

}

为了方便读取,JAVA专门的提供了Blob接口进行二进制文件的读取操作:

BlobDemo01.java

import java.sql.Connection ;

import java.sql.DriverManager ;

import java.sql.SQLException ;

import java.sql.PreparedStatement ;

import java.sql.Blob ;

import java.sql.ResultSet ;

import java.io.File ;

import java.io.FileOutputStream ;

import java.io.InputStream ;

import java.io.OutputStream ;

public class BlobDemo01{

// 定义MySQL的数据库驱动程序

public static final String DBDRIVER = “org.gjt.mm.mysql.Driver” ;

// 定义MySQL数据库的连接地址

public static final String DBURL = “jdbc:mysql://localhost:3306/sys” ;

// MySQL数据库的连接用户名

public static final String DBUSER = “root” ;

// MySQL数据库的连接密码

public static final String DBPASS = “aaaaaa” ;

public static void main(String args[]) throws Exception{ // 所有异常抛出

Connection conn = null ; // 数据库连接

PreparedStatement pstmt = null ;

ResultSet rs = null ;

int id = 1 ;

String sql = “SELECT name,photo FROM userblob WHERE id=?” ;

Class.forName(DBDRIVER) ; // 加载驱动程序

conn = DriverManager.getConnection(DBURL,DBUSER,DBPASS) ;

pstmt = conn.prepareStatement(sql) ;

pstmt.setInt(1,id) ;

rs = pstmt.executeQuery() ; // 执行查询

if(rs.next()){

String name = rs.getString(1) ;

System.out.println(“姓名:” + name) ;

Blob b = rs.getBlob(2) ;

File f = new File(“d:” + File.separator + “load图片2.gif”) ; // 图片文件

OutputStream out = null ;

out = new FileOutputStream(f) ;

out.write(b.getBytes(1,(int)b.length())) ;

out.close() ;

}

pstmt.close() ;

conn.close() ; // 数据库关闭

}

}

从实际角度,把大文件存在MySQL数据库中是很不明智行为。往往采用映射路径方式完成。

API:java.sql.Blob

5.3、使用 ResultSetMetaData 分析结果集


当执行 SQL 查询后可以通过移动记录指针来遍历 ResultSet 的每条记录, 但程序可能不清楚该ResultSet 里包含哪些数据列, 以及每个数据列的数据类型, 那么可以通过 ResultSetMetaData 来获取关于 ResultSet 的描述信息。

ResultSet 里包含一个 getMetaData()方法, 该方法返回该 ResultSet 对应的 ResultSetMetaData 对象。一旦获得了 ResultSetMetaData 对象, 就可通过 ResultSetMetaData 提供的大量方法来返回 ResultSet 的描述信息。 常用的方法有如下三个。

  • int getColuninCount(): 返回该 ResultSet 的列数量。

  • String getColumnName(int column): 返回指定索引的列名。

  • int getColumnType(int column): 返回指定索引的列类型。

API:java.sql.ResultSetMetaData

6、Java的 RowSet

================================================================================

RowSet 接口继承了 ResultSet 接口, RowSet 接口下包含 JdbcRowSet、CachedRowSet 、 FilteredRowSet 、JoinRowSet 和 WebRowSet 常用子接口。 除 JdbcRowSet 需要保持与数据库的连接之外, 其余 4 个子接口都是离线的 RowSet, 无须保持与数据库的连接。

与 ResultSet 相比, RowSet 默认是可滚动、 可更新、 可序列化的结果集, 而且作为 JavaBean 使用,因此能方便地在网络上传输, 用于同步两端的数据。 对于离线 RowSet 而言, 程序在创建 RowSet 时己把数据从底层数据库读取到了内存, 因此可以充分利用计算机的内存, 从而降低数据库服务器的负载,提高程序性能。

RowSet 规范的接口类图

在这里插入图片描述

API:javax.sql.RowSet

6.1、Java 7 新增的 RowSetFactory 与 RowSet


Java 7 新增了 RowSetProvider 类和 RowSetFactory 接 口 , 其 中 RowSetProvider 负 责 创 建RowSetFactory, 而 RowSetFactory 则提供了如下方法来创建 RowSet 实例。

  • CachedRowSet createCachedRowSet(): 创建一个默认的 CachedRowSet 0

  • FilteredRowSet createFilteredRowSet(): 创建一个默认的 FilteredRowSet。

  • JdbcRowSet createJdbcRowSet(): 创建一个默认的 JdbcRowSet。

  • JoinRowSet createJoinRowSet(): 创建一个默认的 JoinRowSet。

  • WebRowSet createWebRowSet(): 创建一个默认的 WebRowSet。

通过使用 RowSetFactory, 就可以把应用程序与 RowSet 实现类分离开, 避免直接使用 JdbcRow Setlmpl等非公开的 API, 也更有利于后期的升级、 扩展。

通过 RowSetFactory 的几个工厂方法不难看出, 使用 RowSetFactory 创建的 RowSet 其实并没有装填数据。

为了让 RowSet 能抓取到数据库的数据, 需要为 RowSet 设置数据库的 URL、 用户名、 密码等连接信息。 因此, RowSet 接口中定义了如下常用方法。

  • setUrl(String url): 设置该 RowSet 要访问的数据库的 URL。

  • setUsemame(String name): 设置该 RowSet 要访问的数据库的用户名。

  • setPassword(String password): 设置该 RowSet 要访问的数据库的密码。

  • setCommand(String sql): 设置使用该 sql 语句的查询结果来装填该 RowSet。

  • execute(): 执行查询。

下面程序通过 RowSetFactory 示范了使用 JdbcRowSet 的可滚动、 可修改特性:

RowSetFactoryTest.java

import java.util.*;

import java.io.*;

import java.sql.*;

import javax.sql.rowset.*;

public class RowSetFactoryTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void update(String sql)throws Exception

{

// 加载驱动

Class.forName(driver);

// 使用RowSetProvider创建RowSetFactory

RowSetFactory factory = RowSetProvider.newFactory();

try(

// 使用RowSetFactory创建默认的JdbcRowSet实例

JdbcRowSet jdbcRs = factory.createJdbcRowSet())

{

// 设置SQL查询语句

jdbcRs.setCommand(sql);

// 设置必要的连接信息

jdbcRs.setUrl(url);

jdbcRs.setUsername(user);

jdbcRs.setPassword(pass);

// 执行查询

jdbcRs.execute();

jdbcRs.afterLast();

// 向前滚动结果集

while (jdbcRs.previous())

{

System.out.println(jdbcRs.getString(1)

  • “\t” + jdbcRs.getString(2)

  • “\t” + jdbcRs.getString(3));

if (jdbcRs.getInt(“student_id”) == 3)

{

// 修改指定记录行

jdbcRs.updateString(“student_name”, “孙悟空”);

jdbcRs.updateRow();

}

}

}

}

API:javax.sql.rowset.RowSetFactory

API:javax.sql.rowset.RowSetProvider

6.2、离线 RowSet


在使用 ResultSet 的时代, 程序查询得到 ResultSet 之后必须立即读取或处理它对应的记录, 否则一旦 Connection 关闭, 再去通过 ResultSet 读取记录就会引发异常。 在这种模式下, JDBC 编程十分痛苦——假设应用程序架构被分为两层: 数据访问层和视图显示层, 当应用程序在数据访问层查询得到ResultSet 之后, 对 ResultSet 的处理有如下两种常见方式。

  • 使用迭代访问 ResultSet 里的记录, 并将这些记录转换成 Java Bean, 再将多个 Java Bean 封装成等资源 个 List , 然后将 集合, 也就是完成 Java Bean“集合传到视图显示层 ResultSet—Java Bean, 集合 视图显示层可以显示查询得到的数据 ”的转换。 转换完成后可以关闭 Connection 。

  • 直 接 将 ResultSet 传到视图显示层一一这要求当视图显示层显示数据时, 底层 Connection 必须一直处于打开状态, 否则 ResultSet 无法读取记录。

第一种方式比较安全, 但编程十分烦琐; 第二种方式则需要 Connection 一直处于打开状态, 这不仅不安全, 而且对程序性能也有较大的影响。

通过使用离线 RowSet 可以十分“ 优雅” 地处理上面的问题, 离线 RowSet 会直接将底层数据读入内存中, 封装成 RowSet 对象, 而 RowSet 对象则完全可以当成 Java Bean 来使用。 因此不仅安全, 而且编程十分简单。CachedRowSet 是所有离线 RowSet 的父接口, 因此下面以 CachedRowSet 为例进行介绍。

看下面程序:

CachedRowSetTest.java

import java.util.*;

import java.io.*;

import java.sql.*;

import javax.sql.*;

import javax.sql.rowset.*;

public class CachedRowSetTest

{

private static String driver;

private static String url;

private static String user;

private static String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public CachedRowSet query(String sql)throws Exception

{

// 加载驱动

Class.forName(driver);

// 获取数据库连接

Connection conn = DriverManager.getConnection(url , user , pass);

Statement stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery(sql);

// 使用RowSetProvider创建RowSetFactory

RowSetFactory factory = RowSetProvider.newFactory();

// 创建默认的CachedRowSet实例

CachedRowSet cachedRs = factory.createCachedRowSet();

// 使用ResultSet装填RowSet

cachedRs.populate(rs); // ①

// 关闭资源

rs.close();

stmt.close();

conn.close();

return cachedRs;

}

public static void main(String[] args)throws Exception

{

CachedRowSetTest ct = new CachedRowSetTest();

ct.initParam(“mysql.ini”);

CachedRowSet rs = ct.query(“select * from student_table”);

rs.afterLast();

// 向前滚动结果集

while (rs.previous())

{

System.out.println(rs.getString(1)

  • “\t” + rs.getString(2)

  • “\t” + rs.getString(3));

if (rs.getInt(“student_id”) == 3)

{

// 修改指定记录行

rs.updateString(“student_name”, “孙悟空”);

rs.updateRow();

}

}

// 重新获取数据库连接

Connection conn = DriverManager.getConnection(url

, user , pass);

conn.setAutoCommit(false);

// 把对RowSet所做的修改同步到底层数据库

rs.acceptChanges(conn);

}

}

运行该程序, 可以看到在 Connection 关闭的情况下, 程序依然可以读取、 修改 RowSet 里的记录。为了将程序对离线 RowSet 所做的修改同步到底层数据库, 程序在调用 RowSet 的 acceptChanges()方法时必须传入 Connection。

API:javax.sql.rowset.CachedRowSet

6.3、离线 RowSet 的查询分页


由于 CachedRowSet 会将数据记录直接装载到内存中, 因此如果 SQL 查询返回的记录过大,CachedRowSet 将会占用大量的内存, 在某些极端的情况下, 它甚至会直接导致内存溢出。为了解决该问题, CachedRowSet 提供了分页功能。 所谓分页功能就是一次只装载 ResultSet 里的某几条记录, 这样就可以避免 CachedRowSet 占用内存过大的问题。

CachedRowSet 提供了如下方法来控制分页:

  • populate(ResultSet rs, int startRow): 使用给定的 ResultSet 装填 RowSet, 从 ResultSet 的第 startRow条记录开始装填。

  • setPageSize(int pageSize): 设置 CachedRowSet 每次返回多少条记录。

  • previousPage(): 在底层 ResultSet 可用的情况下, 让 CachedRowSet 读取上一页记录。

  • nextPage(): 在底层 ResultSet 可用的情况下, 让 CachedRowSet 读取下一页记录。

下面程序示范了 CachedRowSet 的分页支持:

import java.util.*;

import java.io.*;

import java.sql.*;

import javax.sql.*;

import javax.sql.rowset.*;

public class CachedRowSetPage

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public CachedRowSet query(String sql , int pageSize

, int page)throws Exception

{

// 加载驱动

Class.forName(driver);

try(

// 获取数据库连接

Connection conn = DriverManager.getConnection(url , user , pass);

Statement stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery(sql))

{

// 使用RowSetProvider创建RowSetFactory

RowSetFactory factory = RowSetProvider.newFactory();

// 创建默认的CachedRowSet实例

CachedRowSet cachedRs = factory.createCachedRowSet();

// 设置每页显示pageSize条记录

cachedRs.setPageSize(pageSize);

// 使用ResultSet装填RowSet,设置从第几条记录开始

cachedRs.populate(rs , (page - 1) * pageSize + 1);

return cachedRs;

}

}

public static void main(String[] args)throws Exception

{

CachedRowSetPage cp = new CachedRowSetPage();

cp.initParam(“mysql.ini”);

CachedRowSet rs = cp.query(“select * from student_table” , 3 , 2); // ①

// 向后滚动结果集

while (rs.next())

{

System.out.println(rs.getString(1)

  • “\t” + rs.getString(2)

  • “\t” + rs.getString(3));

}

}

}

7、事务处理

========================================================================

对于任何数据库应用而言, 事务都是非常重要的, 事务是保证底层数据完整的重要手段, 没有事务支持的数据库应用, 那将非常脆弱。

7.1、事务的概念和 MySQL 事务支持


事务是由一步或几步数据库操作序列组成的逻辑执行单元, 这系列操作要么全部执行, 要么全部放弃执行。 程序和事务是两个不同的概念。 一般而言, 一段程序中可能包含多个事务。

事务具备 4 个特性: 原子性( Atomicity)、 一致性( Consistency)、 隔离性 (Isolation) 和持续性( Durability)。 这 4 个特性也简称为 ACID 性。

  • 原 子 性(Atomicity): 事务是应用中最小的执行单位, 就如原子是自然界的最小颗粒, 具有不可再分的特征一样, 事务是应用中不可再分的最小逻辑执行体。

  • 一致性(Consistency): 事务执行的结果, 必须使数据库从一个一致性状态, 变到另一个一致性状态。 当数据库只包含事务成功提交的结果时, 数据库处于一致性状态。 如果系统运行发生中断, 某个事务尚未完成而被迫中断, 而该未完成的事务对数据库所做的修改已被写入数据库,此时, 数据库就处于一种不正确的状态。 比如银行在两个账户之间转账: 从 A 账户向 B 账户转入 1000 元, 系统先减少 A 账户的 1000 元, 然后再为 B 账户增加 1000 元。 如果全部执行成功,数据库处于于一致性状态; 如果仅执行完 A 账户金额的修改, 而没有增加 B 账户的金额, 则数据库就处于不一致性状态; 因此, 一致性是通过原子性来保证的。

  • 隔 离 性 (Isolation): 各个事务的执行互不干扰, 任意一个事务的内部操作对其他并发的事务都是隔离的。 也就是说, 并发执行的事务之间不能看到对方的中间状态, 并发执行的事务之间不能互相影响。

  • 持续性( Durability): 持续性也称为持久性(Persistence), 指事务一旦提交, 对数据所做的任何改变都要记录到永久存储器中, 通常就是保存进物理数据库。

数据库的事务由下列语句组成:

  • 一组 DML 语句, 经过这组 DML 语句修改后的数据将保持较好的一致性。

  • 一条 DDL 语句。

  • 一条 DCL 语句。

DDL 和 DCL 语句最多只能有一条, 因为 DDL 和 DCL 语句都会导致事务立即提交。

当事务所包含的全部数据库操作都成功执行后, 应该提交( commit) 事务, 使这些修改永久生效。

事务提交有两种方式: 显式提交和自动提交。

  • 显式提交: 使用 commit()

  • 自动提交: 执行 DDL 或 DCL 语句, 或者程序正常退出。

当事务所包含的任意一个数据库操作执行失败后, 应该回滚 ( rollback) 事务, 使该事务中所做的修改全部失效。 事务回滚有两种方式: 显式回滚和自动回滚。

  • 显式回滚: 使用 rollbacko

  • 自动回滚: 系统错误或者强行退出。

MySQL 默认关闭事务( 即打开自动提交), 在默认情况下, 用户在 MySQL 控制台输入一条 DML 语句, 这条 DML 语句将会立即保存到数据库里。 为了幵启 MySQL 的事务支持, 可以显式调用如下命令:

SET AUTOCOMMIT = {0 1 1} o 为关闭自动提交, 即开启事务

一旦在 MySQL 的命令行窗口中输入 set autocommit=0 开启了事务, 该命令行窗口里的所有 DML语句都不会立即生效, 上一个事务结束后第一条 DML 语句将开始一个新的事务, 而后续执行的所有 SQL语句都处于该事务中, 除非显式使用 commit 来提交事务, 或者正常退出, 或者运行 DDL、 DCL 语句导致事务隐式提交。 当然, 也可以使用 rollback 回滚来结束事务, 使用 rollback 结束事务将导致本次事务中 DML 语句所做的修改全部失效。

除此之外, 如果不想关闭整个命令行窗口的自动提交, 而只是想临时性地幵始事务, 则可以使用MySQL 提供的 start transaction 或 begin 两个命令, 它们都表示临时性地开始一次事务, 处于 start transaction 或 begin 后的 DML 语句不会立即生效, 除非使用 commit 显式提交事务, 或者执行 DDL、DCL 语者来隐式提交事务。 如下 SQL 代码将不会对数据库有任何影响。

临时开始事务

begin;

向 student_table 表中插入 3 条记录

insert into student_table

values(null , ‘xx’ , 1);

insert into student_table

values(null ,‘yy’ , 1);

insert into student table

values(null ,‘zz’,1)

查询 student_table 表的记录

select * from student table;

回滚事务

rollback;

再次查询

select from student table; # ②

执行上面 SQL 语句中的第①条查询语句将会看到刚刚插入的 3 条记录, 如果打开 MySQL 的其他命令行窗口将看不到这 3 条记录一一这正体现了事务的隔离性。 接着程序 rollback 了事务中的全部修改,执行第②条查询语句时将看到数据库又恢复到事务开始前的状态。

提交, 不管是显式提交还是隐式提交, 都会结束当前事务; 回滚, 不管是显式回滚还是隐式回滚,都会结束当前事务。

除此之外, MySQL 还提供了 savepoint 来设置事务的中间点, 通过使用savepoint 设置事务的中间点可以让事务回滚到指定中间点, 而不是回滚全部事务。 如下 SQL 语句设置了一个中间点:

savepoint a;

一旦设置了中间点后, 就可以使用 rollback 回滚到指定中间点, 回滚到指定中间点的代码如下:

rollback to a;

7.1、JDBC 的事务支持


JDBC 连接也提供了事务支持, JDBC 连接的事务支持由 Connection 提供, Connection 默认打开自动提交, 即关闭事务, 在这种情况下, 每条 SQL 语句一旦执行, 便会立即提交到数据库, 永久生效,无法对其进行回滚操作。

可以调用 Connection 的 setAutoCommit()方法来关闭自动提交, 开启事务, 如下代码所示:

// 关闭自动提交, 开启事务

conn.setAutoCommit(false);

程序中还可调用 Connection 提供的 getAutoCommit()方法来返回该连接的自动提交模式。一旦事务开始之后, 程序可以像平常一样创建 Statement 对象, 创建了 Statement 对象之后, 可以执行任意多条 DML 语句, 如下代码所示:

stmt.executeUpdate( …);

stmt.executeUpdate(…);

stmt.executeUpdate(…);

上面这些 SQL 语句虽然被执行了, 但这些 SQL 语句所做的修改不会生效, 因为事务还没有结束。 如果所有的 SQL 语句都执行成功, 程序可以调用Connection 的 commit()方法来提交事务, 如下代码所示:

// 提交事务

conn.commit();

如果任意一条 SQL 语句执行失败, 则应该用 Connection 的 rollback()方法来回滚事务,如下代码所示:

// 回 滚 事 务

conn.rollback();

下面程序示范了当程序出现未处理的 SQLException 异常时, 系统将自动回滚事务:

import java.sql.*;

import java.io.*;

import java.util.*;

public class TransactionTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void insertInTransaction(String[] sqls) throws Exception

{

// 加载驱动

Class.forName(driver);

try(

Connection conn = DriverManager.getConnection(url , user , pass))

{

// 关闭自动提交,开启事务

conn.setAutoCommit(false);

try(

// 使用Connection来创建一个Statment对象

Statement stmt = conn.createStatement())

{

// 循环多次执行SQL语句

for (String sql : sqls)

{

stmt.executeUpdate(sql);

}

}

// 提交事务

conn.commit();

}

}

public static void main(String[] args) throws Exception

{

TransactionTest tt = new TransactionTest();

tt.initParam(“mysql.ini”);

String[] sqls = new String[]{

“insert into student_table values(null , ‘aaa’ ,1)”,

“insert into student_table values(null , ‘bbb’ ,1)”,

“insert into student_table values(null , ‘ccc’ ,1)”,

// 下面这条SQL语句将会违反外键约束,

// 因为teacher_table中没有ID为5的记录。

“insert into student_table values(null , ‘ccc’ ,5)” //①

};

tt.insertInTransaction(sqls);

}

}

上面程序中的粗体字代码只是开启事务、 提交事务的代码, 并没有回滚事务的代码。 但当程序执行到 第 4 条 SQL 语句( ①处代码) 时, 这条语句将会引起外键约束异常, 该异常没有得到处理, 引起程序非正常结束, 所以事务自动回滚。

Connection 也提供了设置中间点的方法:setSavepoint(),Connection 提供了两个方法来设置中间点。

  • Savepoint setSavepoint(): 在当前事务中创建一个未命名的中间点, 并返回代表该中间点的Savepoint 对象。

  • Savepoint setSavepoint(String name): 在当前事务中创建一个具有指定名称的中间点, 并返回代表该中间点的 Savepoint 对象。

通常来说, 设置中间点时没有太大的必要指定名称, 因为 Connection 回滚到指定中间点时, 并不是根据名字回滚的, 而是根据中间点对象回滚的,Connection 提供了 rollback(Savepoint savepoint)方法回滚到指定中间点。

7.3、Java 8 增强的批量更新


JDBC 还提供了一个批量更新的功能, 使用批量更新时, 多条 SQL 语句将被作为一批操作被同时收集, 并同时提交。

使用批量更新也需要先创建一个 Statement 对象, 然后利用该对象的addBatch()方法将多条 SQL 语句同时收集起来, 最后调用 Java 8 为 Statement 对象新增的 executeLargeBatch()( 或原有的 executeBatch())方法同时执行这些 SQL 语句。 只要批量操作中任何一条 SQL 语句影响的记录条数可能超过Integer.MAX VALUE, 就应该使用 executeLargeBatch()方法, 而不是 executeBatch()方法。

如下代码片段示范了如何执行批量更新:

Statement stmt = conn.createStatement();

// 使用 Statement 同时收集多条 SQL 语句

stmt.addBatch(sqll);

stmt.addBatch(sql2);

stmt.addBatch(sql3);

……

// 同时执行所有的 SQL 语句

stmt.executeLargeBatch();

执行 executeLargeBatch()方法将返回一个 long[]数组, 因为使用 Statement 执行 DDL、 DML 语句都将返回一个 long 值, 而执行多条 DDL、 DML 语句将会返回多个 long 值, 多个 long 值就组成了这个 long[]数组。 如果在批量更新的 addBatchO方法中添加了 select 查询语句, 程序将直接出现错误。

为了让批量操作可以正确地处理错误, 必须把批量执行的操作视为单个事务, 如果批量更新在执行过程中失败, 则让事务回滚到批量操作开始之前的状态。 为了达到这种效果, 程序应该在开始批量操作之前先关闭自动提交, 然后开始收集更新语句, 当批量操作执行结束后, 提交事务, 并恢复之前的自动

提交模式。

如下代码示范了如何使用 JDBC 的批量更新:

import java.sql.*;

import java.io.*;

import java.util.*;

public class BatchTest

{

private String driver;

private String url;

private String user;

private String pass;

public void initParam(String paramFile)throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void insertBatch(String[] sqls) throws Exception

{

// 加载驱动

Class.forName(driver);

try(

Connection conn = DriverManager.getConnection(url , user , pass))

{

// 关闭自动提交,开启事务

conn.setAutoCommit(false);

// 保存当前的自动的提交模式

boolean autoCommit = conn.getAutoCommit();

// 关闭自动提交

conn.setAutoCommit(false);

try(

// 使用Connection来创建一个Statement对象

Statement stmt = conn.createStatement())

{

// 循环多次执行SQL语句

for (String sql : sqls)

{

stmt.addBatch(sql);

}

// 同时提交所有的SQL语句

stmt.executeLargeBatch();

// 提交修改

conn.commit();

// 恢复原有的自动提交模式

conn.setAutoCommit(autoCommit);

}

// 提交事务

conn.commit();

}

}

public static void main(String[] args) throws Exception

{

TransactionTest tt = new TransactionTest();

tt.initParam(“mysql.ini”);

String[] sqls = new String[]{

“insert into student_table values(null , ‘aaa’ ,1)”,

“insert into student_table values(null , ‘bbb’ ,1)”,

“insert into student_table values(null , ‘ccc’ ,1)”,

};

tt.insertInTransaction(sqls);

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Kafka进阶篇知识点

image

Kafka高级篇知识点

image

44个Kafka知识点(基础+进阶+高级)解析如下

image

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
throws Exception

{

// 使用Properties类来加载属性文件

Properties props = new Properties();

props.load(new FileInputStream(paramFile));

driver = props.getProperty(“driver”);

url = props.getProperty(“url”);

user = props.getProperty(“user”);

pass = props.getProperty(“pass”);

}

public void insertBatch(String[] sqls) throws Exception

{

// 加载驱动

Class.forName(driver);

try(

Connection conn = DriverManager.getConnection(url , user , pass))

{

// 关闭自动提交,开启事务

conn.setAutoCommit(false);

// 保存当前的自动的提交模式

boolean autoCommit = conn.getAutoCommit();

// 关闭自动提交

conn.setAutoCommit(false);

try(

// 使用Connection来创建一个Statement对象

Statement stmt = conn.createStatement())

{

// 循环多次执行SQL语句

for (String sql : sqls)

{

stmt.addBatch(sql);

}

// 同时提交所有的SQL语句

stmt.executeLargeBatch();

// 提交修改

conn.commit();

// 恢复原有的自动提交模式

conn.setAutoCommit(autoCommit);

}

// 提交事务

conn.commit();

}

}

public static void main(String[] args) throws Exception

{

TransactionTest tt = new TransactionTest();

tt.initParam(“mysql.ini”);

String[] sqls = new String[]{

“insert into student_table values(null , ‘aaa’ ,1)”,

“insert into student_table values(null , ‘bbb’ ,1)”,

“insert into student_table values(null , ‘ccc’ ,1)”,

};

tt.insertInTransaction(sqls);

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-avRHo06R-1713108488950)]

[外链图片转存中…(img-tMk7vxnC-1713108488950)]

[外链图片转存中…(img-0LZPgM2G-1713108488951)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Kafka进阶篇知识点

[外链图片转存中…(img-clzVyZf6-1713108488951)]

Kafka高级篇知识点

[外链图片转存中…(img-9F2ulOSi-1713108488951)]

44个Kafka知识点(基础+进阶+高级)解析如下

[外链图片转存中…(img-BQ72dD3Y-1713108488951)]

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值