javaspringspring-bootconfigurationrecord

Expose immutable Set<String> configuration


I need to expose some configuration in the form of a Set in my Application but I need them to be unmodifiable and I'm looking for the best way to achieve this.

My configuration

I need to expose this configuration to perform in the business logic operation like mySet.set1().constains("Value")
This is a configuration in the application.yml

app:
  my-set:
    set-1: 'Value1,Value2'
    set-2: 'Value3,Value4'

The problem

If I try to expose it in a Record or a class written like this:

@ConfigurationProperties("app.my-set")
public record MySet(Set<String> set1, Set<String> set2) {
}

the 2 set are final, so I can't reassign them, but I can still modify the elements that are inside them, just like this:

@SpringBootTest
@Slf4j
 class MySetTest {
    @Autowired
    private MySet mySet;

    @Test
    void testMutableSet(){
        log.info("Initial set: [{}]", mySet);
        assertTrue(mySet.set1().contains("Value1") && mySet.set1().contains("Value2"));
        assertTrue(mySet.set2().contains("Value3") && mySet.set2().contains("Value4"));
        mySet.set1().remove("Value1");
        mySet.set2().remove("Value3");
        mySet.set1().add("Custom1");
        mySet.set2().add("Custom2");
        log.info("Final set: [{}]", mySet);
        assertFalse(mySet.set1().contains("Value1"));
        assertFalse(mySet.set2().contains("Value3"));
        assertTrue(mySet.set1().contains("Custom1") && mySet.set1().contains("Value2"));
        assertTrue(mySet.set2().contains("Custom2") && mySet.set2().contains("Value4"));
    }

}

My solution

To hide the possibility of modify them out of the scope of the class I used a workaround: I implemented the method to return unmodifiableSet:

@ConfigurationProperties("app.my-set")
public record MySet(Set<String> set1, Set<String> set2) {
    
    public Set<String> set1() {
        return Collections.unmodifiableSet(set1);
    }

    public Set<String> set2() {
        return Collections.unmodifiableSet(set2);
    }
}

With this workaround, when I need to inject it into other classes, I can see only the MyImmutableSet interface and I can't modify the values inside the Sets.

@SpringBootTest
@Slf4j
 class MyImmutableSetTest {
    @Autowired
    private MySet mySet;

    @Test
    void testImmutable(){
        log.info("Initial set: [{}]", mySet);
        assertTrue(mySet.set1().contains("Value1") && mySet.set1().contains("Value2"));
        assertTrue(mySet.set2().contains("Value3") && mySet.set2().contains("Value4"));
        assertThrows(UnsupportedOperationException.class, () -> mySet.set1().remove("Value1"));
        assertThrows(UnsupportedOperationException.class, () -> mySet.set2().remove("Value3"));
        assertThrows(UnsupportedOperationException.class, () -> mySet.set1().add("Custom1"));
        assertThrows(UnsupportedOperationException.class, () -> mySet.set2().add("Custom2"));
        log.info("Final set: [{}]", mySet);
        assertFalse(mySet.set1().contains("Custom1"));
        assertFalse(mySet.set2().contains("Custom2"));
        assertTrue(mySet.set1().contains("Value1") && mySet.set1().contains("Value2"));
        assertTrue(mySet.set2().contains("Value3") && mySet.set2().contains("Value4"));
    }

}

What I need

I really don't like this workaround, so I'm looking for a cleaner solution that increase the maintainability, maybe with some annotation. I already tried also with an interface exposing only the two set and removing the public access modifier from the Record, but it looks worse than whit the only Record.
Does anyone have some other solution to share?
Thanks in advance for your help!


Solution

  • What I've done myself is to use the record constructor shorthand to replace the input:

    @ConfigurationProperties("app.my-set")
    public record MySet(Set<String> set1, Set<String> set2) {
    
        public MySet {
            set1 = Set.copyOf(set1);
            set2 = Set.copyOf(set2);
        }
    }
    

    If needed you can make those null-safe: set1 = set1 == null ? Set.of() : Set.copyOf(set1);