Spring configurations
This post is about how Spring configurations are used in this project.
Stars! is structured into modules that represent a feature, e.g. concerning minerals (This module handles mineral concentrations of planets, building mines and mining itself):
mineral
| MineralConfiguration.java
|---api
| |---policies
| | MiningPolicy.java
| ...
|
|---imp
| | MiningCalculator.java
| |---...
| ...
E.g. the MiningCalculator
will load all implementations of MiningPolicy
and calculate how many minerals are mined. A MiningPolicy
might be implemented by another module (e.g. for remote mining).
Configurations for productive code
MineralConfiguration.java
defines which classes are part of the application context:
public interface MineralConfiguration {
@Configuration @ComponentScan(excludeFilters = {... })
@EnableMongoRepositories(basePackageClasses = { MineralRaceRepository.class, MineralPlanetRepository.class })
public static class Provided {
}
@Configuration @Import({ Provided.class, CargoConfiguration.Complete.class, ... })
public static class Complete {
}
}
It actually contains two configurations:
- the
Provided
application context of the module. These are classes added by the module. It cannot be started, because it lacks Spring beans added by other modules (e.g. theCargoProcessor
added byCargoConfiguration
is used byMiningCalculator
). - the
Complete
application context, which really can be started, because it imports all otherComplete
configurations that it needs (e.g.CargoConfiguration.Complete
).
I like this separation, because I can use Provided
for component tests (that test a few classes interacting; together with @MockBean
for beans provided by other modules) and Complete
for productive code and also for integration tests (where (in my case) a module is tested together with other modules).
excludeFilters
The component scan of Provided
needs to exclude the Complete
configuration explicitly. Otherwise both would contain all the beans of Complete
:
@ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Complete.class) })
Configurations for tests
As explained e.g. here, unit tests (that test a single class) usually don’t need a Spring application context. For a component test I do set up an application context.
public class MineralTestConfiguration {
@MockBean
private CargoProcessor cargoProcessor;
...
@Configuration
@Import({ MineralTestConfiguration.class, MineralConfiguration.Provided.class })
@Profile("WithoutPersistence")
public class WithoutPersistence {
@MockBean
private MineralRaceRepository mineralRaceRepository;
@MockBean
private MineralPlanetRepository mineralPlanetRepository;
}
@Configuration
@EnableAutoConfiguration // Required by @DataMongoTest
@DataMongoTest
@Import({ MineralTestConfiguration.class, MineralConfiguration.Provided.class })
@Profile("WithPersistence")
public class WithPersistence {
}
}
The noteworthy detail is that I again have two configurations:
- An application context that mocks repositories for tests that do not need persistence.
- An application context that starts up a MongoDB for the test, when persistence is tested.
Testing with the MongoDB slows down the tests significantly, so it is worth making this distinction. I save 20 seconds in a 90-seconds-maven-install by not starting the DB every time.
Profile
The two configurations are marked with @Profile
. This is partnered with @ActiveProfiles
in the tests that use them. This is done, so that the two profiles are not used at the same time (due to the component scan).
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MineralTestConfiguration.WithoutPersistence.class)
@ActiveProfiles("WithoutPersistence")
public class MineralConfigurationTest {
...
Integration tests
The Complete
configurations are used in integration tests:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { GameConfiguration.Complete.class, MineralConfiguration.Complete.class,
PersistenceTestConfiguration.class, RaceTestApiConfiguration.class, MineralTestApiConfiguration.class })
public class MineralGenerationIntegrationTest {
...
Things I have given up
Bean vs. ComponentScan
I started out using @Bean
-methods to define every bean instead of using @ComponentScan
. I had hoped that would be more explicit, reducing the Spring magic. It does get tedious quickly though and clutters the configuration class with repetitive methods that do not contain any logic. So I switched to component scan.
Instead I use @Bean
, where it is really needed, e.g. for a list of beans, where the length depends on the configuration:
/** Create the mineral types that are configured in the properties. */
@Bean
public List<MineralType> mineralTypes(final MineralProperties mineralProperties) {
return mineralProperties.getTypeIds().stream().map(GenericMineralType::new).collect(Collectors.toList());
}
The component scan is the reason for the use of @Profile
and @ActiveProfiles
(see above). Nobody’s perfect.
Required beans
I also set out to include an interface (called Required
) that lists the beans needed for the Provided
context to run. The interface would be implemented by the configuration used for tests. Since this is the only place, where it is used, it does not really serve any purpose.
public class MineralTestConfiguration implements MineralConfiguration.Required {
@Bean
@Override
public CargoProcessor cargoProcessor() {
return mock(CargoProcessor.class);
}
...