一、JDBC:
java database connectivity-----java数据库连接,java语言操纵数据库
1、概念:
独立于特定的数据库管理系统,通用的SQL数据库存取和操作的公共接口
2、简单理解:
JDBC,是sun公司提供的一套API,使用这套API,可以实现对具体数据库的操作{
获取连接,
关闭连接,
DML
DDL
DCL
}
3、作用:
实现对sql数据库的同一存取和操作
4、JDBC体系结构:
面向应用API;面向数据库API
面向接口编程思想
{
1、JDBC是sun公司定义并提供的一套用于操作所有关系型数据库的(规则)接口,
2、不同的数据库厂商,需要针对这套接口,提供不同实现。不同实现的集合,
即为不同数据库的驱动。即不同的数据库厂商需要提供数据库驱动jar包。
3、java开发者只需要面向接口编程即可。可以使用这套接口(JDBC)编程,
真正执行的代码是驱动jar包中的实现类。
}
5、好处:
程序员:不需要关注具体的数据库的细节
数据库厂商:只需要提供标准的具体实现
6、数据库驱动:
数据库厂商针对于JDBC这套接口,提供的具体实现类的集合
二、针对MySQL特别小芝士:
1、持久化:
把数据保存到可掉电式的存储设备中以供以后使用
2、事务:
1、概念:
一组逻辑操作单元,使数据从一种状态变换到另一种状态;
一个包含多步骤的业务操作。若此业务操作被事务管理,则这多个步骤要么同时成功,要么同时失败。
>一组逻辑单元:一个或多个DML操作(增删改)
2、事务的处理原则:
保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有事务都被提交(commit)那么这些修改就被永久保存下来,要么数据库管理系统将放弃所有操作,整个事务回滚(rollback)到最初状态。
3、数据一旦提交,就不可回滚
哪些操作会导致数据自动提交了:
>DDL操作一旦执行,都会自动提交
>set autocommit = false 的方式对DDL操作失效
>DML操作默认情况下,一旦执行,就会自动提交
>可以通过set autocommit = false 的方式取消DML操作的自动提交
>默认在关闭连接时,会自动提交数据
4、事务的ACID属性:
A--Atomicity原子性:
指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
C--Consistency一致性:
事务必须使得数据库从一个一致性状态转换到另外一个一致性状态
I--Isolation隔离性:
一个事务的执行不能被其他事务干扰,
即一个事务的内部操作即使用的数据对并发的其他事务是隔离的,
并发执行的各个事务之间不能相互干扰
D--Durability持久性:
持久性是指一个事务一旦被提交,它对数据库中的数据的改变是永久的吗,
接下来其他操作和数据库故障不应该对其有任何影响
5、数据库的并发问题:
多个事务访问数据库中的相同数据,若没有采取必要隔离机制,则导致以下三大类并发问题:
脏读:
对于两个事务T1、T2,T1读取了已经被T2更新但还没有被提交的字段。
之后,若T2回滚,T1读取的内容就是临时且无效的
不可重复读:
对于两个事务T1、T2,T1读取了一个字段,然后T2更新了该字段。
之后T1再次读取同一个字段,值就不同了
幻读:
对于两个事务T1、T2,T1从表中读取了一个字段,然后T2在该表中插入了一些新的行。
之后若T1再次读取同一个表,就会多出几行
6、数据库的四种隔离级别:
READ UNCOMMITTED(读未提交):
对三大中并发问题都没解决,在开发中不使用
READ COMMITED(读已提交数据):
仅仅解决脏读问题
REPEATABLE READ(可重复读):
仅解决脏读,不可重复读问题
SERIALIZABLE(串行化):
解决了脏读,不可重复读和幻读问题,一致性很好,
但其代价就是效率极差,在开发中不使用
对于oracle支持:
READ COMMITED(读已提交数据),SERIALIZABLE(串行化)。
其中默认为READ COMMITED(读已提交数据)
对于MySQL支持四种隔离级别,其中默认为REPEATABLE READ(可重复读)
7、 一致性与并发性:一致性越好,并发性越差
3、Bolb类型数据:
MySql-BOLB类型:
MySQL中的BOLB是一个二进制大对象,有四种类型:
TinyBlob--最大255字节
Blob--最大65KB
MediumBlob--最大16MB
LongBlob--最大4GB
当mysql数据库管理系统中对允许插入的blob大对象的packet值有限制时,可以在mysql的my.ini对packet上限制进行修改
如当mediumBlob最大packet默认为1MB,而需要存储的数据超过1MB时,可以找到my.ini,在其中的最后一行添加:
max_allowed_packet=16M 然后MySQL重启服务
4、ORM编程思想:
{
object relational mapping----对象关系映射
一个数据表对应一个javabean,
表中的一条记录(元组)对应javabean中的一个实例对象
表中的一个字段对应javabean中的一个属性,当表中名称和javabean中的名称不一致时,
由SQL查询语句中的起别名方法解决
}
三、使用步骤:
1、步骤:
1.导入java.sql包,将接口和抽象方法引入程序
2.导入厂商具体的jar包,通过厂商jar包将接口和抽象方法实现
3.加载并注册驱动程序:Driver driver=new com.mysql.jdbc.Driver();
4.创建连接对象:Connection对象
5.创建Statement对象,对数据表进行增删改查操作的对象,具体使用PreparedStatement对象
6.执行sql语句
7.使用ResultSet对象,用于查询和接收结果集,至于增删改不用结果集
8.关闭ResultSet对象
9.关闭PreparedStatement对象
10.关闭Connection对象
2、实例:
实例一(初级连接方式)
{
@Test
public void testConnection() throws SQLException {
//获取Driver的实现类对象
Driver driver=new com.mysql.jdbc.Driver();
/*
明确数据库连接
url:"MySQL数据库的url";
协议:jdbc:mysql
ip地址:MySQL数据库的ip地址
端口号:MySQL数据库的端口号
数据库名称:MySQL数据库管理系统中某个数据库的名称
*/
String url="MySQL数据库的url";
//将用户名和密码封装在Properties中
Properties info=new Properties();
info.setProperty("a","数据库用户名称");
info.setProperty("password","数据库密码");
//获取连接
Connection conn = driver.connect(url,info);
System.out.println(conn);
if(conn!=null){
conn.close();
}
}
}
实例二(高级连接方式)
{
jdbc.Properties声明在工程下的src下
jdbc.properties文件中的内容{
a=数据库用户名
password=数据库密码
url=数据库url
driver=数据库驱动
}
@Test
public void testConnection() throws Exception {
/*
将数据库连接的基本信息声明在配置文件中,通过读取配置文件的方式,获取连接
好处:
1.实现数据与代码的分离,实现了解耦
2.如果需要修改配置文件信息,可以避免程序重新打包
*/
//读取配置文件中的信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String a = pros.getProperty("a");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driver = pros.getProperty("driver");
//加载驱动
Class.forName(driver);
//获取连接
Connection conn = DriverManager.getConnection(url, a, password);
System.out.println(conn);
if(conn!=null){
conn.close();
}
}
}
3、步骤即实例中的对象详解:
1. DriverManager:启动管理对象
功能:
1.注册驱动:告诉程序该使用哪一个数据库驱动jar
static void registerDriver(Driver driver):
注册与给定的驱动程序 DriverManager
写代码:
Class.forName("com.mysql.jdbc.Driver");
通过查看源码发现在com.mysql.jdbc.Driver类中存在静态代码块
static{
try{
java.sql.DriverManager.registerDriver(new Driver());
}catch{
throw new RuntimeException("Can't register driver!");
}
}
注意:mysql5之后的驱动jar包可以省略注册驱动的步骤
2.获取数据库连接
方法:
static Connection getConnection(
String url,
String a,
String password
)
参数:
url:指定连接的路径
语法:jdbc:mysql://ip地址(域名):端口号/数据库名称
例子:
"jdbc:mysql://localhost(127.0.0.1):3306/目标数据库",
"用户名称","密码"
细节:
若连接的是本机mysql服务器,
并且mysql服务器默认端口是3306,
则url可以简写为:"jdbc:mysql:///数据库名称
a:用户名
password:密码
2. Connection:数据库连接对象
1. 功能:
1. 获取执行SQL的对象:
PreparedStatement createPreparedStatement();
preparedPreparedStatement preparePreparedStatement(String sql);
2. 管理事务:
开启事务:
setAutoCommit(boolean autoCommit):
调用该方法设置参数为false,即开启事务
提交事务:
commit();
回滚事务:
rollback();
3.Statement:执行SQL的对象
1.执行sql(静态)
1.boolean execute(String sql):可以执行任意的sql
2.int executeUpdate(String sql):
执行DML(insert,update,deleter)语句,DDL(creat,alter,drop)
返回值:
影响的行数,可以通过影响的行数判断DML语句是否执行成功,
返回值大于0,则执行成功,反之则失败
3.ResultSet executeQuery(String sql):执行DQL(select)语句
4.ResultSet:结果集对象,封装查询结果,在数据库中查询语句看到的表就是一个结果集对象
boolean next();
游标(光标)向下移动一行,判断当前行是否是最后一行末尾(是否有数据){
是false;否true
}
getXxx(参数);
获取数据:
Xxx()代表数据类型,
如int <-> getInt(); string <-> getString();
参数:
1.int:代表列的编号,从1开始,如getString(1);
2.String:代表列的名称,如getDouble("列名")
注:使用步骤:1.游标下移,判断数据存在,3取出数据
5. PreparedStatement:执行SQL的对象
1、SQL注入问题:
在拼接sql时,有一些sql的特殊关键字参与字符串的拼接。会造成安全性问题
1. 输入用户随便,输入密码:a' or 'a'='a
2.sql:
SELECT *
FROM 表A
WHERE aname='akjdbfvdjf'
AND password=' a' or 'a'='a '
2、解决sql注入问题:使用PreparedStatement对象解决
3、预编译的sql:参数使用?作为占位符
4、步骤:
1.导入驱动jar包 mysql-connector-java-5.1.25-bin.jar
2.注册驱动
3.获取数据库连接对象Connection
4.定义sql
注:sql的参数使用?作为占位符。如:
SELECT *
FROM 表A
WHERE aname=?
AND password=?;
5.获取执行sql语句的对象 PreparedStatement
Connection.PreparedStatement(String sql);
6.给?赋值
方法:setXxx(参数1,参数2)
参数1:?的位置编号,从1开始
参数2:?的值
7.执行sql,接收返回结果
8.处理结果
9.释放资源
10.后期都会使用PreparedStatement对象来完成增删改查的所有操作
1.可以防止sql注入
2.效率更高
4、关于Statement和PreparedStatement:
使用Statement/PreparedStatement实现CRUD操作
数据库连接被用于向数据库服务器发送命令和SQL语句,并接受数据库服务器返回的结果,其实一个数据库连接就是一个Socket连接
Statement的弊端:
1、需要拼写sql语句,即拼串问题
2、存在SQL注入问题
sql注入指:利用某些系统没有对用户输入的数据进行充分检查,而在用户输入数据中注入非法的SQL语句段或命令{
如:sql注入实例:
SELECT * FROM 表 WHERE 属性1=值1 OR AND属性2=值2 OR 值1=值1;
由实例可知,只要查询条件中有一个恒成立的等式,那么可以将目标表格中的所有信息全部查出
}从而利用系统的SQL引擎完成恶意行为的做法
基于Statement的这两个缺点,才采用将PreparedStatement替代Statement执行对数据库的CRUD操作
PreparedStatement与Statement的异同
1、PreparedStatement继承于Statement
2、开发中用PreparedStatement替换Statement
3、PreparedStatement是预编译sql语句的,可以提高程序运行效率,有效防止sql注入,有效解决Statement的两个问题
4、易于操作blob类型数据
5、使用PreparedStatement进行批量操作优于Statement,PreparedStatement可以实现更高效的批量插入,减少sql的校验次数
小节:
两个接口均是sun公司提供的数据库接口(即JDBC定义的规范),其中PreparedStatement是Statement的子接口,两个接口的作用均是将java程序中写好的sql语句传送到sql数据库服务器中,让服务器执行语句。在实际开发中使用PreparedStatement替换Statement进行对数据库的CRUD操作。因为Statement存在拼串操作繁琐并且存在sql注入问题。使用PreparedStatement可以对数据库中的blob数据类型进行操作,因为其含有预编译操作,而Statement无法对blob数据类型进行操作
四、JDBCUtils:
1、抽取JDBC工具类:JDBCUtils
目的:简化书写
分析:
1.抽取注册驱动
2.抽取一个方法获取连接
需求:不想传递参数(麻烦),还得保证公局类的通用性
解决:配置文件
jdbc.properties{
url=目标数据库的url
a=目标数据库的用户名称
password=密码
}
3.抽取一个方法释放资源
2、实现
{
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static String url;
private static String a;
private static String password;
private static String driver;
/**
* 文件的读取,只需要读取一次即可拿到这些值。使用静态代码块
*/
static{
//读取在源文件,获取值,静态代码块只能访问静态变量
try {
//1.创建Properties集合类
Properties pro=new Properties();
//获取src路径下的文件方式--> ClassLoader类加载器
ClassLoader classLoader = JDBCUtils.class.getClassLoader();
URL res = classLoader.getResource("jdbc.properties");
String path = res.getPath();
//2.加载文件
pro.load(new FileReader(path));
//3.获取属性赋值
JDBCUtils.url =pro.getProperty("url");
a=pro.getProperty("a");
password=pro.getProperty("password");
driver=pro.getProperty("driver");
//注册驱动
Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接
* @return 连接对象
*/
public static Connection getConnection() throws SQLException{
return DriverManager.getConnection(url,a,password);
}
/**
* 释放资源
* @param ps
* @param conn
*/
public static void closeResource(PreparedStatement ps,Connection conn){
if(ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 释放资源
* @param rs
* @param ps
* @param conn
*/
public static void closeResource(
ResultSet rs,
PreparedStatement ps,
Connection conn
){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
五、对Blob类型数据;批量;数据库事务的具体实现
1、使用PreparedStatement操作Blob类型的数据(
对Blob类型变量的操作:
写入:setBlob(InputStream is);
读取:
Blob blob=getBlob(int index);
InputStream is=blob.getBinaryStream();
)
{
建立数据库中目标表格的标准JavaBean
public class Images {
private int id;
private String name;
}
对Bolb数据类型进行操作
import org.junit.Test;
import java.io.*;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class BlobTest {
//查询数据表images中的Blob类型的字段
@Test
public void testQuery(){
Connection conn =null;
PreparedStatement ps =null;
ResultSet rs =null;
InputStream is =null;
FileOutputStream fos =null;
try {
conn = JDBCUtils.getConnection();
String sql="SELECT id,`name`,image FROM images WHERE id=?;";
ps=conn.PreparedStatement(sql);
ps.setObject(1,1);
rs = ps.executeQuery();
if(rs.next()){
int id = rs.getInt(1);
String name = rs.getString(2);
Images images = new Images(id, name);
System.out.println(images);
//将blob类型的图片下载下来,,以文件方式保存在本地
Blob image = rs.getBlob("image");
is = image.getBinaryStream();
fos = new FileOutputStream("全路径\\目标Copy.jpg");
byte[] buffer = new byte[1024];
int len=0;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
}
}catch (Exception e){ e.printStackTrace(); }finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
JDBCUtils.closeResource(conn,ps,rs);
}
}
//向数据表images中插入Blob类型的字段
@Test
public void testInsert(){
Connection conn =null;
PreparedStatement ps =null;
try {
conn = JDBCUtils.getConnection();
String sql="INSERT INTO images(`name`,image)VALUES(?,?);";
ps=conn.PreparedStatement(sql);
ps.setObject(1,"02");
FileInputStream is = new FileInputStream(
new File("全路径\\目标文件.jpg")
);
ps.setBlob(2,is);
ps.execute();
}catch (Exception e){ e.printStackTrace(); }finally {
JDBCUtils.closeResource(conn,ps);
}
}
}
}
2、JDBC控制事务(
1.操作:
1.开启事务;
2.提交事务;
3.回滚事务
2.使用Connection对象来管理事务:
开启事务:setAutoCommit(boolean autoCommit):调用该方法设置参数为false,即开启事务
在执行sql之前开启事务
提交事务:commit();
当所有sql都执行完提交事务
回滚事务:rollback();
在catch中回滚事务
)
{
import org.junit.Test;
import java.lang.reflect.Field;
import java.sql.*;
public class TransactionTeat {
//**********************数据库隔离级别*********************************
@Test
public void testTransactionSelect00(){
Connection conn =null;
try {
conn = JDBCUtils.getConnection();
//获取当前连接的隔离级别
System.out.println(conn.getTransactionIsolation());
//设置数据库的隔离级别
//conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
//取消自动提交数据,开启事务
conn.setAutoCommit(false);
String sql="SELECT id,aname,`password`,birthday FROM `表A` WHERE id=?;";
表A 表A = getInstance(conn, 表A.class, sql, 1);
System.out.println(表A);
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(conn,null);
}
}
@Test
public void testTransactionUpdate00(){
Connection conn =null;
try {
conn = JDBCUtils.getConnection();
//取消自动提交数据,开启事务
conn.setAutoCommit(false);
String sql="UPDATE `表A` SET `password`=? WHERE id=?;";
update01(conn,sql,311,1);
Thread.sleep(15000);//睡眠15秒
System.out.println("修改结束");
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(conn,null);
}
}
//通用的查询操作,用于返回数据表中的一条记录(version--2.0,考虑事务)
public T getInstance(Connection conn,Class clazz,String sql,Object...args){
PreparedStatement ps =null;
ResultSet rs =null;
try {
ps=conn.PreparedStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
//执行获取结果集
rs= ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数
int columnCount = rsmd.getColumnCount();
if(rs.next()){
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的名称:结果集的元数据
String columnLabel = rsmd.getColumnLabel(i + 1);
//通过反射将对象指定名columnName的属性赋值为指定的值columnValue
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
return t;
}
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(null,ps,rs);
}
return null;
}
//******************考虑数据库事务的一个转账操作*******************
@Test
public void testUpdateWithTx00(){
Connection conn =null;
try {
conn=JDBCUtils.getConnection();
System.out.println(conn.getAutoCommit());//true
//取消数据的自动提交,开启事务
conn.setAutoCommit(false);
String sqlOne="UPDATE 表A_table SET balance=balance+100 WHERE aname=?;";
update01(conn,sqlOne,"张三");
//模拟网络异常
//System.out.println(10/0);
String sqlTwo="UPDATE 表A_table SET balance=balance-100 WHERE aname=?;";
update01(conn,sqlTwo,"赵六");
System.out.println("转账成功");
//提交数据,提交事务
conn.commit();
}catch (Exception e){
e.printStackTrace();
//回滚数据,事务的回滚
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}finally {
JDBCUtils.closeResource(conn,null);
}
}
//通用的增删改操作--version2.0(考虑上事务)
public int update01(Connection conn,String sql,Object...args){
PreparedStatement ps =null;
try {
//预编译sql语句,返回PreparedStatement的实例
ps = conn.PreparedStatement(sql);
//填充占位符
/*
SQL当中占位符的个数与可变形参的长度形同
*/
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
/*
ps.execute();
若执行查询操作,有返回结果,则此方法返回的是true;
若执行的是增删改操作,没有返回结果,则此方法返回false
*/
return ps.executeUpdate();
}catch (Exception e){
e.printStackTrace();
}finally {
JDBCUtils.closeResource(null,ps);
}
return 0;
}
//*********未考虑数据库事务的一个转账操作*****************
@Test
public void testUpdate(){
/*
举例理解事务:
针对于数据表表A_table来说
“张三”给“赵六”转账100
UPDATE 表A_table SET balance=balance-100 WHERE aname='张三';
UPDATE 表A_table SET balance=balance+100 WHERE aname='赵六';
*/
String sqlOne="UPDATE 表A_table SET balance=balance-100 WHERE aname=?;";
update00(sqlOne,"张三");
//模拟网络异常
//System.out.println(10/0);
String sqlTwo="UPDATE 表A_table SET balance=balance+100 WHERE aname=?;";
update00(sqlTwo,"赵六");
System.out.println("转账成功");
}
//通用的增删改操作--version1.0
public int update00(String sql,Object...args){
Connection conn =null;
PreparedStatement ps =null;
try {
//获取连接
conn=JDBCUtils.getConnection();
//预编译sql语句,返回PreparedStatement的实例
ps = conn.PreparedStatement(sql);
//填充占位符
/*
SQL当中占位符的个数与可变形参的长度形同
*/
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
/*
ps.execute();
若执行查询操作,有返回结果,则此方法返回的是true;
若执行的是增删改操作,没有返回结果,则此方法返回false
*/
return ps.executeUpdate();
}catch (Exception e){
e.printStackTrace();
}finally {
/*
修改其为自动提交数据
主要针对使用数据库连接池时的使用
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
*/
JDBCUtils.closeResource(conn,ps);
}
return 0;
}
}
}
3、JDBC下的批量操作
{
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
/*
演示使用PreparedPreparedStatement实现批量数据的操作
update、delete本身就具有批量操作的效果
此时的批量操作,主要指定是批量插入,使用PreparedStatement如何实现更高效的批量插入
向goods表中插入25条数据
CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
gname VARCHAR(25)
);
方式一:
使用PreparedStatement
Connection conn JDBCUtils.getConnection();
PreparedStatement st=conn.createPreparedStatement();
for(int i=1;i<25;i++){
String sql="INSERT INTO goods(gname)VALUE('name_"+i+"')"
st.execute(sql)
}
DBServer会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,
所以语句在DBServer的编译器编译后的执行代码被缓存下来,
那么下次调用时只要是相同的编译语句就不需要编译,
只要将参数直接传入编译过的语句执行代码中就会得到执行
使用PreparedPreparedStatement进行批量操作优于PreparedStatement的原因:
在PreparedStatement语句中,即使是相同的操作会因为数据内容不一样,
所以整个语句本身不能匹配,没有缓存语句的意义,
事实是没有数据库会对普通语句编译后的执行代码缓存,
这样每执行一次都要对传入的语句编译一次。每一次都要进行语法检测,
语义检测,翻译成二进制命令,缓存
PreparedStatement与Statement的异同:
两个接口均是sun公司提供的数据库接口(即JDBC定义的规范),
其中PreparedStatement是Statement的子接口,
两个接口的作用均是将java程序中写好的sql语句传送到sql数据库服务器中,
让服务器执行语句。
在实际开发中使用PreparedStatement替换Statement进行对数据库的CRUD操作。
因为Statement存在拼串操作繁琐并且存在sql注入问题。
使用PreparedStatement可以对数据库中的blob数据类型进行操作,
因为其含有预编译操作,而Statement无法对blob数据类型进行操作
*/
public class InsertTest {
//批量插入的方式二,使用PreparedStatement
@Test
public void testInsert00(){
Connection conn =null;
PreparedStatement ps =null;
try {
long start = System.currentTimeMillis();
conn=JDBCUtils.getConnection();
String sql="INSERT INTO goods(gname)VALUE(?);";
ps=conn.PreparedStatement(sql);
for (int i = 0; i < 25; i++) {
ps.setObject(1,"name_"+i);
ps.execute();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间:"+(end-start));//花费的时间:800
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(conn,ps);
}
}
/*
批量插入的方式三:
1.addBatch();executBatch();clearBatch();
2.mysql服务器默认是关闭批处理的,需要通过一个参数,让mysql开启批处理的支持。
?rewriteBatchedPreparedStatements=true 写在配置文件的url后面
3.更新驱动jar包
*/
@Test
public void testInsert01(){
Connection conn =null;
PreparedStatement ps =null;
try {
long start = System.currentTimeMillis();
conn=JDBCUtils.getConnection();
String sql="INSERT INTO goods(gname)VALUE(?);";
ps=conn.PreparedStatement(sql);
for (int i = 0; i <=25; i++) {
ps.setObject(1,"name_"+i);
//1.“攒”sql
ps.addBatch();
if(i%5==0){
//2.执行Batch
ps.executeBatch();
//3.清空Batch
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("花费的时间:"+(end-start));//花费的时间:721
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(conn,ps);
}
}
/*
批量插入的方式四:设置连接不允许自动提交数据
*/
@Test
public void testInsert02(){
Connection conn =null;
PreparedStatement ps =null;
try {
long start = System.currentTimeMillis();
conn=JDBCUtils.getConnection();
//设置不允许自动提交数据
conn.setAutoCommit(false);
String sql="INSERT INTO goods(gname)VALUE(?);";
ps=conn.PreparedStatement(sql);
for (int i = 0; i <=25; i++) {
ps.setObject(1,"name_"+i);
//1.“攒”sql
ps.addBatch();
if(i%5==0){
//2.执行Batch
ps.executeBatch();
//3.清空Batch
ps.clearBatch();
}
}
//提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("花费的时间:"+(end-start));//花费的时间:265
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(conn,ps);
}
}
}
}
六、对数据库的通用操作
1、关于内存泄露
java中的内存泄露指内存中有对象不能够被回收的情况;
C语言的内存泄露指对应指向对象的指针丢失,进而不能将该指针指向的对象进行主动回收
2、具体实现对数据看的通用操作(增删改查)
DAO:data(base)access object--封装了针对于数据表的通用操作
baseDAO的具体实现
{
import JDBCUtils;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
/*
DAO:data(base)access object
封装了针对于数据表的通用操作
java中的内存泄露指内存中有对象不能够被回收的情况
C语言的内存泄露指对应指向对象的指针丢失,进而不能将该指针指向的对象进行主动回收
*/
public abstract class BaseDAO {
private Class clazz=null;
// public BaseDAO() {
//
// }
{
//获取当前baseDAO的子类继承的父类中的泛型
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType paramType=(ParameterizedType)genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments();//获取了父类的泛型参数
clazz= (Class) typeArguments[0];//泛型的第一个参数
}
//通用的增删改操作(考虑上事务)
public int update(Connection conn, String sql, Object...args){
PreparedStatement ps =null;
try {
//预编译sql语句,返回PreparedPreparedStatement的实例
ps = conn.PreparedStatement(sql);
//填充占位符
/*
SQL当中占位符的个数与可变形参的长度形同
*/
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
/*
ps.execute();
若执行查询操作,有返回结果,则此方法返回的是true;
若执行的是增删改操作,没有返回结果,则此方法返回false
*/
return ps.executeUpdate();
}catch (Exception e){
e.printStackTrace();
}finally {
JDBCUtils.closeResource(null,ps);
}
return 0;
}
//通用的查询操作,用于返回数据表中的一条记录(考虑事务)
public T getInstance(Connection conn,String sql,Object...args){
PreparedStatement ps =null;
ResultSet rs =null;
try {
ps=conn.PreparedStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
//执行获取结果集
rs= ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数
int columnCount = rsmd.getColumnCount();
if(rs.next()){
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//通过反射将对象指定名columnName的属性赋值为指定的值columnValue
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
return t;
}
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(null,ps,rs);
}
return null;
}
//通用的查询操作,用于返回数据表中的多条条记录(version--2.0,考虑事务)
public List getForList(Connection conn,String sql, Object...args){
PreparedStatement ps =null;
ResultSet rs =null;
try {
ps=conn.PreparedStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
//执行获取结果集
rs= ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数
int columnCount = rsmd.getColumnCount();
//创建集合对象
ArrayList list = new ArrayList<>();
while (rs.next()){
T t = clazz.newInstance();
//处理结果集一行数据中的每一列:给t对象指定的属性赋值
for (int i = 0; i < columnCount; i++) {
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//通过反射将对象指定名columnName的属性赋值为指定的值columnValue
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
list.add(t);
}
return list;
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(null,ps,rs);
}
return null;
}
//用于查询特殊值的通用方法
public E getValue(Connection conn,String sql,Object...args){
PreparedStatement ps =null;
ResultSet rs =null;
try {
ps = conn.PreparedStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i+1,args[i]);
}
rs=ps.executeQuery();
if(rs.next()){
return (E)rs.getObject(1);
}
}catch (Exception e){e.printStackTrace();}finally {
JDBCUtils.closeResource(null,ps,rs);
}
return null;
}
}
}
针对某一表的DAO接口
{
import 数据库中某一表A类;
import java.sql.Connection;
import java.sql.Date;
import java.util.List;
/*
此接口用于规范针对于表A的常用操作
*/
public interface 表A的DAO接口 {
/**
* 将表A对象添加到数据库当中
* @param conn
* @param a
*/
void insert(Connection conn, 表A a);
/**
* 通过id删除表中的一条记录
* @param conn
* @param id
*/
void deleteById(Connection conn, int id);
/**
* 针对内存中的表A对象,去修改数据表中指定的记录
* @param conn
* @param a
*/
void update(Connection conn, 表A a);
/**
* 针对指定id,查询对应的一个表A对象
* @param conn
* @param id
*/
表A get表AById(Connection conn, int id);
/**
* 查询表中的所有记录构成的集合
* @param conn
* @return
*/
List<表A> getAll(Connection conn);
/**
* 返回数据表中数据的条目数
* @param conn
* @return
*/
Long getCount(Connection conn);
/**
* 返回数据表中最大的生日
* @param conn
* @return
*/
Date getMaxBirth(Connection conn);
}
}
针对某一表的DAO接口的实现
{
import 数据库中某一表A类;
import java.sql.Connection;
import java.sql.Date;
import java.util.List;
public class 表A的DAOImpl extends BaseDAO<表A> implements 表A的DAO接口 {
@Override
public void insert(Connection conn, 表A a) {
String sql="INSERT INTO `表A`(id,aname,password,birthday)VALUES(?,?,?,?);";
update(conn,sql,a.getId(),a.getaname(),a.getPassword(),a.getBirthday());
}
@Override
public void deleteById(Connection conn, int id) {
String sql="DELETE FROM `表A` WHERE id=?;";
update(conn,sql,id);
}
@Override
public void update(Connection conn, 表A a) {
String sql="UPDATE `表A` SET aname=?,`password`=?,birthday=? WHERE id=?;";
update(conn,sql,a.getaname(),a.getPassword(),a.getBirthday(),a.getId());
}
@Override
public 表A get表AById(Connection conn, int id) {
String sql="SELECT id,aname,`password`,birthday FROM `表A` WHERE id=?;";
表A a = getInstance(conn, sql, id);
return a;
}
@Override
public List<表A> getAll(Connection conn) {
String sql="SELECT id,aname,`password`,birthday FROM `表A`;";
List<表A> list = getForList(conn, sql);
return list;
}
@Override
public Long getCount(Connection conn) {
String sql="SELECT COUNT(*) FROM `表A`;";
return getValue(conn,sql);
}
@Override
public Date getMaxBirth(Connection conn) {
String sql="SELECT MAX(birthday) FROM `表A`;";
return getValue(conn,sql);
}
}
}
对通用操作的测试
{
import JDBCUtils;
import 数据库中某一表A类;
import 表A的DAOImpl;
import org.junit.Test;
import java.sql.Connection;
import java.sql.Date;
import java.util.List;
public class 表A的DAOImplTest {
private 表A的DAOImpl dao=new 表A的DAOImpl();
@Test
public void testInsert(){
Connection conn =null;
try {
conn = JDBCUtils.getConnection();
表A a = new 表A(6, "小王", "1133", new Date(73468137632L));
dao.insert(conn,a);
System.out.println("添加成功");
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtils.closeResource(conn,null);
}
}
}
}
3、小节:
1、获取连接:
Connection conn=JDBCUtils.getConnection();
conn.setAutoCommit(false)
2、如下的多个DML操作,作为一个事务出现
操作1:需要使用通用的增删改查操作
操作2:需要使用通用的增删改查操作
操作3:需要使用通用的增删改查操作
conn.commit();
3、若出现异常,则:
conn.rollback();
4、关闭资源:
JDBCUtils.closeResource(,,);
七、数据库连接池、dbutils及Spring JDBC:
1、数据库连接池:
1、概念:
一个容器(集合),存放数据库连接的容器当系统初始化好后,容器被创建,容器会申请一些连接对象,
当用户来访问数据库时,从容器中获取连接对象,用户访问完之后,会将连接对象归还给容器
2、好处:
1、提高程序的响应速度(减少了创建连接响应的时间)
2、节约资源。降低资源的消耗(可以重复使用已进提供好的连接)
3、便于连接的管理
4、用户访问高效
3、实现:
1.标准接口:DataSource javax.sql包下的
1.方法:
获取连接:
getConnection();
归还连接:
Connection.close();若连接对象Connection是从连接池中获取的,
那么调用Connection.close()方法,则不会再关闭连接,而是归还连接
2.一般由数据库厂商实现
1. C3P0:数据库连接池技术
2. Druid:数据库连接池实现技术,由阿里巴巴提供
4、C3P0:数据库连接池技术:
步骤:
1.导入jar包(两个):c3p0-0.9.5.2.jar、mchange-commons-java-0.2.12.jar和数据库的驱动jar包
2.定义配置文件
名称:c3p0-config.xml或者c3p0.properties
路径:直接将文件放在src目录下即可
3.创建核心对象 数据库连接池对象 ComboPooledDataSource
4.获取连接对象:getConnection
2、几个相关配置文件
{
druid.properties{
driverClassName=com.mysql.jdbc.Driver
url=目标数据库的URL
username=目标数据库用户名
password=目标数据库密码
# 初始化连接数量
initialSize=5
# 最大连接数量
maxActive=10
# 最大等待时间
maxWait=3000
}
jdbc.properties{
url=目标数据库的URL
username=目标数据库用户名
password=目标数据库密码
driver=com.mysql.jdbc.Driver
}
}
3、C3P0,德鲁伊的使用
{
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.DbUtils;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.util.Properties;
public class JDBCUtils {
/**
* 使用c3p0的数据库连接池技术
* @return
* @throws SQLException
*/
//1.创建数据库连接池对象,将此对象设置为全局常量,避免每次调用方法时重新创建对象,保证数据库连接池只有一个
private static DataSource ds=new ComboPooledDataSource();
public static Connection getConnection00() throws SQLException {
//2.获取连接对象
Connection conn = ds.getConnection();
return conn;
}
/**
* 使用druid的数据库连接池技术
*/
private static DataSource source00;
static {
try {
Properties pros = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
pros.load(is);
source00 = DruidDataSourceFactory.createDataSource(pros);
}catch (Exception e){e.printStackTrace();}
}
public static Connection getConnection01() throws Exception {
Connection conn = source00.getConnection();
return conn;
}
public static void closeResource(Connection conn, PreparedStatement ps, ResultSet rs){
try {
DbUtils.close(conn);
} catch (SQLException e) {
e.printStackTrace();
}
try {
DbUtils.close(ps);
} catch (SQLException e) {
e.printStackTrace();
}
try {
DbUtils.close(rs);
} catch (SQLException e) {
e.printStackTrace();
}
/**
* 也可以使用dbutils.jar中提供的DbUtils类,实现资源关闭
*
* DbUtils.closeQuietly(conn,ps,rs);
*/
}
}
}
4、commons-dbutils的使用(
commons-dbutils 是Apache组织提供的一个JDBC工具类,封装了针对于数据库增删改查操作
)
{
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.*;
import org.junit.Test;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
/*
commons-dbutils 是Apache组织提供的一个JDBC工具类,封装了针对于数据库增删改查操作
*/
public class QueryRunnerTest {
//测试插入
@Test
public void testInsert00(){
Connection conn =null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getConnection();
String sql = "INSERT INTO `user`(id,username,password,birthday)VALUES(?,?,?,?);";
int insertCount = runner.update(conn, sql, 6, "张三丰", "1133", "1997-09-08");
System.out.println("添加了" + insertCount + "条记录");
}catch (Exception e){e.printStackTrace();}finally {
if(conn!=null) {
try {
conn.close();
}catch (Exception e){e.printStackTrace();}
}
}
}
/*
测试查询
BeanHandler:
是ResultSetHandler接口的实现类,用于封装表中一条记录
BeanListHandler:
是ResultSetHandler接口的实现类,用于封装表中多条记录构成的集合
MapHandler:
是ResultSetHandler接口的实现类,对应表中一条记录,
将字段即相应字段的值作为map中的键key和值value
MapListHandler:
是ResultSetHandler接口的实现类,对应表中多条记录,
将字段即相应字段的值作为map中的键key和值value,构成map集合
ScalarHandler:
是ResultSetHandler接口的实现类,用于执行sql中的特殊查询
*/
@Test
public void testQuery00(){
Connection conn =null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getConnection();
String sql="SELECT id,username,`password`,birthday FROM `user` WHERE id=?;";
BeanHandler handler = new BeanHandler<>(User.class);
User user = runner.query(conn, sql, handler, 2);
System.out.println(user);
}catch (Exception e){e.printStackTrace();}finally {
if(conn!=null) {
try {
conn.close();
}catch (Exception e){e.printStackTrace();}
}
}
}
@Test
public void testQuery01(){
Connection conn =null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getConnection();
String sql="SELECT id,username,`password`,birthday FROM `user` WHERE id;";
BeanListHandler listHandler = new BeanListHandler<>(User.class);
List list = runner.query(conn, sql, listHandler, 5);
for (User user : list) {
System.out.println(user);
}
}catch (Exception e){e.printStackTrace();}finally {
if(conn!=null) {
try {
conn.close();
}catch (Exception e){e.printStackTrace();}
}
}
}
@Test
public void testQuery02(){
Connection conn =null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getConnection();
String sql="SELECT id,username,`password`,birthday FROM `user` WHERE id=?;";
MapHandler handler = new MapHandler();
Map map = runner.query(conn, sql, handler, 5);
System.out.println(map);
}catch (Exception e){e.printStackTrace();}finally {
if(conn!=null) {
try {
conn.close();
}catch (Exception e){e.printStackTrace();}
}
}
}
@Test
public void testQuery03(){
Connection conn =null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getConnection();
String sql="SELECT id,username,`password`,birthday FROM `user` WHERE id;";
MapListHandler listHandler = new MapListHandler();
List
5、Spring JDBC(
Spring框架对JDBC的简单封装,提供了一个JDBCTemplate对象简化JDNC开发
步骤:
1.导入jar包
2.创建JDBCTemplate对象。依赖于数据源dataSource
JdbcTemplate template = new JdbcTemplate(ds);
3.调用JdbcTemplate的方法来完成CRUD(增删查改)的操作
update();执行DML语句。增删改语句
queryForMap();查询结果将结果集封装为map集合,将列名作为key,将值作为value,将这条记录封装为一个map集合
注:查询的结果集只能是1
queryForList();查询结果将结果集封装为list集合
注:将每一条记录封装为一个map集合,再将map集合装载到List集合中
query();查询结果,将结果封装为javaBean对象
注:query的参数:RowMapper
一般使用BeanPropertyRowMapper实现类。可以完成数据到javaBean的自动封装
new BeanPropertyRowMapper<类型>(类型.class)
queryForObject();查询结果,将结果封装为对象
一般用于聚合函数的查询
)
{
利用德鲁伊数据库连接池建立一个JDBCUtils
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.util.Properties;
/**
* Druid连接池工具类
*/
public class JDBCUtils {
//1.定义一个成员变量DataSource
private static DataSource ds ;
static{
try {
//1.加载配置文件
Properties pro = new Properties(); pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
//2.获取DataSource
ds= DruidDataSourceFactory.createDataSource(pro);
}catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接的方法
*/
public static Connection getConnection01() throws SQLException {
return ds.getConnection();
}
/**
* 释放资源
*/
public static void close01(PreparedStatement ps,Connection conn){
close01(null,ps,conn);
}
public static void close01(ResultSet rs,PreparedStatement ps, Connection conn){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();//归还连接池
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 获取连接池方法
*/
public static DataSource getDataSource01(){
return ds;
}
}
JdbcTemplate入门
public class testJdbcTemplate {
public static void main(String[] args) {
//1.导入jar包
//2.创建JDBCTemplate对象
JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource01());
//3.定义sql
String sql="UPDATE user SET password=246 WHERE id=?";
//3.调用方法
int count = template.update(sql, 1);
System.out.println(count);
}
}
Spring JDBC练习
需求:
1.修改1号数据的 password 为100
2.增加一条记录
3.删除一条记录
4.查询id为1的记录,将其封装为Map集合
5.查询所有记录,将其封装为list集合
6.查询所有记录,将其封装为user00对象的集合
7.查询总记录数
{
import org.junit.Test;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
public class testJdbcTemplateOne {
//1.获取JDBCTemplate对象
JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource01());
/**
* 1.修改1号数据的 password 为100
*/
@Test
public void tset01(){
/*
//2.定义sql
String sql="UPDATE user SET password='321' WHERE id=1";
//3.执行sql
int count = template.update(sql);
System.out.println(count);
*/
String sql1="UPDATE user SET password=? WHERE id=?";
int update = template.update(sql1, "123", 1);
System.out.println(update);
}
/**
* 2.增加一条记录
*/
@Test
public void tset02(){
//2.定义sql
String sql="INSERT INTO `user`VALUES(?,?,?)";
int count = template.update(sql, null, "田七", "900");
System.out.println(count);
}
/**
* 3.删除一条记录
*/
@Test
public void tset03(){
//2.定义sql
String sql="DELETE FROM user WHERE id=?";
int count = template.update(sql, 4);
System.out.println(count);
}
/**
* 4.查询id为1的记录,将其封装为Map集合
* 注:查询的结果集只能是1
*/
@Test
public void tset04(){
//2.定义sql
String sql="SELECT * FROM user WHERE id=?";
Map map = template.queryForMap(sql, 1);
System.out.println(map);//{id=1, username=张三, password=321}
}
/**
* 5.查询所有记录,将其封装为list集合
*/
@Test
public void tset05(){
//2.定义sql
String sql="SELECT * FROM user";
List
tip:
1、两种思想:
面向接口编程思想
ORM编程思想
2、两种技术:
反射:
创建对应的运行时类的对象
在运行时,动态调用指定运行时类的属性,方法
结果集的元数据:ResultSetMetaData
>getColumnCount():获取列数
>getColumnLable():获取列的别名
以上是本篇小节,不喜勿喷,感谢理解
相关链接:
【JAVAEE基础学习(1)】--HTML&CSS篇_lixxkv的博客-CSDN博客
【JAVASE(1)】JAVASE学习--基础篇_lixxkv的博客-CSDN博客_javase学习