MyBatis映射配置文件

具体内容

映射文件指导着MyBatis如何进行数据库的增删改查,所以具有非常重要的意义,映射文件中包含的标签:

1. 一级标签

< mapper namespace=“对应接口的全类名”></ mapper>,表示我们的这个SQL映射文件式对应哪个接口的。也意味着一个接口就存在一个sql映射文件。

2. 二级标签

  • cache:配置二级缓存的标签
  • cache-ref:应用其他命名空间的二级缓存配置
  • resultMap:自定义映射结果集
  • sql:抽取可重用的sql片段
  • insert:新增操作
  • delete:删除操作
  • select:查询操作
  • parameterMap:已废弃的参数映射

3. 三级标签:

  • where
  • if
  • foreach
  • set
  • trim

增删改查

  • 在Dao接口中,定义增删改查方法。
	//新增用户
	public void insertEmp(Emp emp);
	//删除用户
	public void deleteEmp(Integer empId);
	//修改用户
	public void updateEmp(Emp emp);
	//查找用户
	public List<Emp> selectEmp();
	
  • 编写sql映射文件
<!-- 
		public void insertEmp(Emp emp);
		如果传入的参数为实体类的对象,那么在sql中,必须使用#{实体类JavaBean风格的属性名} 进行参数绑定
	 -->
	 <insert id = "insertEmp">
	 	insert into emp(emp_name,emp_mail,emp_gender,dept_id) values(#{empName},#{empMail},#{empGender},#{deptId})
	 </insert>
	 <!-- 
	 	public void deleteEmp(Integer empId)
	  -->
	 <delete id="deleteEmp">
	 	delete from emp where emp_id = #{empId}
	 </delete>
	<!-- 
		public void updateEmp(Emp emp);
	 -->
	<update id="updateEmp">
		update emp set emp_name = #{empName},emp_mail=#{empMail} where emp_id = #{empId}
	</update>
	<!-- 
		public List<Emp> selectEmp();
	 -->
	<select id="selectEmp" resultType="com.nhkj.entity.Emp">
		select * from emp
	</select >

如何知道增删改是否成功?
实际上MyBatis已经为我们封装好了Integer,int,Long,long,Boolean,boolean类型参数,如果我们接口中的增删改方法需要获取这些的参数,我们只需要修改增删改的方法,将返回值设置为对应类型的返回值即可,而且sql映射文件不必做任何修改。Integer,int,Long,long返回值类型返回的是影响数据的笔数,Boolean,boolean表示影响数据笔数为0的情况下返回false,否则返回true。

	public int insertEmp(Emp emp);
	
	public Long deleteEmp(Integer empId);
	
	public Integer updateEmp(Emp emp);

测试类

	@Test
	public void testEmpUpdate() throws Exception {
		InputStream input = Resources.getResourceAsStream("mybatis-conf.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(input);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		EmpDao empDao = sqlSession.getMapper(EmpDao.class);
		Emp emp = new Emp();
		emp.setEmpId(90);
		emp.setEmpName("张益达");
		emp.setEmpMail("[email protected]");
		Integer cont = empDao.updateEmp(emp); //调用接口中修改方法,并且回去相应的返回值
		System.out.println(cont + "=========================");// 结果 1=========================
		sqlSession.commit();
		sqlSession.close();
	}

获取自增主键的值

Mysql是支持主键自增的,我们在开发中,有这样的一个需求,传递一个没有主键的实体类对象进行新增操作,新增完成之后,需要这个实体类封装上主键值。

Emp -- > insert

Java程序中对象

新增之前
empe_id emp_name emp_mail emp_gender dept_id
null    张三	    zs@163.com	 1		  1
新增之后
empe_id emp_name emp_mail emp_gender dept_id
	10	张三	  zs@163.com	 1		  1
  • Mysql数据库的相应操作
<!-- 
		public void insertEmp(Emp emp);
		如果传入的参数为实体类的对象,那么在sql中,必须使用#{实体类JavaBean风格的属性名} 进行参数绑定
		获取自增主键的值:
			keyColumn : 数据表中的主键列名
			keyProperty : 对应主键实体类的属性名
			useGeneratedKeys : 表示是否通过自增主键获取主键值
				- true- false-->
	 <insert id = "insertEmp" keyColumn="emp_id" keyProperty="empId" useGeneratedKeys="true">
	 	insert into emp(emp_name,emp_mail,emp_gender,dept_id) values(#{empName},#{empMail},#{empGender},#{deptId})
	 </insert>

	//第二种方式:
	 <insert id = "insertEmp">
	 	<!-- 配置保存时获取插入的 id --> 
	 	<selectKey keyColumn="emp_id" keyProperty="empId" resultType="int">
			select last_insert_id();
		</selectKey>
	 	insert into emp(emp_name,emp_mail,emp_gender,dept_id) values(#{empName},#{empMail},#{empGender},#{deptId})
	 </insert>
  • Oracle中使用序列来完成自增主键值的获取

Oracle中没有主键自增,而是通过序列完成。

create table emp_721(
       emp_id number(11) primary key,
       emp_name varchar2(20) not null,
       emp_mail varchar2(30) not null,
       emp_gender number(1) not null,
       dept_id number(10) not null
);

select * from emp_721;

insert into emp_721 (emp_id,emp_name,emp_mail,emp_gender,dept_id)
values(1,'张大炮','[email protected]',1,1);

-- 以下sql报错,那么如何实现主键自增呢
insert into emp_721 (emp_name,emp_mail,emp_gender,dept_id)
values('张益达','[email protected]',1,1);

-- oracle数据库中要实现主键自增,我们必须使用序列
create sequence seq_2020721;

-- sequence查询
select seq_2020721.nextval from dual; -- 查询下一个值

select seq_2020721.currval from dual; -- 查询序列当前的使用的值

-- 所以开发中,Oracle数据库表数据新增,对NUmber的主键来说,都使用序列进行自增
insert into emp_721 (emp_id,emp_name,emp_mail,emp_gender,dept_id)
values(seq_2020721.nextval,'张大炮','[email protected]',1,1);

方式①:BEFORE版:先查询后新增数据

<!--
		Oracle环境下获取非自增主键的值 
		public void insertEmp(Emp emp);
	 -->
	 <insert id="insertEmp" databaseId="oracle">
	 	<!-- 
	 		selectKey : 表示查询一个值,作为新增数据的主键
	 			keyColumn : 数据表中的主键列名
				keyProperty : 对应主键实体类的属性名
				order:表示是在新增之前执行查询还是新增之后执行查询
					BEFORE : 表示新增之前
					AFTER : 表示新增之后
	 	 -->
	 	<selectKey keyColumn="emp_id" keyProperty="empId" resultType="int" order="BEFORE">
	 		select seq_2020721.nextval from dual
	 	</selectKey>
	 	insert into emp_721(emp_id,emp_name,emp_mail,emp_gender,dept_id) values(#{empId},#{empName},#{empMail},#{empGender},#{deptId})
	 </insert>

方式②:AFTER版:先新增数据,后查询

<!--
		Oracle环境下获取非自增主键的值 
		public void insertEmp(Emp emp);
	 -->
	 <insert id="insertEmp" databaseId="oracle">
	 	<selectKey keyColumn="emp_id" keyProperty="empId" resultType="int" order="AFTER">
	 		select seq_2020721.currval from dual
	 	</selectKey>
	 	insert into emp_721(emp_id,emp_name,emp_mail,emp_gender,dept_id) values(seq_2020721.nextval,#{empName},#{empMail},#{empGender},#{deptId})
	 </insert>

参数的处理

在正常的开发中,参数可能是单个参数,也可能是多个参数,也可能是实体类的对象,这些是较常见的,还有一些特殊的,比如参数是map键值对,是list,除了map和list之外还有其他的多个参数。

1. 单个参数(Integer empId):

单个参数MyBatis不会做任何特殊处理,在SQL映射文件中使用任意的参数名称都可以取得该参数,如:#{abc},但一般都要有语义#{empId}。

2. 多个参数(String empName,Integer pageCurrent,Integer pageSIze):
首先我们测试一下多个参数,看情况。

<!-- 
		public List<Emp> selectEmpByEmpNameLike(String empName,Integer startSize,Integer pageSize);
	 -->
	<select id="selectEmpByEmpNameLike" resultType="com.wanbangee.entities.Emp">
		select * from emp where emp_name like #{empName} limit #{startSize},#{pageSize}
	</select>

以上程序运行错误提示:Available parameters are [0, 1, 2, param3, param1, param2],这是因为在传递多个参数的情况下,MyBatis会将参数自动的封装成Map键值对,而且每一个参数都有两个键和两个值,这两个键不同,但是值相同,以上程序最终封装的键值对如下:

Key Value
0 第一个参数的值
param1 第一个参数的值
1 第二个参数的值
param2 第二个参数的值
2 第三个参数的值
param3 第三个参数的值

那么在我们的SQl映射文件中,#{key},在执行的时候就会传入相应的键对应的值。如下:

<!-- 
		public List<Emp> selectEmpByEmpNameLike(String empName,Integer startSize,Integer pageSize);
	 -->
	<select id="selectEmpByEmpNameLike" resultType="com.wanbangee.entities.Emp">
		select * from emp where emp_name like #{param1} limit #{1},#{param3}
	</select>

当然我们也可以给参数指定键(key),在接口方法的入参前使用@Param注解,给参数指定封装的Map的key,如下:

<!-- 
		public List<Emp> selectEmpByEmpNameLike(@Param("empName")String empName,@Param("startSize")Integer startSize,@Param("pageSize")Integer pageSize);
	 -->
	<select id="selectEmpByEmpNameLike" resultType="com.wanbangee.entities.Emp">
		select * from emp where emp_name like #{empName} limit #{startSize},#{pageSize}
	</select>

现在注解后可以在SQL映射文件中,使用指定注解的key获得参数的值,那么默认的param1…或者0,1…还能用吗?
答:Param1,param2…可以正常使用,0,1,2…不能使用了。

3. 传递的参数是一个实体类对象

会将参数封装成一个Map键值对,键对应的是实体类对象的属性名,值对应的是该实体类属性的值。比如,传递对象为Emp,则封装的Map兼职对为:

Key Value
empName empName属性值
empMail empMail的属性值
empId empId的属性值
empGender empGender的属性值
deptId deptId属性值
<insert id = "insertEmp">
	insert into emp(emp_name,emp_mail,emp_gender,dept_id) values(#{empName},#{empMail},#{empGender},#{deptId}) 
</insert>

4. Map作为参数(Map<String,Object>传入参数为Map的情况下,直接在sql映射文件中使用#{键} 就可以传递相应的值

5. List【Set,Array】作为参数(List< String >)

一个list参数是很好处理的,集合会被封装成键值对,键为list和collection,值为List集合的值,所以在sql映射文件中想要取得集合的某个位置的具体的值:#{list[索引位置]},#{collection[索引位置]},同理Set集合或者Array数据处理相似。

<!-- 
		public Emp selectEmpByID2(List<Integer> ids); 去主键集合中的第一个元素作为查询条件
	 -->
	 <select id="selectEmpByID2" resultType="com.wanbangee.entities.Emp">
	 	select * from emp where emp_id = #{collection[2]} 或者 #{list[2]}
	 </select>
<!-- 
		public Emp selectEmpByID2(Integer[] ids); 去主键集合中的第一个元素作为查询条件
	 -->
	 <select id="selectEmpByID2" resultType="com.wanbangee.entities.Emp">
	 	select * from emp where emp_id = #{array[2]}
	 </select>

6. Map,List,和其他参数(Map<String,Object>,List,String empName,Integer pageCurrent,Integer pageSIze):

会将参数封装成Map键值对,而且是符合②所述多个参数的封装规则。

<!-- 
		public List<Emp> selectEmpByEmpNameLike(String empName,Integer startSize,Integer pageSize,List<Integer> ids);
	 -->
	<select id="selectEmpByEmpNameLike" resultType="com.wanbangee.entities.Emp">
		select * from emp where emp_name like #{param1} and emp_id = #{param4[2]} limit #{1},#{2}
	</select>

#{}与${}的区别

  • #{}表示一个占位符号

通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,
#{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类
型值,#{}括号中可以是 value 或其它名称。
在这里插入图片描述

  • ${}表示拼接 sql 串

通过${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换, p o j o p a r a m e t e r T y p e {}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值, {}括号中只能是 value。
在这里插入图片描述

查询返回的结果(resultType)

查询记录返回List
如果查询的结果集为多笔数据【每一笔数据都对应实体类的一个对象】,那么我们可以使用List接收,那么每一条数据都会被分装为实体类的一个对象,而List中就是存放了查询结果集的多个对象。


public List<Emp> selectEmp();


<select id="selectEmp" resultType="com.wanbangee.entities.Emp" databaseId="mysql">
		select * from emp
	</select>

注意一点:requestType设置为实体类的全类名,而不是List全类名。

查询记录返回Map
在查询结果集映射为多个实体列对象情况下,不能使用Map接收,只有查询结果集为单行的情况下,才能使用Map接收。单笔数据的情况下,使用Map接收,Map的键为查询的数据列明,值为对应列明的查询结果。

<!-- public Map<String,Object> selectEmp(); -->

<!-- map是MyBatis中定义好的Map别名-->
	<select id="selectEmp" resultType="map">
		select * from emp where emp_id = 1
	</select>
Key Value
empName empName属性值
empMail empMail的属性值
empId empId的属性值
empGender empGender的属性值
deptId deptId属性值

查询结果集是单行单列
比如我们查询数据笔数,那么就是单行单列的值。这种情况下,可以直接将结果集定义为查询结果集的类型。

<!-- 
		public Integer selectEmpCount();
	 -->
	 <select id="selectEmpCount" resultType="int">
	 	select count(*) from emp
	 </select>

单行单列也可以使用Map接收:

<!-- 
		public Map<String,Object> selectEmpCount();
	 -->
	 <select id="selectEmpCount" resultType="map">
	 	select count(*) from emp
	 </select>

查询记录返回resultMap
在属性名和列名不同的情况下,我们解决映射关系的方式有两种:

  • 自动驼峰,但是在某些情况下,自动驼峰解决不了
    如,列名:empno, 属性名:empId
  • 查询的sql语句中,给列名取一个别名,别名可以映射上实体类的属性
    除了以上的两种解决方案,还有另外一种解决方案,叫做自定义结果集映射,这个时候必须使用resultMap标签来进行自定义。
<!-- 自定义封装结果集映射 
			type : 封装后结果集的类型全类名或者别名
			id : 表示自定义封装结果的id,唯一的
		id 标签: 表示自定义主键封装映射规则
		result 标签:表示自定义非主键封装映射规则
			- column:查询结果集的列名
			- property : 列明对应的属性名
		在select标签,如果要使用自定义封装结果集,必须使用resultMap属性声明,而且resultMap和resultType 不能同时存在
	-->
	<resultMap type="com.wanbangee.entities.Emp" id="myEmp">
		<id column="empno" property="empId"/>
		<result column="ename" property="empName"/>
	</resultMap>
	<!-- 
		public Emp selectEmpById(Integer id);
	 -->
	<select id="selectEmpById" resultMap="myEmp">
		select empno ,ename  from emp where empno = #{id}
	</select>

级联属性的封装

现在我们在Emp实体类中引用Dept,在查询Emp的同时,要求将对应引用的Dept属性也能够封装上,按照传统的写法如下:

<!-- 
		public Emp selectEmpById(Integer id);
	 -->
	<select id="selectEmpById" resultType="com.wanbangee.entities.Emp" databaseId="mysql">
		select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id,b.dept_id `dept.deptId`, b.dept_name `dept.deptName` from emp a
		left join dept b on a.dept_id = b.dept_id where emp_id = #{id}
	</select>

发现程序运行正常了,实际上我们还有一种写法,叫做使用resultMap自定义封装结果集:

<resultMap type="com.wanbangee.entities.Emp" id="myEmp">
		<id column="emp_id" property="empId"/>
		<result column = "emp_name" property="empName"/>
		<result column = "emp_mail" property="empMail"/>
		<result column = "emp_gender" property="empGender"/>
		<result column = "dept_id" property="deptId"/>
		<result column = "dept_id" property="dept.deptId"/>
		<result column = "dept_name" property="dept.deptName"/>
	</resultMap>
	<!-- 
		public Emp selectEmpById(Integer id);
	 -->
	<select id="selectEmpById" resultMap="myEmp" databaseId="mysql">
		select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id,b.dept_name  from emp a
		left join dept b on a.dept_id = b.dept_id where emp_id = #{id}
	</select>

association关联映射

Association是MyBatis中提供的一个多对一的关联映射的一个标签,使用在resultMap标签中,表示多对一的关系,在多的一端引用一的一端,比如雇员和部门的关系,每个雇员都存在于一个部门。现在查询雇员的同时要关联对应的部门,雇员对应的部门只有一个,这个时候我们就可以使用association进行关联映射:

<!-- 自定义封装结果集映射 
			type : 封装后结果集的类型全类名或者别名
			id : 表示自定义封装结果的id,唯一的
		id 标签: 表示自定义主键封装映射规则
		result 标签:表示自定义非主键封装映射规则
			- column:查询结果集的列名
			- property : 列明对应的属性名
		在select标签,如果要使用自定义封装结果集,必须使用resultMap属性声明,而且resultMap和resultType 不能同时存在
		
		association : 使用之后,可以将引用属性进行分装,比如Emp中封装dept属性
	-->
	<resultMap type="com.wanbangee.entities.Emp" id="myEmp">
		<id column="emp_id" property="empId"/>
		<result column="emp_name" property="empName"/>
		<result column="emp_mail" property="empMail"/>
		<result column="emp_gender" property="empGender"/>
		<result column="dept_id" property="deptId"/>
		<association property="dept" javaType="com.wanbangee.entities.Dept">
			<id column="dept_id" property="deptId"/>
			<result column="dept_name" property="deptName"/>
		</association>
	</resultMap>
	<!-- 
		public Emp selectEmpById(Integer id);
	 -->
	<select id="selectEmpById" resultMap="myEmp" databaseId="mysql">
		select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id,b.dept_name  from emp a
		left join dept b on a.dept_id = b.dept_id where emp_id = #{id}
	</select>

以上的查询虽然结果正确,但是在数据量特别大的时候,效率很低,因为关联查询一定存在笛卡尔乘积现象。所以后期开发中,几乎不会使用这种封装形式。而是使用分布查询。

association分步查询

Association是支持进行分布查询的,第一步先查询雇员信息,第二步查询对应 部门信息。

	<!-- 自定义封装结果集映射 
			
		association : 分步查询
			property : 设置需要封装的属性
			select : 调用其他的查询方法
			column : 查询所需要传递的参数
	-->
	<resultMap type="com.wanbangee.entities.Emp" id="myEmp">
		<id column="emp_id" property="empId"/>
		<result column="emp_name" property="empName"/>
		<result column="emp_mail" property="empMail"/>
		<result column="emp_gender" property="empGender"/>
		<result column="dept_id" property="deptId"/>
		<association property="dept"  select="com.wanbangee.dao.DeptDaoPlus.selectDeptByDeptId" column="dept_id">
		</association>
	</resultMap>
	<!-- 
		public Emp selectEmpById(Integer id);
	 -->
	<select id="selectEmpById" resultMap="myEmp" databaseId="mysql">
		select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id from emp a
	 	 where a.emp_id = #{id}
	</select>

在对应DeptDao和DeptDao.xml中写selectDeptByDeptId()方法和配置

	/**
	 * 根据部门id查询部门信息
	 * @param deptId
	 * @return
	 */
	public Dept selectDeptByDeptId(Integer deptId);

	<resultMap type="dept" id="deptMap">
		<id column="dept_id"  property="deptId"/>
		<result column="dept_name" property="deptName"/>
	</resultMap>
	
	<!-- 根据Id查询部门信息 -->
	<select id="selectDeptByDeptId" resultMap="deptMap">
		select * from dept where dept_id = #{deptId}
	</select>

association分步查询,延迟加载

延迟加载策略可以大大的提升数据库的查询性能,比如我们在查询Emp对象的时候,不应该将Dept查询出来,而是要在Emp对象需要使用Dept的时候,再进行查询部门,这种查询策略叫做延迟加载策略,又称按需加载,又称懒加载。默认情况下,MyBatis提供的分布查询策略就是即时加载,所以我们要通过配置开启延迟加载策略。
①:改变MyBatis运行时的行为,表示要开启延迟加载


<!-- 全局配置文件 开启延迟加载策略 -->
		<setting name="lazyLoadingEnabled" value="true"/>
		<setting name="aggressiveLazyLoading" value="false"/>

添加了上述配置之后,我们分布查询就是延迟加载策略了,那么在开启了延迟加载策略之后,某些配置需要即时加载,有怎么配置呢?我们可以在association标签中配置fetchType属性

<!-- 自定义封装结果集映射 
			type : 封装后结果集的类型全类名或者别名
			id : 表示自定义封装结果的id,唯一的
		id 标签: 表示自定义主键封装映射规则
		result 标签:表示自定义非主键封装映射规则
			- column:查询结果集的列名
			- property : 列明对应的属性名
		在select标签,如果要使用自定义封装结果集,必须使用resultMap属性声明,而且resultMap和resultType 不能同时存在
		
		association : 使用之后,可以将引用属性进行分装,比如Emp中封装dept属性
		association : 分步查询
			property : 设置需要封装的属性
			select : 调用其他的查询方法
			column : 查询所需要传递的参数
			fetchType : 设置提取策略
				- lazy : 默认为延迟加载
				- eager : 即时加载
	-->
	<resultMap type="com.wanbangee.entities.Emp" id="myEmp">
		<id column="emp_id" property="empId"/>
		<result column="emp_name" property="empName"/>
		<result column="emp_mail" property="empMail"/>
		<result column="emp_gender" property="empGender"/>
		<result column="dept_id" property="deptId"/>
		<association property="dept"  select="com.wanbangee.dao.DeptDaoPlus.selectDeptByDeptId" column="dept_id" fetchType="eager">
		</association>
	</resultMap>

collection分布查询及延迟加载

<!-- 
		public Dept selectDeptByDeptId(Integer deptId);
	 -->
	 
	 <resultMap type="com.wanbangee.entities.Dept" id="myDept">
	 	<id column="dept_id" property="deptId"/>
	 	<result column="dept_name" property="deptName"/>
	 	<collection property="emps" select="com.wanbangee.dao.EmpDaoPlus.selectEmpByDeptId" column="dept_id" javaType="java.util.List" >
	 	</collection>
	 </resultMap>
	 
	 <select id="selectDeptByDeptId" resultMap="myDept">
	 	select * from dept where dept_id = #{deptId}
	 </select>

相应是编写empDao中的查询

	/**
	 * 根据部门id查询员工的信息 
	 * @param EmpId
	 * @return
	 */
	public Emp selectEmpByDeptId(Integer deptId);


	//配置
	!-- 根据部门Id查询员工信息 -->
	<select id="selectEmpByDeptId" resultType="com.nhkj.entity.Emp">
		select * from emp where dept_id = #{deptId}
	</select>
	

分布查询传递多列值的情况

不管是association还是collection,上面的程序只传递了一列值作为分布查询的查询条件参数,在开发中,还存在另外一种情况,分布查询值传递两个参数,也就意味着传递两列值作为分布查询的查询条件参数

 <resultMap type="com.wanbangee.entities.Dept" id="myDept">
	 	<id column="dept_id" property="deptId"/>
	 	<result column="dept_name" property="deptName"/>
	 	<!-- 
	 		在传递多列值的情况下使用{abc=dept_id,bcd=dept_name},根据我们参数规则,
	 		多个参数在MyBatis中会封装了Map键值对,此时Map封装的效果如下:
	 		key ======================value
	 		abc					  dept_id数据列的值
	 		bcd					  dept_name数据列的值
	 	 -->
	 	<collection property="emps" select="com.wanbangee.dao.EmpDaoPlus.selectEmpByDeptId" 
	 		column="{abc=dept_id,bcd=dept_name}" javaType="java.util.List" >
	 	</collection>
	 </resultMap>

猜你喜欢

转载自blog.csdn.net/qq_44866169/article/details/107586267