深入理解SpringJDBC的解决方案
csdh11 2024-11-30 14:14 4 浏览
数据访问
本部分关注高效访问关系型数据的相关实践。我们将系统讨论基于JDBC以及ORM框架实现数据访问的常见开发陷阱及其解决方法,同时,将进一步基于缓存机制分析如何使用它来优化数据访问性能。
通过这一部分的学习,读者将掌握如何系统性地分析和解决关系型数据访问过程中的开发问题,并加深对Spring JDBC、Spring Data JPA等框架的理解。
Spring JDBC解决方案
从本章开始,我们将进入Spring Boot中另一个核心技术体系的讨论,这个技术体系就是数据访问。无论互联网应用还是传统软件,对于任何一个系统而言,基于关系型数据库的存储和访问都是不可缺少的。
然而,数据库交互的过程通常也是应用程序性能的最大瓶颈。针对关系型数据库,Java世界中应用最广泛的就是JDBC规范,本章首先对这个经典规范展开讨论。
然后,将介绍基于Spring JDBC的数据库交互过程。在Spring JDBC中,为开发人员提供了JdbcTemplate这一非常实用的模板工具类,我们会对基于该工具类实现数据查询和插入的过程进行详细介绍,并深入剖析JdbcTemplate背后的实现原理。最后,将研究如何优化Spring JDBC的各项参数和使用方式
JDBC规范
JDBC是Java DataBase Connectivity的简称,它的设计初衷是提供一套能够应用于各种数据库的统一标准。不同的数据库厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。作为统一标准,JDBC规范具有完整的架构,如图8-1所示。
从图8-1中可以看到,Java应用程序通过JDBC所提供的API进行数据访问,而这些API中包含了开发人员所需要掌握的各个核心编程对象,包括DriverManger、DataSource、Connection、Statement以及ResultSet。使用这些JDBC API进行数据访问的示例代码如代码清单8-1所示。
代码清单8-1 使用JDBC API进行数据访问的示例代码
String query = "SELECT COUNT(*) FROM USER";
try{
Connection conn = dataSource.getConnection();
Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)
if (resultSet.next()) {
int count = resultSet.getInt(1);
System.out.println("count : " + count);
}
}catch(SQLException e){
...
}
在上述代码中,我们看到JDBC API的异常检查机制迫使开发人员处理错误,这增加了应用程序中使用JDBC的代码复杂性。
同时,必须手工关闭数据库连接,如果开发人员忘记关闭连接,就会导致资源泄漏。事实上,上述代码中只有处理ResultSet部分的内容需要开发人员根据具体的业务对象进行定制化处理,而打开连接、执行SQL、关闭连接和执行异常处理部分的代码对于每次SQL操作而言都是重复的。Spring Boot针对使用JDBC过程中的复杂性和重复性的问题提供了Spring JDBC这套解决方案。
Spring JDBC解决方案
在Spring Boot中,JdbcTemplate模板工具类是我们基于JDBC规范实现数据访问的强大工具,它对常见的CRUD操作做了封装并提供了一大批简化的API。本节我们将分别针对查询和插入这两大类数据操作给出基于JdbcTemplate的实现方案。特别是针对插入场景,我们还将引入SimpleJdbcInsert工具类来简化这一操作,而SimpleJdbcInsert也是构建在JdbcTemplate基础之上的。
Spring JDBC工具类概览
针对8.1节给出的基于JDBC规范进行数据操作的示例代码,如果使用Spring JDBC,那么两行代码就可以完成同样的工作,如代码清单8-2所示。
代码清单8-2 使用Spring JDBC进行数据访问的示例代码
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM USER",
Integer.class);
可以看到,这里引入了一个Spring JDBC提供的JdbcTemplate模板工具类,而Jdbc-Template只是Spring JDBC众多工具类中的一个。
正如我们在前面的示例中看到的,Spring通过使用JDBC工具类简化了处理数据库访问的过程。JDBC工具类在内部使用JDBC API,但能够自动释放数据库连接以清理系统资源,并将JDBC的SQLException转换为RuntimeException,从而提供更好的错误检测机制。Spring JDBC工具类提供了多种直接编写SQL查询的方法,帮助我们移除了重复性代码,让开发人员只需要关注SQL查询动作本身。图8-2展示了Spring JDBC工具类在整个数据访问过程中所处的位置。
在Spring JDBC中,除了前面介绍的JdbcTemplate之外,还存在一大批实用的工具类,包括NamedParameterJdbcTemplate、SimpleJdbcTemplate、SimpleJdbcInsert和SimpleJdbcCall等。
JdbcTemplate应用
JdbcTemplate是Spring JDBC中最核心的一个工具类,本小节我们结合一个常见的应用场景来演示JdbcTemplate的具体使用方法。
设计一个系统的用户模块时,通常会涉及对用户权限的管理需求。通常,一个用户拥有一个账户(Account),而一个账户包含了多个权限(Authority)。反过来,一个权限可以属于多个不同的账户。这里的Account和Authority实体类定义如代码清单8-3所示。
代码清单8-3 Account和Authority类定义代码
public class Account {
private Long id;
private String accountNumber;
private String accountName;
private List<Authority> authorities;
}
public class Authority {
private Long id;
private String authorityCode;
private String authorityName;
private String description;
}
在设计关系型数据库表时,一般做法是构建一个中间表来保存Account和Authority之间的一对多关系。所以,在创建数据库时,我们会有三张表——account、authority以及中间表account_authority,其中account_authority表保存着account表和authority表中对应主键的映射关系。这三张表的SQL脚本如代码清单8-4所示。
代码清单8-4 Account和Authority相关表定义脚本
DROP TABLE IF EXISTS `account`;
DROP TABLE IF EXISTS `authority`;
DROP TABLE IF EXISTS `account_authority`;
create table `account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_number` varchar(50) not null,
`account_name` varchar(100) not null,
`create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
create table `authority` ( `id` bigint(20) NOT NULL AUTO_INCREMENT,
`authority_code` varchar(50) not null,
`authority_name` varchar(50) not null,
`description` varchar(100) not null,
`create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
create table `account_authority` (
`account_id` bigint(20) not null,
`authority_id` bigint(20) not null,
foreign key(`account_id`) references `account`(`id`),
foreign key(`authority_id`) references `authority`(`id`)
);
1. 原生JDBC实现方案
在讨论JdbcTemplate之前,为了做对比,我们首先给出基于原生JDBC的实现方案。因为原生JDBC的使用方式不是本书的重点,所以我们只提供getAccountById()方法的实现过程,如代码清单8-5所示。
代码清单8-5 getAccountById()方法的原生JDBC实现代码
public class AccountRawJdbcRepository {
@Autowired
private DataSource dataSource;
@Override
public Account getAccountById(Long accountId) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
statement = connection.prepareStatement("select id,
account_number, account_name from `account` where id=?");
statement.setLong(1, accountId);
resultSet = statement.executeQuery(); Account account = null;
if (resultSet.next()) {
account = new Account(resultSet.getLong("id"),
resultSet.getString("account_number"),
resultSet.getString("account_name"));
}
return account;
} catch (SQLException e) {
System.out.print(e);
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
}
}
}
return null;
}
}
可以看到,上述代码使用JDBC原生DataSource、Connection、PreparedStatement、ResultSet等核心编程对象完成了针对account表的一次查询。代码功能比较简单,但流程显然比较烦琐。
2. 基于JdbcTemplate实现查询
回顾完原生JDBC的使用方法,接下来就引出本节的重点——JdbcTemplate模板工具类。我们创建一个AccountJdbcRepository类,并实现如代码清单8-6所示的getAccountById()方法。
代码清单8-6 getAccountById()方法的JdbcTemplate实现代码
public Account getAccountById(Long accountId) {
Account account = jdbcTemplate.queryForObject("select id,
account_number, account_name from `account` where id=?",
this::mapRowToAccount, accountId);
return account;
}
可以看到,这里使用了JdbcTemplate的queryForObject()方法来执行查询操作,该方法传入目标SQL、参数以及一个RowMapper对象。其中RowMapper的作用就是将来自数据库中的数据映射成领域对象。在这个示例中,mapRowToAccount()方法的实现过程如代码清单8-7所示。
代码清单8-7 RowMapper使用示例代码
private Account mapRowToAccount(ResultSet rs, int rowNum) throws
SQLException {
return new Account(rs.getLong("id"),
rs.getString("account_number"), rs.getString("account_name"));
}
讲到这里,你可能注意到getAccountById()方法实际上只是获取了Account对象中的账户部分信息,并不包含用户权限数据。接下来,我们再来设计一个getAuthorities-ByAccount()方法,用于根据账户编号获取用户账户以及账户对应的权限信息,如代码清单8-8所示。
代码清单8-8 getAuthoritiesByAccount()方法实现代码
public Account getAuthoritiesByAccount(String accountNumber) {
//获取Account基础信息
Account account = jdbcTemplate.queryForObject("select id,
account_number, account_name from `account` where account_number=?",
this::mapRowToAccount, accountNumber);
if (account == null) {
return account;
}
//获取Account与Authority之间的关联关系,找到Account中的所有AuthorityId
Long accountId = account.getId();
List<Long> authorityIds = jdbcTemplate.query("select account_id,
authority_id from account_authority where account_id=?",
newResultSetExtractor<List<Long>>() {
public List<Long> extractData(ResultSet rs) throws
SQLException, DataAccessException {
List<Long> list = new ArrayList<Long>();
while (rs.next()) {
list.add(rs.getLong("authority_id"));
}
return list;
}
}, accountId);
//根据AuthorityId分别获取Authority信息并填充到Account对象中
for (Long authorityId : authorityIds) {
Authority authority = getAuthorityById(authorityId);
account.addAuthority(authority);
}
return account;
}
上述代码有点复杂,可以分成几个部分来讲解。首先,我们获取Account基础信息,并通过Account中的ID编号从中间表中获取所有Authority的ID列表。然后遍历这个ID列表,再分别获取Authority信息。最后将Authority信息填充到Account中,从而构建一个完整的Account对象。这里通过ID获取Authority数据的实现方法也与getAccountById()方法的实现过程一样,如代码清单8-9所示。
代码清单8-9 getAccountById()方法实现代码
private Authority getAuthorityById(Long authorityId) {
return jdbcTemplate.queryForObject("select id, authority_code,
authority_name, description from authority where id=?",
this::mapRowToAuthority, authorityId);
}
private Authority mapRowToAuthority(ResultSet rs, int rowNum) throws
SQLException {
return new Authority(rs.getLong("id"),
rs.getString("authority_code"), rs.getString("authority_name"),
rs.getString("description"));
}
3. 基于JdbcTemplate实现插入
在JdbcTemplate中,可以通过update()方法来实现数据的插入和更新。
针对Account和Authority中的关联关系,插入一个Account对象需要同时完成两张表的更新,即account表和account_authority表,所以插入Account的实现过程也是分成两个阶段,如代码清单8-10所示的addAccount()方法展示了这一过程。
代码清单8-10 addAccount()方法实现代码
private Account addAccount(Account account) {
//插入Account基础信息
Long accountId = saveAccount(account);
account.setId(accountId); //插入Account与Authority的关联关系
List<Authority> authorityList = account.getAuthorities();
for (Authority authority : authorityList) {
saveAuthoritiesToAccount(authority, accountId);
}
return account;
}
可以看到,这里同样先是插入Account的基础信息,然后再遍历Account中的Authority列表并逐条进行插入。其中的saveAccount()方法如代码清单8-11所示。
代码清单8-11 saveAccount()方法实现代码
private Long saveAccount(Account account) {
PreparedStatementCreator psc = new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection
con) throws SQLException {
PreparedStatement ps = con.prepareStatement("insert into
`account` (account_number, account_name) values (?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(1, account.getAccountNumber());
ps.setString(2, account.getAccountName());
return ps;
}
};
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(psc, keyHolder);
return keyHolder.getKey().longValue();
}
上述saveAccount()方法比想象中的要复杂,主要原因在于需要在插入account表的同时返回数据库中生成的自增主键值。因此,这里使用了PreparedStatementCreator这个工具类来封装PreparedStatement对象的构建过程,并在PreparedStatement的创建过程中设置了Statement.RETURN_GENERATED_KEYS属性用于返回自增主键。然后,我们同样构建了一个GeneratedKeyHolder对象用于保存所返回的自增主键。这是使用JdbcTemplate实现带有自增主键数据插入的一种标准做法,你可以参考这一做法并应用到日常开发过程中。
至于用于插入Account与Authority关联关系的saveAuthorityToAccount()方法就比较简单了,直接调用JdbcTemplate的update()方法向account_authority表中插入数据即可,如代码清单8-12所示。
代码清单8-12 saveAuthorityToAccount()方法实现代码
private void saveAuthorityToAccount(Authority authority, long
accountId) {
jdbcTemplate.update("insert into account_authority (account_id,
authority_id) " + "values (?, ?)", accountId, authority.getId());
}
SimpleJdbcInsert应用
通过JdbcTemplate的update()方法可以完成数据的正确插入,但我们发现这个实现过程还是比较复杂的,尤其是涉及自增主键处理的部分,代码显得有点臃肿。那么,有没有更加简单的实现方法呢?
答案是肯定的,Spring JDBC针对数据插入场景专门提供了一个SimpleJdbcInsert工具类。SimpleJdbcInsert本质上是在JdbcTemplate的基础上添加了一层封装,提供了一组execute()、executeAndReturnKey()以及executeBatch()重载方法来简化数据插入操作。通常,可以基于JdbcTemplate对SimpleJdbcInsert进行初始化,如代码清单8-13所示。
代码清单8-13 初始化SimpleJdbcInsert代码
SimpleJdbcInsert accountInserter = new
SimpleJdbcInsert(jdbcTemplate).withTableName("account").usingGenerated
KeyColumns("id");
SimpleJdbcInsert accountAuthorityInserter = new
SimpleJdbcInsert(jdbcTemplate).withTableName("account_authority");
可以看到,这里基于JdbcTemplate并针对account表和account_authority表分别初始化了两个SimpleJdbcInsert对象accountInserter和accountAuthorityInserter。其中accountInserter中还使用了usingGeneratedKeyColumns()方法来设置自增主键列。
基于SimpleJdbcInsert,完成Account对象的插入就显得非常简单了,实现方式如代码清单8-14所示。
代码清单8-14 基于SimpleJdbcInsert的saveAccount()代码
private Long saveAccountWithSimpleJdbcInsert(Account account) {
Map<String, Object> values = new HashMap<String, Object>();
values.put("account_number", account.getAccountNumber());
values.put("account_name", account.getAccountName());
Long accountId =
accountInserter.executeAndReturnKey(values).longValue();
return accountId;
}
我们构建一个Map对象,然后把需要添加的字段设置成一组键值对。通过Simple-JdbcInsert的executeAndReturnKey()方法就可以在插入数据的同时直接返回自增主键。同样,完成account_authority表的操作也只需要几行代码,如代码清单8-15所示。
代码清单8-15 基于SimpleJdbcInsert的saveAuthorityToAccount()代码
private void saveAuthorityToAccountWithSimpleJdbcInsert(Authority
authority, long accountId) {
Map<String, Object> values = new HashMap<>();
values.put("account_id", accountId);
values.put("authority_id", authority.getId());
accountAuthorityInserter.execute(values);
}
这里用到了SimpleJdbcInsert提供的execute()方法。我们可以把这些方法组合起来对原有基于JdbcTemplate的addAccount()方法进行重构,从而得到如代码清单8-16所示的新的addAccount()方法。
代码清单8-16 基于SimpleJdbcInsert的addAccount ()方法代码
private Account addAccount(Account account) {
//插入Account基础信息
Long accountId = saveAccountWithSimpleJdbcInsert(account);
account.setId(accountId);
//插入Account与Authority的关联关系
List<Authority> authorityList = account.getAuthorities();
for (Authority authority : authorityList) {
saveAuthorityToAccountWithSimpleJdbcInsert(authority,
accountId);
} return account;
}
可以看到,整个代码执行流程并没有任何改变,但具体的执行过程已经采用了更加高效的实现方式。
Spring JDBC案例分析
事实上,前面几小节展示的内容已经构成了一个完整的案例。我们可以通过Jdbc-Template完成对数据的查询和插入,以及使用SimpleJdbcInsert来简化数据插入过程。完整的案例源码可以参考:
https://github.com/tianminzheng/spring-boot
examples/tree/main/SpringJdbcExample。
请注意,要想在应用程序中使用JdbcTemplate,首先需要引入对它的依赖,如代码清单8-17所示。
代码清单8-17 spring-boot-starter-jdbc Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
同时,不要忘了在Spring Boot应用程序的配置文件中添加数据源配置,如代码清单8-18所示。
代码清单8-18 数据源配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/account
username: root
password: root
在后面讨论Spring Data JPA时,我们还将基于这个案例对目前的实现方案进行重构。
本文给大家讲解的内容是springboot数据访问:Spring JDBC解决方案
- 下文给大家讲解的是springboot数据访问: JdbcTemplate实现原理
相关推荐
- Micheal Nielsen's神经网络学习之二
-
依然是跟着MichaelNielsen的神经网络学习,基于前一篇的学习,已经大概明白了神经网络的基本结构和BP算法,也能通过神经网络训练数字识别功能,之后我试验了一下使用神经网络训练之前的文本分类,...
- CocoaPods + XCTest进行单元测试 c单元测试工具
-
在使用XCTest进行单元测试时,我们经常会遇到一些CocoaPods中的开源框架的调用,比如“Realm”或“Alamofire”在测试的时候,如果配置不当,会导致“frameworknotfo...
- Java基础知识回顾第四篇 java基础讲解
-
1、&和&&的区别作为逻辑运算符:&(不管左边是什么,右边都参与运算),&&(如果左边为false,右边则不参与运算,短路)另外&可作为位运算符...
- 项目中的流程及类似业务的设计模式总结
-
说到业务流程,可能是我做过的项目中涉及业务最多的一个方面了。除了在流程设计之外,在一些考核系统、产业审批、还有很多地方,都用到相似的设计思路,在此一并总结一下。再说到模式,并不是因为流行才用这个词,而...
- 联想三款显示器首批获得 Eyesafe Certified 2.0 认证
-
IT之家7月31日消息,据外媒报道,三款全新联想显示器是全球首批满足EyesafeCertified2.0的设备。据报道,联想获得EyesafeCertified2.0认证的显...
- maven的生命周期,插件介绍(二) 一个典型的maven构建生命周期
-
1.maven生命周期一个完整的项目构建过程通常包括清理、编译、测试、打包、集成测试、验证、部署等步骤,Maven从中抽取了一套完善的、易扩展的生命周期。Maven的生命周期是抽象的,其中的具体任务都...
- 多线程(3)-基于Object的线程等待与唤醒
-
概述在使用synchronized进行线程同步中介绍了依赖对象锁定线程,本篇文章介绍如何依赖对象协调线程。同synchronized悲观锁一样,线程本身不能等待与唤醒,也是需要对象才能完成等待与唤醒的...
- jquery mobile + 百度地图 + phonegap 写的一个"校园助手"的app
-
1jquerymobile+百度地图+phonegap写的一个"校园助手"的app,使用的是基于Flat-UI的jQueryMobile,请参考:https://github.com/...
- Apache 服务启动不了 apache系统服务启动不了
-
{我是新手,从未遇到此问题,请各位大大勿喷}事由:今天早上上班突然发现公司网站出现问题。经过排查,发现是Apache出现问题。首先检查配置文件没有出问题后,启动服务发现Apache服务能启动,但是没法...
- 健康债和技术债都不能欠 公众号: 我是攻城师(woshigcs)
-
在Solr4.4之后,Solr提供了SolrCloud分布式集群的模式,它带来的主要好处是:(1)大数据量下更高的性能(2)更好扩展性(3)更高的可靠性(4)更简单易用什么时候应该使用Sol...
- Eye Experience怎么用?HTC告诉你 eyebeam怎么用
-
IT之家(www.ithome.com):EyeExperience怎么用?HTC告诉你HTC上周除了发布HTCDesireEYE自拍机和HTCRE管状运动相机之外,还发布了一系列新的智能手机...
- Android系统应用隐藏和应用禁止卸载
-
1、应用隐藏与禁用Android设置中的应用管理器提供了一个功能,就是【应用停用】功能,这是针对某些系统应用的。当应用停用之后,应用的图标会被隐藏,但apk还是存在,不会删除,核心接口就是Packag...
- 计算机软件技术分享--赠人玫瑰,手遗余香
-
一、Netty介绍Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty...
- Gecco爬虫框架的线程和队列模型 爬虫通用框架
-
简述爬虫在抓取一个页面后一般有两个任务,一个是解析页面内容,一个是将需要继续抓取的url放入队列继续抓取。因此,当爬取的网页很多的情况下,待抓取url的管理也是爬虫框架需要解决的问题。本文主要说的是g...
- 一点感悟(一) 初识 初读感知的意思
-
时间过得很快,在IT业已从业了两年多。人这一辈子到底需要什么,在路边看着人来人往,大部分人脸上都是很匆忙。上海真是一个魔都,它有魅力,有底蕴,但是一个外地人在这里扎根置业,真的是举全家之力,还贷3...
- 一周热门
-
-
Boston Dynamics Founder to Attend the 2024 T-EDGE Conference
-
IDC机房服务器托管可提供的服务
-
详解PostgreSQL 如何获取当前日期时间
-
新版腾讯QQ更新Windows 9.9.7、Mac 6.9.25、Linux 3.2.5版本
-
一文看懂mysql时间函数now()、current_timestamp() 和sysdate()
-
流星蝴蝶剑:76邵氏精华版,强化了流星,消失了蝴蝶
-
PhotoShop通道
-
查看 CAD文件,电脑上又没装AutoCAD?这款CAD快速看图工具能帮你
-
WildBit Viewer 6.13 快速的图像查看器,具有幻灯片播放和编辑功能
-
光与灯具的专业术语 你知多少?
-
- 最近发表
-
- Micheal Nielsen's神经网络学习之二
- CocoaPods + XCTest进行单元测试 c单元测试工具
- Java基础知识回顾第四篇 java基础讲解
- 项目中的流程及类似业务的设计模式总结
- 联想三款显示器首批获得 Eyesafe Certified 2.0 认证
- maven的生命周期,插件介绍(二) 一个典型的maven构建生命周期
- 多线程(3)-基于Object的线程等待与唤醒
- jquery mobile + 百度地图 + phonegap 写的一个"校园助手"的app
- Apache 服务启动不了 apache系统服务启动不了
- 健康债和技术债都不能欠 公众号: 我是攻城师(woshigcs)
- 标签列表
-
- serv-u 破解版 (19)
- huaweiupdateextractor (27)
- thinkphp6下载 (25)
- mysql 时间索引 (31)
- mydisktest_v298 (34)
- sql 日期比较 (26)
- document.appendchild (35)
- 头像打包下载 (61)
- oppoa5专用解锁工具包 (23)
- acmecadconverter_8.52绿色版 (39)
- oracle timestamp比较大小 (28)
- f12019破解 (20)
- np++ (18)
- 魔兽模型 (18)
- java面试宝典2019pdf (17)
- beamoff下载 (17)
- unity shader入门精要pdf (22)
- word文档批量处理大师破解版 (36)
- pk10牛牛 (22)
- server2016安装密钥 (33)
- mysql 昨天的日期 (37)
- 加密与解密第四版pdf (30)
- pcm文件下载 (23)
- jemeter官网 (31)
- iteye (18)