为什么在调用其 getter 方法时我的变量会发生改变?

huangapple go评论69阅读模式
英文:

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(&quot;${default.allowed.ids}&quot;)
private String defaultAllowedIds;

// I&#39;ve also tried and removed the static with the same behaviour
private static HashSet&lt;Index&gt; 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&lt;IndexSetup&gt; getListOfDefaultAllowedIndex() {
    
    System.err.println(&quot;TEST : DEFAULT LIST CONTAINS &quot; + (listOfDefaultAllowedIndex != null ? 
            listOfDefaultAllowedIndex.size() + &quot; index&quot; : &quot;NULL&quot;));
            // This is null at the beginning then it grows
    
    System.err.println(&quot;TEST : THE DEFAULT STRING IS &quot; + defaultAllowedIndexIds); 
    // This is always an empty string

    if (listOfDefaultAllowedIndex == null) {


        // 1) Gets the ids from the property
        HashSet&lt;String&gt; defaultIds = new HashSet(Stream.of(
                defaultAllowedIndexIds.split(
                        SEMI_COLON_SEPARATOR)).collect(Collectors.toSet()));
        
        System.err.println(&quot;TEST : DEFAULT IDS : &quot; + defaultIds); // THis is empty
                    

        // 2) Converts them into list of Index object
        listOfDefaultAllowedIndex = new HashSet(StreamSupport
                .stream(indexRepository.findAll().spliterator(),
                        false)
                .filter(idx -&gt; defaultIds.contains(Integer.toString(
                idx.getId())))
                .collect(Collectors.toSet()));
        
        System.err.println(&quot;TEST : CONVERTED INDEX : &quot;);
        listOfDefaultAllowedIndex.stream().forEach(
                    idx -&gt; System.err.println(idx.getName() + &quot; (&quot; + idx.getId() + &quot;)&quot;)); // THis is empty

    }
    
     System.err.println(&quot;TEST : READING FROM CACHE (FROM THREAD &quot; + Thread.currentThread().getName() + &quot;) FOR THE DEFAULT IDS : &quot;);
        listOfDefaultAllowedIndex.stream().forEach(
                    idx -&gt; System.err.println(idx.getName() + &quot; (&quot; + idx.getId() + &quot;)&quot;));
                    // 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 listOfDefaultAllowedIndexis 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&lt;Index&gt; 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

huangapple
  • 本文由 发表于 2020年10月15日 17:59:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/64369129.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定