第1章MyBatis框架配置文件详解
1.1 typeHandlers类型转换器
每当MyBatis 设置参数到PreparedStatement 或者从ResultSet 结果集中取得值时,就会使用TypeHandler 来处理数据库类型与java 类型之间转换。下表描述了默认
TypeHandlers

1.1.1 自定义类型转换器
假设表中字段是int类型,而实体类与之对应的属性是boolean类型,此时可以采用自定义类型转换器进行对应
(1)实体类
1 package com.chenyanbin.beans;
2
3 public class Dept {
4 private Integer deptNo;
5 private String dname;
6 private String loc;
7 private boolean flag;
8 public Integer getDeptNo() {
9 return deptNo;
10 }
11 public boolean isFlag() {
12 return flag;
13 }
14 public void setFlag(boolean flag) {
15 this.flag = flag;
16 }
17 public void setDeptNo(Integer deptNo) {
18 this.deptNo = deptNo;
19 }
20 public String getDname() {
21 return dname;
22 }
23 public void setDname(String dname) {
24 this.dname = dname;
25 }
26 public String getLoc() {
27 return loc;
28 }
29 public void setLoc(String loc) {
30 this.loc = loc;
31 }
32 }
(2) 表中字段

(3) 开发自定义类型转换器:MyTypeHandler.java
继承并实现接口:TypeHandler.java
1 package com.chenyanbin.util;
2
3 import java.sql.CallableStatement;
4 import java.sql.PreparedStatement;
5 import java.sql.ResultSet;
6 import java.sql.SQLException;
7
8 import org.apache.ibatis.jdbc.Null;
9 import org.apache.ibatis.type.JdbcType;
10 import org.apache.ibatis.type.TypeHandler;
11 /*
12 * setParameter:这个方法在生成SQL语句时才被调用
13 *
14 * getResult:查询结束之后,在将ResultSet数据行转换为实体类对象时,通知TypeHandler将当前数据行某个字段转换为何种类型
15 *
16 *
17 */
18 public class MyTypeHandler implements TypeHandler {
19
20 public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
21 if (parameter==null) { //dept.flag=null insertsql flag设置0
22 ps.setInt(i, 0);
23 return;
24 }
25 Boolean flag=(Boolean)parameter;
26 if (flag==true) {
27 ps.setInt(i, 1);
28 }
29 else {
30 ps.setInt(i, 0);
31 }
32 }
33
34 public Object getResult(ResultSet rs, String columnName) throws SQLException {
35 int flag = rs.getInt(columnName); //1 or 0
36 Boolean myFlag=Boolean.FALSE;
37 if (flag==1) {
38 myFlag=Boolean.TRUE;
39 }
40 return myFlag;
41 }
42
43 public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
44 // TODO Auto-generated method stub
45 return null;
46 }
47
48 public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
49 // TODO Auto-generated method stub
50 return null;
51 }
52
53 }
(4) 在MyBatis核心配置文件注册自定义类型转换器:
myBatis-config.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE configuration
3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-config.dtd">
5 <configuration>
6 <!-- 属性配置 -->
7 <properties resource="config.properties"></properties>
8 <!-- 别名配置 -->
9 <typeAliases>
10 <package name="com.chenyanbin.beans" />
11 <package name="com.chenyanbin.dao" />
12 </typeAliases>
13 <!-- 类型处理器 -->
14 <typeHandlers>
15 <!-- 从java中的Boolean转jdbc中的NUMERIC -->
16 <typeHandler handler="com.chenyanbin.util.MyTypeHandler"
17 javaType="Boolean" jdbcType="NUMERIC" />
18 </typeHandlers>
19 <!-- 环境配置 -->
20 <environments default="development">
21 <!-- 环境配置 -->
22 <environment id="development">
23 <!-- 事务管理器 -->
24 <transactionManager type="JDBC"></transactionManager>
25 <!-- 数据源 -->
26 <dataSource type="pooled">
27 <property name="driver" value="${jdbc.driver}" />
28 <property name="url" value="${jdbc.url}" />
29 <property name="username" value="${jdbc.username}" />
30 <property name="password" value="${jdbc.password}" />
31 </dataSource>
32 </environment>
33 </environments>
34 <!-- 映射器 -->
35 <mappers>
36 <package name="com.chenyanbin.dao" />
37 </mappers>
38 </configuration>
config.properties
1 jdbc.driver=com.mysql.jdbc.Driver 2 jdbc.url=jdbc:mysql://localhost:3306/sam 3 jdbc.username=root 4 jdbc.password=root
(5) 创建接口:DeptMapper.java
1 package com.chenyanbin.dao;
2
3 import java.util.List;
4 import com.chenyanbin.beans.Dept;
5
6 public interface DeptMapper {
7 public void deptSave(Dept dept);
8
9 public List<Dept> deptFind();
10 }
(6) DeptMapper.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE mapper
3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="com.chenyanbin.dao.DeptMapper">
6 <insert id="deptSave">
7 insert into dept (DEPTNO,DNAME,LOC,flag)
8 values(#{deptNo},#{dname},#{loc},#{flag})
9 </insert>
10 <select id="deptFind" resultType="Dept">
11 select deptNo,dname,loc,flag from dept
12 </select>
13 </mapper>

1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE configuration
3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-config.dtd">
5 <configuration>
6 <!-- 属性配置 -->
7 <properties resource="config.properties"></properties>
8 <!-- 别名配置 -->
9 <typeAliases>
10 <package name="com.chenyanbin.beans" />
11 <package name="com.chenyanbin.dao" />
12 </typeAliases>
13 <!-- 环境配置 -->
14 <environments default="development">
15 <!-- 环境配置 -->
16 <environment id="development">
17 <!-- 事务管理器 -->
18 <transactionManager type="JDBC"></transactionManager>
19 <!-- 数据源 -->
20 <dataSource type="pooled">
21 <property name="driver" value="${jdbc.driver}" />
22 <property name="url" value="${jdbc.url}" />
23 <property name="username" value="${jdbc.username}" />
24 <property name="password" value="${jdbc.password}" />
25 </dataSource>
26 </environment>
27 </environments>
28 <!-- 映射器 -->
29 <mappers>
30 <package name="com.chenyanbin.dao" />
31 </mappers>
32 </configuration>

1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE mapper
3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="com.chenyanbin.dao.DeptMapper">
6 <insert id="deptSave">
7 insert into dept (DEPTNO,DNAME,LOC,flag)
8 values(#{deptNo},#{dname},#{loc},#{flag})
9 </insert>
10 <resultMap type="dept" id="deptMap">
11 <result column="flag" property="flag" typeHandler="com.chenyanbin.util.MyTypeHandler"/>
12 </resultMap>
13 <select id="deptFind" resultType="Dept">
14 select deptNo,dname,loc,flag from dept
15 </select>
16 </mapper>
(7) 执行单元测试:TestMain_01.java
1 package com.chenyanbin.test;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.List;
6 import org.apache.ibatis.io.Resources;
7 import org.apache.ibatis.session.SqlSession;
8 import org.apache.ibatis.session.SqlSessionFactory;
9 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
10 import org.junit.After;
11 import org.junit.Before;
12 import org.junit.Test;
13 import com.chenyanbin.beans.Dept;
14 import com.chenyanbin.dao.DeptMapper;
15
16 public class TestMain_01 {
17 private SqlSession session;
18
19 @Before
20 public void Start() {
21 try {
22 InputStream inputStream = Resources.getResourceAsStream("myBatis-config.xml");
23 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
24 session = factory.openSession();
25 } catch (Exception e) {
26 e.printStackTrace();
27 }
28 }
29
30 @After
31 public void end() {
32 if (session == null) {
33 session.close();
34 }
35 }
36
37 @Test
38 public void test01() throws IOException {
39 Dept d2 = new Dept();
40 d2.setDname("上海事业部");
41 d2.setLoc("上海");
42 d2.setFlag(false);
43 session.insert("deptSave", d2);
44 session.commit();
45 session.close();
46 }
47
48 @Test
49 public void test02() {
50 DeptMapper dao=session.getMapper(DeptMapper.class);
51 List<Dept> deptList=dao.deptFind();
52 System.out.println("ok");
53 }
54
55 }
(8) 项目目录结构

1.2 objectFactory 对象工厂
MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。
1.2.1 自定义对象工厂
表结构:dept

实体类:Dept.java
1 package com.chenyanbin.beans;
2
3 public class Dept {
4 private Integer deptNo;
5 private String dname;
6 private String loc;
7 private Boolean flag;
8 private String country;
9 public String getCountry() {
10 return country;
11 }
12 public void setCountry(String country) {
13 this.country = country;
14 }
15 public Integer getDeptNo() {
16 return deptNo;
17 }
18 public Boolean getFlag() {
19 return flag;
20 }
21 public void setFlag(Boolean flag) {
22 this.flag = flag;
23 }
24 public void setDeptNo(Integer deptNo) {
25 this.deptNo = deptNo;
26 }
27 public String getDname() {
28 return dname;
29 }
30 public void setDname(String dname) {
31 this.dname = dname;
32 }
33 public String getLoc() {
34 return loc;
35 }
36 public void setLoc(String loc) {
37 this.loc = loc;
38 }
39 }
(1) 继承与DefaultObjectFactory:MyObjectFactory.java
1 package com.chenyanbin.util;
2
3 import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
4
5 import com.chenyanbin.beans.Dept;
6
7 public class MyObjectFactory extends DefaultObjectFactory {
8
9 @Override
10 public Object create(Class type) {// 重新定义Dept类实例对象创建规则,其他类实例对象创建规则不想改变
11 if (Dept.class == type) {
12 // 依靠父类提供create方法创建Dept对象
13 Dept dept = (Dept) super.create(type);
14 // 设置自定义规则
15 dept.setCountry("China");
16 return dept;
17 }
18 return super.create(type);
19 }
20
21 }
(2) 在MyBatis核心文件中注册自定义工厂
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE configuration
3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-config.dtd">
5 <configuration>
6 <!-- 属性配置 -->
7 <properties resource="config.properties"></properties>
8 <!-- 别名配置 -->
9 <typeAliases>
10 <package name="com.chenyanbin.beans" />
11 <package name="com.chenyanbin.dao" />
12 </typeAliases>
13 <!-- ObjectFactory对象工厂 -->
14 <objectFactory type="com.chenyanbin.util.MyObjectFactory"></objectFactory>
15 <!-- 类型处理器 -->
16 <!-- <typeHandlers>
17 从java中的Boolean转jdbc中的NUMERIC
18 <typeHandler handler="com.chenyanbin.util.MyTypeHandler"
19 javaType="Boolean" jdbcType="NUMERIC" />
20 </typeHandlers> -->
21 <!-- 环境配置 -->
22 <environments default="development">
23 <!-- 环境配置 -->
24 <environment id="development">
25 <!-- 事务管理器 -->
26 <transactionManager type="JDBC"></transactionManager>
27 <!-- 数据源 -->
28 <dataSource type="pooled">
29 <property name="driver" value="${jdbc.driver}" />
30 <property name="url" value="${jdbc.url}" />
31 <property name="username" value="${jdbc.username}" />
32 <property name="password" value="${jdbc.password}" />
33 </dataSource>
34 </environment>
35 </environments>
36 <!-- 映射器 -->
37 <mappers>
38 <package name="com.chenyanbin.dao" />
39 </mappers>
40 </configuration>
1.3 Plugins 拦截器
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。
对于拦截器Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。我们先来看一下这个接口的定义:

我们可以看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。
定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。
对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。
对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。
创建自己的拦截器:MySimpleInterceptor.java
1 package com.chenyanbin.util;
2
3 import java.util.Properties;
4
5 import org.apache.ibatis.executor.Executor;
6 import org.apache.ibatis.mapping.MappedStatement;
7 import org.apache.ibatis.plugin.Interceptor;
8 import org.apache.ibatis.plugin.Intercepts;
9 import org.apache.ibatis.plugin.Invocation;
10 import org.apache.ibatis.plugin.Plugin;
11 import org.apache.ibatis.plugin.Signature;
12 import org.apache.ibatis.session.ResultHandler;
13 import org.apache.ibatis.session.RowBounds;
14
15 @Intercepts({ @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,
16 RowBounds.class, ResultHandler.class }) })
17 public class MySimpleInterceptor implements Interceptor {
18 /*
19 * 参数:Invocation {代理对象,被监控的方法对象,当前被监控方法运行时需要实参}
20 */
21 public Object intercept(Invocation invocation) throws Throwable {
22 // TODO Auto-generated method stub
23 System.out.println("被拦截方法执行之前,做的辅助服务。。。。。");
24 Object object = invocation.proceed(); // 执行被拦截方法
25 System.out.println("被拦截方法执行之后,做的辅助服务。。。。。");
26 return object;
27 }
28
29 /*
30 * 参数:target 表示被拦截的对象,应该是Executor接口实例对象 作用: 如果 被拦截的对象所在的类是有实现接口就为当前拦截对象生成一个代理对象
31 * 如果被拦截的对象所在的类没有指定接口,这个对象之后的行为不会被代理操作
32 */
33 public Object plugin(Object target) {
34 // TODO Auto-generated method stub
35 return Plugin.wrap(target, this);
36 }
37
38 public void setProperties(Properties properties) {
39 // TODO Auto-generated method stub
40
41 }
42
43 }
MyBatis核心配置文件:myBatis-config.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE configuration
3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-config.dtd">
5 <configuration>
6 <!-- 属性配置 -->
7 <properties resource="config.properties"></properties>
8 <!-- 别名配置 -->
9 <typeAliases>
10 <package name="com.chenyanbin.beans" />
11 <package name="com.chenyanbin.dao" />
12 </typeAliases>
13 <!-- ObjectFactory对象工厂 -->
14 <objectFactory type="com.chenyanbin.util.MyObjectFactory"></objectFactory>
15 <!-- Plugins拦截器 -->
16 <plugins>
17 <plugin interceptor="com.chenyanbin.util.MySimpleInterceptor"></plugin>
18 </plugins>
19 <!-- 类型处理器 -->
20 <!-- <typeHandlers> 从java中的Boolean转jdbc中的NUMERIC <typeHandler handler="com.chenyanbin.util.MyTypeHandler"
21 javaType="Boolean" jdbcType="NUMERIC" /> </typeHandlers> -->
22 <!-- 环境配置 -->
23 <environments default="development">
24 <!-- 环境配置 -->
25 <environment id="development">
26 <!-- 事务管理器 -->
27 <transactionManager type="JDBC"></transactionManager>
28 <!-- 数据源 -->
29 <dataSource type="pooled">
30 <property name="driver" value="${jdbc.driver}" />
31 <property name="url" value="${jdbc.url}" />
32 <property name="username" value="${jdbc.username}" />
33 <property name="password" value="${jdbc.password}" />
34 </dataSource>
35 </environment>
36 </environments>
37 <!-- 映射器 -->
38 <mappers>
39 <package name="com.chenyanbin.dao" />
40 </mappers>
41 </configuration>
单元测试
1 package com.chenyanbin.test;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.List;
6 import org.apache.ibatis.io.Resources;
7 import org.apache.ibatis.plugin.Interceptor;
8 import org.apache.ibatis.session.SqlSession;
9 import org.apache.ibatis.session.SqlSessionFactory;
10 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
11 import org.junit.After;
12 import org.junit.Before;
13 import org.junit.Test;
14 import com.chenyanbin.beans.Dept;
15 import com.chenyanbin.dao.DeptMapper;
16
17 public class TestMain_01 {
18 private SqlSession session;
19
20 @Before
21 public void Start() {
22 try {
23 InputStream inputStream = Resources.getResourceAsStream("myBatis-config.xml");
24 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
25 session = factory.openSession();
26 } catch (Exception e) {
27 e.printStackTrace();
28 }
29 }
30
31 @After
32 public void end() {
33 if (session == null) {
34 session.close();
35 }
36 }
37
38 @Test
39 public void test01() throws IOException {
40 Dept d2 = new Dept();
41 d2.setDname("上海事业部");
42 d2.setLoc("上海");
43 d2.setFlag(false);
44 session.insert("deptSave", d2);
45 session.commit();
46 session.close();
47 }
48
49 @Test
50 public void test02() {
51 Interceptor ccInterceptor;
52 DeptMapper dao=session.getMapper(DeptMapper.class);
53 List<Dept> deptList=dao.deptFind();
54 System.out.println("ok");
55 }
56
57 }


1 ##define an appender named console 2 log4j.appender.console=org.apache.log4j.ConsoleAppender 3 #The Target value is System.out or System.err 4 log4j.appender.console.Target=System.out 5 #set the layout type of the apperder 6 log4j.appender.console.layout=org.apache.log4j.PatternLayout 7 #set the layout format pattern 8 log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n 9 10 ##define a logger 11 log4j.rootLogger=debug,console
MyBatis自定义拦截器,可以拦截接口只有四种.
- Executor.class
- StatementHandler.class
- ParameterHandler.class
- ResultSetHandler.class
第二章 MyBatis框架Mapper配置文件详解
2.1 参数(#{参数名})
#{}实现的是向prepareStatement中的预处理语句中设置参数值,sql语句中#{}表示一个占位符即?

使用#{参数名},将参数的内容添加到sql语句中指定位置.
如果当前sql语句中只有一个参数,此时参数名称可以随意定义
但是,如果当前sql语句有多个参数,此时参数名称应该是与当前表关联[实体类的属性名]或则[Map集合关键字]

上述SQL语句在调用时,我们可以分别采用如下两种方式输入参数

使用#{}读取实体类对象属性内容

使用#{}读取map集合中关键字的值
2.2 #{}和${}区别
在MyBatis中提供了两种方式读取参数的内容到SQL语句中,分别是
#{参数名} :实体类对象或则Map集合读取内容
${参数名} :实体类对象或则Map集合读取内容
为了能够看到两种方式的区别,需要看到MyBatis执行时输送的SQL情况.因此
需要借助Log4J日志进行观察
第一步: 加载Log4j日志工具包到项目

第二步:将Log4j配置文件添加到src/main/resources下

接下来,我们可以查看

输出结果

从这里我们可以看出两者的区别:
#{} : 采用预编译方式,可以防止SQL注入
${}: 采用直接赋值方式,无法阻止SQL注入攻击
在大多数情况下,我们都是采用#{}读取参数内容.但是在一些特殊的情况下,我们还是需要使用${}读取参数的.
比如 有两张表,分别是emp_2017 和 emp_2018 .如果需要在查询语句中动态指定表名,就只能使用${}
<select>
select * from emp_${year}
<select>
再比如.需要动态的指定查询中的排序字段,此时也只能使用${}
<select>
select * from dept order by ${name}
</select>
简单来说,在JDBC不支持使用占位符的地方,都可以使用${}
2.3 resultMap
MyBatis框架中是根据表中字段名到实体类定位同名属性的.如果出现了实体类属性名与表中字段名不一致的情况,则无法自动进行对应.此时可以使用resultMap来重新建立实体类与字段名之间对应关系.

2.4 Sql标签
首先,我们如下两条SQL映射

这两条查询映射中要查询的表以及查询的字段是完全一致的.因此可以<sql>标签
将[select * from dept]进行抽取.

在需要使用到这个查询的地方,通过<include>标签进行引用

第三章 MyBatis动态SQL
3.1 什么是MyBatis动态SQL
根据用户提供的参数,动态决定查询语句依赖的查询条件或则SQL语句的内容
3.2 动态SQL依赖标签
- if
- choose、when、otherwise
- trim、where、set
- foreach
if使用

choose、when、otherwise
类似于Java中的switch case default. 只有一个条件生效,也就是只执行满足的条件when,没有满足的条件就执行otherwise,表示默认条件

when的使用

set使用
会在成功拼接的条件前加上SET单词且最后一个”,”号会被无视掉

trim使用

foreach的使用
foreach标签用于对集合内容进行遍历,将得到内容作为SQL语句的一部分.
在实际开发过程中主要用于in语句的构建和批量添加操作
foreach元素的属性主要有 item,index,collection,open,separator,close。

案例1.使用foreach实现批处理添加

案例2.使用foreach遍历list集合作为查询条件

案例3.使用foreach遍历数组作为查询条件

案例4.使用foreach遍历Map作为查询条件

-------------------------------------------
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,关注一波行不行,哈哈哈(っ•̀ω•́)っ✎⁾⁾!
联系方式:个人微信、QQ

