JDBC及开源连接池Druid和C3P0的使用
1.JDBC
前面我们讲解了如何创建我们的第一个JDBC程序,接下来就详细的介绍我们的第一个JDBC程序
JDBC概述:JDBC是一种用于执行sql语句的java API,可以为多种关系数据库提供统一访问,它由一组java语言编写的类和接口组成,JDBC提供了一种基准,据此1就可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
什么是数据库驱动
两个设备(应用)之间通信的桥梁
为什么要学习JDBC
没有JDBC的时候,如果现在要开发一套系统,使用java连接MySQL数据库,那么这时候java程序员需要了解MySQL驱动API,如果使用java连接Oracle数据库,那么这个时候java程序源需要了解Oracle数据库驱动API,后来,SUN公司就提供了一套统一的规范(接口),然后各个数据库生产商提供这套接口的实现,这套接口规范就是JDBC的规范了
JDBC的环境部署(以下JDBC操作的数据库都是这个数据库)
创建数据库:
create database my;
创建表:
-- 用户表
CREATE TABLE user(
nid int not null auto_increment PRIMARY KEY,
username varchar(20),
password varchar(20),
role varchar(20)
);
-- 银行账户表
create table account(
nid int not null auto_increment PRIMARY key,
name varchar(20),
money int
);
添加数据:
//往用户表添加数据
insert into student(username, `password`, role) VALUES("luyi", "123", "student"), ("luer","321", "teacher")
//往银行账户表添加数据
insert into account(name, money) values("luyi", 1000), ("luer", 1000)
JDBC的使用步骤
1.加载驱动
2.获得连接
3.基本操作(增删改查)
4.释放资源
代码示例:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class JDBCDemo {
public static void main(String[] args) throws Exception {
//加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获得连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my?serverTimezone=UTC", "root", "123");
//获得执行sql的对象
Statement statement = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql语句
ResultSet rs = statement.executeQuery(sql);
//遍及结果集
while(rs.next()){
System.out.println(rs.getInt("id") + " ");
System.out.println(rs.getString("username") + " ");
System.out.println(rs.getString("password") + " ");
System.out.println(rs.getString("role") + " ");
}
//释放资源
rs.close();
statement.close();
conn.close();
}
}
2.JDBC的API详解
DriverManager:驱动管理类
- registerDriver(Driver driver):该方法可以完成驱动的注册,但是实际开发中一般不会使用这个方法完成驱动的注册,因为通过查看源代码我们可以知道,在代码中有一段静态代码块,静态代码块已经调用了注册驱动的方法,所以如果我们手动调用该方法注册驱动,就会导致驱动被注册两次,实际开发中我们使用Class.forName("com.mysql.cj.jdbc.Driver"),通过加载这个类,然后执行静态代码块的代码,注册了驱动
- getConnection(url, user, password):这个方法就是用来获得与数据库连接的,这个方法的三个参数,返回值是一个Connection实现类对象:
- url:与数据库连接的路径:格式为jdbc:mysql://localhost:3306/mydb,如果连接的是本机,则可简写为jdbc:mysql:///mydb
- jdbc:连接数据库的协议
- mysql:是jdbc的子协议,我们用的是mysql数据
- localhost:连接的MySQL数据库服务区的主机地址(连接本机的数据所以写出localhost,如果不是本机的数据库,就需要写上连接主机的ip地址)
- 3306:MySQL数据库服务器的端口号
- mydb:数据库名称
- user:与数据库连接的用户名
- password:与数据库连接的密码
- url:与数据库连接的路径:格式为jdbc:mysql://localhost:3306/mydb,如果连接的是本机,则可简写为jdbc:mysql:///mydb
Connection:与数据库连接的对象,是一个接口
Connection的实现类对象可以干嘛呢?
1.创建执行sql语句的对象:
- createStatement():创建一个Statement对象,用于将sql语句发送到数据库
- prepareCall(String sql):创建一个CallableStatement对象来调用数据库存储过程
- prepareStatement(String sql):创建一个PreparedStatment对象来将参数化的sql语句发送到数据库,用于对sql进行预处理,解决sql注入漏洞
2.管理事务
- setAutoCommit(boolean autoCommit):用于设置是否自动提交事务
- commit():用于提交事务
- rollback():事务回滚的方法
Statement:用于执行静态sql语句并返回它所生成结果的对象,一个接口
Statement的实现类对象可以干嘛呢?
1.执行sql:
- boolean execute(String sql):执行查询,修改,添加,删除的sql语句,不经常使用
- ResultSet executeQuery(String sql):执行查询语句,返回结果集ResultSet
- int executeUpdate(String sql):执行修改,添加,删除的sql语句
2.执行批处理
- void addBatch(String sql):将给定的sql命令添加到此Statement对象的当前命令列表中(添加批处理)
- void clearBatch():清空此Statement对象的当前sql命令列表(删除批处理)
- int[] executeBatch():将命令提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组(执行批处理)
ResultSet(结果集):表示的是数据库结果集的数据表,通常通过执行查询数据库的语句生成,是一个接口
1.结果集的遍历和获取:
- next():将光标从当前位置向前移动一行
- getXxx(int columnIndex):根据第几列第几列来获取数据
- getXxx(String columnName):根据列名来获取数据
JDBC资源释放
概述:JDBC程序执行结束后,将与数据库进行交互的对象释放掉,通常是ResultSet,Statement,Connection,这几个对象中尤其是Connection对象是非常稀有的,这个对象一定要做到尽量晚创建,尽早释放掉
标准的释放代码应该写入finally代码中,而且需要手动将这几个对象置为空,以便及早被垃圾回收器发现,回收
标准的释放资源代码如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JDBCDemo {
public static void main(String[] args) {
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
try{
//加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获得连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my?serverTimezone=UTC", "root", "123");
//获得执行sql的对象
statement = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql语句
rs = statement.executeQuery(sql);
//遍及结果集
while(rs.next()){
System.out.println(rs.getInt("id") + " ");
System.out.println(rs.getString("username") + " ");
System.out.println(rs.getString("password") + " ");
System.out.println(rs.getString("role") + " ");
}
}catch(Exception e){
e.printStackTrace();
}finally{
//标准的释放资源
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
statement = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
conn = null;
}
if(rs != null){
try {
rs.close();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
rs = null;
}
}
}
}
3.JDBC的CRUD操作
代码示例(对数据库进行增删改查):
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JDBCDemo {
public static void main(String[] args) {
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
try{
//加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获得连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my?serverTimezone=UTC", "root", "123");
//获得执行sql的对象
statement = conn.createStatement();
//编写sql语句
String sql1 = "insert into user(username, password, role) value('卢一', '123', 'doctor')";//添加操作
//String sql2 = "delete from user where username = 'luyi'";//删除操作
//String sql3 = "update user set password = '666' where username = 'luer'";//修改操作
String sql4 = "select * from user";//查询操作
//增删改操作对应返回影响的行数
int column = statement.executeUpdate(sql1);
if(column > 0){
System.out.println("操作成功");
}
/*//查询操作对应返回结果集
* //执行sql语句
rs = statement.executeQuery(sql4);
//遍及结果集
while(rs.next()){
System.out.print(rs.getInt("id") + " ");
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}*/
}catch(Exception e){
e.printStackTrace();
}finally{
//标准的释放资源
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
statement = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
conn = null;
}
if(rs != null){
try {
rs.close();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
rs = null;
}
}
}
}
4.对JDBC的工具类的抽取以及提取配置信息到配置文件
从上面对JDBC的增删改查操作可以看出
1.工具类:
没有对象封装数据体现,只有方法,禁止了对象创建
2.配置文件:
- 属性文件
- 格式:扩展名是.properties
- 内容:key=value:键值对形式出现
- XML文件
示例代码:
//db.properties属性配置文件
driverClassName = com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/my?serverTimezone=UTC
username=root
password=123
//JDBCUtils.java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/*
* JDBC的工具类
*/
public class JDBCUtils {
/*
* 注册驱动的方法
*/
private static final String driverClassName;
private static final String url;
private static final String username;
private static final String password;
static{
//获取属性文件中的内容
Properties properties = new Properties();
try {
properties.load(new FileInputStream("src/db.properties"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
driverClassName = properties.getProperty("driverClassName");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
}
public static void loadDriver(){
try {
Class.forName(driverClassName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/*
* 获得连接的方法
*/
public static Connection getConnection(){
Connection conn = null;
try {
conn = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/*
* 释放资源的方法
*/
public static void release(Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
stmt = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
}
}
public static void release(ResultSet rs, Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
stmt = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
}
if(rs != null){
try {
rs.close();
} catch (SQLException e1) {
e1.printStackTrace();
}
rs = null;
}
}
}
//JDBCUtilsText
/*
* JDBC工具类的测试类
*/
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
public class JDBCUtilsText {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//获得连接
conn = JDBCUtils.getConnection();
//sql操作
stmt = conn.createStatement();
String sql = "select * from user";
rs = stmt.executeQuery(sql);
//遍历结果集
while(rs.next()){
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}
}catch(Exception e){
e.printStackTrace();
}finally{
JDBCUtils.release(rs, stmt, conn);
}
}
}
5.JDBC的SQL注入及解决方式
sql注入的概述:SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库
sql注入攻击实例:
String sql = "select * from user_table where username=
' "+userName+" ' and password=' "+password+" '";
--当输入了上面的用户名和密码,上面的SQL语句变成:
SELECT * FROM user_table WHERE username=
'’or 1 = 1 -- and password='’
"""
--分析SQL语句:
--条件后面username=”or 1=1 用户名等于 ” 或1=1 那么这个条件一定会成功;
--然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用,这样语句永远都--能正确执行,用户轻易骗过系统,获取合法身份。
--这还是比较温柔的,如果是执行
SELECT * FROM user_table WHERE
username='' ;DROP DATABASE (DB Name) --' and password=''
--其后果可想而知…
"""
JDBC的SQL注入漏洞解决
需要采用PreparedStatement对象解决SQL注入漏洞,这个对象将SQL预先进行编译1,使用?作为占位符,?所代表的内容就是SQL所固定的,再次传入变量(包含SQL的关键字),这个时候也不会识别这些关键字
使用PrepareStatement对象进行增删改查:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class JDBCUtilsText {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//获得连接
conn = JDBCUtils.getConnection();
//预编译sql操作
//添加操作
/*String sql1 = "insert into user(username, password, role) value(?, ?, ?)";
pstmt = conn.prepareStatement(sql1);
pstmt.setString(1, "赵露思");
pstmt.setString(2, "888");
pstmt.setString(3, "actor");*/
//删除操作
/*String sql2 = "delete from user where role = ?";
pstmt.setString(1, "doctor");*/
//修改操作
String sql3 = "update user set password = ? where username = ?";
pstmt = conn.prepareStatement(sql3);
pstmt.setString(1, "000000");
pstmt.setString(2, "赵露思");
int column = pstmt.executeUpdate();
if(column > 0){
System.out.println("操作成功");
}
//查询操作
/*String sql4 = "select * from user where username = ? and password = ?";
pstmt = conn.prepareStatement(sql4);
pstmt.setString(1, "luer");
pstmt.setString(2, "666");
rs = pstmt.executeQuery();
if(rs.next()){
System.out.println(rs.getString("username"));
System.out.println(rs.getString("password"));
}*/
}catch(Exception e){
e.printStackTrace();
}finally{
JDBCUtils.release(rs, pstmt, conn);
}
}
}
6.JDBC的批处理
批量插入数据
import java.sql.Connection;
import java.sql.PreparedStatement;
public class JDBCDemo2 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
try{
JDBCUtils.loadDriver();
conn = JDBCUtils.getConnection();
String sql = "insert into user(username, password, role) values(?, '123', 'student')";
pstmt = conn.prepareStatement(sql);
for(int i = 1; i <= 10000; i ++){
pstmt.setString(1, "luyi" + i);
//添加到批处理
pstmt.addBatch();
//每添加1000条就执行一次批处理并释放
if(i % 1000 == 0){
//执行批处理
pstmt.executeBatch();
//清空批处理
pstmt.clearBatch();
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
JDBCUtils.release(pstmt, conn);
}
}
}
//注意:db.properiter属性文件中url需要传递一个参数表示允许批处理,修改后如下:
url=jdbc:mysql://localhost:3306/my?serverTimezone=UTC&rewriteBatchedStatements=true
7.JDBC的事务管理
事务的概念:事务是指逻辑上的一组操作,组成这组操作的各个逻辑单元要么全部成功,要么全部失败
JDBC的事务管理案例(银行转账案例):
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AffairDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//获得连接
conn = JDBCUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
//编写sql语句,用luyi账户给luer账户转账
String sql = "update account set money = money + ? where name = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, -100);
pstmt.setString(2, "luyi");
//执行sql
pstmt.executeUpdate();
//使其报错
int i = 1/0;
//给luer账号加100
pstmt.setInt(1, 100);
pstmt.setString(2, "luer");
pstmt.executeUpdate();
//提交事务
conn.commit();
}catch(Exception e){
//事务回滚
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JDBCUtils.release(pstmt, conn);
}
}
}
//如果没有添加事务,程序里面的这个报错就会使得luyi转账没了100,但是luer却没有收到这100块
8.连接池
概述:连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用;连接对象创建和销毁是需要耗费时间的,在服务器初始化的时候就初始化一些连接,把这些连接放入到内存中,使用的时候可以从内存中获取,使用完成之后将连接放入连接池中,从内存中获取和归还的效率要远远高于创建和销毁的效率
自定义连接池
概述:通过实现DataSource接口实现自定义连接池
代码示例:
//MyDataSource.java
package cn.luyi.demo1;
/*
* 自定义连接池
*/
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class MyDataSource implements DataSource {
//将一些连接存入到内存中,可以定义一个集合,用于存储连接对象,这个集合就相当于连接池
private List<Connection> connList = new ArrayList<Connection>();
//在初始化的时候提供一些连接,添加连接到内存中
public MyDataSource(){
//初始化连接
for(int i = 1; i <= 3; i ++){
connList.add(JDBCUtils.getConnection());
}
}
//从连接池中获得连接的方法
@Override
public Connection getConnection() throws SQLException {
Connection conn = connList.remove(0);
return conn;
}
/*
* 将连接归还到连接池的方法
*/
public void addBack(Connection conn){
connList.add(conn);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
// TODO Auto-generated method stub
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
}
//MyDataSourceText.java
package cn.luyi.demo1;
/*
* 连接池测试类
*/
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class MyDataSourceText {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
MyDataSource mdb = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//从连接池获取连接
mdb = new MyDataSource();
conn = mdb.getConnection();
stmt = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql,获取结果集
rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}
}catch(Exception e){
e.printStackTrace();
}finally{
//释放资源
if(rs != null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
rs = null;
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
stmt = null;
//归还连接到连接池
mdb.addBack(conn);
}
}
}
以上是我们对自定义连接池的基本实现,但是我们的代码存在两点不合理的地方:
- 使用接口的实现类完成构造,而不是接口来完成构造
- 额外提供了连接归还连接池的方法,也正是因为有这个额外的方法,我们无法用接口来完成构造
那么,我们怎么样才能不提供额外的方法,还能实现连接归还连接池呢?
回想之前我们释放Connection时调用的是close方法,那我们是否有办法把close完成的释放资源的功能改为归还连接的功能呢?
答案是肯定的,把原有的close方法的逻辑改为归还连接,也就是增强一个类中的方法,我们有三种方法:
1.采用继承的方式,继承这种方法最简单,但是是有使用条件的:必须能够控制这个类的构造,也就是可以new这个类创建对象
2.采用装饰者模式:使用条件
- 增强的类和被增强的类实现相同的接口
- 在增强的类中获得被增强的类的引用
3.采用动态代理模式
采用装饰者模式实现把原有的close方法的逻辑改为归还连接代码示例:
//ConnectionImpl.java
package cn.luyi.demo1;
/*
* 模板类
*/
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
public class ConnectionImpl implements Connection{
private Connection conn;
public ConnectionImpl(Connection conn){
super();
this.conn = conn;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return conn.unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return conn.isWrapperFor(iface);
}
@Override
public Statement createStatement() throws SQLException {
return conn.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return conn.prepareStatement(sql);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return conn.prepareCall(sql);
}
@Override
public String nativeSQL(String sql) throws SQLException {
return nativeSQL(sql);
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
}
@Override
public boolean getAutoCommit() throws SQLException {
return conn.getAutoCommit();
}
@Override
public void commit() throws SQLException {
conn.commit();
}
@Override
public void rollback() throws SQLException {
conn.rollback();
}
@Override
public void close() throws SQLException {
conn.close();
}
@Override
public boolean isClosed() throws SQLException {
return conn.isClosed();
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return conn.getMetaData();
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
conn.setReadOnly(readOnly);
}
@Override
public boolean isReadOnly() throws SQLException {
return conn.isReadOnly();
}
@Override
public void setCatalog(String catalog) throws SQLException {
conn.setCatalog(catalog);
}
@Override
public String getCatalog() throws SQLException {
return conn.getCatalog();
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
conn.setTransactionIsolation(level);
}
@Override
public int getTransactionIsolation() throws SQLException {
return conn.getTransactionIsolation();
}
@Override
public SQLWarning getWarnings() throws SQLException {
return conn.getWarnings();
}
@Override
public void clearWarnings() throws SQLException {
conn.clearWarnings();
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return conn.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
throws SQLException {
return conn.prepareStatement(sql, resultSetType, resultSetConcurrency);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return conn.prepareCall(sql, resultSetType, resultSetConcurrency);
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return conn.getTypeMap();
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
conn.setTypeMap(map);
}
@Override
public void setHoldability(int holdability) throws SQLException {
conn.setHoldability(holdability);
}
@Override
public int getHoldability() throws SQLException {
return conn.getHoldability();
}
@Override
public Savepoint setSavepoint() throws SQLException {
return conn.setSavepoint();
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return conn.setSavepoint();
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
conn.rollback();
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
conn.releaseSavepoint(savepoint);
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
return conn.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
return conn.prepareStatement(sql, resultSetType, resultSetConcurrency);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
return conn.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return conn.prepareStatement(sql, autoGeneratedKeys);
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return conn.prepareStatement(sql, columnIndexes);
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return conn.prepareStatement(sql, columnNames);
}
@Override
public Clob createClob() throws SQLException {
return conn.createClob();
}
@Override
public Blob createBlob() throws SQLException {
return conn.createBlob();
}
@Override
public NClob createNClob() throws SQLException {
return conn.createNClob();
}
@Override
public SQLXML createSQLXML() throws SQLException {
return conn.createSQLXML();
}
@Override
public boolean isValid(int timeout) throws SQLException {
return conn.isValid(timeout);
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
conn.setClientInfo(name, value);
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
conn.setClientInfo(properties);
}
@Override
public String getClientInfo(String name) throws SQLException {
return getClientInfo(name);
}
@Override
public Properties getClientInfo() throws SQLException {
return conn.getClientInfo();
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return conn.createArrayOf(typeName, elements);
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return conn.createStruct(typeName, attributes);
}
@Override
public void setSchema(String schema) throws SQLException {
conn.setSchema(schema);
}
@Override
public String getSchema() throws SQLException {
return conn.getSchema();
}
@Override
public void abort(Executor executor) throws SQLException {
conn.abort(executor);
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
conn.setNetworkTimeout(executor, milliseconds);
}
@Override
public int getNetworkTimeout() throws SQLException {
return conn.getNetworkTimeout();
}
}
//MyConnectionWrapper.java
package cn.luyi.demo1;
/*
* 装饰者模式增强类中的close方法
*/
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
public class MyConnectionWrapper extends ConnectionImpl {
private Connection conn;
private List<Connection> connList;
public MyConnectionWrapper(Connection conn, List<Connection> connList) {
super(conn);
this.conn = conn;
this.connList = connList;
}
//增强了close方法:将释放资源功能改为归还连接功能
@Override
public void close() throws SQLException {
connList.add(conn);
}
}
//MyDataSource.java
package cn.luyi.demo1;
/*
* 自定义连接池
*/
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class MyDataSource implements DataSource {
//将一些连接存入到内存中,可以定义一个集合,用于存储连接对象,这个集合就相当于连接池
private List<Connection> connList = new ArrayList<Connection>();
//在初始化的时候提供一些连接,添加连接到内存中
public MyDataSource(){
//初始化连接
for(int i = 1; i <= 3; i ++){
connList.add(JDBCUtils.getConnection());
}
}
//从连接池中获得连接的方法
@Override
public Connection getConnection() throws SQLException {
Connection conn = connList.remove(0);
//使用装饰者模式把close重写为归还连接的方法
MyConnectionWrapper connWrapper = new MyConnectionWrapper(conn, connList);
return connWrapper;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
// TODO Auto-generated method stub
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
}
//MyDataSourceText.java
package cn.luyi.demo1;
/*
* 连接池测试类
*/
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
public class MyDataSourceText {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
DataSource mdb = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//从连接池获取连接
mdb = new MyDataSource();
conn = mdb.getConnection();
stmt = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql,获取结果集
rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}
}catch(Exception e){
e.printStackTrace();
}finally{
//释放资源
JDBCUtils.release(rs, stmt, conn);
}
}
}
开源连接池Druid
Druid概述:Druid是阿里旗下的开源连接池产品,使用非常简单,可以与spring框架进行快速整合,在很多的开源网站上都可以找到它,下载它的jar包就可以使用了
Druid的使用(两种方式):
//db.properties
driverClassName = com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/my?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=123
//DruidDemo.java
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
public class DruidDemo {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//使用Druid连接池获取Connection对象
DruidDataSource dataSource = new DruidDataSource();
/*//方式一:手动设置数据库连接的参数
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/my?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("123");
conn = dataSource.getConnection();*/
//方式二:从配置文件里读取
Properties properties = new Properties();
properties.load(new FileInputStream("src/db.properties"));
DataSource datasource = DruidDataSourceFactory.createDataSource(properties);
conn = datasource.getConnection();
stmt = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql,获取结果集
rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}
}catch(Exception e){
e.printStackTrace();
}finally{
//释放资源
JDBCUtils.release(rs, stmt, conn);
}
开源连接池C3P0
概述:C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate、Spring等,下载它的jar包就可以使用了,需要注意的是C3P0版本0.9.1之前是只有一个包的,而版本0.9.2之后就被分离了一个mchange-commons-java包出来,所以使用版本0.9.2以上就需要导入两个包
C3P0的连接池使用:
//c3p0-config.xml
<?xml version="1.0" encoding="utf-8"?>
<c3p0-config>
<!-- 默认配置 -->
<default-config>
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/my?serverTimezone=UTC</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
<!-- 设置其他的配置 -->
<named-config name="otherc3p0">
</named-config>
</c3p0-config>
//C3P0Demo.java
package cn.luyi.demo1;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class C3P0Demo {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
//加载驱动
JDBCUtils.loadDriver();
//使用C3P0连接池获取Connection对象
/*//方式一:手动设置参数
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/my?serverTimezone=UTC");
dataSource.setUser("root");
dataSource.setPassword("123");
conn = dataSource.getConnection();*/
//方式二:使用配置文件
ComboPooledDataSource dataSource = new ComboPooledDataSource();
conn = dataSource.getConnection();
stmt = conn.createStatement();
//编写sql语句
String sql = "select * from user";
//执行sql,获取结果集
rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.print(rs.getString("username") + " ");
System.out.print(rs.getString("password") + " ");
System.out.println(rs.getString("role"));
}
}catch(Exception e){
e.printStackTrace();
}finally{
//释放资源
JDBCUtils.release(rs, stmt, conn);
}
}
}
连接池版的工具类
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class JDBCUtils2 {
//创建一个连接池,但是这个连接池只需要创建一次即可
private static final ComboPooledDataSource dataSource = new ComboPooledDataSource();
/*
* 获得连接的方法
*/
public static Connection getConnection() throws SQLException{
return dataSource.getConnection();
}
/*
* 获得连接池
*/
public static DataSource getDataSource(){
return dataSource;
}
/*
* 释放资源的方法
*/
public static void release(Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
stmt = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
}
}
public static void release(ResultSet rs, Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
stmt = null;
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
}
if(rs != null){
try {
rs.close();
} catch (SQLException e1) {
e1.printStackTrace();
}
rs = null;
}
}
}
9.DBUtils
概述:Commons DbUtils是Apache组织提供的一个对JDBC进行简单封装的开源工具类库,使用它能够简化JDBC应用程序的开发,同时也不会影响程序的性能,下载它的jar包即可使用
为什么要学习DBUtils?
因为JDBC手写比较麻烦,而且有非常多的代码是类似的,比如获得连接,预编译SQL,释放资源登,那么可以将这些代码抽取出来放到工具类中,大大简化JDBC的编程
DBUtils核心类QueryRunner
有两套常用方法:
1.在一般情况下如果执行CRUD的操作:
- QueryRunner(DataSource ds):带参构造方法
- int update(String sql, Object...args):增删改操作
- T query(String sql, ResultSetHandler rsh, Object...args):查询操作
2.如果有事务管理的话则用这一套:
- QuerryRunner():无参构造方法
- int update(Connection conn, String sql, Object...args):增删改操作
- T query(Connection conn, String sql, ResultSetHandler rsh, Object...args):查询操作
批量处理方法:
- int[] batch(Connection conn, String sql, Object[][] params)
- int[] batch(String sql, Object[][] params)
DbUtils类
主要方法:
- static void commitAndCloseQuietly(Connection conn):提交事务,会把异常也处理了
- static void rollbackAndCloseQuiety(Connection conn):回滚事务,会把异常也处理了
代码示例1(DBUtils的增删改):
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
public class DBUtilsDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
//insert();
//delete();
update();
}catch(Exception e){
e.printStackTrace();
}finally{
JDBCUtils2.release(rs, pstmt, conn);
}
}
/*
*添加数据操作
*/
public static void insert() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
queryRunner.update("insert into user value(null, ?, ?, ?)", "黄伊", "123", "student");
}
/*
* 删除操作
*/
public static void delete() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
queryRunner.update("delete from user where username = ?", "luer");
}
/*
* 修改操作
*/
public static void update() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
queryRunner.update("update user set password = ? where username = ?", "520", "黄伊");
}
}
代码示例2(DBUtils的查询操作):
//User.java
package cn.luyi.demo1;
public class User {
private String username;
private String password;
private String role;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + ", role=" + role + "]";
}
}
// DBUtilsDemo2.java
package cn.luyi.demo1;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
public class DBUtilsDemo2 {
public static void main(String[] args) throws SQLException {
//querySingle();
queryMany();
}
/*
* 单条查询
*/
public static void querySingle() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
User user = queryRunner.query("select * from user where username = ?", new ResultSetHandler<User>(){
@Override
public User handle(ResultSet rs) throws SQLException {
User user = new User();
if(rs.next()){
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setRole(rs.getString("role"));
}
return user;
}
}, "lusan");
System.out.println(user);
}
/*
* 多条查询
*/
public static void queryMany() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
//用一个集合来装User对象
List<User> list = queryRunner.query("select * from user", new ResultSetHandler<List<User>>(){
@Override
public List<User> handle(ResultSet rs) throws SQLException {
//创建一个集合用于封装数据
List<User> list = new ArrayList<User>();
while(rs.next()){
User user = new User();
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setRole(rs.getString("role"));
list.add(user);
}
return list;
}
});
for(User user : list){
System.out.println(user);
}
}
}
ResultHandler的实例
前面我们通过使用DBUtils对数据库进行增删改查,可以看出确实省了很多代码,但是我们在使用查询时,仍然需要一个个手动封装数据到对象中,有没有什么办法可以帮我们封装好数据呢?答案是有的,ResultHandler的不同的实现类就帮我们做了这样的事情,接下来我们就来学习ResultHandler的实例吧
1.ArrayHandler和ArrayListHandler
- ArrayHandler将一条记录封装到一个数组当中,这个数组就是Object[]
- ArrayListHandler:将多条数据封装到一个装有Object[]的List集合中
代码示例:
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ArrayHandler;
import org.apache.commons.dbutils.handlers.ArrayListHandler;
public class DBUtilsDemo3 {
public static void main(String[] args) throws SQLException {
//method1();
method2();
}
/*
* ArrayHandler:将一条记录封装到一个Object数组中
*/
public static void method1() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
Object[] objs = queryRunner.query("select * from user where id = ?", new ArrayHandler(), 3);
System.out.println(Arrays.toString(objs));
}
/*
* ArrayListHandler:将多条数据封装到一个装有Object数组的list集合中
*/
public static void method2() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
List<Object[]> list = queryRunner.query("select * from user", new ArrayListHandler());
for(Object[] obj:list){
System.out.println(Arrays.toString(obj));
}
}
}
2.BeanHandler和BeanListHandler
- BeanHandler:将一条记录封装到一个javaBean中
- BeanListHandler:将多条记录封装到一个装有javaBean的list集合中
代码示例:
//User.java
public class User {
private String username;
private String password;
private String role;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + ", role=" + role + "]";
}
}
//DBUtilsDemo4.java
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
public class DBUtilsDemo4 {
public static void main(String[] args) throws SQLException {
//method1();
method2();
}
/*
* BeanHandler的使用
*/
public static void method1() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
User user = queryRunner.query("select * from user where id = ?", new BeanHandler<User>(User.class), 3);
System.out.println(user);
}
/*
* BeanListHandler的使用
*/
public static void method2() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
List<User> list = queryRunner.query("select * from user", new BeanListHandler<User>(User.class));
for(User user:list){
System.out.println(user);
}
}
}
3.MapHandler和MapListHandler
- MapHandler:将一条记录封装到一个集合中,Map的key是列名,Map的value就是表中列的记录
- MapListHandler:将多条记录封装到一个装有Map的List集合中
代码示例:
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
public class DBUtilsDemo5 {
public static void main(String[] args) throws SQLException {
//method1();
method2();
}
/*
* MapHandler的使用
*/
public static void method1() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
Map<String, Object> map = queryRunner.query("select * from user where id = ?", new MapHandler(), 3);
System.out.println(map);
}
/*
* MapListHandler的使用
*/
public static void method2() throws SQLException{
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
List<Map<String, Object>> list = queryRunner.query("select * from user", new MapListHandler());
for(Map<String, Object> map:list){
System.out.println(map);
}
}
}
4.ColumnListHandler、ScalarHandler、KeyedHandler(了解)
- ColumnListHandler:将数据中的某列封装到List集合中
- ScalarHandler:将单个值进行封装
- KeyedHandler:将一条记录封装到一个Map集合中,将多条记录封装到一个装有Map集合的Map集合中去,而且外面的Map的key是可以指定的
代码示例:
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ColumnListHandler;
import org.apache.commons.dbutils.handlers.KeyedHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
public class DBUtilsDemo6 {
public static void main(String[] args) throws SQLException {
//method1();
//method2();
method3();
}
/*
* ColumnListHandler的使用
*/
public static void method1() throws SQLException {
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
List<Object> list = queryRunner.query("select username, password from user", new ColumnListHandler<Object>("username"));
for (Object obj : list) {
System.out.println(obj);
}
}
/*
* ScalarHandler的使用
*/
public static void method2() throws SQLException {
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
Object obj = queryRunner.query("select count(*) from user", new ScalarHandler<Object>());
System.out.println(obj);
}
/*
* KeyedHandler的使用
*/
public static void method3() throws SQLException {
QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
//指定键为username
Map<Object, Map<String, Object>> map = queryRunner.query("select * from user", new KeyedHandler<Object>("username"));
for(Object key: map.keySet()){
System.out.println(key + "---" + map.get(key));
}
}
}
作者:卢一
出处:http://www.cnblogs.com/luyi001/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。