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

Conversation

alparslanavci
Copy link
Contributor

Fixes #73.

@alparslanavci alparslanavci added this to the 2.2 milestone Dec 15, 2020
Copy link
Contributor

@enozcan enozcan left a comment

Choose a reason for hiding this comment

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

Regardless of the PR, does it make sense to use instance name from configuration to seek for existing instances? If I'm not wrong, this will throw NPE when an ordinary hazelcast.xml with instance-name field is used whereas it's supposed to be a valid configuration to start a standalone instance:

} else if (instanceName != null) {
instance = Hazelcast.getHazelcastInstanceByName(instanceName);

and then

@alparslanavci
Copy link
Contributor Author

@enozcan yes, you are correct. This is a known issue, and actually, it requires much larger refactoring. Maybe we can create a GH issue for tracking, but I am pretty sure that it needs a larger effort to fix it in general.

Copy link

@leszko leszko left a comment

Choose a reason for hiding this comment

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

Added my comments.

Also, I tried this autoconfiguration with the latest Spring Boot Web and got the following exception. Could you check?

15:38:06.275 [main] ERROR o.s.boot.SpringApplication - Application run failed
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
	at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:60)
	at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:489)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:478)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:140)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:129)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:348)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:252)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:285)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:99)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:751)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:569)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:326)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298)
	at com.example.demo.DemoApplication.main(DemoApplication.java:10)
Caused by: java.lang.IllegalStateException: Failed to introspect Class [com.hazelcast.session.springboot.HazelcastSessionManagerConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@3764951d]
	at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:481)
	at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:358)
	at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:414)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.lambda$getTypeForFactoryMethod$2(AbstractAutowireCapableBeanFactory.java:754)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:753)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:692)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:663)
	at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1665)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:570)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:542)
	at org.springframework.boot.autoconfigure.condition.OnBeanCondition.collectBeanNamesForType(OnBeanCondition.java:238)
	at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:231)
	at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:221)
	at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchingBeans(OnBeanCondition.java:169)
	at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:119)
	at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
	... 19 common frames omitted
Caused by: java.lang.NoClassDefFoundError: com/hazelcast/config/Config
	at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
	at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3325)
	at java.base/java.lang.Class.getDeclaredMethods(Class.java:2466)
	at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463)
	... 35 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.hazelcast.config.Config
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 39 common frames omitted

tomcat-core/pom.xml Outdated Show resolved Hide resolved
@@ -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 👍

README.md Outdated

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.


@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 👍

/**
* 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.

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.


@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 👍


@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.

Copy link
Contributor Author

@alparslanavci alparslanavci left a comment

Choose a reason for hiding this comment

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

@leszko, you got the exception because most likely you haven't added Hazelcast to the classpath. It should be also provided, I'll update the readme accordingly.

README.md Outdated

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
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?

tomcat-core/pom.xml Outdated Show resolved Hide resolved
@@ -28,16 +28,16 @@

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

class ClientServerConfigLoader {
public class ClientServerConfigLoader {
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?

/**
* This package provides Spring Boot auto-configuration classes
*/
package com.hazelcast.session.springboot;
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.

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
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.


@SpringBootApplication
@PropertySource("classpath:clientServer.properties")
public class 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.

Done 👍


@SpringBootApplication
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = ClientServerApplication.class))
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.


@Bean
@ConditionalOnMissingBean(type = "com.hazelcast.config.Config")
@ConditionalOnProperty(name = "tsm.client.only", havingValue = "false", matchIfMissing = true)
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 👍

@leszko
Copy link

leszko commented Dec 17, 2020

@leszko, you got the exception because most likely you haven't added Hazelcast to the classpath. It should be also provided, I'll update the readme accordingly.

Wouldn't it be possible to detect it at runtime and give a proper warning/error message?


@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.

Copy link
Contributor Author

@alparslanavci alparslanavci left a comment

Choose a reason for hiding this comment

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

@leszko regarding the error you got, we cannot give a proper error message for that since the exception is thrown from Spring Boot itself.

However, I removed the tsm.client.only parameter as you suggested. Now, it is auto-configured accordingly with the provided Hazelcast configuration file.


@Bean(name = "hazelcastTomcatSessionManagerCustomizer")
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> customizeTomcat(HazelcastInstance hazelcastInstance) {
return new WebServerFactoryCustomizer<TomcatServletWebServerFactory>() {
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.

@leszko
Copy link

leszko commented Jan 4, 2021

@leszko regarding the error you got, we cannot give a proper error message for that since the exception is thrown from Spring Boot itself.

Wouldn't it work if you check it somewhere here? I mean to check if you have Hazelcast on the classpath?

The other option is to change the Hazelcast dependency scope to not be provided. What do you think?

Copy link

@leszko leszko left a comment

Choose a reason for hiding this comment

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

Thanks for the changes Alparslan. Added some replies to your comments.

Copy link

@leszko leszko left a comment

Choose a reason for hiding this comment

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

Please make the classes package-private. The rest looks good 👍

@alparslanavci alparslanavci merged commit 0de5d69 into hazelcast:master Jan 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify Tomcat Session Manager with Spring-Boot
3 participants