Java面试总结
redis持久化(RDB、AOF)
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
redis五种数据类型
StringRedisTemplate 常用方法
方法 | 描述 |
---|---|
opsForValue() | 操作字符串 |
delete(key) | 根据key删除记录 |
opsForHash() | 操作hash |
opsForList() | 操作list |
opsForSet() | 操作set |
opsForZSet() | 操作有序set |
String
String通常用于保存单个字符串或JSON字符串数据
计数器
- opsForValue().set(key,value):向redis中插入数据。因为这个没有设置过期时间所以是永久存储的
- opsForValue().set(key,value,time,timeUtil):向redis中插入数据。第三个参数是一个long型的时间。最后一个参数是时间的单位
- opsForValue().get(key):获取redis中指定key 的value值
Hash
常用于存储一个对象
购物车:以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素。
新增hashMap值:
put(H key, HK hashKey, HV value):
redisTemplate.opsForHash().put(“hashValue”,“key”,“value”);
获取指定变量中的hashMap值:
values(H key):
List hashList = redisTemplate.opsForHash().values(“hashValue”);
获取变量中的键值对:
entries(H key):
Map<Object,Object> map = redisTemplate.opsForHash().entries(“hashValue”) :
System.out.println(“通过values(H key)方法获取变量中的hashMap值:” + hashList)
获取变量中的指定map键是否有值,如果存在该map键则获取值,没有则返回null
get(H key, Object hashKey):
Object mapValue = redisTemplate.opsForHash().get(“hashValue”,“map1”);
System.out.println(“通过get(H key, Object hashKey)方法获取map键的值:” + mapValue);
判断变量中是否有指定的map键
hasKey(H key, Object hashKey):
boolean hashKeyBoolean = redisTemplate.opsForHash().hasKey(“hashValue”,“map3”)
System.out.println(“通过hasKey(H key, Object hashKey)方法判断变量中是否存在map键:” + hashKeyBoolean)
获取变量中的键
keys(H key):
Set keySet = redisTemplate.opsForHash().keys(“hashValue”)
System.out.println(“通过keys(H key)方法获取变量中的键:” + keySet);
获取变量的长度
size(H key):
long hashLength = redisTemplate.opsForHash().size(“hashValue”)
System.out.println(“通过size(H key)方法获取变量的长度:” + hashLength)
使变量中的键以double值的大小进行自增长
increment(H key, HK hashKey, double delta):
double hashIncDouble = redisTemplate.opsForHash().increment(“hashInc”,“map1”,3)
System.out.println(“通过increment(H key, HK hashKey, double delta)方法使变量中的键以值的大小进行自增长:” + hashIncDouble)
使变量中的键以long值的大小进行自增长
increment(H key, HK hashKey, long delta):
long hashIncLong = redisTemplate.opsForHash().increment(“hashInc”,“map2”,6)
System.out.println(“通过increment(H key, HK hashKey, long delta)方法使变量中的键以值的大小进行自增长:” + hashIncLong)
以集合的方式获取变量中的值
multiGet(H key, Collection hashKeys):
List list = new ArrayList()
list.add(“map1”)
list.add(“map2”)
List mapValueList = redisTemplate.opsForHash().multiGet(“hashValue”,list)
System.out.println(“通过multiGet(H key, Collection hashKeys)方法以集合的方式获取变量中的值:”+mapValueList)
以map集合的形式添加键值对
putAll(H key, Map<? extends HK,? extends HV> m):
Map newMap = new HashMap()
newMap.put(“map3”,“map3-3”)
newMap.put(“map5”,“map5-5”)
redisTemplate.opsForHash().putAll(“hashValue”,newMap)
map = redisTemplate.opsForHash().entries(“hashValue”)
System.out.println(“通过putAll(H key, Map<? extends HK,? extends HV> m)方法以map集合的形式添加键值对:” + map)
删除变量中的键值对,可以传入多个参数,删除多个键值对
delete(H key, Object… hashKeys):
redisTemplate.opsForHash().delete(“hashValue”,“map1”,“map2”)
map = redisTemplate.opsForHash().entries(“hashValue”)
System.out.println(“通过delete(H key, Object… hashKeys)方法删除变量中的键值对后剩余的:” + map)
List
对数据量大的集合数据删减
任务队列
在变量左边添加元素值
leftPush(K key, V value)
redisTemplate.opsForList().leftPush(“list”,“a”)
redisTemplate.opsForList().leftPush(“list”,“b”)
redisTemplate.opsForList().leftPush(“list”,“c”)
获取集合指定位置的值
index(K key, long index)
String listValue = redisTemplate.opsForList().index(“list”,1) + “”
获取指定区间的值
range(K key, long start, long end)
List list = redisTemplate.opsForList().range(“list”,0,-1)
以集合的方式向左边批量添加元素
leftPushAll(K key, Collection values)
List newList = new ArrayList()
newList.add(“o”)
newList.add(“p”)
newList.add(“q”)
redisTemplate.opsForList().leftPushAll(“list”,newList)
获取集合长度
size(K key)
long listLength = redisTemplate.opsForList().size(“list”)
Set
利用集合操作,可以取不同兴趣圈子的交集,以非常方便的实现如共同关注、共同喜好、二度好友等功能。对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存储到一个新的集合中
利用唯一性,可以统计访问网站的所有独立 IP、存取当天[或某天]的活跃用户列表
add(K key, V… values)
向变量中批量添加值
redisTemplate.opsForSet().add(“setValue”,“A”,“B”,“C”,“B”,“D”,“E”,“F”)
获取变量中的值
members(K key)
Set set = redisTemplate.opsForSet().members(“setValue”)
获取变量中值的长度
size(K key)
long setLength = redisTemplate.opsForSet().size(“setValue”)
Zset
统计注册 IP 数
统计每日访问 IP 数
统计页面实时 UV 数
统计在线用户数
统计用户每天搜索不同词条的个数
统计真实文章阅读数
添加元素到变量中同时指定元素的分值
add(K key, V value, double score)
redisTemplate.opsForZSet().add(“zSetValue”,“A”,1)
redisTemplate.opsForZSet().add(“zSetValue”,“B”,3)
redisTemplate.opsForZSet().add(“zSetValue”,“C”,2)
redisTemplate.opsForZSet().add(“zSetValue”,“D”,5)
获取变量指定区间的元素
range(K key, long start, long end)
Set zSetValue = redisTemplate.opsForZSet().range(“zSetValue”,0,-1)
Mysql的优化
explain执行语句
explain select * from T_EW_OUT_GAUGE where F_DT_DATA_CREATE_TIME >='2021-12-25'
type:指的是查询的类型,分为全表扫描和索引扫描。全表扫描是低效的。索引扫描又分为几个级别,包含辅助索引扫描和聚集索引扫描,各个级别不一样,性能也不一样。
索引扫描对应的执行计划分别为:index,range,ref,eq_ref,const(system),NULL
索引扫描按上面的排序,从左到右性能依次变好
select_type:主要用来分辨SELECT的类型,是普通查询还是联合查询还是子查询:
- simple(简单表,即不用表连接或子查询)
- primary(主查询,即外部查询)
- union(union中的第二个或者后面的查询语句)
- subquery(子查询中的第一个select)
possible_keys:此次查询中可能选用的索引
key:此次查询中确切使用到的索引
ref:哪个字段或常数与 key 一起被使用
索引–B+树
- 索引数据和实际数据都存储在磁盘中
- 当需要读取数据时,磁盘中的索引会加载到内存中
- 分块进行数据读取
索引的分类:主键索引、唯一索引、普通索引、全文索引、组合索引
索引并不是越多越好:索引的维护会非常麻烦、占用的存储空间会变大,会导致IO增多。
主从复制
- 有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读
- 做数据的热备
- 架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能
原理:
- 从库会生成两个线程,一个I/O线程,一个SQL线程
- I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中
- 主库会生成一个log dump线程,用来给从库I/O线程传binlog
- SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行
读写分离
master:
url:
username:
password:
driver-class-name: com.mysql.jdbc.Driver
slave1:
url:
username:
password:
driver-class-name: com.mysql.jdbc.Driver
slave2:
url:
username:
password:
driver-class-name: com.mysql.jdbc.Driver
synchronized和lock
synchronized和lock的区别
synchronized
- synchronized是java的一个关键字
- 在发生异常时候会自动释放占有的锁,因此不会出现死锁
- synchronized只能等待锁的释放,不能响应中断
- 如果性能竞争不激烈,synchronized和lock性能是差不多的,如果性能竞争激烈,synchronized比lock性能差
- synchronized使用Object对象本身的wait 、notify、notifyAll调度机制
- synchronized可以加在方法上、变量上
- synchronized采用的是悲观锁机制、
lock
- lock是一个接口
- 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
- Lock可以使用Condition进行线程之间的调度
- Lock采用的是乐观锁机制
分布式Id的生成方案
- UUID
- 数据库自增
- redis自增
- 雪花算法
雪花算法由哪些部分组成
- 符号位,占用1位
- 时间戳,占用41位
- 机器ID占用10位
- 序列号,占用12位
分布式锁的介绍
分布式锁在哪些应用场景使用
- 系统是一个分布式系统,集群,Java的锁已经锁不住了
- 操作共享资源,比如库里唯一的用户数据
- 同步访问,即多个进程同时操作共享资源
分布式锁有哪些解决方案
- redis分布式锁,setnx,Redission
- Zookeeper,顺序临时节点
- mysql分布式锁,基于主键或索引
分布式锁,死锁有哪些情况,如何解决
- 情况1:加锁,没有释放锁,需要加释放锁的操作
- 加锁后,程序还没有执行锁,程序挂了,需要用的key的过期机制
JAVA虚拟机的运行原理
首先Java源文件经过前端编译器将.java文件编译为Java字节码文件,然后JRE加载Java字节码文件,载入系统分配给JVM的内存区,然后执行引擎解释或编译类文件,再由即时编译器将字节码转化为机器码。
类加载
类加载指将类的字节码文件中的二进制数据读入内存,将其放在运行时数据区的方法内,然后在堆上创建java.lang.class对象,封装类在方法区内的数据结构。类加载的最终产品是位于堆中的类对象,类对象封装了类在方法区内的数据结构,并且向JAVA程序提供了访问方法区内数据结构的接口。
- 启动类加载器:在JVM运行时被创建,负责加载存放在JDK安装目录下的jre\lib的类文件,或者被-XbootClasspath参数指定的路径,并且能被虚拟机识别的类库。
- 扩展类加载器:该类加载器负责加载JDK安装目录下的\jre\lib\ext的类,或者由jva.ext.dirs系统变量指定路径中的所有类库。
- 应用程序类加载器:负责加载用户类路径所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有定义过自己的类加载器,该类加载器为默认的类加载器。
运行时数据区
运行时数据区,主要分为方法区、Java堆、虚拟机栈、本地方法栈、程序计数器。其中方法区和Java堆一样,是各个线程共享的内存区域,而虚拟机栈、本地方法栈、程序计数器是线程私有的内存区。
微服务架构SpringCloud
微服务架构原理是什么
更细小粒度服务的拆分,将功能分解到各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。
注册中心的原理是什么
服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中获取服务列表来完成服务调用。
配置中心的原理是什么
在服务运行之前,将所需的配置信息从配置仓库拉取到本地服务,达到统一配置管理的目的。
内存溢出的原因
【情况一】:
java.lang.OutOfMemoryError: Java heap space:这种是java堆内存不够,一个原因是真不够,另一个原因是程序中有死循环;
如果是java堆内存不够的话,可以通过调整JVM下面的配置来解决:
< jvm-arg>-Xms3062m < / jvm-arg>
< jvm-arg>-Xmx3062m < / jvm-arg>
【情况二】:
java.lang.OutOfMemoryError: GC overhead limit exceeded
【解释】:JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。
【解决方案】:
1、查看系统是否有使用大内存的代码或死循环;
2、通过添加JVM配置,来限制使用内存:
< jvm-arg>-XX:-UseGCOverheadLimit< /jvm-arg>
【情况三】:
java.lang.OutOfMemoryError: PermGen space:这种是P区内存不够,可通过调整JVM的配置:
< jvm-arg>-XX:MaxPermSize=128m< /jvm-arg>
< jvm-arg>-XXermSize=128m< /jvm-arg>
【注】:
JVM的Perm区主要用于存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space,这个区域成为年老代,GC在主程序运行期间不会对年老区进行清理,默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128m足够。
【情况四】:
java.lang.OutOfMemoryError: Direct buffer memory
调整-XX:MaxDirectMemorySize= 参数,如添加JVM配置:
< jvm-arg>-XX:MaxDirectMemorySize=128m< /jvm-arg>
【情况五】:
java.lang.OutOfMemoryError: unable to create new native thread
【原因】:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了。
【解决】:由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS /MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的。因此遇到这个错误,可以通过两个途径解决:
1.通过 -Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError);
2.通过-Xms -Xmx 两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。
【情况六】:
java.lang.StackOverflowError
【原因】:这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。
【解决】:优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小
设计模式
什么是设计模式
- 设计模式是解决软件开发某些特定问题而提出的一些解决方案,也可以理解为解决问题的一些固定思路
- 通过设计模式可以帮助我们增强代码的可复用性、可扩展性、灵活性
- 我们使用设计模式的最终目的是实现代码的高内聚、低耦合
设计模式的七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里式替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
你是否在你的代码里面使用过任何设计模式?
(1)单例模式
- JDK种的runtime,Spring种的singeton。
(2)简单工厂模式
- Spring的BeanFactory,根据传入一个唯一标识来获得bean对象。
(3)原型模式
- clone()
(4)代理模式
- Spring的AOP中,Spring实现AOP功能的原理就是代理模式,①JDK动态代理。②CGLIB动态代理,使用Advice(通知)对类进行方法级别的切面增强。
(5)装饰器模式
- 为类添加新的功能,防止类爆炸
- IO流、数据源包装,Spring中用到的装饰器模式表现在Wrapper
使用工厂模式最主要的好处是什么?在哪里使用?
(1)、工厂模式好处
- 良好的封装性、代码结构清晰;
- 扩展性好,如果想增加一个产品,只需扩展一个工厂类即可;
- 典型的解耦框架;
2、在哪里使用?
- 需要生成对象的地方;
- 不同数据库的访问;
定时任务
@EnableScheduling
- 在配置类上使用,开启计划任务的支持(类上)
@Scheduled
- 来申明这是一个任务,包括cron,fixDelay,fixRate等类型(方法上,需先开启计划任务的支持)
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
MYSQL语法
模糊查询
SELECT * from user where userName like '%三%'
字符串拼接
- CONCAT():将多个字符串连接成一个字符串
- CONCAT_WS() :是CONCAT()的特殊形式,第一个参数是其它参数的分隔符,分隔符显示在要连接的两个字符串之间
select CONCAT(userName,password) from user
SELECT CONCAT_WS(',',userName,password) from user
截取字符串
select left(userName,1) from user
select SUBSTR(userName,2,3) from user
替换
select REPLACE(userName,"张三","**") from user
格式化
select FORMAT(password,2) from user 保留小数点后两位
select truncate(3.14159,2) from items 保留小数点后两位
获取当前日期
SELECT CURRENT_DATE
SELECT CURRENT_TIME
SELECT CURRENT_TIMESTAMP
select now()
#. 1.当前系统时间 2020-03-26 10:55:56 SELECT NOW(); SELECT SYSDATE();
# 2.获得当前日期 2020-03-26 SELECT CURDATE();
# 3.获得当前时间 10:55:56 SELECT CURTIME();
# 4.获得指定日期在一年中为第几周 13 SELECT WEEK(CURDATE())+1;
# 5.获取指定日期中的年 2020 SELECT YEAR(CURDATE());
# 6.获取指定日期中的月 3 SELECT MONTH(CURDATE());
# 7.获取指定日期中的日 26 SELECT DAY(CURDATE());
# 8.获取指定日期中的时 10 SELECT HOUR(SYSDATE());
# 9.获取指定日期中的分 55 SELECT MINUTE(SYSDATE());
# 10.获取指定日期中的秒 56 SELECT SECOND(SYSDATE());
# 11.获取date1和date2之间相隔的天数 366 SELECT DATEDIFF(SYSDATE(), '2019-03-26');
# 12.获取指定天数后的日期 2020-04-01 10:55:56 SELECT ADDDATE(SYSDATE(), 6);
# 13.当月最后一天 SELECT LAST_DAY(SYSDATE()) AS '本月最后1天日期'; # 2020-03-31
# 14.格式化日期格式 SELECT DATE_FORMAT(NOW(),'%b %d %Y %h:%i %p'), # Mar 26 2020 10:02 PM DATE_FORMAT(NOW(),'%m-%d-%Y'), # 03-26-2020
DATE_FORMAT(NOW(),'%d %b %y'), # 26 Mar 20 DATE_FORMAT(NOW(),'%d %b
%Y %T:%f'); # 26 Mar 2020 22:02:23:000000
日期格式化
select date_format(now(),'%Y年%m月%d日')
if函数
select *,if(password>5000,'好贵','还行') as remark from user
是否为空
select id,userName,password,IFNULL(password,0) as pic from user
select * from user where age is null
select * from user where ISNULL(age)
case 搜索语句
select name,price,
CASE
when price>6000 then '好贵'
when price>4000 then '还行'
else '便宜'
end remark
from items
当查询结果大于6000时,返回“好贵”,小于6000且大于4000时,返回“还行”,其他情况返回“便宜”
视图
-- 创建视图
create view user_order_view
as
select `user`.*,orders.id as oid,note,number,user_id,createtime from user
inner join orders where `user`.id=orders.user_id
-- 视图中不能出现重复列
-- 使用视图
select * from user_order_view
select username from user_order_view where oid=3
-- 修改视图
alter view user_order_view
as
select `user`.*,orders.id as orderid,note,number,user_id,createtime from user
inner join orders where `user`.id=orders.user_id
-- 删除视图
drop view user_order_view
多表连接
内连接
隐式内连接
select userName,class from user,student where user.id=student.userId
显式内连接
select username,class from user inner join student on user.id=student.userId;
外连接
返回符合条件的user表数据
select * from user where id in(select userId from student);
返回全部数据(user和student)
select * from user right jOin student on user.id=student.userId;
子嵌套查询
数据联合查询
select * from user left join student on user.id=student.userId union select * from user right join student on user.id=student.userId;
两个表同时更新
update user u, student g set u.age=1,g.score=1 where u.id=2 and u.id=g.userId;
mybatis的九种动态标签
if
<select id="findName" resultType="String">
SELECT stu.name FROM tab_stu stu WHERE age = 20
<if test="name != null">
AND name like #{name}
</if>
</select>
choose、when、otherwise
choose、when、otherwise : choose标签是按顺序判断其内部when标签中的test条件是否成立,如果有一个成立,则choose结束;如果所有的when条件都不满足时,则执行otherwise中的SQL。类似于java的switch语句。
<select id="findName" resultType="String">
SELECT stu.name FROM tab_stu stu WHERE age = #{age}
<choose>
<when test="name != null">
AND name like #{name}
</when>
<when test="class != null">
AND class like #{class}
</when>
<otherwise>
AND class = 1
</otherwise>
</choose>
</select>
where
<select id="findName" resultType="String">
SELECT stu.name FROM tab_stu stu WHERE
<if test="age != null">
age = #{age}
</if>
<if test="name!= null">
AND name= #{name}
</if>
<if test="class!= null">
AND class = #{class}
</if>
</select>
set
<update id="updateStu">
Update tab_stu
<set>
<if test="name != null"> name=#{name},</if>
<if test="age != null"> age=#{age},</if>
<if test="class != null"> class=#{class},</if>
<if test="subject != null"> subject=#{subject}</if>
</set>
</update>
trim
trim:trim标签可实现where/set标签的功能
Trim标签有4个属性,分别为prefix、suffix、prefixOverrides、suffixOverrides
prefix:表示在trim标签包裹的SQL前添加指定内容
suffix:表示在trim标签包裹的SQL末尾添加指定内容
prefixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定首部内容,去掉多个内容写法为and |or(中间空格不能省略)(一般用于if判断时去掉多余的AND |OR)
suffixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定尾部内容(一般用于update语句if判断时去掉多余的逗号)
<select id="findName" resultType="String">
SELECT stu.name FROM tab_stu stu
<trim prefix="where" prefixOverrides="and |or">
<if test="age != null">
age = #{age}
</if>
<if test="name!= null">
AND name= #{name}
</if>
<if test="class!= null">
OR class = #{class}
</if>
</trim>
</select>
<update id=”updateStu”>
Update tab_stu
<trim prefix="set" subfix="where id=#{id}" suffixOverrides=",">
<if test="name != null"> name=#{name},</if>
<if test="age != null"> age=#{age},</if>
<if test="class != null"> class=#{class},</if>
<if test="subject != null"> subject=#{subject}</if>
</trim>
</update>
foreach
下面是foreach标签的各个属性:
collection:迭代集合的名称,可以使用@Param注解指定,该参数为必选(java入参,相对于#{listName})
item:表示本次迭代获取的元素,若collection为List、Set或数组,则表示其中元素;若collection为Map,则代表key-value的value,该参数为必选
index:在List、Set和数组中,index表示当前迭代的位置,在Map中,index指元素的key,该参数是可选项
open:表示该语句以什么开始,最常使用的是左括弧”(”,MyBatis会将该字符拼接到foreach标签包裹的SQL语句之前,并且只拼接一次,该参数是可选项
close:表示该语句以什么结束,最常使用的是右括弧”)”,MyBatis会将该字符拼接到foreach标签包裹的SQL语句末尾,该参数是可选项
separator:MyBatis会在每次迭代后给SQL语句添加上separator属性指定的字符,该参数是可选项
<select id="findName" resultType="String">
SELECT stu.name FROM tab_stu stu where id in
<foreach item=”item” index=”index” collection=”listName” open=”(” separator=”,” close=”)”>
#{item}
</foreach>
</select>