6.Mybatis面试题
- 1.什么是Mybatis?为什么说Mybatis是半自动的ORM框架?
- 2.Mybatis中mapper文件书写sql语句的时候#和$的区别。
- 3.通常一个mapper.xml文件,都会对应一个Dao接口,这个Dao接口的工作原理是什么?Dao接口里的方法能重载吗?
- 4.Mybatis都有哪些Executor执行器?它们之间的区别是什么?Mybatis中如何指定使用哪一种Executor执行器?
- 5.Mybatis中的一级缓存和二级缓存。
- 6.Mybatis如何开启二级缓存?二级缓存可以设置的属性有哪些?
- 7.当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
- 8.在mapper中如何传递多个参数?
- 9.Mybatis中的复杂类型的关联或集合如何映射?
- 10.Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
- 11.Mybatis动态sql有什么用?执行原理?有哪些动态sql?
- 12.MyBatis如何实现分页查询?
- 13.MyBatis的优缺点
1.什么是Mybatis?为什么说Mybatis是半自动的ORM框架?
Mybatis是一个半自动ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。
称Mybatis是半自动ORM映射工具,是因为在查询关联对象或关联集合对象时,需要手动编写sql来完成。不像Hibernate这种全自动ORM映射工具,Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
2.Mybatis中mapper文件书写sql语句的时候#和$的区别。
#是用的PreparedStatement预编译形式执行sql语句,性能比较高,没有sql注入风险
$使用Statement执行sql语句,是用字符串拼接成sql语句,有sql注入风险
$也要自己的应用场景,如果是表名、order by的排序字段作为变量时的时候需要用到。 因为#号解析出来的是一个带着双引号的字符串,mysql找不到这样的表名和列名
3.通常一个mapper.xml文件,都会对应一个Dao接口,这个Dao接口的工作原理是什么?Dao接口里的方法能重载吗?
Mapper接口的工作原理是JDK的动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,根据类的全限定名+方法名,可以唯一定位到一个MapperStatement并调用执行器执行这个MapperStatement所对应的sql,然后将sql执行结果返回。
Mapper接口里的方法是不能重载的,因为是使用类的全限定名+方法名作为key值唯一定位一个MapperStatement,所以方法名不允许重复。
Mapper接口的全限定名就是映射文件中的namespace的值,接口的方法名就是映射文件中Mapper的statement的id值;接口方法内的参数,就是传递给sql的参数。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis//DTD Mapper 3.0//EN"
"http://mybatis/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zcc.dao.CategoryMapper"> <!--接口全限定名-->
<!--方法名-->
<select id="selectAll" resultType="com.zcc.pojo.Category">
select * from pms_category;
</select>
</mapper>
4.Mybatis都有哪些Executor执行器?它们之间的区别是什么?Mybatis中如何指定使用哪一种Executor执行器?
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。
5.Mybatis中的一级缓存和二级缓存。
一级缓存:该缓存是session级别的,在同一个session中,如果使用相同语句进行查询,则会使用一级缓存。mybatis默认会开启一级缓存。
二级缓存:二级缓存是mapper(namespace)级别的缓存,每一个namespace的mapper都有一个二级缓存区域,多个SqlSession去操作同一个Mapper的sql语句可以共用二级缓存。如果两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
访问顺序:二级缓存>一级缓存>数据库
6.Mybatis如何开启二级缓存?二级缓存可以设置的属性有哪些?
Mybtais二级缓存默认是不开启的,配置方法很简单,只需要在映射XML文件配置<cache/>就可以开启缓存了,MyBatis要求返回的POJO必须是可序列化的.也就是要求实现Serializable接口。
如果我们配置了二级缓存就意味着:
(1)映射语句文件中的所有select语句会被缓存
(2)映射语句文件中的所有insert,undate和delete语句会刷新缓存
(3)缓存会使用默认的LRU算法来收回,我们可以配置eviction参数指定缓存回收策略。
(4)缓存默认只有当SQL被执行才会去刷新缓存,我们可以配置flushInterval参数来规定缓存刷新的间隔时间,单位是毫秒。
(5)缓存默认最多可以存储1024个对象,我们可以配置size参数来指定缓存的最大存储对象数。
(6)缓存readOnly属性默认是false,表示返回给调用者的缓存对象是不同的实例,这样调用者修改各自的缓存对象不会影响到其他调用者是安全的。如果把readOnly设置为true;那么返回给调用者的是同一个缓存对象实例,如果调用者修改了这个缓存实例就可能会造成其他调用者出现数据不一致的情况。
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
实践:
1.返回的POJO序列化
@Data
public class Category implements Serializable {
private Long cat_id;
private String name;
private Long parent_id;
}
2.在映射xml文件中开启二级缓存<cache />
<mapper namespace="com.zcc.dao.CategoryMapper"> <!--接口全限定名-->
<!--方法名-->
<select id="selectAll" resultType="com.zcc.pojo.Category">
select * from pms_category where parent_cid=0;
</select>
<cache />
</mapper>
3.在Mybatis的主配置文件中开启二级缓存。这个是在Spring.xml中配置的。
<!--配置mybatis-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<!--开启日志-->
<property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"></property>
<!--开启驼峰命名-->
<property name="mapUnderscoreToCamelCase" value="true"></property>
<!--开启二级缓存-->
<property name="cacheEnabled" value="true"></property>
</bean>
</property>
</bean>
如果要在Mybatis主配置文件中配置,先要在spring中指定mybatis主配置文件的位置。
spring.xml
<!--配置mybatis-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<!--指定mybatis的配置文件-->
<property name="configLocation" value="classpath:mybatis.xml"></property>
</bean>
mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis//DTD Config 3.0//EN"
"http://mybatis/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
7.当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
@Data
public class Category implements Serializable {
private Long id; //表:cat_id
private String name; //表:name
private Long pid; //表:parent_cid
}
(1)通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致
<mapper namespace="com.zcc.dao.CategoryMapper">
<select id="selectAll" resultType="com.zcc.pojo.Category">
select cat_id as id,name,parent_cid as pid from pms_category where parent_cid=0;
</select>
</mapper>
(2)通过<resultMap>来映射字段名和实体类属性名的一一对应的关系
<mapper namespace="com.zcc.dao.CategoryMapper">
<resultMap id="categoryResultMap" type="com.zcc.pojo.Category">
<!--用id属性来映射主键字段-->
<id property="id" column="cat_id"></id>
<!--用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性-->
<result property="name" column="name"></result>
<result property="pid" column="parent_cid"></result>
</resultMap>
<select id="selectAll" resultMap="categoryResultMap">
select cat_id,name,parent_cid from pms_category where parent_cid=0;
</select>
<cache />
</mapper>
8.在mapper中如何传递多个参数?
1.使用@param注解:来命名参数
public interface BrandMapper {
public Brand selectOne(@Param("brandId") Long brandId,
@Param("name") String name,
@Param("catalogId") Long catalogId);
}
<mapper namespace="com.zcc.dao.BrandMapper">
<select id="selectOne" resultType="com.zcc.pojo.Brand">
select * from pms_brand where brand_id=#{brandId} and
name=#{name} and catalog_id=#{catalogId}
</select>
</mapper>
2.使用#{arg位置}
参数位置从0开始,引用参数语法 #{ arg位置 } 第一个参数是 #{arg0}, 第二个是 #{arg1}
public interface BrandMapper {
public Brand selectOne(Long brandId,
String name,
Long catalogId);
}
<mapper namespace="com.zcc.dao.BrandMapper">
<select id="selectOne" resultType="com.zcc.pojo.Brand">
select * from pms_brand where brand_id=#{arg0} and
name=#{arg1} and catalog_id=#{arg2}
</select>
</mapper>
3.使用java对象传递参数,java的属性值就是sql需要的参数值。
public interface BrandMapper {
public Brand selectOne(Brand brand);
}
@RequestMapping("brandone.do")
public Brand findOne(){
Brand brand = new Brand();
brand.setName("华为");
brand.setBrandId(1L);
brand.setCatalogId(2L);
return brandMapper.selectOne(brand);
}
<mapper namespace="com.zcc.dao.BrandMapper">
<select id="selectOne" resultType="com.zcc.pojo.Brand">
select * from pms_brand where brand_id=#{brandId} and
name=#{name} and catalog_id=#{catalogId}
</select>
</mapper>
4.使用Map集合一次传入多个参数
Map集合可以存储多个值,用Map<String,Object>存储参数。mapper文件使用#{key}引用参数值。
public interface BrandMapper {
public Brand selectOne(Map map);
}
@RequestMapping("brandone.do")
public Brand findOne(){
HashMap<String, Object> map = new HashMap<>();
map.put("brandId",3L);
map.put("name","小米");
map.put("catalogId",2L);
return brandMapper.selectOne(map);
}
<mapper namespace="com.zcc.dao.BrandMapper">
<select id="selectOne" resultType="com.zcc.pojo.Brand">
select * from pms_brand where brand_id=#{brandId} and
name=#{name} and catalog_id=#{catalogId}
</select>
</mapper>
9.Mybatis中的复杂类型的关联或集合如何映射?
(1)association:一个复杂类型的关联;指的是一对一的关联。
比如我们要查询一个品牌包括它的分类信息
@Data
public class Brand {
private Long brandId;
private String name;
private String descript;
private Category category;
}
@Data
public class Category{
private Long id; //表:cat_id
private String name; //表:name
private Long pid; //表:parent_cid
}
<resultMap id="brandCategoryMap" type="com.zcc.domain.Brand">
<id property="brandId" column="brand_id"></id>
<result property="name" column="name"></result>
<result property="descript" column="descript"></result>
<association property="category" javaType="com.zcc.domain.Category">
<id property="id" column="cat_id"></id>
<result property="name" column="cname"></result>
<result property="pid" column="parent_cid"></result>
</association>
</resultMap>
<select id="selectBrandAndCategory" resultMap="brandCategoryMap">
select pb.*,pc.cat_id,pc.name cname,pc.parent_cid
from pms_brand pb join pms_category pc on pb.catalog_id=pc.cat_id
where brand_id=#{brandId}
</select>
由于brand和category有重复字段名name,所以我们sql语句要给catogory的name字段起别名为cname,否则无法正常映射。
解决上述问题也可以使用关联的嵌套select查询,不设置别名即可解决。
<resultMap id="brandCategoryMap" type="com.zcc.domain.Brand">
<id property="brandId" column="brand_id"></id>
<result property="name" column="name"></result>
<result property="descript" column="descript"></result>
<association property="category" javaType="com.zcc.domain.Category" column="catalog_id" select="selectCategory">
<!--<id property="id" column="cat_id"></id>
<result property="name" column="name"></result>
<result property="pid" column="parent_cid"></result>-->
</association>
</resultMap>
<select id="selectBrandAndCategory" resultMap="brandCategoryMap">
select * from pms_brand pb where brand_id=#{brandId}
</select>
<select id="selectCategory" resultType="com.zcc.domain.Category">
select cat_id id,name,parent_cid pid from pms_category where cat_id=#{catId};
</select>
(2)collection:一个复杂类型的集合;指的是一对多的关系
比如我们要查询一个分类下的所有品牌
@Data
public class Brand {
private Long brandId;
private String name;
private String descript;
private Category category;
}
@Data
public class Category{
private Long id; //表:cat_id
private String name; //表:name
private Long pid; //表:parent_cid
private List<Brand> brands;
}
<resultMap id="categoryResultMap" type="com.zcc.domain.Category">
<!--用id属性来映射主键字段-->
<id property="id" column="cat_id"></id>
<!--用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性-->
<result property="name" column="name"></result>
<result property="pid" column="parent_cid"></result>
<collection property="brands" ofType="com.zcc.domain.Brand">
<id property="brandId" column="brand_id"></id>
<result property="name" column="bname"></result>
<result property="descript" column="descript"></result>
</collection>
</resultMap>
<select id="selectCategoryAndBrands" resultMap="categoryResultMap">
select cat_id,pc.name,parent_cid,brand_id,pb.name bname,pb.descript
from pms_category pc left join pms_brand pb on pc.cat_id=pb.catalog_id where cat_id=#{catId}
</select>
集合的嵌套select查询跟association非常类似,在下面懒加载举例
10.Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
所谓懒加载就是按需加载。Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
延迟加载的开启方式:
spring.xml配置
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="lazyLoadingEnabled" value="true"></property>
<property name="aggressiveLazyLoading" value="false"></property>
</bean>
</property>
</bean>
或者mybatis的主配置文件配置mybatis.xml(要在spring中指定mybatis主配置文件的位置。)
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
</configuration>
lazyLoadingEnabled=true时开启懒加载,默认是false
aggressiveLazyLoading=true时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则每种属性将会按需要加载。3.4.1版本之后默认为false
举例说明:我们想要获取一个分类下的所有品牌
上面的情况是无法实现懒加载的。因为是连接查询,所以在查询时只是执行了一次sql语句,就查询所有的数据。
所以只有使用集合collection或者关联association的嵌套select查询才有可能实现懒加载。
CategoryMapper.xml
<mapper namespace="com.zcc.dao.CategoryMapper">
<resultMap id="categoryResultMap" type="com.zcc.domain.Category">
<!--用id属性来映射主键字段-->
<id property="id" column="cat_id"></id>
<!--用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性-->
<result property="name" column="name"></result>
<result property="pid" column="parent_cid"></result>
<collection property="brands" ofType="com.zcc.domain.Brand" column="cat_id"
select="com.zcc.dao.BrandMapper.selectBrandsByCatId"></collection>
</resultMap>
<select id="selectCategoryAndBrands" resultMap="categoryResultMap">
select * from pms_category where cat_id=#{catId}
</select>
</mapper>
BrandMapper.xml
<mapper namespace="com.zcc.dao.BrandMapper">
<select id="selectBrandsByCatId" resultType="com.zcc.domain.Brand">
select * from pms_brand where catalog_id=#{catId}
</select>
</mapper>
以上这种情况可能出现延迟加载,第一次查询结束之后,如果不需要Category的信息,就不会执行第二次查询。
我们最后测试一下。在不调用brand属性发现只执行一句sql。
@RequestMapping(value = "col/{catId}.do")
@ResponseBody
public String findCategoryAndBrands(@PathVariable("catId") Long catId ){
//只获取查询结果,不调用brand属性
Category category = categoryMapper.selectCategoryAndBrands(catId);
System.out.println(category.getId()+";"+category.getName()+";"+category.getPid());
return "success ok";
}
==> Preparing: select * from pms_category where cat_id=?
==> Parameters: 1(Long)
<== Columns: cat_id, name, parent_cid, cat_level, show_status, sort, icon, product_unit, product_count
<== Row: 1, 图书、音像、电子书刊, 0, 1, 1, 0, null, null, 0
<== Total: 1
如果调用brand属性时,就会发现调用两条sql。
11.Mybatis动态sql有什么用?执行原理?有哪些动态sql?
Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值完成逻辑判断并动态拼接sql的功能。
Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind
if
<select id="findByDeptAndLike" resultType="com.zcc.domain.Emp">
select * from emp
<where>
and deptno=#{deptno}
<if test="similar!=null">
and ename like "%"#{similar}"%"
</if>
</where>
</select>
choose when otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
<select id="findByDeptAndLike" resultType="com.zcc.domain.Emp">
select * from emp
<where>
<choose>
<when test="deptno!=null">
and deptno=#{deptno}
</when>
<when test="similar!=null">
and ename like "%"#{similar}"%"
</when>
<otherwise>
and sal>#{sal}
</otherwise>
</choose>
</where>
</select>
trim|set|where
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
<select id="findByDeptAndLike" resultType="com.zcc.domain.Emp">
select * from emp
<where>
and deptno=#{deptno}
<if test="similar!=null">
and ename like "%"#{similar}"%"
</if>
</where>
</select>
set用于动态更新语句,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
<update id="updateEmp">
update emp
<set>
<if test="ename!=null">ename=#{ename},</if>
<if test="job!=null">job=#{job},</if>
<if test="sal!=null">sal=#{sal},</if>
</set>
where empno=#{empno}
</update>
trim:截取字符串,可以自定义where,set的格式
- prefix:为sql语句整体添加一个前缀
- prefixOverrides:去除整体sql语句前面多余的字符串
- suffixOverrides:去除整体sql语句后面多余的字符串
<select id="findByDeptAndLike" resultType="com.zcc.domain.Emp">
select * from emp
<trim prefix="where" prefixOverrides="and |or ">
<choose>
<when test="deptno!=null">
and deptno=#{deptno}
</when>
<when test="similar!=null">
and ename like "%"#{similar}"%"
</when>
</choose>
or sal>#{sal}
</trim>
</select>
prefixoverrides和suffixOverrides需要额外注意一下。去除的是整体的多余的指定的字符串,并不是每个子句的多余的指定的字符串。
foreach
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符
<select id="selectByIds" resultMap="empResultMap">
select * from emp join dept on emp.deptno=dept.deptno where empno in
<foreach collection="ids" open="(" close=")" separator="," item="id" index="index">
#{id}
</foreach>
</select>
需要注意,如果我们指定了collection=“ids”,所以我们传参的时候一定要加@Param(“ids”)注解,不然就会报错找不到。
List<Emp> selectByIds(@Param("ids") List<Integer> ids);
如果不想使用@Param注解,我们可以直接使用collection=“list”。
List<Emp> selectByIds(List<Integer> ids);
原因是当传递一个 List 实例或者数组作为参数对象传给 MyBatis。当你这么做的时 候,MyBatis 会自动将它包装在一个 Map中,用指定名称作为键。List 实例将会以“list” 作为键,而数组实例将会以“array”作为键。
12.MyBatis如何实现分页查询?
我们一般使用PageHelper插件实现Mybatis的分页查询。
(1)导入maven依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
(2)配置文件加入plugin
spring.xml
<!--配置mybatis-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis.xml"></property>
<property name="plugins">
<list>
<bean class="com.github.pagehelper.PageInterceptor"></bean>
</list>
</property>
</bean>
或者mybatis.xml中加入
<configuration>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
</configuration>
(3)PageHelper对象
Service 方法调用SQL查询前添加 PageHelper.startPage
public List<Emp> selectPage(Integer num,Integer size){
PageHelper.startPage(num,size);
return empDao.selectAll();
}
控制器添加 PageInfo
@RequestMapping("page.do")
@ResponseBody
public PageInfo<Emp> selectByIds(Integer num,Integer size){
List<Emp> emps = empService.selectPage(num, size);
//PageInfo就是一个分页Bean
PageInfo<Emp> pageInfo=new PageInfo<>(emps);
return pageInfo;
}
PageInfo的结构
13.MyBatis的优缺点
优点:
- Mybatis是一个半自动ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。
- SQL写在XML里,从程序代码中彻底分离,降低耦合度
- 提供XML标签,支持编写动态SQL语句(XML中使用if, else)
缺点:
- SQL语句的编写工作量较大,尤其是字段多、关联表多时,更是如此,对开发人员编写SQL语句的功底有一定要求。
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
更多推荐
6.Mybatis面试题
发布评论