I'm not getting session closed or expired events when using embedded Hazelcast session repository in Spring boot application. I do get session creation events. I have a very short timeout for the sessions (30s). I have verified that the session gets expired after 30s by getting "unauthorized" reply from the server. How do you receive session expiration/destruction events?
This is my session configuration:
#Configuration
#EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = 30)
public class SessionConfiguration{
#Bean
#SpringSessionHazelcastInstance
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
config.setClusterName("spring-session-cluster");
// Add this attribute to be able to query sessions by their PRINCIPAL_NAME_ATTRIBUTE's
AttributeConfig attributeConfig = new AttributeConfig()
.setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
// Configure the sessions map
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
// Use use custom serializer to de/serialize sessions faster. This is optional.
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);
return Hazelcast.newHazelcastInstance(config);
}
#Bean
public SessionRepositoryCustomizer<Hazelcast4IndexedSessionRepository> customize() {
return (sessionRepository) -> {
sessionRepository.setFlushMode(FlushMode.IMMEDIATE);
sessionRepository.setSaveMode(SaveMode.ALWAYS);
sessionRepository.setSessionMapName(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
sessionRepository.setDefaultMaxInactiveInterval(30); //this is extra; tried with and without
};
}
}
And this is my listener:
#Component
public class SessionListener {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SessionListener.class);
#EventListener
public void sessionCreated(SessionCreatedEvent event) {
log.info("SESSION:CREATE:ID="+event.getSessionId()); //only this gets called but none of the others
}
#EventListener
public void sessionDeleted(SessionDeletedEvent event) {
log.info("SESSION:DELETE:ID="+event.getSessionId());
}
#EventListener
public void sessionDestroyed(SessionDestroyedEvent event) {
log.info("SESSION:DESTROY:ID="+event.getId());
}
#EventListener
public void sessionExpired(SessionExpiredEvent event) {
log.info("SESSION:EXPIRE:ID="+event.getSessionId());
}
}
Partial answer (I don't know exactly why it works):
If you add a session map listener to HazelcastInstance (in the creating bean) you suddenly start receiving SessionExpiredEvents.
So replace lines:
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
with (set maximum idle seconds to the session map configuration):
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE))
.setMaxIdleSeconds(tout);
and
return Hazelcast.newHazelcastInstance(config);
with (add session entry listener to the session map)
HazelcastInstance instance = Hazelcast.newHazelcastInstance(config);
IMap<Object, Object> map = instance.getMap( Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME );
map.addEntryListener( new HazelcastSessionEntryListener(), true );
return instance;
where HazelcastSessionEntryListener can be defined like this:
#Component
public class HazelcastSessionEntryListener implements EntryListener<Object, Object>
{
public HazelcastSessionEntryListener(){}
#Override
public void entryAdded(EntryEvent<Object, Object> event){}
#Override
public void entryUpdated(EntryEvent<Object, Object> event){}
#Override
public void entryRemoved(EntryEvent<Object, Object> event){}
#Override
public void entryEvicted(EntryEvent<Object, Object> event){}
#Override
public void entryExpired(EntryEvent<Object, Object> event){}
#Override
public void mapCleared(MapEvent event){}
#Override
public void mapEvicted(MapEvent event){}
}
Funny thing is that HazelcastSessionEntryListener is just an empty implementation in my case (it doesn't do anything). Seems like buggy behaviour (but I'm not a Spring expert).
Related
I'm trying to start up a Pub/Sub setup with Spring Data Redis, and I'm able to load the publisher, but the RedisMessageListenerContainer doesn't start up automatically. I'm using Spring Data Redis 2.2.8.RELEASE along with embedded redis server (it.ozimov.embedded-redis version 0.7.2). Does anyone have any insight as to why the RedisMessageListenerContainer won't start up?
Here are my classes.
RedisListenerAutoConfiguration
#Configuration
#ConditionalOnProperty(prefix = "myproj.redis", name = "mode", havingValue = "LISTENER", matchIfMissing = true)
#ComponentScan("com.jcworx.redis.listener")
#ConditionalOnBean(type = "com.jcworx.redis.listener.RedisMessageListener")
#AutoConfigureAfter(RedisAutoConfiguration.class)
#EnableConfigurationProperties(RedisConfigurationProperties.class)
public class RedisListenerAutoConfiguration {
#Autowired
private RedisConfigurationProperties redisConfigurationProperties;
#Bean
public MessageListenerAdapter messageListenerAdapter(RedisMessageListener<?> redisMessageListener){
return new MessageListenerAdapter(redisMessageListener,"onRedisMessage");
}
#Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter messageListenerAdapter){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(messageListenerAdapter, new ChannelTopic(redisConfigurationProperties.getQueueName()));
return container;
}
}
SimpleRedisMessageListener
#Component
public class SimpleRedisMessageListener extends AbstractRedisMessageListener<SimpleType>{
private static final Logger LOG = LoggerFactory.getLogger(SimpleRedisMessageListener.class);
private CountDownLatch countDownLatch;
#Override
public void processRedisMsg(RedisMessage<SimpleType> redisMsg) {
LOG.info("Processing Message. trxId={}, payload={}",redisMsg.getTrxId(),redisMsg.getPayload());
Assert.notNull(countDownLatch,"Count Down Latch cannot be null.");
countDownLatch.countDown();
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
}
RedisServerConfiguration
#Configuration
#Profile("test")
public class RedisServerConfiguration {
private RedisServer redisServer;
#Autowired //redisProperties autowired from RedisAutoConfiguration
public RedisServerConfiguration(RedisProperties redisProperties){
redisServer = new RedisServer(redisProperties.getPort());
}
#PostConstruct
public void postConstruct(){
redisServer.start();
}
#PreDestroy
public void preDestroy(){
redisServer.stop();
}
}
application-test.properties
#application test resources
myproj.redis.queueName=test
spring.redis.host=localhost
spring.redis.port=6379
#set to true when you need to see the auto configuration rules
debug=true
RedisPubSubABTTest
#SpringBootTest(classes = TestRedisApp.class)
#ActiveProfiles("test")
public class RedisPubSubABTTest {
#Autowired
private RedisMessagePublisher redisMessagePublisher;
#Autowired
private SimpleRedisMessageListener simpleRedisMessageListener;
/**
* Send a message to the embedded redis queue and await the listener to respond. If it
* responds, then the countdown latch will count down to 0. Otherwise, it will time out
* and fail to respond.
* #throws InterruptedException
*/
#Test
public void messageSentAndReceived() throws InterruptedException{
//ARRANGE
SimpleType simpleType = new SimpleType();
simpleType.setFirstName("John");
simpleType.setLastName("Smith");
CountDownLatch countDownLatch = new CountDownLatch(1);
simpleRedisMessageListener.setCountDownLatch(countDownLatch);
RedisMessage<SimpleType> redisMsg = new RedisMessage.Builder<SimpleType>().TrxId(UUID.randomUUID().toString())
.payload(simpleType)
.build();
//ACT
redisMessagePublisher.publish(redisMsg);
boolean responded = countDownLatch.await(5, TimeUnit.SECONDS);
//ASSERT
Assertions.assertTrue(responded);
}
}
As it turns out, The MessageListenerAdapter uses the RedisSerializer.string() as the default serializer. This means that any POJO other than a String in the parameter list of the listener method will be ignored. In order to get past this, you need to invoke the setSerializer method and pass in RedisSerializer.java() as the argument. This will let the MessageListenerAdapter know that the POJO is a java class and needs to be serialized/deserialized. Please note that whatever pojo that you decide to pass in MUST implement java.io.Serializable. Please see the example below, and hopefully this helps someone else.
#Bean
public MessageListenerAdapter messageListenerAdapter(RedisMessageListener<?> redisMessageListener){
MessageListenerAdapter msgAdapter = new MessageListenerAdapter(redisMessageListener,"onRedisMessage");
msgAdapter.setSerializer(RedisSerializer.java());
return msgAdapter;
}
I have set the following property
server.servlet.session.timeout=30s
in my application properties but the session time out is not triggerd.
but after setting
server.servlet.session.cookie.max-age=30s
the session time out got trigger but following code for updating logout time is not getting triggerd.
#Component
public class LogoutListener implements ApplicationListener<SessionDestroyedEvent> {
#Override
public void onApplicationEvent(SessionDestroyedEvent event)
{
List<SecurityContext> lstSecurityContext = event.getSecurityContexts();
UserDetails ud;
for (SecurityContext securityContext : lstSecurityContext)
{
ud = (UserDetails) securityContext.getAuthentication().getPrincipal();
us.findAllUsersByEmail(ud.getUsername()).get(0).setLastLogout(LocalDateTime.now());
System.out.println("lastloginspec : " + ud.getUsername() + " : 00 : " + LocalDateTime.now());
}
}
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
Could any one Help me out ?
I have implemented the session listener by following way.
Create a custom http session listener.
#Component
public class CustomHttpSessionListener implements HttpSessionListener{
private static final Logger LOG= LoggerFactory.getLogger(Test.class);
#Override
public void sessionCreated(HttpSessionEvent se) {
LOG.info("New session is created.");
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public void sessionDestroyed(HttpSessionEvent se) {
LOG.info("Session destroyed.");
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}}
Invoke new ServletListenerRegistrationBean and add CustomHttpListener to it and annotate it as #Bean.
#Autowired private CustomHttpSessionListener customHttpSessionListener;
#Bean
public ServletListenerRegistrationBean<CustomSessionListner>sessionListenerWithMetrics() { ServletListenerRegistrationBean<CustomSessionListner>
listenerRegBean = new ServletListenerRegistrationBean<>();
listenerRegBean.setListener(customHttpSessionListener);
return listenerRegBean;
}
Adding a property to application.properties
server.servlet.session.timeout = 15m
This is not a full answer, but a step to isolate and troubleshoot. Replace your LogoutListener with and see when you start the application if it is printing any events. If it is not printing your issue is not specific SessionDestroyedEvent instead generic to your listener.
#Component
public class LogoutListener
implements ApplicationListener<ApplicationEvent> {
#Override
public void onApplicationEvent(ApplicationEvent event)
{
System.out.println("event caught at LogoutListener: " + event);
}
}
And also add this to application.properties to see if event is fired as it should log Publishing event:
logging.level.org.springframework.security.web.session.HttpSessionEventPublisher=DEBUG
I had implement CQRS+ES application using axon and spring-boot. I use separate query model and command model application. I use rabbitmq to publish event from command mode. It works correct. But tracking Processor implementation is not work in my application.
This is my query model
#SpringBootApplication
public class SeatQueryPart1Application {
public static void main(String[] args) {
SpringApplication.run(SeatQueryPart1Application.class, args);
}
#Bean
public SpringAMQPMessageSource statisticsQueue(Serializer serializer) {
return new SpringAMQPMessageSource(new DefaultAMQPMessageConverter(serializer)) {
#RabbitListener(exclusive = false, bindings = #QueueBinding(value = #Queue, exchange = #Exchange(value = "ExchangeTypesTests.FanoutExchange", type = ExchangeTypes.FANOUT), key = "orderRoutingKey"))
#Override
public void onMessage(Message arg0, Channel arg1) throws Exception {
super.onMessage(arg0, arg1);
}
};
}
#Autowired
public void conf(EventHandlingConfiguration configuration) {
configuration.registerTrackingProcessor("statistics");
}
}
this is a event handler class
#ProcessingGroup("statistics")
#Component
public class EventLoggingHandler {
private SeatReservationRepository seatReservationRepo;
public EventLoggingHandler(final SeatReservationRepository e) {
this.seatReservationRepo = e;
}
#EventHandler
protected void on(SeatResurvationCreateEvent event) {
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
Seat seat=new Seat(event.getId(), event.getSeatId(), event.getDate(),timestamp ,true);
seatReservationRepo.save(seat);
}
}
this is yml configuration
axon:
eventhandling:
processors:
statistics.source: statisticsQueue
How can i do it correct. (Can anyone suggest tutorial or code sample)
The SpringAMQPMessageSource is a SubscribableMessageSource. This means you cannot use a tracking event processor to process messages. It is only compatible with a Subscribable Event Processor.
Removing configuration.registerTrackingProcessor("statistics"); and leaving it to the default (subscribing) should do the trick.
I am new to spring
I have this class :
public class Server extends TextWebSocketHandler implements WebSocketHandler {
WebSocketSession clientsession;
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
clientsession = session;
}
I need to detect a client disconnect on clientsession.
implement ApplicationListener but its not clear how I can register the listener?
do I need to do it in my web.xml ?
The WebSocketHandler afterConnectionClosed function is called after a websocket client disconnects. You simply need to override this in the manner that you override handleTextMessage.
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus){
// your code here
}
There may be substantial delay between the client disconnect and your server event detection. See details about real-time disconnection detection.
You will need to override configureClientOutboundChannel and configureClientInboundChannel of AbstractWebSocketMessageBrokerConfigurer, providing your interceptor
Another way is using ApplicationEvents.
Both methods are described here:
http://www.sergialmar.com/2014/03/detect-websocket-connects-and-disconnects-in-spring-4/
public class StompConnectEvent implements ApplicationListener<SessionConnectEvent> {
private final Log logger = LogFactory.getLog(StompConnectEvent.class);
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String company = sha.getNativeHeader("company").get(0);
logger.debug("Connect event [sessionId: " + sha.getSessionId() +"; company: "+ company + " ]");
}
}
I hope that help. Let me know if I need to explain more.
You can use listeners to detect when session is connected or closed.
More information about listeners you can find by this link.
Example how to detect connected session:
#Component
public class SessionConnectedEventListener implements ApplicationListener<SessionConnectedEvent> {
private IWebSocketSessionService webSocketSessionService;
public SessionConnectedEventListener(IWebSocketSessionService webSocketSessionService) {
this.webSocketSessionService = webSocketSessionService;
}
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
webSocketSessionService.saveSession(event);
}
}
Example how to detect when session is disconneted:
#Component
public class SessionDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {
private IWebSocketSessionService webSocketSessionService;
public SessionDisconnectEventListener(IWebSocketSessionService webSocketSessionService) {
this.webSocketSessionService = webSocketSessionService;
}
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
webSocketSessionService.removeSession(event);
}
}
What you probably want to achieve is maintaining multiple sessions. Something like this:
public class Server extends TextWebSocketHandler implements WebSocketHandler {
private List<WebSocketSession> sessions = new ArrayList<>();
#Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// clientsession = session;
// Send individual or broadcast messages here ...
session.sendMessage(new TextMessage(textMessage + "!"));
}
}
How can I access a HttpSession object inside an annotated #WebSocket class in Jetty 9?
I found how to do it using #ServerEndpoint annotation, like here: HttpSession from #ServerEndpoint
Using the #WebSocket annotation, like in the class bellow, how can I do it?
#WebSocket
public class AuctionWebSocket {
// NEED TO ACCESS HttpSession OBJECT INSIDE THESE METHODS:
#OnWebSocketConnect
public void onConnect(Session session) {
System.out.println("onConnect...");
}
#OnWebSocketMessage
public void onMessage(String message) {
System.out.println("Message: " + message);
}
#OnWebSocketClose
public void onClose(int statusCode, String reason) {
System.out.println("onClose...");
}
#OnWebSocketError
public void onError(Throwable t) {
System.out.println("onError...");
}
}
Inside the method onConnect(Session session), I tried to call session.getUpgradeRequest().getSession() which always returns null.
For sake of information, here is how I start embedded Jetty 9:
public class Main {
public static void main(String[] args) throws Exception {
String webPort = System.getenv("PORT");
if (webPort == null || webPort.isEmpty()) {
webPort = "8080";
}
Server server = new Server(Integer.parseInt(webPort));
ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server);
classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
"org.eclipse.jetty.annotations.AnnotationConfiguration");
WebAppContext wac = new WebAppContext();
String webappDirLocation = "./src/main/webapp/";
wac.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/classes/.*");
wac.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
wac.setBaseResource(new ResourceCollection(new String[]{webappDirLocation, "./target"}));
wac.setResourceAlias("/WEB-INF/classes/", "/classes/");
wac.setContextPath("/");
wac.setParentLoaderPriority(true);
/*
* WebSocket handler.
*/
WebSocketHandler wsh = new WebSocketHandler() {
#Override
public void configure(WebSocketServletFactory wssf) {
wssf.register(AuctionWebSocket.class);
}
};
ContextHandler wsc = new ContextHandler();
wsc.setContextPath("/auction-notifications");
wsc.setHandler(wsh);
ContextHandlerCollection chc = new ContextHandlerCollection();
chc.setHandlers(new Handler[]{wac, wsc});
server.setHandler(chc);
server.start();
server.join();
}
}
Let me know if you need more information.
Any help will be appreciated.
Thanks in advance.
You'll want to use the WebSocketCreator concepts.
First you set the WebSocketCreator of your choice in the WebSocketServletFactory that you configure in your WebSocketServlet
public class MySessionSocketServlet extends WebSocketServlet
{
#Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(30000);
factory.setCreator(new MySessionSocketCreator());
}
}
Next, you'll want to grab the HttpSession during the upgrade and pass it into the WebSocket object that you are creating.
public class MySessionSocketCreator implements WebSocketCreator
{
#Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
HttpSession httpSession = req.getSession();
return new MySessionSocket(httpSession);
}
}
Finally, just keep track of that HttpSession in your own WebSocket.
#WebSocket
public class MySessionSocket
{
private HttpSession httpSession;
private Session wsSession;
public MySessionSocket(HttpSession httpSession)
{
this.httpSession = httpSession;
}
#OnWebSocketConnect
public void onOpen(Session wsSession)
{
this.wsSession = wsSession;
}
}
Of note: the HttpSession can expire and be scavenged and cleaned up while a WebSocket is active. Also, the HttpSession contents at this point are not guaranteed to be kept in sync with changes from other web actions (this mostly depends on what Session storage / caching technology you use on the server side)
And one more note: resist the urge to store / track the ServletUpgradeRequest object in your Socket instance, as this object is recycled and cleaned up aggressively by Jetty proper.