MyBatis学习笔记

第一章 如何使用MyBatis?
1.导入MyBatis核心包及其依赖包
2.配置mybatis.xml文件
  (1).配置驱动管理器
      <transactionManager type="JDBC"/>
  (2).配置数据源
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mytest"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
  (3).配置映射文件
      <mappers>
         <mapper resource="映射文件路径">
         <!--例如: <mapper resource="com/xt/mapper/goodsMapper.xml"/>-->
      </mappers>
3.书写一个接口,包含将对数据库执行的操作方法
  例如:
    /**
     * 添加商品信息
     * @param goods
     * @return
     */
    public Integer addGoods(Goods goods);
4.配置映射文件(这里是真正书写sql语句的文件)
  包含的基本内容有对数据库进行增删改查的SQL语句。
  例如:
  <mapper namespace="接口所在的包名+类名">
    <!--insert语句-->
    <insert parameterType="sql语句中的参数类型">
       //sql语句
       insert into goods (goodsName,price,unit,madeTime,shui) values
       (#{goodsName},#{price},#{unit},#{date},#{shui}) 
       //#{}表示占位符;${}表示sql串拼接
    </insert>
    
    <!--delete语句-->
    <delete parameterType="">
      ...
    </delete>
    
    <!--查询语句-->
    <select parameterType="参数类型" resultType="查询结果数据类型">
       select 列名1,列名2... from 表名 where [条件]
       /**
         此处注意:
         如果数据库的列名与实体类的列名不一致,那么可以采用取别名的方式查询;
         后续章节将会使用<resultMap>
       **/
    </select>
  </mapper>
  
5.开始执行对数据库的操作
  (1).读取配置文件
    String resource = "mybatis.xml";  //配置文件的路径名称
    InputStream inputStream = Resources.getResourceAsStream(resource); //读取配置文件
  (2).获得session工厂,获得session
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
  (3).执行
    例如:
    IGoodsDaoMapper goodsDao = session.getMapper(IGoodsDaoMapper.class);
    goodsDao.insert(Goods goods);
  (4).session提交
    session.commit();/**对数据进行增删改时,一定要记得提交**/
    
第二章 查询详解(1)
1.在第一章中提到,在对数据库进行查询的时候,我们需要在查询语句中对查询的列名取别名,并且
  别名的名称要与java实体类中对应。在本章,我们采用<resultMap>来对数据库进行基本的查询。
  回顾第一章查询代码:
  //采用取别名的方式
  <select parameterType="java.lang.Integer" resultType="com.xt.vo.goods">
     select id as id,goods_name as goodsName,goods_price as goodsPrice from goods where id = #{id}
  </select>
  
  //使用<reusltMap></resultMap>
  例如:
  <reusltMap id="goodsResult" resultType="com.xt.co.goods">
    <id property="id" column="id"/>  //主键
    <result property="goodsName" column="goods_name"/>
    <result property="goodsPrice" column="goods_price"/>
                  ......
  </resultMap>
  //查询语句
  <select parameterType="java.lang.Integer" resultMap="goodsResult">
    select * from goods where id = #{id}
  </select>
  
  采用这种方式进行查询,可以避免每次查询都需要进行取别名的繁琐;并且使用<resultMap>可以
  单独提出,这样如果对一张表进行多次查询的时候,可以使用<resultMap>的id属性。这样,查询
  是不是变得简单多了呢?

第三章 查询详解(2)
1.在上一章内容中,我们讲到,可以使用<resultMap>标签来查询,省去了查询时取别名的麻烦。之前
  提到的查询都是对一张表进行简单查询。那么,对于两张表,甚至多张表进行查询时,我们往往会遇到
  "多对一"或者"一对多"的查询。
  例如:多个商品对应一种商品类型或者一种商品类型对应多个商品......
2.多对一的查询
  方式1:
  <association property="" column="" javaType="" select=""></association>
  property:实体类中对应的变量名称
  column:数据库中对应的列名
  javaType:该属性对应的实体类(包+类)
  select:对应的查询语句id
  例如:
  <resultMap id="goodsResult" type="com.xt.vo.Goods">
        <id property="goodsId" column="id" />
        <result property="goodsName" column="goodsName" />
        <result property="price" column="price" />
        <result property="unit" column="unit" />
        <result property="date" column="madeTime" />
        <result property="shui" column="shui" />
        <association property="goodsType" column="typeId" javaType="com.xt.vo.GoodsType" select="findGoodsTypeById">
    
        </association>
  </resultMap>
  <select id="findGoods" resultMap="goodsResult">
     select * from goods
  </select>
  <select id="findGoodsTypeById" parameterType="java.lang.Integer" resultMap="goodsTypeResult">
     select * from tb_type where id = #{id}
  </select>
  方式一的查询流程:
  1.首先将goods表中的数据全部查询出来,包括typeId
  2.将查询出来的每一个typeId作为参数传入到id="findGoodsTypeById"的<select>查询语句中查询,
    并且每一个typeId传入时,都会进行一次查询。假如有3个,就会查3次;有10000个,就会查10000次......
    这种方式显而易见是不够优秀的。
  
  方式2:
  <association property="" column="" javaType=""></association>
  例如:
  <resultMap id="goodsResult" type="com.xt.vo.Goods">
        <id property="goodsId" column="id" />
        <result property="goodsName" column="goodsName" />
        <result property="price" column="price" />
        <result property="unit" column="unit" />
        <result property="date" column="madeTime" />
        <result property="shui" column="shui" />
        <association property="goodsType" column="typeId" javaType="com.xt.vo.GoodsType">
            <id property="id" column="id"/>
            <result property="typeName" column="typeName"/>
        </association>
  </resultMap>
  <select id="findGoods" resultMap="goodsResult">
     select goods.*,tb_type.* from goods inner join tb_type on goods.typeId = tb_type.id
  </select>
  方式2显然是在sql语句中进行了改进,避免了产生多次查询的情况。
  
3.一对多的查询
  <collection property="" javaType="" ofType="">
    ......
  </collection>
  ofType:集合中元素类型
  例如:
  <resultMap id="typeResult" type="com.xt.vo.GoodsType">
    <id property="id" column="id"/>
    <result property="typeName" column="typeName"/>
    <collection property="goodsList" javaType="java.utils.ArrayList" ofType="com.xt.vo.Goods">
       <id property="goodsId" column="id" />
       <result property="goodsName" column="goodsName" />
    </collection>
  </resultMap>
  <select id="findGoodsType" parameterType="java.lang.String" resultMap="typeResult">
    select tb_type.*,goods.* from tb_type inner join goods on tb_type.id = goods.typeId
  </select>
  
第四章 查询详解(3)以及动态SQL
1.在对一张表进行查询的时候,我们通常会进行模糊查询。
  例如:
    <!-- 模糊查询 -->
    <select id="findGoodsByLikeName" parameterType="com.xt.vo.Goods" resultMap="goodsResult">
      select goods.* from goods
      where goodsName like '%${goodsName}%'
    </select>
    此处用到了${},而之前我们队参数的传入一直都是#{}。
    这里就涉及到了#{}与${}的区别:
    (1).#{}能够防止SQL注入,而${}不能
        在学习过程中,我们发现在一般查询过程中,#{}与${}似乎并没有什么区别,都能查询到我们
        想要的结果。
        例如:
        select * from goods where goodsName = #{goodsName}
        select * from goods where goodsName = '${goodsName}'
        但是在动态解析的时候,使用#{}时,会被解析成一个占位符,也就是:
        select * from goods where goodsName = ?
        而${}在动态解析的时候,传入的参数就会被当成一个普通的字符串常量,也就是:
        select * from goods where goodsName = '哇哈哈'
    (2).在用${}进行传参的时候,参数变量名必须是parameterType中的属性,并且该属性有对应的
        setter()和getter()方法。
        在刚才的模糊查询的例子中,我们发现,参数名称为goodsName,而parameterType是Goods实体类
        goodsName是Goods实体类中的属性。在这里,如果把goodsName换成name或者其他名称,就会报错。
        同样的,加入把parameterType换成其他类型,同样会报相同的错误。因为该类中没有对应的该属性
        的setter()和getter()方法。
2.动态SQL
  (1).if判断
    例如:
    <select id="findGoodsByName" parameterType="com.xt.vo.Goods">
       select * from Goods 
       <where>
          <if test="goodsName!=null">
              goodsName = #{goodsName}
          </if>
       </where>
    </select>
    属性:
    test:if判断的内容,并且test中的变量名称必须是parameterType中的属性,且有对应getter()
    
  (2).<foreach>循环
    例如:
    <!--一次插入多行-->
    <insert id="insertToType" parameterType="java.util.List">
      insert into tb_type values
      <foreach collection="list" item="typeName" separator=",">
        (default,#{typeName})
      </foreach>
    </insert>
    <!--一次传入多个参数--> 
    <select id="findGoodsByTypeIdList" parameterType="java.lang.Integer" resultMap="findGoodsResult">
      select goods.*,tb_type.* from goods inner join tb_type on goods.typeId = tb_type.id
       and typeId in
       <foreach collection="list" item="id" open="(" close=")" separator=",">
          #{id}
       </foreach>
   </select>
    属性:
    collection:被循环的类型
    item:元素名称
    separator:分隔符
    #{}中的名称必须与item相同,否则报错
    其他动态SQL标签可自行去官网查看。
    
第五章 MyBatis调用存储过程
    在实际开发中,我们常常会去调用存储过程,那么在MyBatis中,如何去调用存储过程呢?
    (1).对数据库进行增删改,且有返回值时,通常使用Map<Object,Object>做参数
        例如:
        Interface IGoodsTypeMapper中:
        public void findCntByName(Map<String,Object>) throws Exception;
        
        typeMapper.xml中:
        <select id="findCntByName" parameterType="java.util.Map" resultType="java.lang.Integer" useCache="false" statementType="CALLABLE">
          <![CDATA[
            {
               call P_FINDCNTBYNAME(
                 #{type_Name,mode=IN,jdbcType=VARCHAR},
                 #{cnt,mode=OUT,jdbcType=INTEGER}
               )
            }
          ]]>
        </select>
        
        测试类中:
        IGoodsMapper goodsMapper = session.getMapper(IGoodsMapper.class);
        Map<String,Object> parameter = new HashMap<String,Object>();
        parameter.put("type_name", "葫芦娃4");
        goodsMapper.getTypeCntByName(parameter);
        System.out.println("============>"+parameter.get("cnt"));
        
        在测试过程中的map集合,键的名称应当与typeMapper.xml中#{}的名称一致。即:
        parameter.put("type_name", "葫芦娃4");
         #{type_Name,mode=IN,jdbcType=VARCHAR}
        
        存储过程返回的值,会自动放入Map集合中,并且返回结果的键名称就是typeMapper.xml中的名字一致。
        
        
    (2).对于查询数据库的存储过程
        例如:
        Interface IGoodsTypeMapper中:
        public List<GoodsType> getTypeInfo() throws Exception;
        
        typeMapper中:
        <select id="getTypeInfo" resultMap="typeResult" statementType="CALLABLE" useCache="false">
          <![CDATA[
           {
             call P_FINDTYPEINFO()
           }
          ]]>
        </select>
        
        测试类中:
        List<GoodsType> typeList = goodsMapper.getTypeInfo();
        for(GoodsType types : typeList){
            System.out.println("类型编号:"+types.getId()+"\t"+"类型名称:"+types.getTypeName());
        }
    
第六章 MyBatis缓存机制
    MyBatis中包含了一个非常强大的缓存特性,并且配置使用非常方便。缓存能够极大地提升查询效率。
    MyBatis中的缓存分为两个级别的缓存,分别为:一级缓存、二级缓存。
    1.一级缓存
      一级缓存的作用域在sqlSession,默认情况下是开启的。
      二级缓存
      二级缓存的作用域在nameSpace,需要手动开启和配置。
    2.证明一级缓存的存在
      IGoodsMapper goodsDao = session.getMapper(IGoodsMapper.class);
      List<GoodsType> typeList1 = goodsDao.findType();
      List<GoodsType> typeList2 = goodsDao.findType();
      执行日志:
      11:25:58,099 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==>  Preparing: select tb_type.* from tb_type 
      11:25:58,187 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==> Parameters: 
      11:25:58,245 DEBUG com.xt.dao.IGoodsMapper.findType:145 - <==      Total: 28
      事实证明,对数据库进行两次查询,但是只发送了一次sql语句。说明第二次的查询结果是从缓存区中拿的。
      那么一级缓存在什么情况下会失效呢?
      (1).在同一个sqlSession执行查询的过程中,进行了增删改操作(强调:同一个sqlSession)。
          证明:
          IGoodsMapper goodsDao = session.getMapper(IGoodsMapper.class);
          //第一次查询
          List<GoodsType> typeList1 = goodsDao.findType();
          //增加一条记录
          List<String> typeNames = new ArrayList<String>();
          typeNames.add("boom");
          Integer flag = goodsDao.insertGoodsType(typeNames);
          session.commit();
          //第二次查询
          List<GoodsType> typeList2 = goodsDao.findType();
          执行日志:
          11:31:39,553 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==>  Preparing: select tb_type.* from tb_type 
          11:31:39,588 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==> Parameters: 
          11:31:39,630 DEBUG com.xt.dao.IGoodsMapper.findType:145 - <==      Total: 28
          11:31:39,652 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - ==>  Preparing: insert into tb_type values (default,?) 
          11:31:39,653 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - ==> Parameters: boom(String)
          11:31:39,654 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - <==    Updates: 1
          11:31:39,655 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:69 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c5a1b69]
          11:31:39,660 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==>  Preparing: select tb_type.* from tb_type 
          11:31:39,661 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==> Parameters: 
          11:31:39,667 DEBUG com.xt.dao.IGoodsMapper.findType:145 - <==      Total: 29
          事实证明:在第一次查询过后,进行了一次增加操作,那么第二次的查询结果并不是从缓存中拿的,而是又进行了一次查询。
          为什么要强调同一个sqlSession呢?
          刚才的例子中,都是同一个sqlSession,在进行了对数据库的更新后,第二次查询时,并没有从缓存中读取。
          现在看下一个例子:
          SqlSession session1 = sqlSessionFactory.openSession();
          SqlSession session2 = sqlSessionFactory.openSession();
          //第一个sqlSession1
          IGoodsMapper goodsDao1 = session1.getMapper(IGoodsMapper.class);
          //第二个sqlSession2
          IGoodsMapper goodsDao2 = session2.getMapper(IGoodsMapper.class);
          //sqlSession1第一次查询
          List<GoodsType> typeList1 = goodsDao1.findType();
          //sqlSession2添加一条数据
          List<String> typeNames = new ArrayList<String>();
          typeNames.add("boom1");
          Integer flag = goodsDao2.insertGoodsType(typeNames);
          session2.commit();
          //sqlSession1第二次查询
          List<GoodsType> typeList2 = goodsDao1.findType();
          //第一次查询结果
          for(GoodsType type1 : typeList1){
              System.out.println("类型名称:"+type1.getTypeName());
          }
          //第二次查询结果
          for(GoodsType type2 : typeList2){
              System.out.println("类型名称:"+type2.getTypeName());
          }
          查询日志:
          21:37:52,260 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==>  Preparing: select tb_type.* from tb_type 
          21:37:52,298 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==> Parameters: 
          21:37:52,355 DEBUG com.xt.dao.IGoodsMapper.findType:145 - <==      Total: 31
          21:37:52,384 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:136 - Opening JDBC Connection
          21:37:52,407 DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource:387 - Created connection 2028371466.
          21:37:52,408 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:100 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@78e67e0a]
          21:37:52,408 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - ==>  Preparing: insert into tb_type values (default,?) 
          21:37:52,409 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - ==> Parameters: boom1(String)
          21:37:52,413 DEBUG com.xt.dao.IGoodsMapper.insertGoodsType:145 - <==    Updates: 1
          21:37:52,414 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:69 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@78e67e0a]
          分析:很显然sqlSession1的第二次查询是从缓存区拿的数据,并没有重新发送sql语句,这样会产生脏读!
          这个例子证明了两点:
          (1).只有在同一个sqlSession中,在查询过程中对数据库进行增删改,一级缓存才会失效。
          (2).一级缓存只存在数据库内部共享。  
      (2).同一个sqlSession,但是查询条件不同。
      (3).同一个sqlSession,两次查询期间手动清空了缓存。
      
    3.如何开启二级缓存
      (1).在配置文件myBatis.xml中,手动开启。
          <settings>
            <setting name="logImpl" value="LOG4J"/>
            <!-- 开启二级缓存 -->
            <setting name="cacheEnabled" value="true"/>
          </settings>
      (2).在映射文件中配置二级缓存的属性
          <!-- 开启二级缓存 -->
          <cache eviction="LRU" flushInterval="120000" size="1024" readOnly="true"/>
          eviction:
          flushInterval:刷新缓存的时间间隔
          size:缓存对象的大小
          readOnly:是否只读
          
    4.证明二级缓存生效
       SqlSession session = sqlSessionFactory.openSession();
       IGoodsMapper goodsDao = session.getMapper(IGoodsMapper.class);
       List<GoodsType> typeList = goodsDao.findType();
       session.close();
       SqlSession session2 = sqlSessionFactory.openSession();
       IGoodsMapper goodsDao2 = session2.getMapper(IGoodsMapper.class);
       List<GoodsType> typeList2 = goodsDao2.findType();
       执行日志:
       17:27:37,453 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==>  Preparing: select tb_type.* from tb_type 
       17:27:37,484 DEBUG com.xt.dao.IGoodsMapper.findType:145 - ==> Parameters: 
       17:27:37,531 DEBUG com.xt.dao.IGoodsMapper.findType:145 - <==      Total: 32
       17:27:37,531 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:122 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2b552920]
       17:27:37,531 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:90 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@2b552920]
       17:27:37,531 DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource:344 - Returned connection 727001376 to pool.
       17:27:37,531 DEBUG com.xt.dao.IGoodsMapper:62 - Cache Hit Ratio [com.xt.dao.IGoodsMapper]: 0.5
       由上面的执行结果知道,第一次的查询,向数据库发送了sql语句;但是第二个查询,是直接从二级缓存中读取的数据。
       缓存命中为:0.5
       
第七章 MyBatis注解(1)
    在之前的学习过行程中,我们都采用的映射文件的方式来对数据库进行操作。MyBatis还未我们提供了注解,方便我们的使用。
    废话不多说,直接上菜。
    (1).在之前使用映射文件时,在配置文件mybatis.xml中的<mapper></mapper>中是这样的:
        <mappers>
           <mapper resource="com/xt/mapper/goodsMapper.xml"></mapper>
        </mappers>
        使用注解后:
        <mappers>
           <mapper class="com.xt.mapper.goodsMapper"/>
        </mappers>
    (2).在接口中使用注解
        例如:
        /*
         *查询商品类型信息
        */
        @Select("select * from goods")
        @Results(
            value={
                    @Result(id=true,property="goodsId",column="goodsId"),
                    @Result(property="goodsName",column="goodsName")
            }
        )
        public List<Goods> findGoods() throws Exception;
        
        /*
         *删除数据(只有一个参数)
        */
        @Delete("delete from tb_type where id = #{id}")
        public Integer delete(Integer typeId) throws Exception;
        
        /*
         *删除数据(多个参数)
        */
        @Delete("delete from tb_type where id = #{typeId} or typeName = #{typeName}")
        public Integer delete2(@Param("typeId") Integer typeId,@Param("typeName") String typeName) throws Exception;
        
        /*
         *添加一条数据(返回主键)
        */
        @Insert("insert into goods values (default,#{goodName},#{price},#{unit},#{date},#{shui},#{type.id})")
        @Option(useGeneratedKeys=true,keyProperty="id",keyColumn="id")
        public Integer insertGoodsInfo(Goods goods) throws Exception;
        注:
        当传递多个参数的时候,需要使用@Param来指明参数,否则会报错:参数找不到。

第八章 MyBatis注解(2)
    在之前的学习过程中,我们对数据库进行操作是通过映射文件,然后学习了注解。但是我们会发现,单纯的
    注解用来写SQL语句是不够的。所以本章就为大家介绍XXXXProvider的使用方法。
    由于MyBatis相对简单,所以直接上代码:
    (1).GoodsTypeDao.class中的代码:
    @SelectProvider(type=SqlProvider.class,method="selectType")
    @Results(
         value={
                 @Result(id=true,property="id",column="id"),
                 @Result(property="typeName",column="typeName")
         }        
    )
    public List<GoodsType> findType(@Param("id")Integer id,@Param("name")String typeName) throws Exception;
    
    (2).SqlProvider.class中的代码:
    public String selectType(Map<String,Object> param){
        return new SQL()
                .SELECT("*")
                .FROM("tb_type")
                .WHERE("id=#{id} or typeName = #{name}")
                .toString();
    }
    解析:
    type:返回sql语句的类
    method:返回sql语句的方法
    
    注:
    *如果只有一个参数,那么在selectType()中,只需传入一个指定类型的参数就行。在使用的过程中,
     在findType()中传入相应参数,该参数就会传入selectType()中,然后赋给sql语句中。
    *但是有多个参数时,按照刚才的方法就会报错。因此我们通常会在selectType()中传入一个Map<>集合。
     在findType()中,可以使用@Param为传入的参数取别名,这个别名就是Map<>集合中元素的键,在传入
     实参的时候的值就是Map<>集合中的键的值。
     
第九章 没有实体类的查询
    在之前的学习过程中,我们对数据库进行操作时,都是利用实体类,比如查询,返回的结果集是List<Goods>。
    那么在日常开发中,我们有时候并不会去写实体类,因为那样很麻烦。(当然,我们会从数据库反向生成实体类)
    那么如何在没有实体类的情况下,对数据库进行查询呢?
    直接看代码:
    GoodsMapper.class中:
    @SelectProvider(type=SqlProvider.class,method="searchGoodsInfo")
    @Results(
        value={
                @Result(id=true,property="goodsId",column="goodsId"),
                @Result(property="goodsName",column="goodsName")
        }        
    )
    public List<Map<?,?>> searchGoods() throws Exception;
    大家可以看到,这里返回的并不是List<Goods>,而是List<Map<?,?>>。
    
    测试类中:
    IGoodsMapper goodsDao = session.getMapper(IGoodsMapper.class);
    List<Map<?,?>> goodsList = goodsDao.searchGoods();
    for(Map<?,?> map : goodsList){
        System.out.println("商品名称:"+map.get("goodsName"));
    }
    由此可以看出:当从数据库查询到数据后,会将列名作为Map集合的键,值作为Map集合的值
    
    
        

        
        
        
        
  
  
       

  
  
  
  
  

 

posted @ 2018-05-03 15:16  Coder_Song  阅读(171)  评论(0编辑  收藏  举报