文章目录
连接池、代理模式、动态代理
1、数据源和连接池
所谓数据源也就是数据的来源。它存储了所有建立数据库连接需要的信息。算是对数据库的一个抽象映射,即一个数据源对于一个数据库。
数据源有以下属性
- databaseName String数据库名称,即数据库的SID。
- dataSourceName String数据源接口实现类的名称。
- description String 对数据源的描述。
- networkProtocol String 和服务器通讯使用的网络协议名。
- password String 用户登录密码。
- portNumber数据库服务器使用的端口。
- serverName String数据库服务器名称。
- user String 用户登录名。
如果数据是水,数据库就是水库,数据源就是连接水库的管道,终端用户看到的数据集是管道里流出来的水。
Java Web服务器开始时,我们用的最多的数据源是JNDI。
数据库连接池
Web服务器中一般都是配置数据源,然后每个数据源维护一些数据库连接池。我们知道,数据库连接的创建时非常耗时耗资源的,还会受到网速的影响。我们在配置数据源的时候一般都已经配置好数据库连接池的参数,然后数据源会自动帮我们维护这些。所有这些对开发者而言是透明的。连接池是由容器(比如Tomcat)提供的,用来管理池中的连接对象。连接池自动分配连接对象并对闲置的连接进行回收。连接池中的连接对象是由数据源(DataSource)创建的。连接池(Connection Pool)用来管理连接(Connection)对象。
DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池。
参考博客
数据源和连接池
连接池、数据源、JNDI三者间的关系及用法
1、创建基本的数据源类
我们在之前编写的代码里边,每次都是创建连接,然后实现了一些简单的目的以后就关掉了connection。但创建连接的过程是开销很大的。我们可以用连接池的方法,使得当连接数目变多时,建立的数据库连接能重用从而优化效率。
1、创建基本的DataSource类
public class DataSource {
private static String user = "root";
private static String password = "123";
private static String DB_URL = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
private LinkedList<Connection> connectionsPool;//连接池
private int InitCount = 5;//初始化连接池的容
private int MAXCONNECTION=10;//最大连接数
private int currentCount=0;//当前创建的连接数
public DataSource(){
try{
connectionsPool = new LinkedList<>();
for(int i=0;i<InitCount;i++){
connectionsPool.addLast(createConnection());
currentCount++;
}
} catch (SQLException e) {
throw new ExceptionInInitializerError("创建连接失败,已创建数目:"+currentCount);
}
}
private static Connection createConnection() throws SQLException {
return DriverManager.getConnection(DB_URL,user,password);
}
public Connection getConnection() throws SQLException {
synchronized (DataSource.class){
if(this.connectionsPool.size()>0){
return this.connectionsPool.removeFirst();
}else if(currentCount<MAXCONNECTION){
this.connectionsPool.addLast(createConnection());
currentCount++;
return this.connectionsPool.removeFirst();
}else
throw new SQLException("已经达到最大的连接数!无法创建连接!");
}
}
public void free(Connection conn){
this.connectionsPool.addLast(conn);//尾插法
}
}
我们的连接都存放在一个LinkedList里面。
2、修改之前的JDBCUtils类
public final class JDBCUtils {
private static DataSource dataSource=new DataSource();
public static Connection getConnect() throws SQLException {//异常应该抛出
//return DriverManager.getConnection(DB_URL, user, password);
return dataSource.getConnection();
}
static{//使用class.forName()方法一般用于静态代码块,而且该方法注册驱动不依赖具体的类库
try {
//forName进行类的加载时优先加载静态代码块。
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void free(Connection conn, Statement st, ResultSet res) {
try {
if (res != null) //原则1:晚点连接早点释放。原则2:先创建的后释放
res.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (st != null)
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
//conn.close();
dataSource.free(conn);//重用,将创建的连接不关闭,而是将其回收到连接池
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
3、简单测试:
public class Test_1 {
public static void main(String[]args) throws SQLException {
for(int i=0;i<20;i++){
Connection conn = JDBCUtils.getConnect();
System.out.println(conn);
JDBCUtils.free(conn,null,null);
}
}
}
我们设置了连接个数最大只有10个。
2、使用代理模式来保持因使用数据源连接池导致的用户关闭不习惯问题
我们看一下现在的JDBCUtils的代码中free()函数:
public static void free(Connection conn, Statement st, ResultSet res) {
try {
if (res != null) //原则1:晚点连接早点释放。原则2:先创建的后释放
res.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (st != null)
st.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
//conn.close();
dataSource.free(conn);//重用,将创建的连接不关闭,而是将其回收到连接池
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果有些人没有用到我们在线程池里边写的free,那会导致连接池的连接数目越用越少。故我们需要修改以下,以适应使用习惯。
下面是代理模式,来源于菜鸟教程
:
- 在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
- 介绍
- 意图:为其他对象提供一种代理以控制对这个对象的访问。
- 主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
- 何时使用:想在访问一个类时做一些控制。
- 如何解决:增加中间层。
- 关键代码:实现与被代理类组合。
- 应用实例: 1、Windows 里面的快捷方式。 2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。 3、买火车票不一定在火车站买,也可以去代售点。 4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。 5、spring aop。
- 优点: 1、职责清晰。 2、高扩展性。 3、智能化。
- 缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
- 使用场景:按职责来划分,通常有以下使用场景:
1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。- 注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
参考:代理模式|菜鸟教程
Connection在JDBC里是一个接口,而各个厂商的对应设计的Connection则是实现该接口的实体类。
那么我们的代理类则需要内部有这样的一个类来完成实际的功能。
1、我们需要创建一个Proxy_MyConnection 类,来代理Connection
我们要做的就是将close方法重写,不是关闭,而是把它放回到连接池里边。它是无参数的。所以我们需要这个类内部具有一个连接池。故 部分实现代码如下:
/**
* 代理Connection类
*/
public class Proxy_MyConnection implements Connection {//首先得实现该接口
private Connection realConnection;//所代理的类
private DataSource2 dataSource2;//连接池
private int currentUser = 0;//记录当前连接了多少个客户
/**
* @param conn 代理接口
* @param pool2 连接池
*/
Proxy_MyConnection(Connection conn, DataSource2 pool2) {//构造器,别人构造不出来。只有同一包内才能构造。
this.realConnection = conn;
this.dataSource2 = pool2;
}
@Override
public void close() throws SQLException {
//最大连接
int maxConnect = 5;
if (currentUser < maxConnect) {
this.currentUser++;
// System.out.println("user"+currentUser);
this.dataSource2.connectionPool.addLast(this);//this就是代理接口类
} else {
this.realConnection.close();//最大连接数已经达到。我们真的需要关闭了。
this.currentUser--;
}
}
@Override
public Statement createStatement() throws SQLException {
return this.realConnection.createStatement();//使用实际的JDBC connection接口的方法。提高可用性
}
......
......
2、我们需要修改连接池DataSource类的一些代码,第一个是创建连接的方法,第二个是获取连接的方法
//创建连接
private Connection createConnection() throws SQLException {
Connection conn = DriverManager.getConnection(DB_URL,user,password);
Proxy_MyConnection proxy_conn = new Proxy_MyConnection(conn,this);
return proxy_conn;//代理类
}
//获取连接
public Connection getConnection() throws SQLException {
synchronized (DataSource.class){
if(this.connectionPool.size()>0){
return this.connectionPool.removeFirst();
}else if(currentCount<MAXCONNECTION){
this.connectionPool.addLast(createConnection());
currentCount++;
return this.connectionPool.removeFirst();
}else
throw new SQLException("已经达到最大的连接数!无法创建连接!");
}
}
此时JDBCUtils的free方法变为我们所习惯的直接close()
public static void free(Connection conn, Statement st, ResultSet res) {
try {
if (res != null) //原则1:晚点连接早点释放。原则2:先创建的后释放
res.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (st != null)
st.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、测试代码
for(int i=0;i<5;i++){
Connection conn = JDBCUtils.getConnect();
System.out.println(conn);
JDBCUtils.free(conn,null,null);
}
System.out.println("测试是否可用重用:");
for(int i=0;i<5;i++){
Connection conn = JDBCUtils.getConnect();
System.out.println(conn);
JDBCUtils.free(conn,null,null);
}
现在使用的都是我们的代理类Proxy_MyConnection
3、使用动态代理完善连接代理
通过上面的代码我们知道Proxy_MyConnection
是代理了Connection类的功能。而且内部含有一个真正的实例类realConnection
,我们所有的实现方法都是调用该内部实例类的方法。如:
Proxy_MyConnection的内部属性值:
private Connection realConnection;//所代理的类————
private DataSource2 dataSource2;//连接池
private int currentUser = 0;//记录当前连接了多少个客户
实现的方法:都是调用了realConnection的方法。
@Override
public String nativeSQL(String s) throws SQLException {
return this.realConnection.nativeSQL(s);
}
@Override
public void setAutoCommit(boolean b) throws SQLException {
this.realConnection.setAutoCommit(b);
}
@Override
public boolean getAutoCommit() throws SQLException {
return this.realConnection.getAutoCommit();
}
@Override
public void commit() throws SQLException {
this.realConnection.commit();
}
@Override
public void rollback() throws SQLException {
this.realConnection.rollback();
}
所以这样的操作是很繁琐的。我们本来就是用DataSource来创建连接池,维护连接池。则只关心了其close方法的实现。为了让connection能够可重用而重修改了close方法:
@Override
public void close() throws SQLException {
//最大连接
int maxConnect = 5;
if (currentUser < maxConnect) {
this.currentUser++;
// System.out.println("user"+currentUser);
this.dataSource2.connectionPool.addLast(this);//this就是代理接口类
} else {
this.realConnection.close();//最大连接数已经达到。我们真的需要关闭了。
this.currentUser--;
}
}
为了解决这个问题,我们可用使用动态代理模式。
3.1、什么是动态代理
动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。Java动态代理与java反射机制关系紧密,毕竟是运行时创建类
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。代理模式角色分为 3 种:
- Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
- RealSubject(真实主题角色):真正实现业务逻辑的类;
- Proxy(代理主题角色):用来代理和封装真实主题;
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层
代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理等等。
如果根据字节码的创建时机来分类,可以分为
- 静态代理
所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。 - 动态代理
而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
参考博客:Java 动态代理详解
3.2、使用动态代理来实现Proxy_MyConnection类
创建Proxy_MyConnectionHandler类,用于产生代理类。
public class Proxy_MyConnectionHandler implements InvocationHandler {//必须实现该接口。
private DataSource2 dataSource2;
private int currentUser = 0;//记录当前连接了多少个客户
private Connection realConnection;
private Connection warpedConnection;
public Proxy_MyConnectionHandler(DataSource2 dataSource) {
this.dataSource2 = dataSource;
}
/**
* 用于绑定真正的实现类
*
* @param realConnection 真正的连接
* @return 返回包裹后的类,代理类。
*/
Connection bind(Connection realConnection) {//用于绑定真正的连接realConnection
this.realConnection = realConnection;
this.warpedConnection = (Connection) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{Connection.class}, this);
return this.warpedConnection;
}
/**
* 我们关心的方法,我们现在只重写close方法。利用了反射。
*/
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if ("close".equals(method.getName())) {//只要有人调用该方法,就拦截下来进行以下操作。
//最大连接
int maxConnect = 5;
if (currentUser < maxConnect) {
this.currentUser++;
// System.out.println("user"+currentUser);
this.dataSource2.connectionPool.addLast(this.warpedConnection);//this就是代理接口类
} else {
this.realConnection.close();//最大连接数已经达到。我们真的需要关闭了。
this.currentUser--;
}
}
return method.invoke(this.realConnection, objects);//调用该方法。
}
}
注意该方法:
Connection bind(Connection realConnection) {//用于绑定真正的连接realConnection
this.realConnection = realConnection;
this.warpedConnection = (Connection) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{Connection.class}, this);
return this.warpedConnection;
}
Proxy.newProxyInstance()专门给我们动态生成真正的类的方法,这里就相当于实现了Connection的所有的类似方法。new Class[]{Connection.class}表示要实现方法的类。用warpedConnection来包装。
如实现rollback方法,原来要我们手写:
@Override
public void rollback() throws SQLException {
this.realConnection.rollback();
}
这个invoke方法就是我们用来拦截我们想要拦截重写的方法。这里我们需要拦截的是close方法。并将其重写。
public Object invoke(Object o, Method method, Object[] objects) throws Throwable
静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
- 1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类 - 2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
测试代码
1、现修改数据源DataSource的代码,使用动态代理:
private Connection createConnection() throws SQLException {
Connection conn = DriverManager.getConnection(DB_URL,user,password);
/* Proxy_MyConnection proxy_conn = new Proxy_MyConnection(conn,this);
return proxy_conn;//代理类*/
//生成代理类:
Proxy_MyConnectionHandler proxy = new Proxy_MyConnectionHandler(this);//传入的是数据源
return proxy.bind(conn);//绑定。
}
2、测试
for(int i=0;i<10;i++){
Connection conn = JDBCUtils.getConnect();
System.out.println(conn);
JDBCUtils.free(conn,null,null);
运行结果:
我们换回原来写的静态代理类Proxy_MyConnection
再运行测试:
发现了没有:打印的包信息不同。使用动态代理得到的包信息都是MySQL的驱动包信息。