mybatis学习笔记

https://github.com/mybatis

简介:
    Mybatis前身为Ibatis,IBatis由Clinton Begin(克林顿 比根)开发,后来捐给Apache开源基金会,成立了IBatis开源项目,2010年5月该项目由Apache基金会迁移到Google Code,并且改名为MyBatis,通俗说法,IBatis3.0版本以上就是MyBatis

    MyBatis是一个数据持久层(ORM)框架。把实体类和SQL语句之间建立了映射关系,是一种半自动化的ORM实现。

优点:
    1:基于SQL语句,简单易学
    2:能了解底层组装过程
    3:SQL语句封装在配置文件中,便于统一管理与维护,降低了程序的耦合度
    4:调试程序方便

与传统的JDBC方式比较
    1:减少了61%的代码量
    2:最简单的持久化框架
    3:架构级性能增强
    4:SQL代码从程序代码中分离,可重用
    5:增强了项目中的分工
    6:增强了移植性

与Hibernate的比较

    1:Mybatis
        1:是一个SQL语句映射的框架(工具)
        2:注重POJO与SQL之间的映射关系。不会为程序员在运行期自动生成SQL
        3:自动化程度低,手工映射SQL,灵活程度高
        4:需要开发人员熟练掌握SQL
    2:Hibernate
        1:主流的ORM框架,提供了从POJO到数据库表的全套映射机制
        2:会自动生成全套SQL语句
        3:因为自动化程度高,映射配置复杂,API也相对复杂,灵活性低
        4:开发人员不必关系SQL底层语句开发

    MyBatis通过SQL语句中AS来映射到实体类,Hibernate通过配置来映射


第一个程序
    所需jar包
        asm-3.3.1.jar
        cglib-2.2.2.jar
        commons-logging-1.1.1.jar
        javassist-3.17.1-GA.jar
        log4j-1.2.17.jar
        slf4j-api-1.7.5.jar
        slf4j-log4j12-1.7.5.jar
        mybatis-3.2.2.jar
        相应数据库jdbc驱动.jar
    配置一个configuration.xml

    基本要素
        1:configuration.xml全局配置文件
        2:导入生成的文件
            Mapper.xml用于存放sql
            Mapper接口,用于执行xml中的sql语句
            Model对应数据库表的对象,属性一致
        3:获取SqlSession接口
            InputStream is = Resources.getResourceAsStream(filename);
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
            SqlSession session = factory.openSession();
        4:根据mapper接口执行方法调用sql,获得执行结果
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            Student student = mapper.selectByPrimaryKey(1);
            System.out.println(student);
全局Configuration文件配置
    MyBatis的XML配置文件包含了设置和影响MyBatis行为的属性。XML配置文件的层次结构如下:
    configuration配置
        properties配置文件中属性值
        settings修改MyBatis在运行时的行为方式
        typeAliases为Java类型命名一个短的别名
        typeHandlers类型处理器
        objectFactory对象工厂
        plugins插件
        environments环境
            environment环境变量
                transactionManager事务管理器
                dataSource数据源
        mappers映射器


    properties配置文件中属性值,在整个配置文件中通过${}进行引用
        可以引入外部文件
        也可以通过子标签property设置值
        <properties resource="com/galaxy/***.xml">
            <property name="username" value="root" />
            <property name="password" value="root" />
        </properties>

    settings元素下是一些非常重要的设置元素,用于设置和改变MyBatis在运行时的行为方式。以下列出Settings元素支持的属性、默认值及其功能。
    参考网址:http://www.mybatis.org/mybatis-3/zh/configuration.html

    typeAliases     类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。例如:
        <typeAliases>
          <typeAlias alias="Author" type="domain.blog.Author"/>
          <typeAlias alias="Blog" type="domain.blog.Blog"/>
        </typeAliases>
    当这样配置时,Blog可以用在任何使用domain.blog.Blog的地方。

    也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:
    <typeAliases>
      <package name="domain.blog"/>
    </typeAliases>
    每一个在包 domain.blog 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。看下面的例子:
    @Alias("author")
    public class Author {
        ...
    }
    已经为许多常见的 Java 类型内建了相应的类型别名。它们都是大小写不敏感的,需要注意的是由基本类型名称重复导致的特殊处理。
        别名    映射的类型
        _byte    byte
        _long    long
        _short    short
        _int    int
        _integer    int
        _double    double
        _float    float
        _boolean    boolean
        string    String
        byte    Byte
        long    Long
        short    Short
        int    Integer
        integer    Integer
        double    Double
        float    Float
        boolean    Boolean
        date    Date
        decimal    BigDecimal
        bigdecimal    BigDecimal
        object    Object
        map    Map
        hashmap    HashMap
        list    List
        arraylist    ArrayList
        collection    Collection
        iterator    Iterator

    配置环境(environments)
    MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者共享相同 Schema 的多个生产数据库, 想使用相同的 SQL 映射。许多类似的用例。
    不过要记住:尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一。
    所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:
    每个数据库对应一个 SqlSessionFactory 实例
    为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment,properties);
    如果忽略了环境参数,那么默认环境将会被加载,如下所示:
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader,properties);
    环境元素定义了如何配置环境。
    <environments default="development">
      <environment id="development">
        <transactionManager type="JDBC">
          <property name="..." value="..."/>
        </transactionManager>
        <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>
    注意这里的关键点:
    默认的环境 ID(比如:default=”development”)。
    每个 environment 元素定义的环境 ID(比如:id=”development”)。
    事务管理器的配置(比如:type=”JDBC”)。
    数据源的配置(比如:type=”POOLED”)。
    默认的环境和环境 ID 是一目了然的。随你怎么命名,只要保证默认环境要匹配其中一个环境ID。

    事务管理器(transactionManager)
    在 MyBatis 中有两种类型的事务管理器(也就是 type=”[JDBC|MANAGED]”):
        JDBC – 这个配置就是直接使用了 JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
        MANAGED – 这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。例如:
        <transactionManager type="MANAGED">
              <property name="closeConnection" value="false"/>
        </transactionManager>
        NOTE如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器, 因为 Spring 模块会使用自带的管理器来覆盖前面的配置。


    数据源(dataSource)
    dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。
    许多 MyBatis 的应用程序将会按示例中的例子来配置数据源。然而它并不是必须的。要知道为了方便使用延迟加载,数据源才是必须的。
    有三种内建的数据源类型(也就是 type=”[UNPOOLED|POOLED|JNDI]”):
    UNPOOLED– 这个数据源的实现只是每次被请求时打开和关闭连接。虽然一点慢,它对在及时可用连接方面没有性能要求的简单应用程序是一个很好的选择。 不同的数据库在这方面表现也是不一样的,所以对某些数据库来说使用连接池并不重要,这个配置也是理想的。
    POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这是一种使得并发 Web 应用快速响应请求的流行处理方式。
    JNDI– 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。

    typeHandlers
    参考文档:http://blog.csdn.net/yuolligeng/article/details/48684245
    无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
    你可以重写类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 然后可以选择性地将它映射到一个 JDBC 类型。比如:
    public class ExampleTypeHandler extends BaseTypeHandler<String>
    <!-- mybatis-config.xml -->
    <typeHandlers>
      <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
    </typeHandlers>


    plugins插件
    MyBatis插件可以修改MyBatis内部的运行规则。
    通过拦截器实现(拦截器的作用:在某一动作执行之前、执行之后或执行到某一节点进行相应的处理)
    MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
    运行时:Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    参数相关的处理时:ParameterHandler (getParameterObject, setParameters)
    结果相关的处理时:ResultSetHandler (handleResultSets, handleOutputParameters)
    执行时:StatementHandler (prepare, parameterize, batch, update, query)

    1:实现Interceptor接口
    2:使用注解声明要拦截的方法
    3:在全局配置文件中进行注册

    @Intercepts({@Signature(
      type= Executor.class,        //目标类型
      method = "update",        //执行方法
      args = {MappedStatement.class,Object.class})})        //执行方法的参数
    public class ExamplePlugin implements Interceptor {

      //对方法进行拦截的抽象方法
      public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
      }
      //是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理
      public Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
      //用于在MyBatis配置文件中指定一些属性的
      public void setProperties(Properties properties) {
      }
    }
    <plugins>
      <plugin interceptor="org.mybatis.example.ExamplePlugin">
        <property name="someProperty" value="100"/>
      </plugin>
    </plugins>





映射XML文件
Select元素
Insert、Update、Delete元素
Sql元素
参数(Paramenters)
resultMap元素
Cache元素

MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。
MyBatis被创建来专注于SQL,但又给自己的实现极大的空间。
SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):
cache – 给定命名空间的缓存配置。
cache-ref – 其他命名空间缓存配置的引用。
resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
parameterMap – 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
sql – 可被其他语句引用的可重用语句块。
insert – 映射插入语句
update – 映射更新语句
delete – 映射删除语句
select – 映射查询语句


SELECT
查询语句是 MyBatis 中最常用的元素之一,光能把数据存到数据库中价值并不大,如果还能重新取出来才有用,多数应用也都是查询比修改要频繁。对每个插入、更新或删除操作,通常对应多个查询操作。这是 MyBatis 的基本原则之一,也是将焦点和努力放到查询和结果映射的原因。简单查询的 select 元素是非常简单的。
#{}    OGNL表达式,OGNL表达式支持SQL语句
#{0}第一个参数#{1}第二个参数
#{name}参数中hashmap的key或pojo中的属性


SQL
用来定义被其它语句引用的SQL语句块,可以包含在其他语句中。它可以被静态地(在加载参数) 参数化. 不同的属性值通过包含的实例变化. 比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>
属性值可以用于包含的refid属性或者包含的字句里面的属性值,例如:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>


resultMap 元素
是 MyBatis 中最重要最强大的元素。它就是让你远离 90%的需要从结果 集中取出数据的 JDBC 代码的那个东西, 而且在一些情形下允许你做一些 JDBC 不支持的事 情。 事实上, 编写相似于对复杂语句联合映射这些等同的代码, 也许可以跨过上千行的代码。 ResultMap 的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们 的关系。

constructor - 类在实例化时,用来注入结果到构造方法中
idArg - ID 参数;标记结果作为 ID 可以帮助提高整体效能
arg - 注入到构造方法的一个普通结果
id – 一个 ID 结果;标记结果作为 ID 可以帮助提高整体效能
result – 注入到字段或 JavaBean 属性的普通结果
association – 一个复杂的类型关联;许多结果将包成这种类型
嵌入结果映射 – 结果映射自身的关联,或者参考一个
collection – 复杂类型的集
嵌入结果映射 – 结果映射自身的集,或者参考一个
discriminator – 使用结果值来决定使用哪个结果映射
case – 基于某些值的结果映射
嵌入结果映射 – 这种情形结果也映射它本身,因此可以包含很多相 同的元素,或者它可以参照一个外部的结果映射。


有一个Product类,拥有无参构造函数和油两个参数的构造函数(分别为String id和String name),拥有三个属性:id,name,sort,和一个ProductType类型的属性productType
以下示例XML中的column必需为结果集中的列名,也可以使用association标签中的association属性指定一个resultMap
<resultMap type="com.galaxy.model.entity.Product" id="productMap">
    
    <constructor>
        <idArg column="c_id" javaType="java.lang.String" />
        <arg column="c_name" javaType="java.lang.String" />
    </constructor>
    <result column="c_sort" property="sort" jdbcType="INTEGER" />

    写法1
    <association property="productType" javaType="com.galaxy.model.entity.ProductType">
        <id column="c_product_type_id" property="id" />
        <result column="c_product_type_name" property="name" />
    </association>

    写法2
    <association property="productType" resultMap="ProductTypeMap">
    </association>

    写法3
    <association property="productType" javaType="com.galaxy.model.entity.ProductType" select="com.galaxy.dao.ProductTypeMapper.findById" column="c_product_type_id">
    </association>    

</resultMap>


<resultMap type="com.galaxy.model.entity.ProductType" id="productTypeMap">
    
    
    <id column="c_id" javaType="java.lang.String" />
    <result column="c_name" javaType="java.lang.String" />
    
    <collection property="product" ofType="com.galaxy.model.entity.Product">

        //以下property均对应Product中的属性
        <id column="c_product_type_id" property="productTypeId" />
        <result column="c_product_name" property="name" />
    </collection>

</resultMap>


Cache元素

需要用到的jar包
ehcache-core-2.6.8.jar
mybatis-ehcache-1.0.3.jar
默认一级缓存是打开的
要开启二级缓存需要在全局配置文件中使用setting标签设置cacheEnable为true,这样会使所有xml配置文件中的cache配置将生效
最后需要将session  commit或close二级缓存才会生效

MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis 3 中的缓存实现的很多改进都已经实现了,使得它更加强大而且易于配置。
默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环 依赖也是必须的。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:
<cache/>
字面上看就是这样。这个简单语句的效果如下:

映射语句文件中的所有 select 语句将会被缓存。
映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改。比如:
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>
  这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会 导致冲突。

可用的收回策略有:

LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认的是 LRU。

flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。


动态SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。
拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
通常使用动态 SQL 不可能是独立的一部分,MyBatis 当然使用一种强大的动态 SQL 语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中。
动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多的元素需要来了解。
MyBatis 3 大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。

if
choose (when, otherwise)
trim (where, set)
foreach


if
动态 SQL 通常要做的事情是有条件地包含 where 子句的一部分。比如:
以下方法需要有一个String title参数传入,若该参数不为空则进入if判断,还需要有一个Author类型的author参数,先判断该对象是否为空,再判断该对象的name属性是否为空
<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose, when, otherwise
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

where
where元素的作用是会在写入where元素的地方输出一个where,另外一个好处是你不需要考虑where元素里面的条件输出时什么样子的,
MyBatis会智能的帮你处理,如果所有的条件都不满足,那么MyBatis会查出所有的记录,如果输出后时AND开头的,那么MyBatis会把第一个AND忽略,
当然如果是OR开头的,MyBatis也会把它忽略,此外,在where元素中你不需要考虑空格的问题,MyBatis会职能的帮你加上。
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>


trim
使用prefix为条件语句加上前缀,使用prefixOverrides判断如果条件语句开头是以某些单词开头的则忽略这些单词,使用suffix为条件语句添加后缀,使用suffixOvrrides判断如果条件
语句结尾是以某些单词结尾的则忽略这些单词。
<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG WHERE state = ‘ACTIVE’
    <trim prefix="WHERE" prefixOverrides="AND |OR ">

        <if test="title != null">
            title = #{title}
        </if>

        <if test="summary != null">
            AND title = #{summary}
        </if>

        <if test="desc != null">
            AND title = #{desc}
        </if>
    
    </trim>

</select>


set在动态UPDATE语句里,相似的解决办法叫set,这个set元素能够动态的更新列。如:
<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>
以上等同于
<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

foreach
主要用于构建IN条件中,它可以在SQL语句中迭代一个集合。foreach元素的主要属性有item,index,collection,open,separator,close。
item表示集合中每一个元素进行迭代时的别名。
index指定一个名字,用于在迭代过程中,每次迭代到的位置。
open表示该语句从什么时候开始。
separator表示在每次进行迭代之间以什么符号作为分隔符。
close表示什么时候结束。
在时候foreach时,最关键的也是最容易出错的是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下三种情况:
1:如果传入的是单参数,且参数类型是一个List的时候,collection属性值为list
2:如果传入的是单参数且参数类型是一个array数组时,collection属性值为array
3:如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然,单参数也可以封装成Map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成
    一个Map的,Map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的Map里面的key

<foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
</foreach>

原文地址:https://www.cnblogs.com/ScvQ/p/6929099.html