一、前言
最近工作中使用到Spring Ehcache作为一级缓存以减轻对redis的压力,在代码改造后遇到了一个对象拷贝的问题,在这里记录下踩到的坑。
将获取的文章详情使用ehcache缓存到本地内从中,访问量大的时候会先走本地缓存拿数据,然后再Redis、数据库,ehcache相关代码如下:
@Cacheable(value = EhCacheSpaceKey.ContentApi.EHCACHE_DETAIL_CONTENTVO, key = "#root.targetClass.name+#root.methodName+#id")
public ContentApiVo getContentDetail(String id) {
//获取文章详情
return contentApiVo;
}
测试时发现响应确实更快、redis的压力也小了,就在以为大功告成时,发现串数据的问题,看代码是如何翻车的:
public String getContentDetail(String contentId, String id, String userId, String appId, Integer contentType) {
ContentApiVo contentApiVoCache = ehCacheService.getContentDetail(id);
//处理数据
}
拿到缓存中的contentApiVoCache后,进行数据处理然后返回给前端,但是在请求量起来后会出现串数据的现象,排查后发现问题出在了contentApiVoCache这个对象上,原因在于ehcache在命中缓存后,会直接返该对象,也就是再一次缓存周期中,每次返回的都是同一个对象,后续的数据处理必然会改变数据的内容。所以在需要改变缓存结果的操作时,所以要拷贝一个新的对象进行操作。这里就引出了对象拷贝。
二、对象拷贝
1、基本概念
在Java中谈到对象拷贝,我们主要是说浅拷贝(shallow copy)、深拷贝(deep copy)两种方式。
- 浅拷贝 , 就是仅把原来对象的属性值复制到新对象对应属性上,对于基本类型属性,会对值进行复制,而引用类型属性则是直接复制应用对象的地址,这就导致复制后的新对象会依赖原对象的属性。
- 深拷贝,基础类型属性和浅拷贝的一致,引用类型属性则是新生成一个引用,把原属性里的内容复制到新引用中,这样拷贝出来的对象,和原来的对象完全独立。
2、Cloneable接口
我们可以实现Cloneable接口进行代码拷贝,构造两个对象进行说明
Address:
public class Address implements Cloneable{
private String city;
private String street;
//标准构造方法、setter、getter省略
//重写toString方法,便于输出结果
@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
", street='" + street + '\'' +
'}';
}
}
User:
public class User implements Cloneable{
private String name;
private Integer age;
private Address address;
@Override
public Object clone() throws CloneNotSupportedException {
//调用super.clone()方法进行拷贝
return super.clone();
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
Address作为User的一个引用类型的属性,两个对象都实现了Cloneable接口,调用super.clone()方法进行拷贝复制,编写一个测试类,使用clone()复制一个新对象,改变新对象里的引用属性值,观察是否会影响到原对象的值:
public static void main(String[] args) {
Address address= new Address("北京","长安街");
User user = new User("wyj",28,address);
System.out.println("user = " + user.toString());
User copyUser = null;
try {
copyUser = (User)user.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
copyUser.getAddress().setCity("上海");
System.out.println("copyUser = " + copyUser.toString());
System.out.println("second print user = " + user.toString());
}
输出结果:
user = User{name='wyj', age=28, address=Address{city='北京', street='长安街'}}
copyUser = User{name='wyj', age=28, address=Address{city='上海', street='长安街'}}
second print user = User{name='wyj', age=28, address=Address{city='上海', street='长安街'}}
可以看出,将新复制出来的user对象中的address属性的city值由”北京“设置为”上海“,再次输出原user的值,发现city值也变为了”上海“,即可说明,user和copuUser的值都是指向了一个Address,即Cloneable接口默认实现的是浅拷贝,那么如何实现深拷贝呢?
3、重写Clone()方法,实现深拷贝
道理很简单,我们需要重写User类的clone()方法达到深拷贝的目的,代码如下:
@Override
public Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
//复制一个新的address设置到user中
user.address = (Address) this.address.clone();
return user;
}
在clone()方法中,调用super.clone后,我们重新对得到的user的address赋新值,来避免和原user中的address值一样,注意:如果Address中也有引用类型属性,也要对其clone方法进行重写,拷贝对象有多层引用要重写多层clone()方法。再次输出结果:
user = User{name='wyj', age=28, address=Address{city='北京', street='长安街'}}
copyUser = User{name='wyj', age=28, address=Address{city='上海', street='长安街'}}
second print user = User{name='wyj', age=28, address=Address{city='北京', street='长安街'}}
可以看到,这次user和copyUser中的值互不影响,实现了深拷贝。
三、总结
深拷贝、浅拷贝,并未好坏之分,要看业务场景,深拷贝实现时会重新new出一个新的引用,所以开销也要比浅拷贝大,回到ehecache的问题上,如果仅是从缓存中国取出数据返给前端,不需要对数据进行加工,也就无所谓复制新对象了。
欢迎拍砖,欢迎交流~
注:转载请注明出处