英文:
Why is my variable changing when its getter is called?
问题
在我的项目中,涉及到了Spring Boot。因此,在一个Service
中,我创建了一个方法来检索所有用户可用的默认索引:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private IndexRepository indexRepository;
/**
* 从 application.properties 中读取
* 我们只在 application.properties 中存储索引 id
*/
@Value("${default.allowed.ids}")
private String defaultAllowedIds;
// 我也尝试过移除 static,但行为相同
private static HashSet<Index> listOfDefaultAllowedIndex = null;
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
public User findByResetToken(String resetToken) {
return userRepository.findByResetToken(resetToken);
}
/**
* 将用户保存到数据库并使用 bcrypt 加密密码
*
* @param user
*/
public void save(User user) {
// 保存新用户的代码
}
public void update(User user) {
// 更新用户的代码
}
/**
* 列出默认允许所有用户访问的默认索引
*
* @return listOfDefaultAllowedIndex
*/
private HashSet<IndexSetup> getListOfDefaultAllowedIndex() {
// 方法实现
}
}
每当调用 getListOfDefaultAllowedIndex
方法时,listOfDefaultAllowedIndex
会增长(如果它是一个列表,实际上会增长,但它是一个集合),尽管 application.properties 中的属性没有更改,而且 listOfDefaultAllowedIndex
只在该方法中更改(当它为 null 时,也就是服务器重置时)。我在 IDE 中使用了“查找用法”或“在项目中查找”功能,但除了在该方法中找不到其他结果。
我注意到线程名称是不同的,但我无法弄清楚竞争条件如何使值变为非空。
请注意:我注意到,如果我删除类变量 listOfDefaultAllowedIndex
,以及if listOfDefaultAllowedIndex == null
的条件,并在方法中声明listOfDefaultAllowedIndex
,那么它会按预期工作,listOfDefaultAllowedIndex
保持为空(在属性为空的情况下),但我无法解释为什么会这样。
因此,是什么原因导致了这个变量的变化?
编辑
正如 @RalfKleberhoff 指出的,我确实在另一个方法中泄漏了原始的 HashSet
,通过以下方式实现:
HashSet<Index> allowedIndexes = this.getListOfDefaultAllowedIndex();
//...
allowedIndexes.addAll(...);
这个其他的旧问题实际上回答了我的问题。
为了解决这个问题,我必须返回一个不可变的 Set
版本,通过将 Set
包装在 Collections.unmodifiableSet(listOfDefaultAllowedIndex)
中,就像下面的答案中所提到的。哇,我竟然完全忘记了这一点!
如果有任何帮助,我会非常感激。
英文:
My project involves SpringBoot. So in a Service
I've created a method to retrieve the default indices available to all users :
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private IndexRepository indexRepository;
/**
* Read from application.properties
*
* We only store the index ids in the application.properties
*/
@Value("${default.allowed.ids}")
private String defaultAllowedIds;
// I've also tried and removed the static with the same behaviour
private static HashSet<Index> listOfDefaultAllowedIndex = null;
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
public User findByResetToken(String resetToken) {
return userRepository.findByResetToken(resetToken);
}
/**
* Saves the user in the database and encrypts the password with bcrypt.
*
* @param user
*/
public void save(User user) {
// Code to save a new user
}
public void update(User user) {
// COde to update the user
}
/**
* List the default index allowed by default to all users
*
* @return the listOfDefaultAllowedIndex
*/
private HashSet<IndexSetup> getListOfDefaultAllowedIndex() {
System.err.println("TEST : DEFAULT LIST CONTAINS " + (listOfDefaultAllowedIndex != null ?
listOfDefaultAllowedIndex.size() + " index" : "NULL"));
// This is null at the beginning then it grows
System.err.println("TEST : THE DEFAULT STRING IS " + defaultAllowedIndexIds);
// This is always an empty string
if (listOfDefaultAllowedIndex == null) {
// 1) Gets the ids from the property
HashSet<String> defaultIds = new HashSet(Stream.of(
defaultAllowedIndexIds.split(
SEMI_COLON_SEPARATOR)).collect(Collectors.toSet()));
System.err.println("TEST : DEFAULT IDS : " + defaultIds); // THis is empty
// 2) Converts them into list of Index object
listOfDefaultAllowedIndex = new HashSet(StreamSupport
.stream(indexRepository.findAll().spliterator(),
false)
.filter(idx -> defaultIds.contains(Integer.toString(
idx.getId())))
.collect(Collectors.toSet()));
System.err.println("TEST : CONVERTED INDEX : ");
listOfDefaultAllowedIndex.stream().forEach(
idx -> System.err.println(idx.getName() + " (" + idx.getId() + ")")); // THis is empty
}
System.err.println("TEST : READING FROM CACHE (FROM THREAD " + Thread.currentThread().getName() + ") FOR THE DEFAULT IDS : ");
listOfDefaultAllowedIndex.stream().forEach(
idx -> System.err.println(idx.getName() + " (" + idx.getId() + ")"));
// THis should be empty but is growing
return listOfDefaultAllowedIndex;
}
So everytime getListOfDefaultAllowedIndex
is called the listOfDefaultAllowedIndex grows (actually it would grow if it was a List but it is a Set) although the property in application.properties does not change and listOfDefaultAllowedIndex
is only changed in that method (when it is null so when the server is reset). I've searched with Find Usage
or Find in project
functions from the IDE and could not find any results except in that method.
I noticed that the thread name was different but I can't figure out how a race condition would change the value to something different than empty.
PLease note : I've noticed that if I remove the class variable listOfDefaultAllowedIndex
as well as the if listOfDefaultAllowedIndex == null
condition, and declare listOfDefaultAllowedIndex
in the method, then it works as expected, the listOfDefaultAllowedIndex
remains empty (in that case where the property is empty), but I can't explain why.
Consequently what could make the variable change ?
EDIT
As pointed out by @RalfKleberhoff I was indeed leaking the original HashSet
by doing this in another method :
HashSet<Index> allowedIndexes = this.getListOfDefaultAllowedIndex();
//...
allowedIndexes.addAll(...);
This other old question actually answers mine.
And to combat this I must return an immutable version of the Set
by wrapping the Set
into Collections.unmodifiableSet(listOfDefaultAllowedIndex)
as proposed in the answer below. Wow I completely forgot that!
Any help appreciated
答案1
得分: 2
最有可能的原因是:
您正在将内部的 HashSet
泄漏给调用者的 getter 方法。
因此,如果调用者修改了从您的 getter 方法中作为返回值得到的 HashSet
,这个改变将会反映在您的内部字段值中,因为它就是同一个 HashSet
对象。
要进行检查:从您的 getter 方法中返回一个 Collections.unmodifiableSet(listOfDefaultAllowedIndex)
(并将返回类型调整为 Set
,而不是 HashSet
)。然后,任何试图更改这个 Set
的调用者都将会收到异常,而不是对任何内容进行修改。然后您可以决定如何继续:例如,让这个调用者创建一个副本,或者始终返回字段 HashSet
的副本。
英文:
Most probable reason:
You're leaking your internal HashSet
to callers of your getter.
So, if a caller modifies the HashSet
it got as return value from your getter, this change will get reflected in your internal field value, as it's the very same HashSet
object.
To check: return a Collections.unmodifiableSet(listOfDefaultAllowedIndex)
from your getter (and adjust the return type to be a Set
instead of HashSet
). Then any caller trying to change the Set
will get an exception instead of modifying anything. Then you can decide how to go on: e.g. have this caller make a copy, always return a copy of the field's HashSet
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论