ORM

ORM(Object Relational Mapping,对象-关系映射):ORM框架的主要功能就是根据映射配置文件,完成数据在对象模型与关系模型之间的映射,同时也屏蔽了上述重复的代码,只暴露简单的API供开发人员使用。
image.png

看看JDBC操作

  1. public class JdbcTest {
  2. @Test
  3. public void test() throws ClassNotFoundException, SQLException {
  4. Class.forName("com.mysql.jdbc.Driver");
  5. Properties pro = new Properties();
  6. pro.setProperty("user","root");
  7. pro.setProperty("password","123456");
  8. Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC",pro);
  9. //Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","123456");
  10. //Statement statement = connection.createStatement();
  11. PreparedStatement statement = connection.prepareStatement("select * from user");
  12. statement.execute();
  13. ResultSet resultSet = statement.getResultSet();
  14. //ResultSet resultSet = statement.executeQuery("select * from user");
  15. while (resultSet.next()){
  16. System.out.println(resultSet.getString("name")+":"+resultSet.getInt("age"));
  17. }
  18. resultSet.close();
  19. statement.close();
  20. connection.close();
  21. }
  22. }

第一步加载mysql驱动,第二步创建mysql数据库连接,第三步创建Statement,第四步执行sql语句,第五步获取执行结果,第六步关闭所有

原始jdbc开发存在的问题如下: 1、 数据库连接创建、释放频繁造成系统资源浪费【1-4,6】,从⽽影响系统性能。 2、 Sql语句在代码中硬编码,造成代码不易维护,实际应⽤中sql变化的可能较⼤,sql变动需要改变 java代码。 3、 使⽤preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可能 多也可能少,修改sql还要修改代码,系统不易维护。 4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析⽐较⽅便

如果自己设计呢

使用端:

  1. 提供数据源配置文件:mybatis-config.xml
  2. 提供SQL语句配置文件:Mapper.xml

框架端:

  1. 读取使用端提供的配置文件,通过bean来存至内存
    1. Configuration : 存放数据库基本信息、Map<唯⼀标识,MappedStatement> 唯⼀标识:namespace + “.” + id
    2. MappedStatement :sql语句、statement类型、输⼊参数java类型、输出参数java类型
  2. 解析两个配置文件:通过sqlSessionFactoryBuilder类解析
    1. 使⽤dom4j解析配置⽂件,将解析出来的内容封装到Configuration和MappedStatement中
    2. 创建SqlSessionFactory的实现类DefaultSqlSession
  3. 解析配置文件后创建SqlSessionFactory
  4. 通过SqlSessionFactory.openSession()创建sqlSession对象(线程相关),完成查询操作
    1. sqlSession:封装JDBC,完成对数据库表的查询操作(内部通过 Executor 获取connection,然后根据MappedStatement信息进行jdbc操作)

写一个Demo

官网:https://mybatis.org/mybatis-3/zh/getting-started.html

引入pom

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

xml配置mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

image.png

配置mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

image.png

定义mapper接口

public interface BlogMapper {
  Blog selectBlog(int id);
  @Select("SELECT * FROM blog WHERE name = #{name}") //这个等价于mapper.xml,有配置就不用注解
  Blog selectBlogByName(String name);
}

构建SqlSessionFactory

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);//明明是sqlSession封装了jdbc操作,为什么会用mapper进行查询操作?内部使用了代理,最终还是调用session的方法
  Blog blog = mapper.selectBlog(101);
}

image.png

作用域

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。

映射器实例(Mapper实例)

映射器是一些由你创建的、绑定你映射的语句的接口。映射器接口的实例是从 SqlSession 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 SqlSession 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。 也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可丢弃。 并不需要显式地关闭映射器实例,尽管在整个请求作用域保持映射器实例也不会有什么问题,但是你很快会发现,像 SqlSession 一样,在这个作用域上管理太多的资源的话会难于控制。 为了避免这种复杂性,最好把映射器放在方法作用域内。就像示列代码一样。如果SqlSession是注入的,那么映射器实例也可通过依赖注入,并且可忽略其生命周期。

Mybatis-Spring:将MyBatis代码无缝地整合到Spring

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean中
SqlSessionFactoryBean 、 MapperScannerConfigurer
在Spring项目中应用了Mybatis都会有下面的2个bean配置,这2个配置就是实现xml加载、mapper和SqlSession注入的起始配置。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="xxx.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

SqlSessionFactoryBean : 加载xml及build SqlSessionFactory对象

从配置中我们可以看的 SqlSessionFactoryBean 配置了数据源、mapper的xml路径、mybatis-config的xml路径。因此,不难想象,SqlSessionFactoryBean 内部实现了xml配置文件的加载及SqlSessionFactory对象的创建。我们来看下 SqlSessionFactoryBean继承关系图形:
image.png
InitializingBean、FactoryBean接口:有 afterPropertiesSet() 来创建 SqlSessionFactory 对象 和 getObject() 来获取 SqlSessionFactory 对象;
FactoryBean子类都是通过getObject()来获取到实际的Bean对象,这里也就是SqlSessionFactory。

  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      //SqlSessionFactory 肯定是由 afterPropertiesSet() 来实现创建的
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }
  public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }

afterPropertiesSet() 内部首先 验证了 dataSource 和 sqlSessionFactoryBuilder 部位null,最后调用 buildSqlSessionFactory()方法获取到 SqlSessionFactory 对象,并赋值到类字段属性 sqlSessionFactory

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    // 省略了 SqlSessionFactoryBean 的属性(比如:ObjectFactory )赋值到 Configuration 对象中的操作
    //  1 Configuration : Mybatis的核心类之一,主要存放读取到的xml数据,包括mapper.xml 
    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configLocation != null) {
      //  2 创建  xmlConfigBuilder 对象 : 用于解析 mybatis-config.xml 数据
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      configuration.setVariables(this.configurationProperties);
    }

    if (xmlConfigBuilder != null) {
      try {
        //  3  XmlConfigBuilder 解析方法执行 
        xmlConfigBuilder.parse();
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
          //  4 创建  XMLMapperBuilder 对象 : 用于解析 mapper.xml 数据
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    } 
    // 5 通过 SqlSessionFactoryBuilder bulid  SqlSessionFactory 对象
    return this.sqlSessionFactoryBuilder.build(configuration);
  }

整个 buildSqlSessionFactory() 源码主要有以下几个重要的点:

  1. XMLConfigBuilder ,通过调用其 parse() 方法来 解析 mybatis-config.xml 配置(如果 配置有 mapper.xml ,其会通过 XMLMapperBuilder 进行解析加载),并将解析的数据赋值到 Configuration(Mybatis的核心类之一,主要存放读取到的xml数据,包括mapper.xml,该类贯穿整个mybatis,足以见得其重要性)
  2. XMLMapperBuilder : 通过调用其 parse() 方法来 解析 mapper.xml 配置, 并将解析的数据赋值到 Configuration
  3. 将存放有解析数据的 Configuration 作为 sqlSessionFactoryBuilder.build() 参数,创建 SqlSessionFactory 对象

    MapperScannerConfigurer :扫描Mapper接口路径,将 Mapper 偷梁换柱成 MapperFactoryBean

    MapperScannerConfigurer 是 mybatis-spring 项目中为了实现方便加载Mapper接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean:
    image.png
    BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法

    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
     if (this.processPropertyPlaceHolders) {
       processPropertyPlaceHolders();
     }
    
     ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
     scanner.setAddToConfig(this.addToConfig);
     scanner.setAnnotationClass(this.annotationClass);
     scanner.setMarkerInterface(this.markerInterface);
     scanner.setSqlSessionFactory(this.sqlSessionFactory);
     scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
     scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
     scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
     scanner.setResourceLoader(this.applicationContext);
     scanner.setBeanNameGenerator(this.nameGenerator);
     scanner.registerFilters();
     scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
    

    整个方法内部其实就是通过 ClassPathMapperScanner 的 scan() 方法,查看 scan() 实现,发现其内部调用了关键方法 doScan(),那么我们来看下 doScan() 方法实现:

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
     // 1、调用父类 ClassPathBeanDefinitionScanner的 doScan方法 加载路径下所有的mapper接口生成对应的 BeanDefinition 
     Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    
     if (beanDefinitions.isEmpty()) {
       logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
     } else {
       for (BeanDefinitionHolder holder : beanDefinitions) {
         GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
    
         // 2、 设置 被代理的 Bean(也就是Mapper) 的class信息
         definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
         // 3、 偷梁换柱成 MapperFactoryBean 
         definition.setBeanClass(MapperFactoryBean.class);
    
         definition.getPropertyValues().add("addToConfig", this.addToConfig);
    
         boolean explicitFactoryUsed = false;
         // 4、 设置 sqlSessionFactory 
         if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
           definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
           explicitFactoryUsed = true;
         } else if (this.sqlSessionFactory != null) {
           definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
           explicitFactoryUsed = true;
         }
    
         // 5、 设置 sqlSessionTemplate
         if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
           definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
           explicitFactoryUsed = true;
         } else if (this.sqlSessionTemplate != null) {
           definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
           explicitFactoryUsed = true;
         }
    
         if (!explicitFactoryUsed) {
           definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
         }
       }
     }
    
     return beanDefinitions;
    }
    

    整个方法分为3个部分:

  4. 调用父类 ClassPathBeanDefinitionScanner的 doScan()方法 加载路径下所有的mapper接口生成对应的 BeanDefinition

  5. 通过definition.setBeanClass(MapperFactoryBean.class) 偷梁换柱成 MapperFactoryBean
  6. 通过 definition.getPropertyValues().add() 添加 MapperFactoryBean 所需的 字段或者方法参数信息 : sqlSessionFactory 、 mapperInterface等

至此 MapperScannerConfigurer 的使命已经完成, 至于 MapperFactoryBean 的创建就完全交给Spring来完成了

MapperFactoryBean 、SqlSessionTemplate:Mapper与SqlSession解耦的利器

在mybatis中,Mapper是通过 SqlSession创建的,而SqlSession的生命周期仅仅在一次会话中,那么按照这种设计,每一次会话都要去创建SqlSession,然后再通过SqlSession去创建Mapper。我们知道Mapper其实没有必要每次都去创建,它更加适合作为一个单例对象。那么怎么将SqlSession和Mapper解耦呢? 在mybatis-spring项目中通过 MapperFactoryBean 、SqlSessionTemplate 来实现的。

MapperFactoryBean

正如前面我们所看到的一样,MapperFactoryBean 其实可以理解为 Mapper的代理工厂Bean,我们可以通过 MapperFactoryBean 的方法获取到 Mapper的代理对象。先来看下 MapperFactoryBean继承关系 :
image.png
我们可以看到 MapperFactoryBean 实现了 FactoryBean, 那么 肯定通过 实现 getObject() 获取到 Mapper的代理对象:

  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

继续看下 getSqlSession(), 发现其是 父类 SqlSessionDaoSupport 实现,我们看下SqlSessionDaoSupport源码:

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSession sqlSession;

  private boolean externalSqlSession;

  //  创建 SqlSession子类 SqlSessionTemplate 
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
    this.sqlSession = sqlSessionTemplate;
    this.externalSqlSession = true;
  }


  public SqlSession getSqlSession() {
    return this.sqlSession;
  }

  ....

}

发现我们获取到的SqlSession其实是其子类SqlSessionTemplate, 我们查看其构造方法源码:

  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 维护了一个 SqlSession的代理对象
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

其内部维护了一个 SqlSession的字段 sqlSessionProxy ,其赋值的是代理对象 SqlSessionInterceptor。 我们再来看下 SqlSessionInterceptor 的源码:

 private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 通过getSqlSession() 获取一个 SqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

我们发现其代理实现时,通过getSqlSession() 获取一个 全新的SqlSession。也就是说创建Mapper的SqlSession和会话请求的SqlSession不是同一个。这里就完美的解耦了Mapper和SqlSession,并且保障了每次会话SqlSession的生命周期范围。
这里超前提下:getSqlSession().getMapper() 其实 是通过 configuration.getMapper() 来获取的,那么就意味着 configuration内部必须添加了Mapper信息,那么configuration是何时添加的呢? 可以看下 MapperFactoryBean的checkDaoConfig()方法,源码如下:

  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();
    notNull(this.mapperInterface, "Property 'mapperInterface' is required");
    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Throwable t) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
        throw new IllegalArgumentException(t);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

由于父类实现了 InitializingBean 接口,并且其afterPropertiesSet() 调用了 checkDaoConfig() 方法 ,所以,至少在初始化创建MapperFactoryBean 时,就已经向 configuration内部必须添加了Mapper信息。

总结

  • SqlSessionFactoryBuilder: 用于创建 SqlSessionFactory
  • SqlSessionFactory: 用于创建 SqlSession
  • SqlSession: Mybatis工作的最顶层API会话接口,所有访问数据库的操作都是通过SqlSession来的
  • Configuration: 存放有所有的mybatis配置信息,包括mapper.xml、 mybatis-config.xml,Configuration对象的结构和xml配置⽂件的对象⼏乎相同
  • XMLConfigBuilder: 解析 mybatis-config.xml 配置并存放到Configuration中
  • XMLMapperBuilder: 解析 mapper.xml 配置并存放到Configuration中
  • SqlSessionFactoryBean: mybatis整合Spring时的 生成 SqlSessionFactory 的FactoryBean
  • MapperScannerConfigurer: mybatis整合Spring时的 实现方便加载Mapper接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean
  • MapperFactoryBean: 生成 Mapper 代理对象的FactoryBean
  • SqlSessionTemplate: 内部维护有 SqlSession 的代理对象,解耦Mapper和SqlSession的关键对象。

https://www.cnblogs.com/bug9/p/11793728.html https://www.cnblogs.com/dongying/p/4031382.html