diff --git a/.gitignore b/.gitignore index 9bb8bbf66..f35982eab 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ out *.rdb .checkstyle !etc/eclipse/.checkstyle +gradle/ !**/src/**/build .DS_Store spring-session-docs/package-lock.json diff --git a/build.gradle b/build.gradle index 782fcd722..f89588b9f 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ buildscript { plugins { id "com.github.ben-manes.versions" + id 'maven-publish' } apply plugin: 'io.spring.convention.root' @@ -66,3 +67,23 @@ springRelease { apiDocUrl = "https://docs.spring.io/spring-session/reference/{version}/api/java/index.html" replaceSnapshotVersionInReferenceDocUrl = true } + + +configure(subprojects) {project -> { + apply plugin: 'maven-publish' + + publishing { + repositories { + maven { + url = release_url + name = 'kyligence' + if (project.hasProperty('kyligenceRepoUsername')) { + credentials { + username "$kyligenceRepoUsername" + password "$kyligenceRepoPassword" + } + } + } + } + } +}} diff --git a/gradle.properties b/gradle.properties index 3f1517d65..e890893f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.parallel=true -version=3.5.6 +version=3.5.6-kylin-r1-SNAPSHOT + +group_id=org.springframework.session +artifact_id=spring-session-core +release_url=https://repo-ofs.kyligence.com/repository/maven-releases/ diff --git a/spring-session-core/src/main/java/org/springframework/session/MapSession.java b/spring-session-core/src/main/java/org/springframework/session/MapSession.java index 4f6949e08..cfb348698 100644 --- a/spring-session-core/src/main/java/org/springframework/session/MapSession.java +++ b/spring-session-core/src/main/java/org/springframework/session/MapSession.java @@ -17,14 +17,19 @@ package org.springframework.session; import java.io.Serializable; +import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; +import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; +import org.springframework.util.DigestUtils; + /** *

* A {@link Session} implementation that is backed by a {@link java.util.Map}. The @@ -242,7 +247,25 @@ public int hashCode() { } private static String generateId() { - return UUID.randomUUID().toString(); + String id; + if (SpringHttpSessionConfiguration.secureRandomCreateEnabled) { + byte[] salt1 = new byte[36]; + byte[] salt2 = new byte[36]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.setSeed(System.currentTimeMillis()); + secureRandom.nextBytes(salt1); + secureRandom.nextBytes(salt2); + id = DigestUtils.md5DigestAsHex(salt1) + DigestUtils.md5DigestAsHex(salt2); + } + else { + id = UUID.randomUUID().toString(); + } + + if (SpringHttpSessionConfiguration.jdbcEncodeEnable) { + return new String(Base64.getEncoder().encode(id.getBytes())); + } + + return id; } /** diff --git a/spring-session-core/src/main/java/org/springframework/session/config/annotation/web/http/SpringHttpSessionConfiguration.java b/spring-session-core/src/main/java/org/springframework/session/config/annotation/web/http/SpringHttpSessionConfiguration.java index ecf6b588c..016800cef 100644 --- a/spring-session-core/src/main/java/org/springframework/session/config/annotation/web/http/SpringHttpSessionConfiguration.java +++ b/spring-session-core/src/main/java/org/springframework/session/config/annotation/web/http/SpringHttpSessionConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; @@ -106,11 +107,31 @@ public class SpringHttpSessionConfiguration implements InitializingBean, Applica private List httpSessionListeners = new ArrayList<>(); + @Value("${kylin.web.session-skip-header-name:Auto}") + private String skipUpdateSessionHeaderName; + + @Value("${spring.session.timeout:-1}") + private int sessionTimeout; + + public static boolean secureRandomCreateEnabled; + @Override public void afterPropertiesSet() { this.defaultHttpSessionIdResolver.setCookieSerializer(getCookieSerializer()); } + @Value("${kylin.web.session.secure-random-create-enabled:false}") + public void setSecureRandomCreateEnabled(boolean enabled) { + secureRandomCreateEnabled = enabled; + } + + public static boolean jdbcEncodeEnable; + + @Value("${kylin.web.session.jdbc-encode-enabled:false}") + public void setJdbcEncodeEnable(boolean enabled) { + jdbcEncodeEnable = enabled; + } + @Bean public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() { return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners); @@ -121,6 +142,8 @@ public SessionRepositoryFilter springSess SessionRepository sessionRepository) { SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); + sessionRepositoryFilter.setSessionTimeout(this.sessionTimeout); + sessionRepositoryFilter.setSkipCommitSessionHeaderName(this.skipUpdateSessionHeaderName); return sessionRepositoryFilter; } diff --git a/spring-session-core/src/main/java/org/springframework/session/web/http/SaveSessionException.java b/spring-session-core/src/main/java/org/springframework/session/web/http/SaveSessionException.java new file mode 100644 index 000000000..81ee615a7 --- /dev/null +++ b/spring-session-core/src/main/java/org/springframework/session/web/http/SaveSessionException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.session.web.http; + +public class SaveSessionException extends RuntimeException { + + public SaveSessionException() { + super(); + } + + public SaveSessionException(String s) { + super(s); + } + + public SaveSessionException(String message, Throwable cause) { + super(message, cause); + } + + public SaveSessionException(Throwable cause) { + super(cause); + } + +} diff --git a/spring-session-core/src/main/java/org/springframework/session/web/http/SessionRepositoryFilter.java b/spring-session-core/src/main/java/org/springframework/session/web/http/SessionRepositoryFilter.java index 32415a211..ab5eeffe7 100644 --- a/spring-session-core/src/main/java/org/springframework/session/web/http/SessionRepositoryFilter.java +++ b/spring-session-core/src/main/java/org/springframework/session/web/http/SessionRepositoryFilter.java @@ -17,6 +17,7 @@ package org.springframework.session.web.http; import java.io.IOException; +import java.time.Duration; import java.time.Instant; import java.util.List; @@ -105,6 +106,18 @@ public class SessionRepositoryFilter extends OncePerRequestFi private HttpSessionIdResolver httpSessionIdResolver = new CookieHttpSessionIdResolver(); + private String skipCommitSessionHeaderName = "Auto"; + + public void setSkipCommitSessionHeaderName(String skipCommitSessionHeaderName) { + this.skipCommitSessionHeaderName = skipCommitSessionHeaderName; + } + + private int sessionTimeout = -1; + + public void setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + /** * Creates a new instance. * @param sessionRepository the SessionRepository to use. Cannot be null. @@ -223,10 +236,30 @@ private void commitSession() { } } else { + if (Boolean.valueOf(this.getHeader(SessionRepositoryFilter.this.skipCommitSessionHeaderName))) { + return; + } + S session = wrappedSession.getSession(); String requestedSessionId = getRequestedSessionId(); clearRequestedSessionCache(); - SessionRepositoryFilter.this.sessionRepository.save(session); + try { + S sourceSession = SessionRepositoryFilter.this.sessionRepository.findById(session.getId()); + + if (null == sourceSession) { + SessionRepositoryFilter.this.sessionRepository.save(session); + } + else { + long halfExpireMillis = sourceSession.getLastAccessedTime().toEpochMilli() + + session.getMaxInactiveInterval().toMillis() / 2; + if (System.currentTimeMillis() > halfExpireMillis) { + SessionRepositoryFilter.this.sessionRepository.save(session); + } + } + } + catch (Exception ex) { + throw new SaveSessionException("Failed to save session!", ex); + } String sessionId = session.getId(); if (!isRequestedSessionIdValid() || !sessionId.equals(requestedSessionId)) { SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId); @@ -317,6 +350,12 @@ public HttpSessionWrapper getSession(boolean create) { new RuntimeException("For debugging purposes only (not an error)")); } S session = SessionRepositoryFilter.this.sessionRepository.createSession(); + if (SessionRepositoryFilter.this.sessionTimeout > 60) { + session.setMaxInactiveInterval(Duration.ofSeconds(SessionRepositoryFilter.this.sessionTimeout)); + } + else if (SessionRepositoryFilter.this.sessionTimeout > 0) { + session.setMaxInactiveInterval(Duration.ofSeconds(60)); + } session.setLastAccessedTime(Instant.now()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); 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 98b46de85..59e50cda3 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 @@ -207,6 +207,25 @@ public class JdbcIndexedSessionRepository implements WHERE EXPIRY_TIME < ? """; + // @formatter:off + private static final String DELETE_SESSION_ATTRIBUTE_BY_SESSION_ID_QUERY = "" + + "DELETE FROM %TABLE_NAME%_ATTRIBUTES " + + "WHERE SESSION_PRIMARY_ID in (" + + " SELECT PRIMARY_ID FROM %TABLE_NAME%" + + " WHERE SESSION_ID = ? " + + " AND MAX_INACTIVE_INTERVAL >= 0" + + ")"; + // @formatter:on + + // @formatter:off + private static final String DELETE_SESSION_ATTRIBUTE_BY_EXPIRY_TIME_QUERY = "" + + "DELETE FROM %TABLE_NAME%_ATTRIBUTES " + + "WHERE SESSION_PRIMARY_ID in (" + + " SELECT PRIMARY_ID FROM %TABLE_NAME%" + + " WHERE EXPIRY_TIME < ?" + + ")"; + // @formatter:on + private static final Log logger = LogFactory.getLog(JdbcIndexedSessionRepository.class); private final JdbcOperations jdbcOperations; @@ -240,6 +259,10 @@ public class JdbcIndexedSessionRepository implements private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + private String deleteSessionAttributeBySessionIdQuery; + + private String deleteSessionAttributeByExpiryTimeQuery; + private IndexResolver indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>()); private ConversionService conversionService = createDefaultConversionService(); @@ -383,6 +406,16 @@ public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTime this.deleteSessionsByExpiryTimeQuery = getQuery(deleteSessionsByExpiryTimeQuery); } + public void setDeleteSessionAttributeBySessionIdQuery(String deleteSessionAttributeBySessionIdQuery) { + Assert.hasText(deleteSessionAttributeBySessionIdQuery, "Query must not be empty"); + this.deleteSessionAttributeBySessionIdQuery = getQuery(deleteSessionAttributeBySessionIdQuery); + } + + public void setDeleteSessionAttributeByExpiryTimeQuery(String deleteSessionAttributeByExpiryTimeQuery) { + Assert.hasText(deleteSessionAttributeByExpiryTimeQuery, "Query must not be empty"); + this.deleteSessionAttributeByExpiryTimeQuery = getQuery(deleteSessionAttributeByExpiryTimeQuery); + } + /** * Set the maximum inactive interval in seconds between requests before newly created * sessions will be invalidated. A negative time indicates that the session will never @@ -502,8 +535,12 @@ public JdbcSession findById(final String id) { @Override public void deleteById(final String id) { - this.transactionOperations.executeWithoutResult((status) -> JdbcIndexedSessionRepository.this.jdbcOperations - .update(JdbcIndexedSessionRepository.this.deleteSessionQuery, id)); + this.transactionOperations.executeWithoutResult((status) -> { + JdbcIndexedSessionRepository.this.jdbcOperations + .update(JdbcIndexedSessionRepository.this.deleteSessionAttributeBySessionIdQuery, id); + JdbcIndexedSessionRepository.this.jdbcOperations + .update(JdbcIndexedSessionRepository.this.deleteSessionQuery, id); + }); } @Override @@ -644,9 +681,13 @@ public int getBatchSize() { } public void cleanUpExpiredSessions() { - Integer deletedCount = this.transactionOperations - .execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations - .update(JdbcIndexedSessionRepository.this.deleteSessionsByExpiryTimeQuery, System.currentTimeMillis())); + Integer deletedCount = this.transactionOperations.execute((status) -> { + long currentTimeMillis = System.currentTimeMillis(); + JdbcIndexedSessionRepository.this.jdbcOperations.update( + JdbcIndexedSessionRepository.this.deleteSessionAttributeByExpiryTimeQuery, currentTimeMillis); + return JdbcIndexedSessionRepository.this.jdbcOperations + .update(JdbcIndexedSessionRepository.this.deleteSessionsByExpiryTimeQuery, System.currentTimeMillis()); + }); if (logger.isDebugEnabled()) { logger.debug("Cleaned up " + deletedCount + " expired sessions"); @@ -674,6 +715,9 @@ private void prepareQueries() { this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY); this.listSessionsByPrincipalNameQuery = getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY); this.deleteSessionsByExpiryTimeQuery = getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY); + this.deleteSessionAttributeBySessionIdQuery = getQuery(DELETE_SESSION_ATTRIBUTE_BY_SESSION_ID_QUERY); + this.deleteSessionAttributeByExpiryTimeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_BY_EXPIRY_TIME_QUERY); + } private LobHandler getLobHandler() { @@ -886,82 +930,87 @@ private void flushIfRequired() { } private void save() { - if (this.isNew) { - JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> { - Map indexes = JdbcIndexedSessionRepository.this.indexResolver - .resolveIndexesFor(JdbcSession.this); - JdbcIndexedSessionRepository.this.jdbcOperations - .update(JdbcIndexedSessionRepository.this.createSessionQuery, (ps) -> { - ps.setString(1, JdbcSession.this.primaryKey); - ps.setString(2, getId()); - ps.setLong(3, getCreationTime().toEpochMilli()); - ps.setLong(4, getLastAccessedTime().toEpochMilli()); - ps.setInt(5, (int) getMaxInactiveInterval().getSeconds()); - ps.setLong(6, getExpiryTime().toEpochMilli()); - ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME)); - }); - Set attributeNames = getAttributeNames(); - if (!attributeNames.isEmpty()) { - insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames)); - } - }); - } - else { - List deltaActions = JdbcSession.this.changed ? new ArrayList<>(4) : new ArrayList<>(); - if (JdbcSession.this.changed) { - deltaActions.add(() -> { + try { + if (this.isNew) { + JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> { Map indexes = JdbcIndexedSessionRepository.this.indexResolver - .resolveIndexesFor(JdbcSession.this); + .resolveIndexesFor(JdbcSession.this); JdbcIndexedSessionRepository.this.jdbcOperations - .update(JdbcIndexedSessionRepository.this.updateSessionQuery, (ps) -> { - ps.setString(1, getId()); - ps.setLong(2, getLastAccessedTime().toEpochMilli()); - ps.setInt(3, (int) getMaxInactiveInterval().getSeconds()); - ps.setLong(4, getExpiryTime().toEpochMilli()); - ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME)); - ps.setString(6, JdbcSession.this.primaryKey); - }); + .update(JdbcIndexedSessionRepository.this.createSessionQuery, (ps) -> { + ps.setString(1, JdbcSession.this.primaryKey); + ps.setString(2, getId()); + ps.setLong(3, getCreationTime().toEpochMilli()); + ps.setLong(4, getLastAccessedTime().toEpochMilli()); + ps.setInt(5, (int) getMaxInactiveInterval().getSeconds()); + ps.setLong(6, getExpiryTime().toEpochMilli()); + ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME)); + }); + Set attributeNames = getAttributeNames(); + if (!attributeNames.isEmpty()) { + insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames)); + } }); } + else { + List deltaActions = JdbcSession.this.changed ? new ArrayList<>(4) : new ArrayList<>(); + if (JdbcSession.this.changed) { + deltaActions.add(() -> { + Map indexes = JdbcIndexedSessionRepository.this.indexResolver + .resolveIndexesFor(JdbcSession.this); + JdbcIndexedSessionRepository.this.jdbcOperations + .update(JdbcIndexedSessionRepository.this.updateSessionQuery, (ps) -> { + ps.setString(1, getId()); + ps.setLong(2, getLastAccessedTime().toEpochMilli()); + ps.setInt(3, (int) getMaxInactiveInterval().getSeconds()); + ps.setLong(4, getExpiryTime().toEpochMilli()); + ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME)); + ps.setString(6, JdbcSession.this.primaryKey); + }); + }); + } - List addedAttributeNames = JdbcSession.this.delta.entrySet() - .stream() - .filter((entry) -> entry.getValue() == DeltaValue.ADDED) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - if (!addedAttributeNames.isEmpty()) { - deltaActions.add(() -> insertSessionAttributes(JdbcSession.this, addedAttributeNames)); - } + List addedAttributeNames = JdbcSession.this.delta.entrySet() + .stream() + .filter((entry) -> entry.getValue() == DeltaValue.ADDED) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!addedAttributeNames.isEmpty()) { + deltaActions.add(() -> insertSessionAttributes(JdbcSession.this, addedAttributeNames)); + } - List updatedAttributeNames = JdbcSession.this.delta.entrySet() - .stream() - .filter((entry) -> entry.getValue() == DeltaValue.UPDATED) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - if (!updatedAttributeNames.isEmpty()) { - deltaActions.add(() -> updateSessionAttributes(JdbcSession.this, updatedAttributeNames)); - } + List updatedAttributeNames = JdbcSession.this.delta.entrySet() + .stream() + .filter((entry) -> entry.getValue() == DeltaValue.UPDATED) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!updatedAttributeNames.isEmpty()) { + deltaActions.add(() -> updateSessionAttributes(JdbcSession.this, updatedAttributeNames)); + } - List removedAttributeNames = JdbcSession.this.delta.entrySet() - .stream() - .filter((entry) -> entry.getValue() == DeltaValue.REMOVED) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - if (!removedAttributeNames.isEmpty()) { - deltaActions.add(() -> deleteSessionAttributes(JdbcSession.this, removedAttributeNames)); - } + List removedAttributeNames = JdbcSession.this.delta.entrySet() + .stream() + .filter((entry) -> entry.getValue() == DeltaValue.REMOVED) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!removedAttributeNames.isEmpty()) { + deltaActions.add(() -> deleteSessionAttributes(JdbcSession.this, removedAttributeNames)); + } - if (!deltaActions.isEmpty()) { - JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> { - for (Runnable action : deltaActions) { - action.run(); - } - }); + if (!deltaActions.isEmpty()) { + JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> { + for (Runnable action : deltaActions) { + action.run(); + } + }); + } } + clearChangeFlags(); + } + catch (DataIntegrityViolationException e) { + logger.error(e); + throw new DataIntegrityViolationException("The data exceeds the limit of length for column ?session_id?. Please adjust its maximum value and try again."); } - clearChangeFlags(); } - } private class SessionResultSetExtractor implements ResultSetExtractor> { 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 341b5e350..27e34b6e0 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 @@ -239,6 +239,34 @@ void setDeleteSessionsByLastAccessTimeQueryEmpty() { .withMessage("Query must not be empty"); } + @Test + void setDeleteSessionAttributeBySessionIdQueryNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.setDeleteSessionAttributeBySessionIdQuery(null)) + .withMessage("Query must not be empty"); + } + + @Test + void setDeleteSessionAttributeBySessionIdQueryEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.setDeleteSessionAttributeBySessionIdQuery(" ")) + .withMessage("Query must not be empty"); + } + + @Test + void setDeleteSessionAttributeByExpiryTimeQueryNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.setDeleteSessionAttributeByExpiryTimeQuery(null)) + .withMessage("Query must not be empty"); + } + + @Test + void setDeleteSessionAttributeByExpiryTimeQueryEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.setDeleteSessionAttributeByExpiryTimeQuery(" ")) + .withMessage("Query must not be empty"); + } + @Test void setLobHandlerNull() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setLobHandler(null)) @@ -573,7 +601,7 @@ void getSessionExpired() { assertThat(session).isNull(); verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)); - verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(expired.getId())); + verify(this.jdbcOperations, times(2)).update(startsWith("DELETE"), eq(expired.getId())); } @Test @@ -600,7 +628,7 @@ void delete() { this.repository.deleteById(sessionId); - verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId)); + verify(this.jdbcOperations, times(2)).update(startsWith("DELETE"), eq(sessionId)); } @Test @@ -658,7 +686,7 @@ void findByIndexNameAndIndexValuePrincipalIndexNameFound() { void cleanupExpiredSessions() { this.repository.cleanUpExpiredSessions(); - verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong()); + verify(this.jdbcOperations, times(2)).update(startsWith("DELETE"), anyLong()); } @Test // gh-1120