Introduce IndexResolver

This commit introduces IndexResolver as a strategy interface for resolving index values in FindByIndexNameSessionRepository implementations.

Resolves: #557
This commit is contained in:
Vedran Pavic
2019-04-30 21:37:00 +02:00
parent 099be441dd
commit a6f6042831
10 changed files with 364 additions and 122 deletions

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2014-2019 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;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An {@link IndexResolver} that resolves indexes using multiple @{link IndexResolver}
* delegates.
*
* @param <S> the type of Session being handled
* @author Vedran Pavic
* @since 2.2.0
*/
public class DelegatingIndexResolver<S extends Session> implements IndexResolver<S> {
private final List<IndexResolver<S>> delegates;
public DelegatingIndexResolver(List<IndexResolver<S>> delegates) {
this.delegates = Collections.unmodifiableList(delegates);
}
@SafeVarargs
public DelegatingIndexResolver(IndexResolver<S>... delegates) {
this(Arrays.asList(delegates));
}
public Map<String, String> resolveIndexesFor(S session) {
Map<String, String> indexes = new HashMap<>();
for (IndexResolver<S> delegate : this.delegates) {
indexes.putAll(delegate.resolveIndexesFor(session));
}
return indexes;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2014-2019 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;
import java.util.Map;
/**
* Strategy interface for resolving the {@link Session}'s indexes.
*
* @param <S> the type of Session being handled
* @author Rob Winch
* @author Vedran Pavic
* @since 2.2.0
* @see FindByIndexNameSessionRepository
*/
public interface IndexResolver<S extends Session> {
/**
* Resolve indexes for the session.
* @param session the session
* @return a map of resolved indexes, never {@code null}
*/
Map<String, String> resolveIndexesFor(S session);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2014-2019 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;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* {@link IndexResolver} to resolve the principal name from session attribute named
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} or Spring Security
* context stored in the session under {@code SPRING_SECURITY_CONTEXT} attribute.
*
* @param <S> the type of Session being handled
* @author Vedran Pavic
* @since 2.2.0
*/
public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexResolver<S> {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final SpelExpressionParser parser = new SpelExpressionParser();
public PrincipalNameIndexResolver() {
super(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
}
public String resolveIndexValueFor(S session) {
String principalName = session.getAttribute(getIndexName());
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2014-2019 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;
import java.util.Collections;
import java.util.Map;
import org.springframework.util.Assert;
/**
* Base class for {@link IndexResolver}s that resolve a single index.
*
* @param <S> the type of Session being handled
* @author Rob Winch
* @author Vedran Pavic
* @since 2.2.0
*/
public abstract class SingleIndexResolver<S extends Session> implements IndexResolver<S> {
private final String indexName;
protected SingleIndexResolver(String indexName) {
Assert.notNull(indexName, "Index name must not be null");
this.indexName = indexName;
}
protected String getIndexName() {
return this.indexName;
}
public abstract String resolveIndexValueFor(S session);
public final Map<String, String> resolveIndexesFor(S session) {
String indexValue = resolveIndexValueFor(session);
return (indexValue != null) ? Collections.singletonMap(this.indexName, indexValue) : Collections.emptyMap();
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2014-2019 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;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DelegatingIndexResolver}.
*
* @author Vedran Pavic
*/
class DelegatingIndexResolverTests {
private DelegatingIndexResolver<MapSession> indexResolver;
@BeforeEach
void setUp() {
this.indexResolver = new DelegatingIndexResolver<>(new TestIndexResolver("one"), new TestIndexResolver("two"));
}
@Test
void resolve() {
MapSession session = new MapSession();
session.setAttribute("one", "first");
session.setAttribute("two", "second");
Map<String, String> indexes = this.indexResolver.resolveIndexesFor(session);
assertThat(indexes).hasSize(2);
assertThat(indexes.get("one")).isEqualTo("first");
assertThat(indexes.get("two")).isEqualTo("second");
}
private static class TestIndexResolver implements IndexResolver<MapSession> {
private final String supportedIndex;
TestIndexResolver(String supportedIndex) {
this.supportedIndex = supportedIndex;
}
public Map<String, String> resolveIndexesFor(MapSession session) {
return Collections.singletonMap(this.supportedIndex, session.getAttribute(this.supportedIndex));
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2014-2019 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;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PrincipalNameIndexResolver}.
*
* @author Vedran Pavic
*/
class PrincipalNameIndexResolverTests {
private static final String PRINCIPAL_NAME = "principalName";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private PrincipalNameIndexResolver<Session> indexResolver;
@BeforeEach
void setUp() {
this.indexResolver = new PrincipalNameIndexResolver<>();
}
@Test
void resolveFromPrincipalName() {
MapSession session = new MapSession();
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, PRINCIPAL_NAME);
assertThat(this.indexResolver.resolveIndexValueFor(session)).isEqualTo(PRINCIPAL_NAME);
}
@Test
void resolveFromSpringSecurityContext() {
Authentication authentication = new UsernamePasswordAuthenticationToken(PRINCIPAL_NAME, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext context = new SecurityContextImpl();
context.setAuthentication(authentication);
MapSession session = new MapSession();
session.setAttribute(SPRING_SECURITY_CONTEXT, context);
assertThat(this.indexResolver.resolveIndexValueFor(session)).isEqualTo(PRINCIPAL_NAME);
}
}

View File

@@ -36,10 +36,11 @@ import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
@@ -252,8 +253,6 @@ public class RedisOperationsSessionRepository
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
static PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
/**
* The default Redis database used by Spring Session.
*/
@@ -281,6 +280,8 @@ public class RedisOperationsSessionRepository
private final RedisSessionExpirationPolicy expirationPolicy;
private final IndexResolver<RedisSession> indexResolver;
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
@@ -311,6 +312,7 @@ public class RedisOperationsSessionRepository
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
this::getSessionKey);
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
configureSessionChannels();
}
@@ -533,7 +535,8 @@ public class RedisOperationsSessionRepository
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(session);
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(session);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
@@ -689,8 +692,9 @@ public class RedisOperationsSessionRepository
RedisSession(MapSession cached) {
Assert.notNull(cached, "MapSession cannot be null");
this.cached = cached;
this.originalPrincipalName = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
}
public void setNew(boolean isNew) {
@@ -800,7 +804,9 @@ public class RedisOperationsSessionRepository
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
@@ -851,26 +857,4 @@ public class RedisOperationsSessionRepository
}
/**
* Principal name resolver helper class.
*/
static class PrincipalNameResolver {
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}
}

View File

@@ -45,15 +45,9 @@ 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.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.PrincipalNameResolver;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent;
@@ -597,32 +591,6 @@ class RedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.boundHashOperations);
}
@Test
void resolvePrincipalIndex() {
PrincipalNameResolver resolver = RedisOperationsSessionRepository.PRINCIPAL_NAME_RESOLVER;
String username = "username";
RedisSession session = this.redisRepository.createSession();
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
assertThat(resolver.resolvePrincipal(session)).isEqualTo(username);
}
@Test
void resolveIndexOnSecurityContext() {
String principal = "resolveIndexOnSecurityContext";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext context = new SecurityContextImpl();
context.setAuthentication(authentication);
PrincipalNameResolver resolver = RedisOperationsSessionRepository.PRINCIPAL_NAME_RESOLVER;
RedisSession session = this.redisRepository.createSession();
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
assertThat(resolver.resolvePrincipal(session)).isEqualTo(principal);
}
@Test
void flushModeOnSaveCreate() {
this.redisRepository.createSession();

View File

@@ -40,10 +40,11 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent;
@@ -129,10 +130,10 @@ public class HazelcastSessionRepository
private static final Log logger = LogFactory.getLog(HazelcastSessionRepository.class);
private static final PrincipalNameResolver principalNameResolver = new PrincipalNameResolver();
private final HazelcastInstance hazelcastInstance;
private final IndexResolver<HazelcastSession> indexResolver;
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
@Override
@@ -162,6 +163,7 @@ public class HazelcastSessionRepository
public HazelcastSessionRepository(HazelcastInstance hazelcastInstance) {
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
this.hazelcastInstance = hazelcastInstance;
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
}
@PostConstruct
@@ -425,7 +427,8 @@ public class HazelcastSessionRepository
this.delegate.setAttribute(attributeName, attributeValue);
this.delta.put(attributeName, attributeValue);
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
String principal = (attributeValue != null) ? principalNameResolver.resolvePrincipal(this) : null;
Map<String, String> indexes = HazelcastSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
@@ -460,26 +463,4 @@ public class HazelcastSessionRepository
}
/**
* Resolves the Spring Security principal name.
*/
static class PrincipalNameResolver {
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}
}

View File

@@ -40,16 +40,17 @@ import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@@ -197,12 +198,12 @@ public class JdbcOperationsSessionRepository
private static final Log logger = LogFactory.getLog(JdbcOperationsSessionRepository.class);
private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
private final JdbcOperations jdbcOperations;
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
private final IndexResolver<JdbcSession> indexResolver;
private TransactionOperations transactionOperations = new TransactionOperations() {
@Override
@@ -271,6 +272,7 @@ public class JdbcOperationsSessionRepository
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
this.jdbcOperations = jdbcOperations;
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
this.conversionService = createDefaultConversionService();
prepareQueries();
}
@@ -406,6 +408,8 @@ public class JdbcOperationsSessionRepository
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(session);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.createSessionQuery, (ps) -> {
ps.setString(1, session.primaryKey);
@@ -414,7 +418,7 @@ public class JdbcOperationsSessionRepository
ps.setLong(4, session.getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(6, session.getExpiryTime().toEpochMilli());
ps.setString(7, session.getPrincipalName());
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
});
Set<String> attributeNames = session.getAttributeNames();
if (!attributeNames.isEmpty()) {
@@ -430,13 +434,15 @@ public class JdbcOperationsSessionRepository
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (session.isChanged()) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(session);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.updateSessionQuery, (ps) -> {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(4, session.getExpiryTime().toEpochMilli());
ps.setString(5, session.getPrincipalName());
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
ps.setString(6, session.primaryKey);
});
}
@@ -742,10 +748,6 @@ public class JdbcOperationsSessionRepository
this.delta.clear();
}
String getPrincipalName() {
return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
}
Instant getExpiryTime() {
return getLastAccessedTime().plus(getMaxInactiveInterval());
}
@@ -838,30 +840,6 @@ public class JdbcOperationsSessionRepository
}
/**
* Resolves the Spring Security principal name.
*
* @author Vedran Pavic
*/
static class PrincipalNameResolver {
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {
@Override