Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added auto-configuration for Spring Boot #103

Merged
merged 11 commits into from
Jan 4, 2021
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [Enabling Session Replication in Multi-App Environments](#enabling-session-replication-in-multi-app-environments)
* [Sticky Sessions and Tomcat](#sticky-sessions-and-tomcat)
* [Tomcat Failover and the jvmRoute Parameter](#tomcatfailover-and-the-jvmroute-parameter)
* [Spring Boot Auto-configuration](#spring-boot-auto-configuration)


# Tomcat Based Web Session Replication
Expand Down Expand Up @@ -243,5 +244,23 @@ When Tomcat Failure happens and Load Balancer cannot redirect the request to the
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-8080">
```

# Spring Boot Auto-configuration

Starting with v2.2, Hazelcast Tomcat Session Manager supports auto-configuration when used with Spring Boot. The only thing you need to do is to add Hazelcast Tomcat Session Manager (for Tomcat 9) library to the classpath. This will set Hazelcast Session Manager as the session manager of the Tomcat. If you would like to configure the session manager properties, you can setup the following properties in your `application.properties` file:

- `tsm.hazelcast.config.location`: Allows to provide Hazelcast member configuration. If not provided, `hazelcast.xml` in the classpath is used by default. Only works if `tsm.client.only` is **not** to set true.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it common to use such abbreviations as prefix? Like tsm? Did you see other projects using it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I haven't checked but I used it to separate from other properties with the same name for other modules. Do you have any other suggestion?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yeah, makes sense, otherwise, they'd collide with other properties. But would it maybe make sense to stick to some convention from Spring, like here: https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/boot-features-hazelcast.html

For example, in Spring you define the Hazelcast configuration with spring.hazelcast.config and in your configuration it's tsm.hazelcast.config.location. So from what I see, Spring just added a prefix spring. to all the Hazelcast properties. So, maybe you should call yours like tsm.hazelcast.config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preferred to use the same config property names as we use in Tomcat Session Manger itself, just adding tsm. in front of them (See: https://github.com/hazelcast/hazelcast-tomcat-sessionmanager#configuring-manager-element-for-tomcat). I think this makes more sense from the user's point of view. I changed the tsm.hazelcast.config.location to tsm.config.location accordingly.

- `tsm.hazelcast.client.config.location`: Allows to provide Hazelcast client configuration. If not provided, `hazelcast-client.xml` in the classpath is used by default. Only works if `tsm.client.only` is to set true.
- `tsm.client.only`: When set to `true`, Hazelcast Session Manager initializes in client-only mode and tries to connect to a Hazelcast cluster using the provided configuration.
- `tsm.map.name`: Use this property if you have a specially configured map for special cases like WAN Replication, Eviction, MapStore, etc.
- `tsm.sticky`: Allows to set sticky mode. Its default value is `true`.
- `tsm.process.expires.frequency`: It specifies the frequency of session validity check, in seconds. Its default value is 6 and the minimum value that you can set is 1.
- `tsm.deferred.write`: Allows to set deferred write mode. See [this section](#controlling-session-caching-with-deferredWrite) for details.
- `tsm.hazelcast.instance.name`: It specifies an existing Hazelcast instance to use for session replication. The same can be achieved by setting `instanceName` property in Hazelcast configuration. If no instance name is configured, Hazelcast instance starts with a default instance name (`SessionManager.DEFAULT_INSTANCE_NAME`).

## Notes about Spring Boot Auto-configuration

- If a `com.hazelcast.client.config.ClientConfig` bean is configured explicitly, then `ClientServerLifecycleListener.setConfig(clientConfig);` should be called to enable this configuration to be used in Hazelcast Session Manager.
- If a `com.hazelcast.config.Config` bean is configured explicitly, then both `hazelcastInstance` configuration and `tsm.hazelcast.instance.name` property should be set.

# License
Hazelcast Tomcat Session Manager is available under the Hazelcast Community License.
12 changes: 12 additions & 0 deletions tomcat-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@
<version>${tomcat-version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.1.RELEASE</version>
leszko marked this conversation as resolved.
Show resolved Hide resolved
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@

import static com.hazelcast.internal.config.ConfigLoader.locateConfig;

class ClientServerConfigLoader {
public class ClientServerConfigLoader {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea: maybe we could get rid of the com.hazelcast.session.springboot package and keep these package-private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but I think it will be better to keep springboot package since there are also test classes under the same package. Otherwise, all classes will be in the same parent package. WDYT?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible to put tests in a separate package, but keep the code together? Like we already have separate packages for different tests: nonsticky, sticky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, surely we can do it. I just thought it is tidier :) I removed the springboot package now 👍

private ConfigRecognizer xmlConfigRecognizer;
private ConfigRecognizer yamlConfigRecognizer;

ClientServerConfigLoader() throws Exception {
public ClientServerConfigLoader() throws Exception {
xmlConfigRecognizer = new ClientXmlConfigRootTagRecognizer();
yamlConfigRecognizer = new ClientYamlConfigRootTagRecognizer();
}

ClientConfig load(final String path) throws Exception {
public ClientConfig load(final String path) throws Exception {
final URL url = locateConfig(path);
if (url == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public void setConfigLocation(String configLocation) {
this.configLocation = configLocation;
}

public static void setConfig(ClientConfig config) {
ClientServerLifecycleListener.config = config;
}

public static ClientConfig getConfig() {
return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@

import static com.hazelcast.internal.config.ConfigLoader.locateConfig;

class P2PConfigLoader {
public class P2PConfigLoader {
private ConfigRecognizer xmlConfigRecognizer;
private ConfigRecognizer yamlConfigRecognizer;

P2PConfigLoader() throws Exception {
public P2PConfigLoader() throws Exception {
xmlConfigRecognizer = new MemberXmlConfigRootTagRecognizer();
yamlConfigRecognizer = new MemberYamlConfigRootTagRecognizer();
}

Config load(final String path) throws Exception {
public Config load(final String path) throws Exception {
final URL url = locateConfig(path);
if (url == null) {
return null;
Expand Down
12 changes: 12 additions & 0 deletions tomcat9/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@
<version>${tomcat-version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.4.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2020 Hazelcast Inc.
*
* Licensed under the Hazelcast Community License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* http://hazelcast.com/hazelcast-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package com.hazelcast.session.springboot;

import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.session.ClientServerConfigLoader;
import com.hazelcast.session.ClientServerLifecycleListener;
import com.hazelcast.session.HazelcastSessionManager;
import com.hazelcast.session.P2PConfigLoader;
import com.hazelcast.session.SessionManager;
import org.apache.catalina.Context;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;

@org.springframework.context.annotation.Configuration
@ConditionalOnClass(HazelcastSessionManager.class)
public class HazelcastSessionManagerConfiguration {
private final Log log = LogFactory.getLog(HazelcastSessionManager.class);

@Value("${tsm.hazelcast.config.location:hazelcast-default.xml}")
private String configLocation;
@Value("${tsm.hazelcast.client.config.location:hazelcast-client-default.xml}")
private String clientConfigLocation;
@Value("${tsm.client.only:false}")
private boolean clientOnly;
@Value("${tsm.map.name:default}")
private String mapName;
@Value("${tsm.sticky:true}")
private boolean sticky;
@Value("${tsm.process.expires.frequency:6}")
private int processExpiresFrequency;
@Value("${tsm.deferred.write:true}")
private boolean deferredWrite;
@Value("${tsm.hazelcast.instance.name:" + SessionManager.DEFAULT_INSTANCE_NAME + "}")
private String hazelcastInstanceName;

@Bean
@ConditionalOnMissingBean(type = "com.hazelcast.config.Config")
@ConditionalOnProperty(name = "tsm.client.only", havingValue = "false", matchIfMissing = true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, how Spring Boot now detects if P2P or Client-Server is used? Maybe we can piggy-back on this and not introduce our own parameter tsm.client.only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check this 👍

public Config hazelcastConfig() throws Exception {
Config config = new P2PConfigLoader().load(configLocation);
if (config.getInstanceName() == null) {
config.setInstanceName(SessionManager.DEFAULT_INSTANCE_NAME);
}
return config;
}

@Bean
@ConditionalOnMissingBean(type = "com.hazelcast.client.config.ClientConfig")
@ConditionalOnProperty(name = "tsm.client.only", havingValue = "true")
public ClientConfig hazelcastClientConfig() throws Exception {
ClientConfig clientConfig = new ClientServerConfigLoader().load(clientConfigLocation);
ClientServerLifecycleListener.setConfig(clientConfig);
enozcan marked this conversation as resolved.
Show resolved Hide resolved
return clientConfig;
}

@Bean(name = "hazelcastTomcatSessionManagerCustomizer")
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> customizeTomcat(HazelcastInstance hazelcastInstance) {
return new WebServerFactoryCustomizer<TomcatServletWebServerFactory>() {
Copy link
Contributor

@enozcan enozcan Dec 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I afraid in client.only mode, a client will be created here via HazelcastInstance bean with the client config above. However, another client will also be created at HazelcastSessionManager#startInternal for the manager. Is that the intent? For instance, this one fails in ClientServerSpringBootConfigurationTest:

@Test
public void createOnlyOneClient() {
    Assert.assertEquals(1, HazelcastClient.getAllHazelcastClients().size());
    // java.lang.AssertionError:
    // Expected :1
    // Actual   :2
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@enozcan yes, you were correct. I fixed it by using the Hazelcast client instance initiated by Spring Boot.

@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers(new TomcatContextCustomizer() {
enozcan marked this conversation as resolved.
Show resolved Hide resolved
@Override
public void customize(Context context) {
HazelcastSessionManager manager = new HazelcastSessionManager();
manager.setClientOnly(clientOnly);
manager.setMapName(mapName);
manager.setSticky(sticky);
manager.setProcessExpiresFrequency(processExpiresFrequency);
manager.setDeferredWrite(deferredWrite);
manager.setHazelcastInstanceName(hazelcastInstanceName);
context.setManager(manager);
log.info(String.format(
"Tomcat context is configured with HazelcastSessionManager => clientOnly: %s, mapName: %s, "
+ "isSticky: %s, processExpiresFrequency: %d, deferredWrite: %s, "
+ "hazelcastInstanceName: %s",
clientOnly, mapName, sticky, processExpiresFrequency, deferredWrite, hazelcastInstanceName));
}
});
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Hazelcast Inc.
*
* Licensed under the Hazelcast Community License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* http://hazelcast.com/hazelcast-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

/**
* This package provides Spring Boot auto-configuration classes
*/
package com.hazelcast.session.springboot;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I already mentioned somewhere, consider removing this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied under the other comment.

2 changes: 2 additions & 0 deletions tomcat9/src/main/resources/META-INF/spring.factories
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration= \
com.hazelcast.session.springboot.HazelcastSessionManagerConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.hazelcast.session.springboot;

import com.hazelcast.core.DistributedObject;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.Test;
import org.springframework.context.ApplicationContext;

import java.util.LinkedList;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public abstract class AbstractSpringBootConfigurationTest {
ApplicationContext applicationContext;

abstract void setup();

abstract void clean();

@Test()
public void testManagerCustomizerBean() {
assertNotNull(applicationContext.getBean("hazelcastTomcatSessionManagerCustomizer"));
}

@Test()
public void testSessionManager()
throws Exception {
HazelcastInstance hazelcastInstance = (HazelcastInstance) applicationContext.getBean("hazelcastInstance");
LinkedList<DistributedObject> distributedObjects = (LinkedList<DistributedObject>) hazelcastInstance.getDistributedObjects();
assertEquals("Session map should be created.", 1, distributedObjects.size());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a good practice to mix assertions with the test preparation. The test should have a clear structure given when then. Could you either split into multiple tests or remove this assertion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed them, but I am not sure if we should follow given when then structure here. These are more likely integration tests in general.


CookieStore cookieStore = new BasicCookieStore();
HttpClient client = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();
HttpGet request = new HttpGet("http://localhost:9999/set");
client.execute(request);

IMap<Object, Object> sessionMap = hazelcastInstance.getMap(distributedObjects.get(0).getName());
assertEquals("Session should be created.",1, sessionMap.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.hazelcast.session.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@PropertySource("classpath:clientServer.properties")
public class ClientServerApplication {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about embedding this class into ClientServerSpringBootConfigurationTest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

public static void main(String[] args) {
SpringApplication.run(ClientServerApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.hazelcast.session.springboot;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.After;
import org.junit.Before;
import org.springframework.boot.SpringApplication;

public class ClientServerSpringBootConfigurationTest
extends AbstractSpringBootConfigurationTest {

private HazelcastInstance hazelcastInstance;

@Before
public void setup() {
hazelcastInstance = Hazelcast.newHazelcastInstance();
applicationContext = SpringApplication.run(ClientServerApplication.class);
}

@After
public void clean() {
SpringApplication.exit(applicationContext);
hazelcastInstance.shutdown();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hazelcast.session.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = ClientServerApplication.class))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need classes = ClientServerApplication.class? Isn't this class independent from ClientServerApplication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientServerApplication adds clientServer.properties as property source, and it is a component since they are all in the same package. I excluded it in the P2PApplication to prevent tsm.client.only=true take effect.

public class P2PApplication {
public static void main(String[] args) {
SpringApplication.run(P2PApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hazelcast.session.springboot;

import org.junit.After;
import org.junit.Before;
import org.springframework.boot.SpringApplication;

public class P2PSpringBootConfigurationTest
extends AbstractSpringBootConfigurationTest {

@Before
public void setup() {
applicationContext = SpringApplication.run(P2PApplication.class);
}

@After
public void clean() {
SpringApplication.exit(applicationContext);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hazelcast.session.springboot;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
public class TestController {

@RequestMapping("/set")
public void defaultMapping(HttpSession session){
session.setAttribute("testAttr", 1);
}
}
2 changes: 2 additions & 0 deletions tomcat9/src/test/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
logging.level.root=INFO
server.port=9999
Loading