diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java index 60e82ed0..4b4e9b83 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java @@ -27,6 +27,8 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.NestedExceptionUtils; @@ -38,6 +40,10 @@ import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.util.ByteUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.scheduling.support.CronTrigger; import org.springframework.session.DelegatingIndexResolver; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.FlushMode; @@ -249,12 +255,18 @@ import org.springframework.util.Assert; * @since 2.2.0 */ public class RedisIndexedSessionRepository - implements FindByIndexNameSessionRepository, MessageListener { + implements FindByIndexNameSessionRepository, MessageListener, + InitializingBean, DisposableBean { private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class); private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + /** + * The default cron expression used for expired session cleanup job. + */ + public static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; + /** * The default Redis database used by Spring Session. */ @@ -309,6 +321,10 @@ public class RedisIndexedSessionRepository private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; + private String cleanupCron = DEFAULT_CLEANUP_CRON; + + private ThreadPoolTaskScheduler taskScheduler; + /** * Creates a new instance. For an example, refer to the class level javadoc. * @param sessionRedisOperations the {@link RedisOperations} to use for managing the @@ -322,6 +338,28 @@ public class RedisIndexedSessionRepository configureSessionChannels(); } + @Override + public void afterPropertiesSet() { + if (!Scheduled.CRON_DISABLED.equals(this.cleanupCron)) { + this.taskScheduler = createTaskScheduler(); + this.taskScheduler.initialize(); + this.taskScheduler.schedule(this::cleanUpExpiredSessions, new CronTrigger(this.cleanupCron)); + } + } + + private static ThreadPoolTaskScheduler createTaskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setThreadNamePrefix("spring-session-"); + return taskScheduler; + } + + @Override + public void destroy() { + if (this.taskScheduler != null) { + this.taskScheduler.destroy(); + } + } + /** * Sets the {@link ApplicationEventPublisher} that is used to publish * {@link SessionDestroyedEvent}. The default is to not publish a @@ -382,6 +420,21 @@ public class RedisIndexedSessionRepository this.saveMode = saveMode; } + /** + * Set the cleanup cron expression. + * @param cleanupCron the cleanup cron expression + * @since 3.0.0 + * @see CronExpression + * @see Scheduled#CRON_DISABLED + */ + public void setCleanupCron(String cleanupCron) { + Assert.notNull(cleanupCron, "cleanupCron must not be null"); + if (!Scheduled.CRON_DISABLED.equals(cleanupCron)) { + Assert.isTrue(CronExpression.isValidExpression(cleanupCron), "cleanupCron must be valid"); + } + this.cleanupCron = cleanupCron; + } + /** * Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}. * @param database the database index to use @@ -420,7 +473,7 @@ public class RedisIndexedSessionRepository } } - public void cleanupExpiredSessions() { + public void cleanUpExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java index 8be4a5be..a90b836b 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java @@ -106,6 +106,6 @@ public @interface EnableRedisIndexedHttpSession { * The cron expression for expired session cleanup job. By default runs every minute. * @return the session cleanup cron expression */ - String cleanupCron() default RedisIndexedHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; + String cleanupCron() default RedisIndexedSessionRepository.DEFAULT_CLEANUP_CRON; } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java index 1635c17a..e8a65bca 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java @@ -41,9 +41,6 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.session.IndexResolver; import org.springframework.session.Session; import org.springframework.session.data.redis.RedisIndexedSessionRepository; @@ -68,9 +65,7 @@ public class RedisIndexedHttpSessionConfiguration extends AbstractRedisHttpSessionConfiguration implements EmbeddedValueResolverAware, ImportAware { - static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; - - private String cleanupCron = DEFAULT_CLEANUP_CRON; + private String cleanupCron = RedisIndexedSessionRepository.DEFAULT_CLEANUP_CRON; private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); @@ -102,6 +97,7 @@ public class RedisIndexedHttpSessionConfiguration } sessionRepository.setFlushMode(getFlushMode()); sessionRepository.setSaveMode(getSaveMode()); + sessionRepository.setCleanupCron(this.cleanupCron); int database = resolveDatabase(); sessionRepository.setDatabase(database); getSessionRepositoryCustomizers() @@ -247,25 +243,4 @@ public class RedisIndexedHttpSessionConfiguration } - /** - * Configuration of scheduled job for cleaning up expired sessions. - */ - @EnableScheduling - @Configuration(proxyBeanMethods = false) - class SessionCleanupConfiguration implements SchedulingConfigurer { - - private final RedisIndexedSessionRepository sessionRepository; - - SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) { - this.sessionRepository = sessionRepository; - } - - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions, - RedisIndexedHttpSessionConfiguration.this.cleanupCron); - } - - } - } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java index 5b6c22fa..825f1ca2 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java @@ -44,6 +44,7 @@ import org.springframework.data.redis.core.BoundValueOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.FlushMode; import org.springframework.session.MapSession; @@ -451,7 +452,7 @@ class RedisIndexedSessionRepositoryTests { Set expiredIds = new HashSet<>(Arrays.asList("expired-key1", "expired-key2")); given(this.boundSetOperations.members()).willReturn(expiredIds); - this.redisRepository.cleanupExpiredSessions(); + this.redisRepository.cleanUpExpiredSessions(); for (Object id : expiredIds) { String expiredKey = "spring:session:sessions:" + id; @@ -744,6 +745,25 @@ class RedisIndexedSessionRepositoryTests { .withMessage("flushMode cannot be null"); } + @Test + void setCleanupCronNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setCleanupCron(null)) + .withMessage("cleanupCron must not be null"); + } + + @Test + void setCleanupCronInvalid() { + assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setCleanupCron("test")) + .withMessage("cleanupCron must be valid"); + } + + @Test + void setCleanupCronDisabled() { + this.redisRepository.setCleanupCron(Scheduled.CRON_DISABLED); + this.redisRepository.afterPropertiesSet(); + assertThat(this.redisRepository).extracting("taskScheduler").isNull(); + } + @Test void changeRedisNamespace() { String namespace = "foo:bar"; diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java index d9b96d63..8d41058f 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java @@ -117,10 +117,8 @@ class RedisIndexedHttpSessionConfigurationTests { void customCleanupCronAnnotation() { registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class); - RedisIndexedHttpSessionConfiguration configuration = this.context - .getBean(RedisIndexedHttpSessionConfiguration.class); - assertThat(configuration).isNotNull(); - assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); + RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); + assertThat(sessionRepository).extracting("cleanupCron").isEqualTo(CLEANUP_CRON_EXPRESSION); } @Test diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java index e6f3df39..16f9eab2 100644 --- a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java @@ -34,6 +34,8 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.GenericConversionService; @@ -48,6 +50,10 @@ import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.LobCreator; import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.scheduling.support.CronTrigger; import org.springframework.session.DelegatingIndexResolver; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.FlushMode; @@ -130,14 +136,19 @@ import org.springframework.util.StringUtils; * @author Craig Andrews * @since 2.2.0 */ -public class JdbcIndexedSessionRepository - implements FindByIndexNameSessionRepository { +public class JdbcIndexedSessionRepository implements + FindByIndexNameSessionRepository, InitializingBean, DisposableBean { /** * The default name of database table used by Spring Session to store sessions. */ public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION"; + /** + * The default cron expression used for expired session cleanup job. + */ + public static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; private static final String CREATE_SESSION_QUERY = """ @@ -241,6 +252,10 @@ public class JdbcIndexedSessionRepository private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; + private String cleanupCron = DEFAULT_CLEANUP_CRON; + + private ThreadPoolTaskScheduler taskScheduler; + /** * Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided * {@link JdbcOperations} and {@link TransactionOperations} to manage sessions. @@ -255,6 +270,28 @@ public class JdbcIndexedSessionRepository prepareQueries(); } + @Override + public void afterPropertiesSet() { + if (!Scheduled.CRON_DISABLED.equals(this.cleanupCron)) { + this.taskScheduler = createTaskScheduler(); + this.taskScheduler.initialize(); + this.taskScheduler.schedule(this::cleanUpExpiredSessions, new CronTrigger(this.cleanupCron)); + } + } + + private static ThreadPoolTaskScheduler createTaskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setThreadNamePrefix("spring-session-"); + return taskScheduler; + } + + @Override + public void destroy() { + if (this.taskScheduler != null) { + this.taskScheduler.destroy(); + } + } + /** * Set the name of database table used to store sessions. * @param tableName the database table name @@ -397,6 +434,21 @@ public class JdbcIndexedSessionRepository this.saveMode = saveMode; } + /** + * Set the cleanup cron expression. + * @param cleanupCron the cleanup cron expression + * @since 3.0.0 + * @see CronExpression + * @see Scheduled#CRON_DISABLED + */ + public void setCleanupCron(String cleanupCron) { + Assert.notNull(cleanupCron, "cleanupCron must not be null"); + if (!Scheduled.CRON_DISABLED.equals(cleanupCron)) { + Assert.isTrue(CronExpression.isValidExpression(cleanupCron), "cleanupCron must be valid"); + } + this.cleanupCron = cleanupCron; + } + @Override public JdbcSession createSession() { MapSession delegate = new MapSession(); diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/EnableJdbcHttpSession.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/EnableJdbcHttpSession.java index 2f7698ea..d60753ed 100644 --- a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/EnableJdbcHttpSession.java +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/EnableJdbcHttpSession.java @@ -96,7 +96,7 @@ public @interface EnableJdbcHttpSession { * @return the session cleanup cron expression * @since 2.0.0 */ - String cleanupCron() default JdbcHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; + String cleanupCron() default JdbcIndexedSessionRepository.DEFAULT_CLEANUP_CRON; /** * Flush mode for the sessions. The default is {@code ON_SAVE} which only updates the diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java index bbc7eee2..b1a30d3a 100644 --- a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java @@ -43,9 +43,6 @@ import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.LobHandler; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.session.FlushMode; import org.springframework.session.IndexResolver; import org.springframework.session.MapSession; @@ -80,13 +77,11 @@ import org.springframework.util.StringValueResolver; public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { - static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; - private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; private String tableName = JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME; - private String cleanupCron = DEFAULT_CLEANUP_CRON; + private String cleanupCron = JdbcIndexedSessionRepository.DEFAULT_CLEANUP_CRON; private FlushMode flushMode = FlushMode.ON_SAVE; @@ -126,6 +121,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); sessionRepository.setFlushMode(this.flushMode); sessionRepository.setSaveMode(this.saveMode); + sessionRepository.setCleanupCron(this.cleanupCron); if (this.indexResolver != null) { sessionRepository.setIndexResolver(this.indexResolver); } @@ -281,25 +277,4 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration return conversionService; } - /** - * Configuration of scheduled job for cleaning up expired sessions. - */ - @EnableScheduling - @Configuration(proxyBeanMethods = false) - class SessionCleanupConfiguration implements SchedulingConfigurer { - - private final JdbcIndexedSessionRepository sessionRepository; - - SessionCleanupConfiguration(JdbcIndexedSessionRepository sessionRepository) { - this.sessionRepository = sessionRepository; - } - - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.addCronTask(this.sessionRepository::cleanUpExpiredSessions, - JdbcHttpSessionConfiguration.this.cleanupCron); - } - - } - } diff --git a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java index 859bcb8f..e9392d03 100644 --- a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java +++ b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java @@ -38,6 +38,7 @@ import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.TemporaryLobCreator; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -243,6 +244,25 @@ class JdbcIndexedSessionRepositoryTests { .withMessage("saveMode must not be null"); } + @Test + void setCleanupCronNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCleanupCron(null)) + .withMessage("cleanupCron must not be null"); + } + + @Test + void setCleanupCronInvalid() { + assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCleanupCron("test")) + .withMessage("cleanupCron must be valid"); + } + + @Test + void setCleanupCronDisabled() { + this.repository.setCleanupCron(Scheduled.CRON_DISABLED); + this.repository.afterPropertiesSet(); + assertThat(this.repository).extracting("taskScheduler").isNull(); + } + @Test void createSessionDefaultMaxInactiveInterval() { JdbcSession session = this.repository.createSession(); diff --git a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java index 27d3857f..b2df1bd6 100644 --- a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java +++ b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java @@ -136,18 +136,16 @@ class JdbcHttpSessionConfigurationTests { void customCleanupCronAnnotation() { registerAndRefresh(DataSourceConfiguration.class, CustomCleanupCronExpressionAnnotationConfiguration.class); - JdbcHttpSessionConfiguration configuration = this.context.getBean(JdbcHttpSessionConfiguration.class); - assertThat(configuration).isNotNull(); - assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); + JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class); + assertThat(repository).extracting("cleanupCron").isEqualTo(CLEANUP_CRON_EXPRESSION); } @Test void customCleanupCronSetter() { registerAndRefresh(DataSourceConfiguration.class, CustomCleanupCronExpressionSetterConfiguration.class); - JdbcHttpSessionConfiguration configuration = this.context.getBean(JdbcHttpSessionConfiguration.class); - assertThat(configuration).isNotNull(); - assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); + JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class); + assertThat(repository).extracting("cleanupCron").isEqualTo(CLEANUP_CRON_EXPRESSION); } @Test