diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index ae3cdc34e9..daf51b16e6 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -87,6 +87,7 @@ dependencies { testRuntimeOnly("javax.xml.bind:jaxb-api") testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el") testRuntimeOnly("org.glassfish.jersey.ext:jersey-spring5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.hsqldb:hsqldb") } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java index d2c23f3a73..b421ee4e7a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -16,12 +16,16 @@ package org.springframework.boot.actuate.hazelcast; +import java.lang.reflect.Method; +import java.util.UUID; + import com.hazelcast.core.HazelcastInstance; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * {@link HealthIndicator} for Hazelcast. @@ -43,10 +47,22 @@ public class HazelcastHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Health.Builder builder) { this.hazelcast.executeTransaction((context) -> { - builder.up().withDetail("name", this.hazelcast.getName()).withDetail("uuid", - this.hazelcast.getLocalEndpoint().getUuid()); + builder.up().withDetail("name", this.hazelcast.getName()).withDetail("uuid", extractUuid()); return null; }); } + private String extractUuid() { + try { + return this.hazelcast.getLocalEndpoint().getUuid(); + } + catch (NoSuchMethodError ex) { + // Hazelcast 4 + Method endpointAccessor = ReflectionUtils.findMethod(HazelcastInstance.class, "getLocalEndpoint"); + Object endpoint = ReflectionUtils.invokeMethod(endpointAccessor, this.hazelcast); + Method uuidAccessor = ReflectionUtils.findMethod(endpoint.getClass(), "getUuid"); + return ((UUID) ReflectionUtils.invokeMethod(uuidAccessor, endpoint)).toString(); + } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java index 8319ecabc5..c07d5d211a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java @@ -16,11 +16,15 @@ package org.springframework.boot.actuate.metrics.cache; +import java.lang.reflect.Method; + import com.hazelcast.spring.cache.HazelcastCache; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import org.springframework.util.ReflectionUtils; + /** * {@link CacheMeterBinderProvider} implementation for Hazelcast. * @@ -31,7 +35,25 @@ public class HazelcastCacheMeterBinderProvider implements CacheMeterBinderProvid @Override public MeterBinder getMeterBinder(HazelcastCache cache, Iterable tags) { - return new HazelcastCacheMetrics(cache.getNativeCache(), tags); + try { + return new HazelcastCacheMetrics(cache.getNativeCache(), tags); + } + catch (NoSuchMethodError ex) { + // Hazelcast 4 + return createHazelcast4CacheMetrics(cache, tags); + } + } + + private MeterBinder createHazelcast4CacheMetrics(HazelcastCache cache, Iterable tags) { + try { + Method nativeCacheAccessor = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache"); + Object nativeCache = ReflectionUtils.invokeMethod(nativeCacheAccessor, cache); + return HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class).newInstance(nativeCache, + tags); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to create MeterBinder for Hazelcast", ex); + } } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast4HazelcastHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast4HazelcastHealthIndicatorTests.java new file mode 100644 index 0000000000..505b9fb406 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast4HazelcastHealthIndicatorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-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.boot.actuate.hazelcast; + +import java.io.IOException; + +import com.hazelcast.core.HazelcastException; +import com.hazelcast.core.HazelcastInstance; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastInstanceFactory; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HazelcastHealthIndicator} with Hazelcast 4. + * + * @author Dmytro Nosan + * @author Stephane Nicoll + */ +@ClassPathExclusions("hazelcast*.jar") +@ClassPathOverrides("com.hazelcast:hazelcast:4.0") +class Hazelcast4HazelcastHealthIndicatorTests { + + @Test + void hazelcastUp() throws IOException { + HazelcastInstance hazelcast = new HazelcastInstanceFactory(new ClassPathResource("hazelcast-4.xml")) + .getHazelcastInstance(); + try { + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", + "actuator-hazelcast-4"); + assertThat(health.getDetails().get("uuid")).asString().isNotEmpty(); + } + finally { + hazelcast.shutdown(); + } + } + + @Test + void hazelcastDown() { + HazelcastInstance hazelcast = mock(HazelcastInstance.class); + when(hazelcast.executeTransaction(any())).thenThrow(new HazelcastException()); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java index 1340e6058f..2aa157c3bb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java @@ -16,14 +16,16 @@ package org.springframework.boot.actuate.hazelcast; -import com.hazelcast.core.Endpoint; +import java.io.IOException; + import com.hazelcast.core.HazelcastException; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.transaction.TransactionalTask; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastInstanceFactory; +import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -38,28 +40,27 @@ import static org.mockito.Mockito.mock; */ class HazelcastHealthIndicatorTests { - private final HazelcastInstance hazelcast = mock(HazelcastInstance.class); - @Test - void hazelcastUp() { - Endpoint endpoint = mock(Endpoint.class); - when(this.hazelcast.getName()).thenReturn("hz0-instance"); - when(this.hazelcast.getLocalEndpoint()).thenReturn(endpoint); - when(endpoint.getUuid()).thenReturn("7581bb2f-879f-413f-b574-0071d7519eb0"); - when(this.hazelcast.executeTransaction(any())).thenAnswer((invocation) -> { - TransactionalTask task = invocation.getArgument(0); - return task.execute(null); - }); - Health health = new HazelcastHealthIndicator(this.hazelcast).health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", "hz0-instance") - .containsEntry("uuid", "7581bb2f-879f-413f-b574-0071d7519eb0"); + void hazelcastUp() throws IOException { + HazelcastInstance hazelcast = new HazelcastInstanceFactory(new ClassPathResource("hazelcast.xml")) + .getHazelcastInstance(); + try { + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", + "actuator-hazelcast"); + assertThat(health.getDetails().get("uuid")).asString().isNotEmpty(); + } + finally { + hazelcast.shutdown(); + } } @Test void hazelcastDown() { - when(this.hazelcast.executeTransaction(any())).thenThrow(new HazelcastException()); - Health health = new HazelcastHealthIndicator(this.hazelcast).health(); + HazelcastInstance hazelcast = mock(HazelcastInstance.class); + when(hazelcast.executeTransaction(any())).thenThrow(new HazelcastException()); + Health health = new HazelcastHealthIndicator(hazelcast).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-4.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-4.xml new file mode 100644 index 0000000000..40ed902d1b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-4.xml @@ -0,0 +1,13 @@ + + actuator-hazelcast-4 + + + + + + + + diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml index 1a61470e60..3e528bf80e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml +++ b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml @@ -2,6 +2,7 @@ xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.12.xsd" xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + actuator-hazelcast diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/build.gradle new file mode 100644 index 0000000000..54d13f0e6b --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Hazelcast 4 smoke test" + +configurations.all { + resolutionStrategy { + force "com.hazelcast:hazelcast:4.0.1" + force "com.hazelcast:hazelcast-spring:4.0.1" + } +} + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-cache")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + implementation("com.hazelcast:hazelcast") + implementation("com.hazelcast:hazelcast-spring") + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux")) +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/Country.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/Country.java new file mode 100644 index 0000000000..e4efa0e7ae --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/Country.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-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 smoketest.hazelcast4; + +import java.io.Serializable; + +@SuppressWarnings("serial") +public class Country implements Serializable { + + private final String code; + + public Country(String code) { + this.code = code; + } + + public String getCode() { + return this.code; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Country country = (Country) o; + + return this.code.equals(country.code); + } + + @Override + public int hashCode() { + return this.code.hashCode(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/CountryRepository.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/CountryRepository.java new file mode 100644 index 0000000000..508da8964c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/CountryRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-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 smoketest.hazelcast4; + +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +@Component +@CacheConfig(cacheNames = "countries") +public class CountryRepository { + + @Cacheable + public Country findByCode(String code) { + System.out.println("---> Loading country with code '" + code + "'"); + return new Country(code); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/SampleHazelcast4Application.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/SampleHazelcast4Application.java new file mode 100644 index 0000000000..fc9055d592 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/java/smoketest/hazelcast4/SampleHazelcast4Application.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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 smoketest.hazelcast4; + +import com.hazelcast.spring.cache.HazelcastCacheManager; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.metrics.cache.CacheMetricsRegistrar; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +@EnableCaching +public class SampleHazelcast4Application { + + public static void main(String[] args) { + SpringApplication.run(SampleHazelcast4Application.class, args); + } + + @Bean + public ApplicationRunner registerCache(CountryRepository repository, HazelcastCacheManager cacheManager, + CacheMetricsRegistrar registrar) { + return (args) -> { + repository.findByCode("BE"); + registrar.bindCacheToRegistry(cacheManager.getCache("countries")); + }; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/application.properties new file mode 100644 index 0000000000..f896a300ee --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/application.properties @@ -0,0 +1,2 @@ +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=* diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/hazelcast.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/hazelcast.xml new file mode 100644 index 0000000000..1dd42796db --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/main/resources/hazelcast.xml @@ -0,0 +1,21 @@ + + + 600 + + + + + true + true + + + + + + + + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/test/java/smoketest/hazelcast4/SampleHazelcast4ApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/test/java/smoketest/hazelcast4/SampleHazelcast4ApplicationTests.java new file mode 100644 index 0000000000..5a63b767eb --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-hazelcast4/src/test/java/smoketest/hazelcast4/SampleHazelcast4ApplicationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 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 smoketest.hazelcast4; + +import com.hazelcast.spring.cache.HazelcastCacheManager; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cache.CacheManager; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +class SampleHazelcast4ApplicationTests { + + @Autowired + private WebTestClient webClient; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private CountryRepository countryRepository; + + @Test + void cacheManagerIsUsingHazelcast() { + assertThat(this.cacheManager).isInstanceOf(HazelcastCacheManager.class); + } + + @Test + void healthEndpointHasHazelcastContributor() { + this.webClient.get().uri("/actuator/health/hazelcast").exchange().expectStatus().isOk().expectBody() + .jsonPath("status").isEqualTo("UP").jsonPath("details.name").isNotEmpty().jsonPath("details.uuid") + .isNotEmpty(); + } + + @Test + void metricsEndpointHasCacheMetrics() { + this.webClient.get().uri("/actuator/metrics/cache.entries").exchange().expectStatus().isOk(); + } + +}