在项目中缓存是经常要用到的,之前用的缓存都是Redis做为缓存的,但是在实际工作中用到缓存的地方是非常多,但是又不是只有Redis这一种 *** 作,实际中可以用到的缓存还有SpringBoot,中的@Cacheable这个注解可以充当Spring的缓存,还有Spring整合Redis做缓存的,之前一直对缓存的实际用处云里雾里的不清楚,其实在实际落地过程中最简单的应用就是在获取省市区街道这些数据的时候需要做缓存,那这些做缓存存到哪里,怎样 *** 作最简单,毋庸置疑,使用@Cacheable *** 作是最简单的, *** 作的时候只用上注解的就可以了,但是如果数据库修改了,缓存还是以前的数据怎么办,这个时候就需要对缓存进行删除,然后再重新加载缓存就可以了,这个是我下面写的代码
@Cacheable(cacheNames = "address:info", key = "#provinceId")
public List findCityList(String provinceId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(provinceId);
}
@Cacheable(cacheNames = "address:info", key = "#cityId")
public List findOrganyList(String cityId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(cityId);
}
@Cacheable(cacheNames = "address:info", key = "#organId")
public List findStreetList(String organId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(organId);
}
@CacheEvict(cacheNames = "address:info", allEntries = true)
public void refresh() {
}
就上面的整个代码非常的简单,其实主要还是针对对应的SpringBoot整合之后的@Cacheable的缓存 *** 作,最后的@CacheEvict的 *** 作其实是修改缓存的意思的值,这个我描述一下我的简单理解意思,可以理解为,@Cacheable作为缓存之后,中间的cacheNames="XX"这个其实是缓存的名字,后面的key可以定义为缓存的键,#cityId,这种形式,因为每次给方法中传入的形参都不一样,导致这个key都不一样,所以每次都会进入到方法中,当如果读取到同样的值的时候,他就不会在执行这个方法了,大概的意思就是第二次在加载同样的方法,并且是同样的入参的时候,就不会执行查询语句了;
总结:使用场景:比如网站首页的文章列表、电商网站首页的商品列表、微博等社交媒体热搜的文章等等,当大量的用户都去请求同样的接口,同样的数据,如果每次都去查数据库,那对数据库来说是一个不可承受的压力。通常是把高频的查询进行缓存,我们称它为“热点
使用
主要使用@Cacheable,以下示例在第一次调用getTime后,获得到的值就会存储在redis中,在过期时间失效前,再次调用getTime都不会进入到实际的方法体内了,当然你也可以手工的去清除缓存,调用带@CacheEvict的方法
public class CacheTestService {
@Cacheable(cacheNames = "myCache", key = "#key")
public long getTime(String key) {
System.out.println("not from cache.");
return System.currentTimeMillis();
}
@CacheEvict(cacheNames = "myCache", key = "#key") //清空某一个key的缓存
public void reload(String key) {
}
@CacheEvict(cacheNames = "myCache", allEntries = true) //全部清空
public void reset() {
}
//当获取对象方法添加缓存,该对象进行更新时,注意清除缓存
@CacheEvict(cacheNames = "myCache", key = "#obj.id")
public void updateObj(Object obj) {
}
}
key的不规范使用,会造成缓存混淆,造成不可预测的后果
spring-cache
提供KeyGenerator用以自动生成key
这里可指定key生成策略,用来覆盖spring默认的生成策略。spring默认的生成策略以方法参数值为key,会造成不同方法,如果入参参数值一样得到的缓存值不一致的情况,因此框架中拼接了key的前缀"cache:"+cacheName+":"
,用以区分。网络上有建议重写KeyGenerator key值为className+methodName+参数值列表,助益不大(遇到方法重载还是无法区分),不建议使用,key值过长,而且写模式下设置缓存失效key值难以对应,还是通过cacheName命名以及使用规范上进行约束。
@Cacheable 这个注解「一般用在查询方法上」。
// 简单使用
@Cacheable(cacheNames = "myCache")
public Object getData (String id){
// 获取业务数据
return data;
}
@CacheEvict 这个注解会清空指定缓存。「一般用在更新或者删除的方法上」。
// 更新 *** 作后删除缓存
@CacheEvict(cacheNames = {"name1"},key = "#obj.id")
@Transaction
public void update(Object obj){
// do something...
}
// 删除 *** 作后删除缓存
@CacheEvict(cacheNames = {"name1"})
@Transaction
public void delete(String id){
// do something...
}
// 删除某个分区下的所有数据
// 约定:存储同一类型的数据,都可以指定成同一个分区,以后好删除
@CacheEvict(value = {"name1"}, allEntries = true)
@Transaction
public void update(){
// do something...
}
注解使用注意事项
spring-cache
的注解是基于Spring AOP代理类, 同一个类中方法A调用方法B, 方法B添加了注解,是不生效的。
//不生效
public void A(String key) {
// do something...
//不生效
this.B();
}
@Cacheable(cacheNames = "myCache")
public void B(String key) {
// do something...
}
使用缓存带来的问题
使用缓存会带来许多问题,尤其是高并发下,包括缓存穿透、缓存击穿、缓存雪崩、双写不一致等问题。
-
缓存穿透:大量查询一个null的数据。解决:缓存空数据 cache-null-values: true
-
缓存雪崩:大量key同时过期。解决:加随机时间;其实只要加上过期时间就可以满足要求 time-to-live: 1000
-
双写不一致:这是一个比较常见的问题,其中一个常用的解决方案是,更新的时候,先删除缓存,再更新数据库。所以Spring Cache的@CacheEvict会有一个beforeInvocation的配置。
但使用缓存通常会存在缓存中的数据和数据库中不一致的问题,尤其是调用第三方接口,你不会知道它什么时候更新了数据。但使用缓存的业务场景很多时候并不需求数据的强一致,比如首页的热点文章,我们可以让缓存一分钟失效,这样就算一分钟内,不是最新的热点排行也没关系。
补充补充一些关于Cacheable的知识的
@Cacheable
1、方法运行之前,先去查询Cache(缓仔组件),按照cacheNames指定的名字获取;
(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
2.去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;
key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key
3、1没有查到缓存就调用目标方法;
4、将目标方法返回的结果,放进缓存中
@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存;
演示代码:
@Cacheable(cacheNames = "provinceId:info",key ="#provinceId" )
public List findCityList(String provinceId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(provinceId);
}
@Cacheable(cacheNames = "cityId:info",key ="#cityId" )
public List findOrganyList(String cityId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(cityId);
}
@Cacheable(cacheNames = "organId:info",key ="#organId" )
public List findStreetList(String organId) {
return areaRepository.findAllByParentIdAndDelFlagIsFalse(organId);
}
@Cacheable(cacheNames = "areaCode:info",key = "#code")
public String findByCode(String code) {
Area area = areaRepository.findFirstByCodeAndDelFlagIsFalse(code);
if (area != null && StringUtils.isNotEmpty(area.getCode())) {
return area.getName();
}
return code;
}
@Caching(evict ={
@CacheEvict(cacheNames = "provinceId:info",allEntries = true),
@CacheEvict(cacheNames = "organId:info",allEntries = true),
@CacheEvict(cacheNames = "areaCode:info",allEntries = true),
@CacheEvict(cacheNames = "cityId:info",allEntries = true)
})
public void refresh() {
}
分析:要解释下面的refrsh()上面的方法注解,其实可以先看一下,对应缓存中现在存值的状况在进行实际的情况进行分析,会更加的简单明了,在执行对应的每一个方法的时候,进行拆解的过程中一直不太明白,cacheNames="cityId:info"key="#cityId"这到底是什么意思,当去Redis中看了缓存的分布一下子就明白了,在我执行了若干的查询之后,上面的查询的结果就会通过查询的返回值放到缓存中,可以看到cacheNames="cityId:info"key="#cityId",它的实际含义cacheNames其实就类似一个文件夹的层级,key:cache:cityId:info:ccc08c34d157430fb595e10bcd448786,对应的value就是我后面查询出来的值,如果不写key
上面可以清楚的看到了端倪,如果不写key值,那他这个地方默认的key就是参数的入参的key
通过分析了这个对应的@CacheNames 之后他用:进行隔离其实还是为了更好的组合对应的key ,spring默认的生成策略以方法参数值为key,会造成不同方法,如果入参参数值一样得到的缓存值不一致的情况,因此框架中拼接了key的前缀"cache:"+cacheName+":"
,这个就是可以看到
key:cache:cityId:info:ccc08c34d157430fb595e10bcd448786,对应的 后面的这个ccc08c34d157430fb595e10bcd448786其实就是对应的入参参数
下面对整个SpringBoot中的@Cache相关的 *** 作做过总结 1 基于注解的支持Spring为我们提供了几个注解来支持Spring Cache。其核心主要是@Cacheable和@CacheEvict。使用@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。下面我们将来详细介绍一下Spring基于注解对Cache的支持所提供的几个注解。
这里其实主要学习几个注解:@CachePut、@Cacheable、@CacheEvict、@CacheConfig。
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,这个value
参数 | 解释 | example |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
这个需要主要的是在先有的框架中在进行开发的时候是@Cacheable(cacheNames="XX")这个里面的写法更加规范一些,其实用value也是可以替代里面的cachNames的
@CachePut
@CachePut 的作用 主要针对方法配置,能够根据方法的返回值对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用,在其他地方写的是根据方法的请求参数对其结果进行缓存,实际是按方法返回值进行缓存的,这里我就遇到了一个坑,我开始的时候是在Mybatis的Mapper层进行缓存的,如下面的代码。但是缓存到Redis的是Null值,今天看了一博友的博客,交流了一下,才知道它缓存的是方法的返回值,如果把下面update的返回值该为int,在redis中保存的是int类型,报的错误是int无法转换成User对象。
@CachePut(value="user",key = "#p0.id")
@Update({"UPDATE user SET name=#{name},age=#{age} WHERE id =#{id}"})
void update(User user);
参数 | 解释 | example |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CachePut(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CachePut(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
@CachEvict
@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
参数 | 解释 | example |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CacheEvict(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CacheEvict(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
@CacheConfig
所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。
1.1 @Cacheable@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。
1.1.1 value属性指定Cache名称value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。
@Cacheable("cache1")//Cache是发生在cache1上的
public User find(Integer id) {
returnnull;
}
@Cacheable({"cache1", "cache2"})//Cache是发生在cache1和cache2上的
public User find(Integer id) {
returnnull;
}
1.1.2 使用key属性自定义key
key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们这里先来看看自定义策略,至于默认策略会在后文单独介绍。
自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。
例子看下面
1.1.3 condition属性指定发生的条件有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。如下示例表示只有当user的id为偶数时才会进行缓存。
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
System.out.println("find user by user " + user);
return user;
}
1.2 @CachePut
在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。
@CachePut("users")//每次都会执行方法,并将结果存入指定的缓存中
public User find(Integer id) {
returnnull;
}
1.3 @CacheEvict
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除 *** 作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除 *** 作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除 *** 作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。
1.3.1 allEntries属性allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
1.3.2 beforeInvocation属性
清除 *** 作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除 *** 作。使用beforeInvocation可以改变触发清除 *** 作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
其实除了使用@CacheEvict清除缓存元素外,当我们使用Ehcache作为实现时,我们也可以配置Ehcache自身的驱除策略,其是通过Ehcache的配置文件来指定的。
其他知识点补充 1. 基于注解的支持Spring为我们提供了几个注解来支持Spring Cache。其核心主要是@Cacheable和@CacheEvict。使用@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。下面我们将来详细介绍一下Spring基于注解对Cache的支持所提供的几个注解。
1.1 @Cacheable@Cacheable可以标记在一个方法上,也可以标记在一个类上。
- 当标记在一个方法上时表示该方法是支持缓存的
- 当标记在一个类上时则表示该类所有的方法都是支持缓存的
对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。
需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。
@Cacheable可以指定三个属性,value、key和condition。
alue属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。
//Cache是发生在cache1上的
@Cacheable("cache1")
public User find(Integer id) {
returnnull;
}
//Cache是发生在cache1和cache2上的
@Cacheable({"cache1", "cache2"})
public User find(Integer id) {
returnnull;
}
1.1.2 使用key属性自定义key
key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们这里先来看看自定义策略,至于默认策略会在后文单独介绍。
自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。
@Cacheable(value="users", key="#id")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#p0")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#user.id")
public User find(User user) {
returnnull;
}
@Cacheable(value="users", key="#p0.id")
public User find(User user) {
returnnull;
}
除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。
属性名称 | 描述 | 示例 |
---|---|---|
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的Cache | #root.caches[0].name |
当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
returnnull;
}
1.1.3 condition属性指定发生的条件
有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。如下示例表示只有当user的id为偶数时才会进行缓存。
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
System.out.println("find user by user " + user);
return user;
}
1.2 @CachePut
在支持Spring Cache的环境下
- @Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。
- @CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
- @CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。
//每次都会执行方法,并将结果存入指定的缓存中
@CachePut("users")
public User find(Integer id) {
returnnull;
}
1.3 @CacheEvict
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。
当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除 *** 作。
@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除 *** 作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除 *** 作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。
allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
1.3.2 beforeInvocation属性
清除 *** 作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除 *** 作。使用beforeInvocation可以改变触发清除 *** 作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
1.4 @Caching
@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
@CacheEvict(value = "cache3", allEntries = true) })
public User find(Integer id) {
returnnull;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)