2-29 JDBC_03和连接池和BDUtils
创始人
2024-03-31 14:54:54
0

DAO数据访问对象(Data Access Object)

什么DAO:主要作用就是执行【CRUD(增删改查)】

DAO是一个数据访问接口,数据访问:顾名思义就是与数据库打交道

在核心JavaEE中是这样介绍DAO模式的:为了建立一个健壮的JavaEE的应用程序,应该将所有对数据库的访问抽象的封装在一个公共API中

用程序设计的语言来说,其实就是建议一个接口,接口中定了此应用程序中将会用到所有事务方法

在应用程序中,当需要和数据源进行交互的时候,则直接使用这个接口,并编写一个实现接口类,这个类中将实现接口所有的方法,并且对外提供暴露,外部只需要调用这个类就可以完成对数据库的操作,这个操作就是DAO包中核心

扩展 :DAO包中设计方式

DAO包的设计规范:

DAO包中组件:【DAO接口、DAO实现类、DAO的测试类】

分包的规范:

【以下仅以我开发经验而言】

com.公司的域名.项目名称.dao --> 存储着dao包中接口文件

com.公司的域名.项目名称.dao.impl --> 存储着Dao包中接口的实现类

com.公司的域名.项目名称.dao.domian --> 存放着Dao包中需要使用到实体类【ORM思想】

PS:有的的地方习惯将实体类单独抽取出来一个包entity,含义和domian是一样的

com.公司的域名.项目名称.dao.test --> 存测试文件

PS:在实际项目提交【甲方爸爸】,需要将test全员删除

​ JavaSE是以jar为打包,可以提供对外使用

​ JavaEE是以war为打包,可以提供对外使用

具体包所创建的东西

domain包:描述对象的,一般就是存储实体类 例如:Person.java

dao包: 接口文件【定义操作数据库的方法CRUD】 例如: IPersonDao 或 IPersonDAO

impl包: 这个包就是对dao中接口的具体实现类【CRUD】 例如:IPersonDAOImpl

test包: 测试包对impl包中实现类测试是否可以使用,能否争取完成操作 例如:PersonDAOTest

创建DAO包之后,我们需要使用面向接口编程:

所有触发DAO包的操作都需要使用接口来实现对象创建

例如: IPersonDAO pd = new IPersonDAOImpl();

DAO包的封装

需求:创建一张表Person表

id  : int 主键自增长
name :  varchar(20) 非空
age :  int 非空
bornDate : Date
email : varchar(100)
adrress : varchar(255)

封装DAO

步骤1:需要两个包 ,一个包是存实体类domian 另一包DAO包下所有实现【包含实现类的包】

步骤2:添加实体和定义接口

添加实体类

package com.qfedu.JDBC_03.dao.domain;
//数据库中表的实体映射【ORM】import java.util.Date;public class Person {private int id;private String name;private int age;//虽然这个最终数据会存储到mysql中,但是现阶段编写数据位置 是Java代码,所以这里需要//提供的是java.util包private Date bronDate;private String email;private String address;public Person() {}public Person(int id, String name, int age, Date bronDate, String email, String address) {this.id = id;this.name = name;this.age = age;this.bronDate = bronDate;this.email = email;this.address = address;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Date getBronDate() {return bronDate;}public void setBronDate(Date bronDate) {this.bronDate = bronDate;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}//打印获取表中结果集数据展示,可以重写toString@Overridepublic String toString() {return "Person{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", bronDate=" + bronDate +", email='" + email + '\'' +", address='" + address + '\'' +'}';}
}

编写数据库操作接口【CRUD】

package com.qfedu.JDBC_03.dao;import com.qfedu.JDBC_03.dao.domain.Person;import java.util.List;//提供定义方法【操作数据库中表使用】
public interface IPersonDAO {/*** 数据库插入数据* @param person 数据库表中实体映射* @return   操作表得到返回结果【影响行数】* @throws Exception 处理SQLExeption异常*/int insert(Person person) throws Exception;/*** 数据库更新数据* @param person 数据库表中实体映射* @return   操作表得到返回结果【影响行数】* @throws Exception 处理SQLExeption异常*/int update(Person person) throws Exception;/*** 数据库删除数据* @param id 通过ID删除数据* @return   操作表得到返回结果【影响行数】* @throws Exception 处理SQLExeption异常*///ps:删除方法的设计/*1. 通过某一个条件删除,一般删除条件就是主键列【非空且唯一】2. 可以设置实体类,通过实体类的传递进行删除*/int delete(int id) throws Exception;/***  单条数据的查询* @param id 通过ID查询* @return  返回实体类的对象* @throws Exception 处理SQLExeption异常*/Person select(int id) throws  Exception;/*** 查询多条数据* @return  返回时所有数据的结果集【封装到一个List集合中】* @throws Exception 处理SQLExeption异常*/List selectAll() throws Exception;}

步骤3:编写接口实现类

package com.qfedu.JDBC_03.dao.impl;import com.qfedu.JDBC_03.dao.IPersonDAO;
import com.qfedu.JDBC_03.dao.domain.Person;
import com.qfedu.JDBC_03.util.DBUtil;import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;//接口的具体实现CRUD主要实现者
public class PersonDAOImpl  implements IPersonDAO {@Overridepublic int insert(Person person) throws Exception {//实现插入String sql = "insert into person(name,age,brondate,email,address)values(?,?,?,?,?)";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setString(1,person.getName());preparedStatement.setInt(2,person.getAge());//这个位置的传递先放置,先传递NUll,后续在回来修改preparedStatement.setDate(3,null);preparedStatement.setString(4,person.getEmail());preparedStatement.setString(5,person.getAddress());int i = preparedStatement.executeUpdate();DBUtil.closeAll(connectionInstance,null,preparedStatement,null);return  i;}@Overridepublic int update(Person person) throws Exception {//更新String sql = "update person set name=?,age=?,brondate=?,email=?,address=? where id = ?";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setString(1,person.getName());preparedStatement.setInt(2,person.getAge());//这个位置的传递先放置,先传递NUll,后续在回来修改preparedStatement.setDate(3,null);preparedStatement.setString(4,person.getEmail());preparedStatement.setString(5,person.getAddress());preparedStatement.setInt(6,person.getId());int i = preparedStatement.executeUpdate();DBUtil.closeAll(connectionInstance,null,preparedStatement,null);return  i;}@Overridepublic int delete(int id) throws Exception {//删除:String sql  = "delete from person where id = ?";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setInt(1,id);int i = preparedStatement.executeUpdate();DBUtil.closeAll(connectionInstance,null,preparedStatement,null);return  i;}@Overridepublic Person select(int id) throws Exception {//单数据查询String sql = "select * from person where id = ?";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setInt(1,id);ResultSet resultSet = preparedStatement.executeQuery();Person person = null;if(resultSet.next()){int id1 = resultSet.getInt("id");String name = resultSet.getString("name");int age = resultSet.getInt("age");Date brondate = resultSet.getDate("brondate");String email = resultSet.getString("email");String address = resultSet.getString("address");person = new Person(id1,name,age,brondate,email,address);}return person;}@Overridepublic List selectAll() throws Exception {//全体查询String sql = "select * from person";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);ResultSet resultSet = preparedStatement.executeQuery();List list = new ArrayList<>();while(resultSet.next()){int id1 = resultSet.getInt("id");String name = resultSet.getString("name");int age = resultSet.getInt("age");Date brondate = resultSet.getDate("brondate");String email = resultSet.getString("email");String address = resultSet.getString("address");Person person = new Person(id1,name,age,brondate,email,address);list.add(person);}return list;}
}

步骤4:DAO包下创建一个test包,这个包中做的就是对实现类的测试类

什么是测试:

软件测试: 这是软件生命周期中的一部分.—>好的软件都不是开放出来的,都是测试出来的.黑盒测试: 软件测试工程师.

黑盒测试也称功能测试,它是通过测试来检测每个功能是否都能正常使用。

在测试中,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。

黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。

黑盒测试是以用户的角度,从输入数据与输出数据的对应关系出发进行测试的。

很明显,如果外部特性本身设计有问题或规格说明的规定有误,用黑盒测试方法是发现不了的。

作用:

黑盒测试法注重于测试软件的功能需求,主要试图发现下列几类错误。

功能不正确或遗漏;

界面错误;

输入和输出错误;

数据库访问错误;

性能错误;

初始化和终止错误等。

**白盒测试:**由软件开放工程师来测试,只有自己开放的东西自己才知道是怎么运作的…

又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。

它是按照程序内部的结构测试程序,通过测试来检测产品内部动作是否按照设计规格说明书的规定正常进行,

检验程序中的每条通路是否都能按预定要求正确工作。 这一方法是把测试对象看作一个打开的盒子,测试人员依据程序内部逻辑结构相关信息,设计或选择测试用例,对程序所有逻辑路径进行测试,通过在不同点检查程序的状态,确定实际的状态是否与预期的状态一致。

白盒测试是一种测试用设计方法,盒子指的是被测试的软件,白盒指的是盒子是可视的,你清楚盒子内部的东西以及里面是如何运作的。

"白盒"法全面了解程序内部逻辑结构、对所有逻辑路径进行测试。测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。

单元测试(junit)属于白盒测试.

Java的单元测试

Junit,存在两个版本.

1):junit3.x针对于Java5之前的版本,没有注解,得按照规范来写测试.,Android中使用junit3.x .

2):junit4.x针对于Java5以及之后的版本,使用注解,推荐.

3):junit5.x针对于Java8以及之后的版本,使用注解和4的注解有所区别

使用junit4.x:

junit4.x基于Java5开始的版本,支持注解.

步骤:

1.把junit4.x的测试jar,添加到该项目中来;

2.选择要测试的类,选中类的名字 ctrl+shift+t 创建测试类

请添加图片描述

3.在PersonDAOTest中编写测试方法:如

@Testpublic void testXxx() throws Exception {}注意:方法是public修饰的,无返回的,该方法上必须贴有@Test标签,XXX表示测试的功能名字.

4.选择某一个测试方法,鼠标右键选择 [run as junit],或则选中测试类,表示测试该类中所有的测试方法.

以后单元测试使用最多的方式:

若要在测试方法之前做准备操作:

PersonDAOTest随意定义一个方法并使用@Before标注:

@Before

public void xx() throws Exception方法

若要在测试方法之后做回收操作:

​ PersonDAOTest随意定义一个方法并使用@After标注:

@After

public void xx() throws Exception方法

特点:每次执行测试方法之前都会执行Before方法,每次执行测试方法之后都会执行After方法;

有没有方式之初始化一次,和最终销毁一次呢?

@BeforeClass标签:在所有的Before方法之前执行,只在最初执行一次. 只能修饰静态方法

@AfterClass标签:在所有的After方法之后执行,只在最后执行一次. 只能修饰静态方法

行顺序: BeforeClass->(Before->Test-After多个测试方法)–>AfterClass

使用断言:

一般的,我们使用断言方式来做单元测试.

什么是断言: 期望结果和真实的一样.

期望值:

真实值: 程序运行之后真正的结果.

断言成功: 真实值和期望值相同. 显示绿条.

断言失败: 真实值和期望值不同. 显示红条.

①:Assert.assertEquals(message, expected, actual):比较的值,比较两个对象值存储的数据.

三个参数:

message: 断言失败的提示信息,断言成功不会显示.

expected: 期望值

actual: 真实值

若真实值和期望值想等,则断言成功.

②:Assert.assertSame(message, expected, actual):比较地址,是同一个对象

Assert.assertNotSame(message, expected, actual):断言不是同一个对象

③:Assert.assertTrue(message, condition):断言condition应该为TRUE.

④:Assert.assertFalse(message, condition):断言condition应该为FALSE.

⑤:Assert.assertNull(message, object):断言对象object为null.

⑥:Assert.assertNotNull(message, object):断言对象object不为null.

⑦:@Test(expected=ArithmeticException.class)

期望该方法报错,报错ArithmeticException.

⑧:@Test(timeout=400)

期望该方法在400毫秒之内执行完成.

Ps:若是无返回值的方法,我们一般会类中定一个变量,然后在测试的时候来看变量的值是否符合预期

package com.qfedu.JDBC_03.dao.test;import com.qfedu.JDBC_03.dao.IPersonDAO;
import com.qfedu.JDBC_03.dao.domain.Person;
import com.qfedu.JDBC_03.dao.impl.PersonDAOImpl;
import com.qfedu.JDBC_03.util.DBUtil;
import com.qfedu.JDBC_03.util.DateUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;import java.sql.Connection;
import java.sql.Date;import static org.junit.Assert.*;public class PersonDAOImplTest {@Beforepublic void setUp() throws Exception {}@Afterpublic void tearDown() throws Exception {}@Testpublic void insert() throws Exception {//1.获取当前dao的对象,对数据库进行操作IPersonDAO personDAO = new PersonDAOImpl();Person person =new Person(0,"张三",20, DateUtil.strToUtilDate("2010-10-01"),"zhangsan@qq.com","火星");int insert = personDAO.insert(person);if (insert>0){System.out.println("插入成功");}}@org.junit.Testpublic void update() {}@org.junit.Testpublic void delete() {}@org.junit.Testpublic void select() {}@org.junit.Testpublic void selectAll() {}
}

Junit4和Junit5的注解对比

JUnit4JUnit5说明
@Test@Test该方法表示一个测试方法
@BeforeClass@BeforeAll表示使用了该方法在当前类中所有使用@Test 的方法之前 执行一次
@AfterClass@AfterAll该方法在当前类中所有使用@Test 的方法之后 执行一次
@Before@BeforeEach该方法在当前类中每一个使用了@Test方法之前 执行一次
@After@AfterEach该方法在当前类中每一个使用了@Test方法之后执行一次
@Ignore@Disabled该方法表示不执行(关闭)该测试方法
@Parameters@ParameterizedTest该方法是用于参数化数据的

封装Date工具类

问题:现阶段这个DAO包的封装,在实体类和实现类都需要对是数据日期进行操作

​ 在Java代码中,需要对日期进行操作需要使用java.util包

​ 在Mysql中操作,需要使用包java.sql包

当前两个包中类是不能互相兼容 ,所以我们就需要提供一个工具类,这个工具类的作用就是对日期进行转换

回顾

java.util.Date【日期类】

Java语言中常规应用层面的日期类型,它可以通过字符串进行日期对象操作

ps: 虽然这个类操这个方法过时了,但是使用方式还是最多

SimpleDateFromat【日期进行进行格式化操作】

通过指定的日期格式对日期进行格式化操作,也可以通过和这个格式对日期进行解析

格式化: 日期 --》 字符串

解析 : 字符串 --》 日期

数据库时间类 java.sql.Date

不允许通过字符串创建时间对象,只能通过毫秒值创建对象,可以直接将时间插入到数据库中

PS:Java8中提供一个全新时间类,这个类的实际使用。。。。。。。

时间类简单测试

package com.qfedu.JDBC_03.Date;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateTest {public static void main(String[] args) throws ParseException {//1.获取当前系统时间System.out.println(new Date());//自定义日期String str = "2020-02-02";//通过SimpleDateFormat将字符串 变成  具体的时间【Date】SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//通过SimpleDateFormat对象 可以将 字符串解析为时间【Date】Date parse = sdf.parse(str);System.out.println(parse);//对解析的数据可以再次进行 格式化 将 Date 转换 StringString format = sdf.format(new Date());//以上这些都是Java中所提供,在Java应用程序的层面可以//sql中Date不支持字符串,只支持毫秒java.sql.Date sqlDate = new java.sql.Date(parse.getTime());}
}

封装Date工具类

package com.qfedu.JDBC_03.util;import java.text.SimpleDateFormat;/*** 日期转换工具类   util.Date 和 sql.Date 进行转换*/
public class DateUtil {private  DateUtil(){}private  static  final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//1. 字符串转换为 util.Date对象public static java.util.Date strToUtilDate(String str)throws  Exception{return   sdf.parse(str);}//2.util.Date对象转换为字符串public static String utilDateToStr(java.util.Date date){return  sdf.format(date);}//3.将util.Date 转换为 sql.Datepublic  static java.sql.Date utilDateToSqlDate(java.util.Date date){return  new  java.sql.Date(date.getTime());}}

再次整合DAO包

修改:dao包中impl包下的日期存储

insert方法 和 uptdate方法 需要进行修改 --》 将util.Date 转换为 sql.Date//这个位置的传递先放置,先传递NUll,后续在回来修改,通过当前DateUtil类中提供方法进行转换preparedStatement.setDate(3, DateUtil.utilDateToSqlDate(person.getBronDate()));   
@Testpublic void insert() throws Exception {//1.获取当前dao的对象,对数据库进行操作IPersonDAO personDAO = new PersonDAOImpl();Person person =new Person(0,"张三",20, DateUtil.strToUtilDate("2010-10-01"),"zhangsan@qq.com","火星");int insert = personDAO.insert(person);if (insert>0){System.out.println("插入成功");}}

总结:

在实际的web端凯多时候,一般在后端编写代码时,需要获取用户输入日期,我们就需要对这个日期进行【格式化和解析操作】,以达到对日期的操作,还需要对用户输入的日期存到数据库中,此时还是对日期进行转换,此时就需要提供自己的日期转换工具

PS:在某些情况下,我们可以做简化工作,日期所有的处理都是String和varchar

Service业务逻辑

请添加图片描述

说明:

其实业务就是一个方法(行为/功能),这个方法是对具体功能的实现,这个方法干什么,这个方法主要就是处理数据操作,外部调用这个方法就相当于间接的对数据库进行操作,那么这个过程进行抽象,抽象的得到总体就是Service

Service层级开发【转账】

请添加图片描述

1.先创建一个表account表

cardNo 主键字符串
password 密码 字符串 非空
name  名字  字符串 非空
balance  金额  double 非空
create table account(carNo varchar(20) PRIMARY KEY ,password varchar(20) not null ,name varchar(20) not null ,balance double not null
)

2.向表插入两天测试数据

insert into account(carNo, password, name, balance) value ('1001','1234','zhangsan',10000)
insert into account(carNo, password, name, balance) value ('1002','1234','lisi',0)

编写整个业务开发流程

步骤1:先编写DAO层

1.构建实体类

package com.qfedu.JDBC_03.dao.domain;
//account表的实体类映射
public class Account {private String cardNo;private String password;private String name;private double balance;public Account() {}public Account(String cardNo, String password, String name, double balance) {this.cardNo = cardNo;this.password = password;this.name = name;this.balance = balance;}public String getCardNo() {return cardNo;}public void setCardNo(String cardNo) {this.cardNo = cardNo;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//查看账户信息@Overridepublic String toString() {return "Account{" +"cardNo='" + cardNo + '\'' +", password='" + password + '\'' +", name='" + name + '\'' +", balance=" + balance +'}';}
}

2.构建数据库操作【提供操作接口】

package com.qfedu.JDBC_03.dao;import com.qfedu.JDBC_03.dao.domain.Account;//表操作接口
public interface IAccountDAO {/***  更新数据库表中数据* @param account  数据库表映射* @return  返回受影响行数* @throws Exception 统一处理SQLException*/int update(Account account)throws Exception;/*** 数据库单行数据查询* @param cardNo  卡号* @return   返回查询的结果* @throws Exception 统一处理SQLException*/Account select(String cardNo)throws  Exception;
}

3.实现数据库操作接口

package com.qfedu.JDBC_03.dao.impl;import com.qfedu.JDBC_03.dao.IAccountDAO;
import com.qfedu.JDBC_03.dao.domain.Account;
import com.qfedu.JDBC_03.util.DBUtil;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;//DAO包的实现
public class AccountDaoImpl implements IAccountDAO {@Overridepublic int update(Account account) throws Exception {String sql = "update account set password = ?,name = ?,balance = ? where carNo = ?";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setString(1,account.getPassword());preparedStatement.setString(2,account.getName());preparedStatement.setDouble(3,account.getBalance());preparedStatement.setString(4,account.getCardNo());int i = preparedStatement.executeUpdate();DBUtil.closeAll(connectionInstance,null,preparedStatement,null);return i;}@Overridepublic Account select(String cardNo) throws Exception {String sql = "select * from account where  carNO = ?";Connection connectionInstance = DBUtil.getConnectionInstance();PreparedStatement preparedStatement = connectionInstance.prepareStatement(sql);preparedStatement.setString(1,cardNo);ResultSet set = preparedStatement.executeQuery();Account account  = null;if (set.next()){String cardNo1 = set.getString("carNo");String password = set.getString("password");String name = set.getString("name");double balance = set.getDouble("balance");account = new Account(cardNo1,password,name,balance);}DBUtil.closeAll(connectionInstance,null,preparedStatement,null);return account;}
}

步骤2:编写Service层

1.提供业务处理逻辑的接口【即 这个接口中封装就是处理业务的方法】

package com.qfedu.JDBC_03.service;
//业务逻辑接口
/*
提供业务逻辑法方法,这个方法可以操作dao包中数据库对数据进行操作并反馈给用户
封装接口可以提供一个或多个方法 --》 具体方法个数是根据业务决定的*/
public interface IAccountService {/*** 转账方法* @param fromCardNo  转账卡号* @param pwd         转账卡号的密码* @param toCardNo    到账卡号* @param money       金额* @return   提示是否成功*/String  transfer(String fromCardNo,String pwd,String toCardNo,double money);
}

2.实现这个业务接口

package com.qfedu.JDBC_03.service.impl;import com.qfedu.JDBC_03.dao.IAccountDAO;
import com.qfedu.JDBC_03.dao.domain.Account;
import com.qfedu.JDBC_03.dao.impl.AccountDaoImpl;
import com.qfedu.JDBC_03.service.IAccountService;
//业务的具体实现
public class AccountServiceImpl implements IAccountService {@Overridepublic String transfer(String fromCardNo, String pwd, String toCardNo, double money) throws Exception {// 业务层的核心逻辑就是处理数据层的【DAO层】//1.获取Dao层的对象IAccountDAO accountDAO = new AccountDaoImpl();//判断账户是否和法Account fromAcc = accountDAO.select(fromCardNo);//验证卡号if(fromAcc == null){throw  new RuntimeException("卡号不存在");}//验证密码是正确if ( !fromAcc.getPassword().equals(pwd)){throw  new RuntimeException("密码错误");}//验证余额是否充足if(fromAcc.getBalance() < money){throw  new RuntimeException("余额不足");}//收款账户Account toAcc = accountDAO.select(toCardNo);if(toAcc == null){throw  new  RuntimeException("收款卡号不存");}//修改金额进行转换fromAcc.setBalance(fromAcc.getBalance() - money);accountDAO.update(fromAcc);//写一个异常,模拟程序崩溃int a = 1/0;toAcc.setBalance(toAcc.getBalance()+money);accountDAO.update(toAcc);return "转账成功!";}
}

现阶段和这个代码有一些问题,在业务层处理数据的时候,发现如果出现异常,会出现转出成功,但是收款方是收不到钱,所以为了解决这个问题那么添加事务

事务

事务的原理

数据库会为每一个客户端维护一个独立空间【缓冲区(回滚段)】,一个事务中所有的增删改语句的执行结果都存在这个回滚段,只有当事务中所有SQL,语句都正常结束(commit),才会将回滚段数据同步到数据库中。否则无论因为哪种原因失败,整个事务都会进行(rollback)

事务的特性(ACID)

原子性:表示一个事务内所有的操作都是一个整体,要么都成功,要么都失败

一致性:表示一个事务内有一个操作失败,所有的更改过的数据都必须回滚到之前的状态

隔离性:事务查看数据操作时数据所处的状态,要么是另一个并发事务修改它之前的状态,要么是另一个事务修改它之后的状态不,事务不会查看到中间状态数据

持久性:事务完成之后,它对数据库系统影响是永久性的

在JDBC中事务应用

基于增删改语句的操作结果,可通过程序逻辑手动控制事务的提交和回滚

如何在JDBC中使用事务

1.在JDBC中使用使用事务需要获取到Connection对象,通过和这个对象设置对事务提交的权限

//setAutoCommit 这个方法是一个boolean,通过参数控制可以修改当前事务状态
//true 属于自动提交事务   需要手动干预需要将自动提交关闭,设置为false
connection.setAutoCommit(false)//提交事务
connection.Commit();手动提交
//回滚事务
connetion.rollback();手动回滚    

修改service包中代码AccountServiceImpl,在这个操作数据的业务中添加事务,以保证数据可以正常执行

package com.qfedu.JDBC_03.service.impl;import com.qfedu.JDBC_03.dao.IAccountDAO;
import com.qfedu.JDBC_03.dao.domain.Account;
import com.qfedu.JDBC_03.dao.impl.AccountDaoImpl;
import com.qfedu.JDBC_03.service.IAccountService;
import com.qfedu.JDBC_03.util.DBUtil;import java.sql.Connection;
import java.sql.SQLException;//业务的具体实现
public class AccountServiceImpl implements IAccountService {@Overridepublic String transfer(String fromCardNo, String pwd, String toCardNo, double money) {// 业务层的核心逻辑就是处理数据层的【DAO层】//1.获取Dao层的对象IAccountDAO accountDAO = new AccountDaoImpl();//2.获取一个Connection对象Connection connection = null;try {connection = DBUtil.getConnectionInstance();//开始手动事务,关闭自动提交connection.setAutoCommit(false);//判断账户是否和法Account fromAcc = accountDAO.select(fromCardNo);//验证卡号if (fromAcc == null) {throw new RuntimeException("卡号不存在");}//验证密码是正确if (!fromAcc.getPassword().equals(pwd)) {throw new RuntimeException("密码错误");}//验证余额是否充足if (fromAcc.getBalance() < money) {throw new RuntimeException("余额不足");}//收款账户Account toAcc = accountDAO.select(toCardNo);if (toAcc == null) {throw new RuntimeException("收款卡号不存");}//修改金额进行转换fromAcc.setBalance(fromAcc.getBalance() - money);accountDAO.update(fromAcc);//写一个异常,模拟程序崩溃int a = 1 / 0;toAcc.setBalance(toAcc.getBalance() + money);accountDAO.update(toAcc);// 当代码执行到这个位置的时候,证明没有异常问题,所以可以正常提交//进行手动提交connection.commit();} catch (Exception e) {//一旦出现问题,这个catch语句就会被执行,一旦执行就证明数据需要回顾到之前状态try {connection.rollback();} catch (SQLException ex) {throw new RuntimeException("出现回滚错误,不退钱,拜拜!!!!!");}throw  new RuntimeException("转账失败!!!!");}return  "转账成功";}}

问题:当前在service业务层中添加了事务,但是事务没有生效

原因: 回滚了吗? 回滚了,这个回滚是在service层中执行,这里并没有控制到对数据操作,原因在于service层中开启了事务,当时创建方式使用BDUtil工具类来创建的这个对象,通过查看BDUtil这个类中创建Connection方法

public static Connection getConnectionInstance(){try {return DriverManager.getConnection(p.getProperty("url"),p.getProperty("username"),p.getProperty("password"));} catch (SQLException e) {throw  new  RuntimeException("数据库获取连接对象失败:"+e.getMessage());}}

只要调用一次这个方法,就会产生一个新的 Connection对象,所以Service层中Connection对象和DAO层中Connection对象不是同一个,所以开启的事务无法控制DAO中数据

如何解决Connection不一致问题

方案1:传递Connection对象【不太推荐】

就是在DAO层中对所有操数据库的方法添加一个Connection参数,Service层调用DAO层的时候通过对Connection对象传递,以达到保证是同一个的目的

不这样做的原因:现阶段我们学习的式JDBC所使用的连接对象是Connection,后期我们会学习更高级的框架,在连接就不能用Connection,那么就需要对原码进行修改,如果一个工程中都使用这个中对Connection进行传递,那么整个工程都需要修改

方法2 ThreadLocal【推荐】

这个ThreadLocal可以将整个线程中,存储一个共享值,它的底层实现和HashMap类似,所有人获取的数据都是同一个

此时只需要以还一个位置即可DBUtil中获取Connection对象这个位置,提供一个ThreadLocal进行存和获取操作

修改工具类DBUtil

1.增加一个ThreadLocal对象
private  static ThreadLocal t1 = new ThreadLocal<>();
2.修改当前获取Connection对象的方法,将Connection对象存储到TheadLocal/*** 获取Connection连接对象* @return Connection连接对象*         会抛出异常,连接对象获取失败*/public static Connection getConnectionInstance(){//1.先从线程中获取到Connection对象Connection connection = t1.get();//可以获取线程中存储的值try {if(connection == null) {connection =  DriverManager.getConnection(p.getProperty("url"), p.getProperty("username"), p.getProperty("password"));//创建对象完成之后,将对象存储到线程中t1.set(connection);//将创建好的Connection对象存储线程中}return connection;} catch (SQLException e) {throw  new  RuntimeException("数据库获取连接对象失败:"+e.getMessage());}}

再次测试事务

出现一个问题,在dao包中我们将Connection对象释放了,所以出现了问题,我们再次修DAO包中

先重构DBUtil类:在和这个类中添加删除

修改释放资源的位置try {if (connection != null) {connection.close();t1.remove();//Connection对象关闭之后,删除已经存在ThreadLocal中对象}

在DBUtil类中提供对事务的封装

 //创建事务的封装/*** 事务开启*/public static void begin(){Connection connection = null;try {connection = getConnectionInstance();connection.setAutoCommit(false);} catch (SQLException e) {throw  new  RuntimeException("手动开启提交事务异常");}}//事务提交public  static void commit(){Connection connection = null;try {connection = getConnectionInstance();connection.commit();} catch (SQLException e) {throw  new  RuntimeException("手动提交事务异常");}finally {//在这里释放资源,不应应该在DAO层操作完毕之后就释放closeAll(connection,null,null,null);}}//回滚提交public static void rollback(){Connection connection = null;try {connection = getConnectionInstance();connection.rollback();} catch (SQLException e) {throw new RuntimeException("手动提交事务异常");}finally {//在这里释放资源,不应应该在DAO层操作完毕之后就释放closeAll(connection,null,null,null);}}

最后修改代码

1.将DAO层中所有释放资源语句删除

2.修改Service包中实现类

package com.qfedu.JDBC_03.service.impl;import com.qfedu.JDBC_03.dao.IAccountDAO;
import com.qfedu.JDBC_03.dao.domain.Account;
import com.qfedu.JDBC_03.dao.impl.AccountDaoImpl;
import com.qfedu.JDBC_03.service.IAccountService;
import com.qfedu.JDBC_03.util.DBUtil;import java.sql.Connection;
import java.sql.SQLException;//业务的具体实现
public class AccountServiceImpl implements IAccountService {@Overridepublic String transfer(String fromCardNo, String pwd, String toCardNo, double money) {// 业务层的核心逻辑就是处理数据层的【DAO层】//1.获取Dao层的对象IAccountDAO accountDAO = new AccountDaoImpl();try {DBUtil.begin();//判断账户是否和法Account fromAcc = accountDAO.select(fromCardNo);//验证卡号if (fromAcc == null) {throw new RuntimeException("卡号不存在");}//验证密码是正确if (!fromAcc.getPassword().equals(pwd)) {throw new RuntimeException("密码错误");}//验证余额是否充足if (fromAcc.getBalance() < money) {throw new RuntimeException("余额不足");}//收款账户Account toAcc = accountDAO.select(toCardNo);if (toAcc == null) {throw new RuntimeException("收款卡号不存");}//修改金额进行转换fromAcc.setBalance(fromAcc.getBalance() - money);accountDAO.update(fromAcc);//写一个异常,模拟程序崩溃int a = 1 / 0;toAcc.setBalance(toAcc.getBalance() + money);accountDAO.update(toAcc);// 当代码执行到这个位置的时候,证明没有异常问题,所以可以正常提交//进行手动提交DBUtil.commit();} catch (Exception e) {//一旦出现问题,这个catch语句就会被执行,一旦执行就证明数据需要回顾到之前状态DBUtil.rollback();throw  new RuntimeException("转账失败!!!!");}return  "转账成功";}}

现阶段完整DBUtil

package com.qfedu.JDBC_03.util;import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;/*** JDBC工具类*/
public class DBUtil {//1.工具类不会创建对象,所以将构造方法私有化private  DBUtil(){}//2.创建一个Properties文件对象以便读取文件中的数据private static final Properties p = new Properties();//增加一个ThreadLocal对象private  static ThreadLocal t1 = new ThreadLocal<>();//3.提供一个静态代码块 用来对要使用数据库连接对象进行配置static {//3.1先文件中内容读取出来try {//获取类的类的加载器ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();//通过类的加载器获取classPath路径下文件【db.properties】InputStream resourceAsStream = contextClassLoader.getResourceAsStream("db.properties");p.load(resourceAsStream);//通过字节流加载} catch (IOException e) {throw  new  RuntimeException("加载classPath路径下的db.properties文件出现失败:"+e.getMessage());}//3.2 加载注册驱动try {Class.forName(p.getProperty("driver"));} catch (ClassNotFoundException e) {throw  new  RuntimeException("数据库驱动加载失败:"+e.getMessage());}}/*** 获取Connection连接对象* @return Connection连接对象*         会抛出异常,连接对象获取失败*/public static Connection getConnectionInstance(){//1.先从线程中获取到Connection对象Connection connection = t1.get();//可以获取线程中存储的值try {if(connection == null) {connection =  DriverManager.getConnection(p.getProperty("url"), p.getProperty("username"), p.getProperty("password"));//创建对象完成之后,将对象存储到线程中t1.set(connection);//将创建好的Connection对象存储线程中}return connection;} catch (SQLException e) {throw  new  RuntimeException("数据库获取连接对象失败:"+e.getMessage());}}/*** 释放JDBC连接资源对象* @param connection 连接对象* @param statement  静态SQL语句对象* @param preparedStatement  预编译语句对象* @param resultSet  结果集*/public static void closeAll(Connection connection, Statement statement, PreparedStatement preparedStatement, ResultSet resultSet){//这里的释放原则,只要创建就是释放 反之不执行try {if (connection != null) {connection.close();t1.remove();//Connection对象关闭之后,删除已经存在ThreadLocal中对象}if (statement != null) {statement.close();}if(preparedStatement != null){preparedStatement.close();}if(resultSet != null){resultSet.close();}}catch (SQLException e){throw  new  RuntimeException("释放资源出出现失败:"+e.getMessage());}}//创建事务的封装/*** 事务开启*/public static void begin(){Connection connection = null;try {connection = getConnectionInstance();connection.setAutoCommit(false);} catch (SQLException e) {throw  new  RuntimeException("手动开启提交事务异常");}}//事务提交public  static void commit(){Connection connection = null;try {connection = getConnectionInstance();connection.commit();} catch (SQLException e) {throw  new  RuntimeException("手动提交事务异常");}finally {//在这里释放资源,不应应该在DAO层操作完毕之后就释放closeAll(connection,null,null,null);}}//回滚提交public static void rollback(){Connection connection = null;try {connection = getConnectionInstance();connection.rollback();} catch (SQLException e) {throw new RuntimeException("手动提交事务异常");}finally {//在这里释放资源,不应应该在DAO层操作完毕之后就释放closeAll(connection,null,null,null);}}}

初识三层架构MVC

请添加图片描述

请添加图片描述

请添加图片描述

请添加图片描述

请添加图片描述

总结:
现阶段的框架我们可以分成层

1.View层:放置一些启动类

2.service层 放置业务处理逻辑

3.dao层 放置进行数据的操作的逻辑

PS:将dao保重domain包单独抽取出来,形成一个单独的实例类包

4.entity层:主要方实体类【即和表名同名的类】

5.util层 封装了一些常用工具类

请添加图片描述

连接池

为什么有连接池?

普通的JDBC数据库链接(Connection对象)使用DriverManager来获取,每次向数据库建立连接时候都需要将Connection对象加载到内存中,连接数据库时需要验证用户名和密码(最少需要花费0.5s~1s)–》这个会后才是真正建立好了JDBC连接

需要数据库链接的时候,就向数据库要求一个连接,执行完毕之后就断开。这样的连接会消耗大量资源和时间

数据库的连接资源并没有很好的得到重复利用,若同时有上万人同时频繁对数据库进行连接,服务器端就会出现两的资源浪费问题,严重的服务会瘫痪【宕机】。

对于一次数据库连接,使用完后都会断开,否则,如果程序出现异常而未被关闭,将导致数据库系统中内出现泄漏,最终将导致数据库需要重启

当前使用DriverManager的开发不能控制被创建连接对象的个数,系统就会无休止开辟空间创建对象,如果连接的人过多,而你的服务器内存较少,服务器崩溃

数据库连接池

在Java中,已经提供了连接池的技术在Java的扩展包中【javax】,Java中它提供了一个统一的接口

【javax.sql.DataSource】而这个接口的实现是由各大厂商自身完成

常见DateSource连接的实现

C3P0:Hibernate推荐,但是该连池在07之后就不在提供更新

DBCP:Apache组织【基金会】提供全世界的新技术并且免费,Spring御用连接池

Druid:阿里巴巴的项目【德鲁伊】,是世界上最好的数据库链接池之一

C3P0连接池使用

1.在工程中创建lib文件夹,将C3P0文件夹中lib文件夹中jar包拷贝到工程中,加载jar

代码

package com.qfedu.DataSource.util;import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.DataSources;import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;/*** JDBC工具类*/
public class ConnectionPoolUtil {//1.工具类不会创建对象,所以将构造方法私有化private ConnectionPoolUtil() {}//1.C3P0连接池创对象【XML文件版本】 自动扫描工程中是够由c3p0的配置文件
//    private static ComboPooledDataSource ds = new ComboPooledDataSource();
//
//    public static Connection getConnection() {
//        try {
//            return ds.getConnection();
//        } catch (SQLException e) {
//            throw  new  RuntimeException("数据库连接池获取失败");
//        }
//    }//2.properties文件private  static DataSource ds;static{try {DataSource dataSource = DataSources.unpooledDataSource();ds = DataSources.pooledDataSource(dataSource);} catch (SQLException e) {e.printStackTrace();}}public static Connection getConnection() {try {return ds.getConnection();} catch (SQLException e) {throw  new  RuntimeException("数据库连接池获取失败");}}/*** 释放JDBC连接资源对象** @param connection        连接对象* @param statement         静态SQL语句对象* @param preparedStatement 预编译语句对象* @param resultSet         结果集*/public static void closeAll(Connection connection, Statement statement, PreparedStatement preparedStatement, ResultSet resultSet) {//这里的释放原则,只要创建就是释放 反之不执行try {if (connection != null) {connection.close();}if (statement != null) {statement.close();}if (preparedStatement != null) {preparedStatement.close();}if (resultSet != null) {resultSet.close();}} catch (SQLException e) {throw new RuntimeException("释放资源出出现失败:" + e.getMessage());}}
}package com.qfedu.DataSource.Test;import com.qfedu.DataSource.util.ConnectionPoolUtil;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class C3P0Test {public static void main(String[] args) throws SQLException {Connection connection = ConnectionPoolUtil.getConnection();PreparedStatement preparedStatement = connection.prepareStatement("select * from t_student");ResultSet resultSet = preparedStatement.executeQuery();while(resultSet.next()){int id = resultSet.getInt("id");String name = resultSet.getString("name");String age = resultSet.getString("age");System.out.println(id+name+age);}ConnectionPoolUtil.closeAll(connection,null,preparedStatement,resultSet);}
}

总结:C3P0配置文件是连接池自动是烧苗,需要注意必须放到资源文件夹中,自动扫描这个文件夹寻找c3p0开头的文件并进行加载,这个名字一定要对,可以参考提供的资源文件名称

DBCP和Druid使用

DBCP和Druid是完全相同的两个连接池,除了厂商不一样之外,构造方法略有不同,配置文件都可以共享

ps:后续学习以Druid为主

1.将DBCP和Druid中jar包粘贴到工程lib目录下,并加载这些jar

package com.qfedu.DataSource.util;import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.DataSources;
import org.apache.commons.dbcp.BasicDataSourceFactory;import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;/*** JDBC工具类*/
public class ConnectionPoolUtil {//1.工具类不会创建对象,所以将构造方法私有化private ConnectionPoolUtil() {}//1.C3P0连接池创对象【XML文件版本】 自动扫描工程中是够由c3p0的配置文件
//    private static ComboPooledDataSource ds = new ComboPooledDataSource();
////2.properties文件
//      private  static DataSource ds;
//    static{
//        try {
//            DataSource dataSource = DataSources.unpooledDataSource();
//            ds = DataSources.pooledDataSource(dataSource);
//        } catch (SQLException e) {
//            e.printStackTrace();
//        }
//
//    }//创建DBCP连接池
//    private  static  DataSource dataSource;
//    static{
//        try {
//            //加载properties文件
//            Properties p = new Properties();
//            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//            InputStream resourceAsStream = contextClassLoader.getResourceAsStream("dbcp.properties");
//            p.load(resourceAsStream);
//            //创建连接池对象
//            dataSource = BasicDataSourceFactory.createDataSource(p);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//    }//Druid创建private  static  DataSource dataSource;static{try {//加载properties文件Properties p = new Properties();ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();InputStream resourceAsStream = contextClassLoader.getResourceAsStream("dbcp.properties");p.load(resourceAsStream);//创建连接池对象dataSource = DruidDataSourceFactory.createDataSource(p);} catch (Exception e) {e.printStackTrace();}}public static Connection getConnection() {try {return dataSource.getConnection();} catch (SQLException e) {throw  new  RuntimeException("数据库连接池获取失败");}}/*** 释放JDBC连接资源对象** @param connection        连接对象* @param statement         静态SQL语句对象* @param preparedStatement 预编译语句对象* @param resultSet         结果集*/public static void closeAll(Connection connection, Statement statement, PreparedStatement preparedStatement, ResultSet resultSet) {//这里的释放原则,只要创建就是释放 反之不执行try {if (connection != null) {connection.close();}if (statement != null) {statement.close();}if (preparedStatement != null) {preparedStatement.close();}if (resultSet != null) {resultSet.close();}} catch (SQLException e) {throw new RuntimeException("释放资源出出现失败:" + e.getMessage());}}
}

总结:DBCP和Druid就是一样的,除了创建方法不一样剩余的配置文件和底层实现原码基本上是99%相同

DBUtils工具类

Commons DbUtils 是Apache组织提供的一个对JDBC进行**【简单封装的开源工具类库**】,使用它能够精简化JDBC应用程序的开发!同时,不会影响程序的性能。

DbUtils简介

  • DbUtils是Java编程中数据库操作实用小工具,小巧、简单、实用
    • 对于数据表的查询操作,可以把结果转换为List、Array、Set等集合。便于操作。
    • 对于数据表的DML操作,也变得很简单(只需要写SQL语句)。

PS:DBUtils是典型ORM映射

DbUtils主要包含
  • ResultSetHandler接口:转换类型接口
    • BeanHandler类:实现类,把一条记录转换成对象
    • BeanListHandler类:实现类,把多条记录转换成List集合。
    • ScalarHandler类:实现类,适合获取一行一列的数据。
  • QueryRunner:执行sql语句的类
    • 增、删、改:update();
    • 查询:query();

DBUtils使用

前提需要将驱动jar包添加到工程中,加载jar

package com.qfedu.DataSource;import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;public class DBUtisTest {private  static DataSource dataSource;static {try {//加载properties文件Properties p = new Properties();ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();InputStream resourceAsStream = contextClassLoader.getResourceAsStream("druid.properties");p.load(resourceAsStream);//创建连接池对象dataSource = DruidDataSourceFactory.createDataSource(p);} catch (Exception e) {e.printStackTrace();}}public static DataSource getDataSource(){return dataSource;}public static void main(String[] args) throws SQLException {//1.创建DBUtil对象,参数是连接对象【连接池对象】QueryRunner qr = new QueryRunner(getDataSource());//增删改int update = qr.update("insert into t_student(name,age)values(?,?)", new Object[]{"杨过", 19});if(update>0){System.out.println("插入成功");}//查询单个结果集【必须是ORM映射】Student student = qr.query("select * from t_student where id = ?",new BeanHandler(Student.class), 4);System.out.println(student);//查询全部结果集List list = qr.query("select * from t_student", new BeanListHandler(Student.class));list.forEach(System.out::println);//聚合函数Long query = qr.query("select count(id) from t_student", new ScalarHandler());System.out.println(query);/*如果出现表中字段和类中属性名不一致, 只需要重新定义ResultSetHandler即可*/Student query1 = qr.query("select * from t_student", new ResultSetHandler() {@Overridepublic Student handle(ResultSet resultSet) throws SQLException {Student s = null;while (resultSet.next()) {s = new Student();s.setId(resultSet.getInt("id"));s.setName(resultSet.getString("name"));s.setAge(resultSet.getInt("age"));}return s;}});}}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...