在mybatis-configuration.xml 文件中,我们进行了如下的配置:

    <!-- 可以配置多个运行环境,但是每个 SqlSessionFactory 实例只能选择一个运行环境常用: 一、development:开发模式 二、work:工作模式 -->
<environments default="development">
<!--id属性必须和上面的default一样 -->
<environment id="development">
<!--使用JDBC的事务管理机制-->
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>

  其中 <transactionManager type="JDBC" /> 是对事务的配置,下篇博客我们会详细介绍。

  本篇博客我们介绍  <dataSource type="POOLED"> 对于数据源的配置。

1、解析 environments 标签

  在 XMLConfigBuilder.java 的  parseConfiguration(XNode root) 中:

  

  进入 environmentsElement(root.evalNode("environments")) 方法:

    private void environmentsElement(XNode context) throws Exception {
//如果<environments>标签不为null
if (context != null) {
//如果 environment 值为 null
if (environment == null) {
//获取<environments default="属性值">中的default属性值
environment = context.getStringAttribute("default");
}
//遍历<environments />标签中的子标签<environment />
for (XNode child : context.getChildren()) {
//获取<environment id="属性值">中的id属性值
String id = child.getStringAttribute("id");
//遍历所有<environment>的时候一次判断相应的id是否是default设置的值
if (isSpecifiedEnvironment(id)) {
//获取配置的事务管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//获取配置的数据源信息
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
} private boolean isSpecifiedEnvironment(String id) {
if (environment == null) {
throw new BuilderException("No environment specified.");
} else if (id == null) {
throw new BuilderException("Environment requires an id attribute.");
} else if (environment.equals(id)) {
return true;
}
return false;
}

  ①、第 3 行代码:if (context != null)  也就是说我们可以不在 mybatis-configuration.xml 文件中配置<environments />标签,这是为了和spring整合时,在spring容器中进行配置。

  ②、第 5 行——第 8 行代码:获取<environments default="属性值">中的default属性值,注意第 5 行 首先判断 environment == null 。因为我们可以配置多个环境,也就是连接多个数据库。

    不过需要记住:尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一,也就是说每个数据库对应一个 SqlSessionFactory 实例。

    可以用如下方法进行区别:

 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

  ③、第 10 行代码:遍历<environments />标签中的子标签<environment />,可以配置多个<environment />标签。

  ④、第 14 行代码:遍历所有<environment  />的时候判断相应的id是否是default设置的值,选择相等的 <environment />标签进行数据源的配置。

  ⑤、第 16 行代码:进行事务的配置(下篇博客进行详解)。

  ⑥、第 18 行代码:进行数据源的配置,下面我们详细讲解。

2、mybatis 的数据源类图

  mybatis 对于数据源的所有类都在如下包中:

  

 

  注意:DataSource 接口不是mybatis包下的,是JDK的 javax.sql 包下的。

3、mybatis 三种数据源类型

  前面我们在 mybatis-configuration.xml 文件中配置了数据源的类型:

  

  mybatis 支持三种数据源类型(也就是 type=”[UNPOOLED|POOLED|JNDI]”):

  ①、UNPOOLED:(不使用连接池)这个数据源的实现只是每次被请求时打开和关闭连接。虽然有点慢,但对于在数据库连接可用性方面没有太高要求的简单应用程序来说,是一个很好的选择。 不同的数据库在性能方面的表现也是不一样的,对于某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。

  ②、POOLED:(使用连接池)这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。

  ③、JNDI : 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。

  ps:关于连接池的概念请看下面详细介绍。

  这三种数据源的类型在 mybatis 在上面所讲的类图中正好对应。那么 mybatis 是如何产生数据源的呢?

4、mybatis 初始化数据源

  看上面的类图,我们可以看到 DataSourceFactory 接口,这是一个工厂方法,mybatis 就是通过工厂模式来创建数据源 DataSource 对象。我们先看看该接口:

 public interface DataSourceFactory {

   void setProperties(Properties props);

   DataSource getDataSource();

 }

  通过调用其 getDataSource() 方法返回数据源DataSource。

  而这个工厂方法也对应上面讲的三种数据源类型的工厂方法。它们分别都实现了 DataSourceFactory 接口(使用连接池的数据源类型 PooledDataSourceFactory 是继承 UnpooledDataSourceFactory,而UnpooledDataSourceFactory 实现了 DataSourceFactory 接口)。

 public class JndiDataSourceFactory implements DataSourceFactory
public class UnpooledDataSourceFactory implements DataSourceFactory
public class PooledDataSourceFactory extends UnpooledDataSourceFactory

  这里的三个数据源工厂也是通过工厂模式来产生对应的三种数据源。

  为什么要这样设计,下面我们来详细介绍。

5、不使用连接池 UnpooledDataSource

  在 mybatis-configuration.xml 文件中,type="UNPOOLED"

  

  回到本篇博客第一小节:解析 environments 标签 的第 18 行代码:通过配置的 <datasource>标签,来实例化一个 DataSourceFactory 工厂。该工厂实际上是根据配置的 type 类型,产生对应的数据源类型工厂。

   private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

  下面我们来看 UnPooledDataSource 的 getConnection() 方法实现:

     public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
} public Connection getConnection(String username, String password) throws SQLException {
return doGetConnection(username, password);
}
private Connection doGetConnection(String username, String password) throws SQLException {
//将用户名、密码、驱动都封装到Properties文件中
Properties props = new Properties();
if (driverProperties != null) {
props.putAll(driverProperties);
}
if (username != null) {
props.setProperty("user", username);
}
if (password != null) {
props.setProperty("password", password);
}
return doGetConnection(props);
} /**
* 获取数据库连接
*/
private Connection doGetConnection(Properties properties) throws SQLException {
//1、初始化驱动
initializeDriver();
//2、从DriverManager中获取连接,获取新的Connection对象
Connection connection = DriverManager.getConnection(url, properties);
//3、配置connection属性
configureConnection(connection);
return connection;
}

  前面的代码比较容易看懂,最后面一个方法获取数据库连接有三步。

  ①、初始化驱动:判断driver驱动是否已经加载到内存中,如果还没有加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。

   private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
if (driverClassLoader != null) {
driverType = Class.forName(driver, true, driverClassLoader);
} else {
driverType = Resources.classForName(driver);
}
// DriverManager requires the driver to be loaded via the system ClassLoader.
// http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
Driver driverInstance = (Driver)driverType.newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}

  ②、创建Connection对象:    使用DriverManager.getConnection()方法创建连接。

  ③、配置Connection对象:    设置是否自动提交autoCommit和隔离级别isolationLevel。

   private void configureConnection(Connection conn) throws SQLException {
if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
conn.setAutoCommit(autoCommit);
}
if (defaultTransactionIsolationLevel != null) {
conn.setTransactionIsolation(defaultTransactionIsolationLevel);
}
}

  ④、返回Connection对象。

  也就是说,使用 UnpooledDataSource 类型的数据源,每次需要连接的时候都会调用 getConnection() 创建一个新的连接Connection返回。实际上创建一个Connection对象的过程,在底层就相当于和数据库建立的通信连接,在建立通信连接的过程会消耗一些资源。有时候我们可能只是一个简单的 SQL 查询,然后抛弃掉这个连接,这实际上是很耗资源的。

  那么怎么办呢?答案就是使用数据库连接池。

6、数据库连接池

  其实对于共享资源,有一个很著名的设计理念:资源池。该理念正是为了解决资源的频繁分配、释放所造成的问题。

  具体思想:初始化一个池子,里面预先存放一定数量的资源。当需要使用该资源的时候,将该资源标记为忙状态;当该资源使用完毕后,资源池把相关的资源的忙标示清除掉,以示该资源可以再被下一个请求使用。

  对应到上面数据库连接的问题,我们可以这样解决:先建立一个池子,里面存放一定数量的数据库连接。当需要数据库连接时,只需从“连接池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。这样就能避免频繁的进行数据库连接和断开耗资源操作。

7、使用连接池 PooledDataSource

  先了解一下 PooledDataSource 的实现原理:

  ①、PooledDataSource将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护。 MyBatis将连接池中的PooledConnection分为两种状态: 空闲状态(idle)和活动状态(active),这两种状态的PooledConnection对象分别被存储到PoolState容器内的idleConnections和activeConnections两个List集合中。

  ②、idleConnections:空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从此集合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。

  ③、activeConnections:活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,如果没有,则看此集合是否已满,如果未满,PooledDataSource会创建出一个PooledConnection,添加到此集合中,并返回。

  

  下面我们看看PooledDataSource 的getConnection()方法获取Connection对象的实现:

   public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
}

  再看看 popConnection() 方法的实现:

  ①、 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第②步。

  ②、查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第③步;

  ③、 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第④步。

  ④、 线程等待,循环2步

   private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0; while (conn == null) {
synchronized (state) {
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// Pool does not have available connection
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// Cannot create new connection
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
log.debug("Bad connection. Could not roll back");
}
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
} } if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
} return conn;
}

  对于PooledDataSource的getConnection()方法内,先是调用类PooledDataSource的popConnection()方法返回了一个PooledConnection对象,然后调用了PooledConnection的getProxyConnection()来返回Connection对象。

  问题:对于连接池,我们会遇到java.sql.Connection对象的回收问题。当我们的程序中使用完Connection对象时,如果不使用数据库连接池,我们一般会调用 connection.close()方法,关闭connection连接,释放资源。但是

调用过close()方法的Connection对象所持有的资源会被全部释放掉,Connection对象也就不能再使用。

  那么,如果我们使用了连接池,我们在用完了Connection对象时,需要将它放在连接池中,该怎样做呢?

  也就是说,我们在调用con.close()方法的时候,不调用close()方法,将其换成将Connection对象放到连接池容器中的代码!

  很容易想到代理模式。为真正的Connection对象创建一个代理对象,代理对象所有的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到连接池中。

  MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库连接java.sql.Connection实例对象的包裹器。PooledConnection对象内持有一个真正的数据库连接java.sql.Connection实例对象和一个java.sql.Connection的代理:

  其部分定义如下:

  PooledConenction实现了InvocationHandler接口,并且 proxyConnection对象也是根据这个它来生成的代理对象:

class PooledConnection implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
} }

  从上述代码可以看到,当我们使用了pooledDataSource.getConnection()返回的Connection对象的close()方法时,不会调用真正Connection的close()方法,而是将此Connection对象放到连接池中。

8、JNDI类型的数据源 JndiDataSource

  对于JNDI类型的数据源DataSource的获取就比较简单,MyBatis定义了一个JndiDataSourceFactory工厂来创建通过JNDI形式生成的DataSource。

  这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。这种数据源配置只需要两个属性:

  ①、initial_context – 个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么 data_source 属性将会直接从 InitialContext 中寻找。

  ②、data_source – 这是引用数据源实例位置的上下文的路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。

   public void setProperties(Properties properties) {
try {
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
} if (properties.containsKey(INITIAL_CONTEXT)
&& properties.containsKey(DATA_SOURCE)) {
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
} } catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}

参考文档:https://blog.csdn.net/luanlouis/article/details/37992171

     http://www.mybatis.org/mybatis-3/zh/configuration.html#environments

 

mybatis源码解读(三)——数据源的配置的更多相关文章

  1. mybatis源码解读(四)——事务的配置

    上一篇博客我们介绍了mybatis中关于数据源的配置原理,本篇博客介绍mybatis的事务管理. 对于事务,我们是在mybatis-configuration.xml 文件中配置的: 关于解析 < ...

  2. Mybatis源码解读-SpringBoot中配置加载和Mapper的生成

    本文mybatis-spring-boot探讨在springboot工程中mybatis相关对象的注册与加载. 建议先了解mybatis在spring中的使用和springboot自动装载机制,再看此 ...

  3. spring IOC DI AOP MVC 事务, mybatis 源码解读

    demo https://gitee.com/easybao/aop.git spring DI运行时序 AbstractApplicationContext类的 refresh()方法 1: pre ...

  4. MyBatis源码解读之延迟加载

    1. 目的 本文主要解读MyBatis 延迟加载实现原理 2. 延迟加载如何使用 Setting 参数配置 设置参数 描述 有效值 默认值 lazyLoadingEnabled 延迟加载的全局开关.当 ...

  5. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

  6. Mybatis源码解读-插件

    插件允许对Mybatis的四大对象(Executor.ParameterHandler.ResultSetHandler.StatementHandler)进行拦截 问题 Mybatis插件的注册顺序 ...

  7. MyBatis源码解读(3)——MapperMethod

    在前面两篇的MyBatis源码解读中,我们一路跟踪到了MapperProxy,知道了尽管是使用了动态代理技术使得我们能直接使用接口方法.为巩固加深动态代理,我们不妨再来回忆一遍何为动态代理. 我相信在 ...

  8. MyBatis源码解读(1)——SqlSessionFactory

    在前面对MyBatis稍微有点了解过后,现在来对MyBatis的源码试着解读一下,并不是解析,暂时定为解读.所有对MyBatis解读均是基于MyBatis-3.4.1,官网中文文档:http://ww ...

  9. 【转】Mybatis源码解读-设计模式总结

    原文:http://www.crazyant.net/2022.html?jqbmtw=b90da1&gsjulo=kpzaa1 虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开 ...

随机推荐

  1. 基于WAMP的Crossbario 安装入门

    简单学习和使用WAMP协议,Router 是crossbario, Client是Autobahn, 了解运作的流程. 测试环境是Centos6 虚拟机一台 目录为 /data/wamp/ ,用的是P ...

  2. Socket编程实践(2) --Socket编程导引

    什么是Socket? Socket可以看成是用户进程与内核网络协议栈的接口(编程接口, 如下图所示), 其不仅可以用于本机进程间通信,可以用于网络上不同主机的进程间通信, 甚至还可以用于异构系统之间的 ...

  3. UVa - 1616 - Caravan Robbers

    二分找到最大长度,最后输出的时候转化成分数,比较有技巧性. AC代码: #include <iostream> #include <cstdio> #include <c ...

  4. 9.4、Libgdx简单字符输入

    (官网:www.libgdx.cn) 如果应用需要输入一个字符,比如用户名和密码,可以通过简单的对话框实现. 在桌面中使用一个Swing对话框,提示用户输入字符. 在Android中将会打开一个标准的 ...

  5. [Python]Flask构建网站分析应用

    原文Saturday morning hacks: Building an Analytics App with Flask - 由orangleliu友情翻译 ,主要是通过埋点技术来实现web网页的 ...

  6. 【一天一道LeetCode】#22. Generate Parentheses

    一天一道LeetCode (一)题目 Given n pairs of parentheses, write a function to generate all combinations of we ...

  7. 《java入门第一季》之面向对象面试题

    1:方法重写和方法重载的区别?方法重载能改变返回值类型吗? 方法重写: 在子类中,出现和父类中一模一样的方法声明的现象. 方法重载: 同一个类中,出现的方法名相同,参数列表不同的现象. 方法重载能改变 ...

  8. 我所犯的JavaScript引用错误

    近期在w3cschool学习JavaScript和php--学完后,开始帮一哥们友情写网站.但是在使用ajax和Jquery的时候发现,我自己写的脚本不能运行.捣鼓了半天,没有发现任何语句错误.调试器 ...

  9. 网站开发进阶(二十三)Address already in use: JVM_Bind <null>:8088

    Address already in use: JVM_Bind <null>:8088 注:请点击此处进行充电! 阿里云服务器又莫名其妙的宕掉!内存泄漏问题依然存在,又出现了端口占用的情 ...

  10. OpenCV 矩形轮廓检测

    转载请注明出处:http://blog.csdn.net/wangyaninglm/article/details/44151213, 来自:shiter编写程序的艺术 基础介绍 OpenCV里提取目 ...