pull/315/head
cheese8 3 years ago
commit 618cb20e42

@ -5,24 +5,47 @@ name: Test with Junit
on:
push:
branches: [ 2021.0 ]
branches:
- main
- 2021.0
- 2020.0
- greenwich
pull_request:
branches: [ 2021.0 ]
branches:
- main
- 2021.0
- 2020.0
- greenwich
jobs:
build:
strategy:
matrix:
java: [ 8, 11, 17 ]
os: [ 'windows-latest', 'macos-latest', 'ubuntu-latest' ]
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout codes
uses: actions/checkout@v2
- name: Set up JDK 8
uses: actions/setup-java@v2
uses: actions/checkout@v3
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'adopt'
distribution: 'temurin'
java-version: ${{ matrix.java }}
- name: Cache local Maven repository
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# - name: Build with Maven
# run: mvn -B package --file pom.xml
- name: Test with Maven
run: mvn -B test --file pom.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: '**/target/site/jacoco/jacoco.xml'

@ -4,4 +4,10 @@
- [Upgrade: fix third-party lib CVEs & upgrade core spring libs version](https://github.com/Tencent/spring-cloud-tencent/pull/263)
- [feat:support reading configuration from application.yml or application.properties.](https://github.com/Tencent/spring-cloud-tencent/pull/262)
- [fix:fix ClassNotFoundException while not importing openfeign when using circuit-breaker module.](https://github.com/Tencent/spring-cloud-tencent/pull/271)
- [fix:solve the chaos code problem on rejectTips](https://github.com/Tencent/spring-cloud-tencent/pull/283)
- [fix:solve ratelimit-callee-service UnknownHostException.](https://github.com/Tencent/spring-cloud-tencent/pull/292)
- [Feature: Add config change listener feature support](https://github.com/Tencent/spring-cloud-tencent/pull/299)
- [Feature: Add config module unit test](https://github.com/Tencent/spring-cloud-tencent/pull/301)
- [Feature:add restTemplate Report Polaris](https://github.com/Tencent/spring-cloud-tencent/pull/304)
- [Update GitHub Actions workflow](https://github.com/Tencent/spring-cloud-tencent/pull/305)
- [fix: 将blocking call改为non-blocking call](https://github.com/Tencent/spring-cloud-tencent/pull/309)
- [fix:solve the chaos code problem on rejectTips](https://github.com/Tencent/spring-cloud-tencent/pull/283)

@ -38,9 +38,11 @@
</scm>
<modules>
<module>spring-cloud-tencent-polaris-context</module>
<module>spring-cloud-tencent-commons</module>
<module>spring-cloud-tencent-polaris-context</module>
<module>spring-cloud-tencent-polaris-loadbalancer</module>
<module>spring-cloud-starter-tencent-metadata-transfer</module>
<module>spring-cloud-starter-tencent-polaris-config</module>
<module>spring-cloud-starter-tencent-polaris-discovery</module>
<module>spring-cloud-starter-tencent-polaris-ratelimit</module>
<module>spring-cloud-starter-tencent-polaris-circuitbreaker</module>
@ -48,8 +50,6 @@
<module>spring-cloud-tencent-dependencies</module>
<module>spring-cloud-tencent-examples</module>
<module>spring-cloud-tencent-coverage</module>
<module>spring-cloud-starter-tencent-polaris-config</module>
<module>spring-cloud-tencent-polaris-loadbalancer</module>
</modules>
<developers>
@ -95,7 +95,7 @@
<spring.framework.version>5.3.21</spring.framework.version>
<!-- Maven Plugin Versions -->
<jacoco.version>0.8.3</jacoco.version>
<jacoco.version>0.8.8</jacoco.version>
<maven-source-plugin.version>3.2.0</maven-source-plugin.version>
<flatten-maven-plugin.version>1.2.7</flatten-maven-plugin.version>
<maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>

@ -19,9 +19,10 @@
package com.tencent.cloud.metadata.core.intercepter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor;
import feign.RequestInterceptor;
@ -61,11 +62,13 @@ public class EncodeTransferMedataFeignInterceptorTest {
private TestApplication.TestFeign testFeign;
@Test
public void test1() {
public void testTransitiveMetadataFromApplicationConfig() {
String metadata = testFeign.test();
Assertions.assertThat(metadata).isEqualTo("{\"b\":\"2\"}");
Assertions.assertThat(metadataLocalProperties.getContent().get("a")).isEqualTo("1");
Assertions.assertThat(metadataLocalProperties.getContent().get("b")).isEqualTo("2");
Assertions.assertThat(metadata).isEqualTo("2");
Assertions.assertThat(metadataLocalProperties.getContent().get("a"))
.isEqualTo("1");
Assertions.assertThat(metadataLocalProperties.getContent().get("b"))
.isEqualTo("2");
}
@SpringBootApplication
@ -77,16 +80,13 @@ public class EncodeTransferMedataFeignInterceptorTest {
public String test(
@RequestHeader(MetadataConstant.HeaderName.CUSTOM_METADATA) String customMetadataStr)
throws UnsupportedEncodingException {
return URLDecoder.decode(customMetadataStr, "UTF-8");
return MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "b");
}
@FeignClient(name = "test-feign", url = "http://localhost:8081")
public interface TestFeign {
@RequestMapping(value = "/test",
headers = {"X-SCT-Metadata-Transitive-a=11",
"X-SCT-Metadata-Transitive-b=22",
"X-SCT-Metadata-Transitive-c=33"})
@RequestMapping("/test")
String test();
}

@ -19,11 +19,12 @@
package com.tencent.cloud.metadata.core.intercepter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -32,6 +33,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@ -51,9 +55,6 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
properties = { "spring.config.location = classpath:application-test.yml" })
public class EncodeTransferMedataRestTemplateInterceptorTest {
@Autowired
private MetadataLocalProperties metadataLocalProperties;
@Autowired
private RestTemplate restTemplate;
@ -61,30 +62,14 @@ public class EncodeTransferMedataRestTemplateInterceptorTest {
private int localServerPort;
@Test
public void test1() {
// HttpHeaders httpHeaders = new HttpHeaders();
// httpHeaders.set(MetadataConstant.HeaderName.CUSTOM_METADATA,
// "{\"a\":\"11\",\"b\":\"22\",\"c\":\"33\"}");
// HttpEntity<String> httpEntity = new HttpEntity<>(httpHeaders);
// String metadata = restTemplate
// .exchange("http://localhost:" + localServerPort + "/test", HttpMethod.GET,
// httpEntity, String.class)
// .getBody();
// Assertions.assertThat(metadata)
// .isEqualTo("{\"a\":\"11\",\"b\":\"22\",\"c\":\"33\"}");
// Assertions.assertThat(metadataLocalProperties.getContent().get("a"))
// .isEqualTo("1");
// Assertions.assertThat(metadataLocalProperties.getContent().get("b"))
// .isEqualTo("2");
// Assertions
// .assertThat(MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "a"))
// .isEqualTo("11");
// Assertions
// .assertThat(MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "b"))
// .isEqualTo("22");
// Assertions
// .assertThat(MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "c"))
// .isEqualTo("33");
public void testTransitiveMetadataFromApplicationConfig() {
HttpHeaders httpHeaders = new HttpHeaders();
HttpEntity<String> httpEntity = new HttpEntity<>(httpHeaders);
String metadata = restTemplate
.exchange("http://localhost:" + localServerPort + "/test", HttpMethod.GET,
httpEntity, String.class)
.getBody();
Assertions.assertThat(metadata).isEqualTo("2");
}
@SpringBootApplication
@ -100,7 +85,7 @@ public class EncodeTransferMedataRestTemplateInterceptorTest {
public String test(
@RequestHeader(MetadataConstant.HeaderName.CUSTOM_METADATA) String customMetadataStr)
throws UnsupportedEncodingException {
return URLDecoder.decode(customMetadataStr, "UTF-8");
return MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "b");
}
}

@ -15,7 +15,7 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker;
package com.tencent.cloud.polaris.circuitbreaker.config;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;

@ -15,7 +15,7 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker;
package com.tencent.cloud.polaris.circuitbreaker.config;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignBeanPostProcessor;
import com.tencent.cloud.polaris.context.PolarisContextAutoConfiguration;
@ -40,8 +40,7 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
*
* @author Haotian Zhang
*/
@ConditionalOnProperty(value = "spring.cloud.polaris.circuitbreaker.enabled", havingValue = "true",
matchIfMissing = true)
@ConditionalOnProperty(value = "spring.cloud.polaris.circuitbreaker.enabled", havingValue = "true", matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.springframework.cloud.openfeign.FeignAutoConfiguration")
@AutoConfigureAfter(PolarisContextAutoConfiguration.class)

@ -0,0 +1,56 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker.config;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisResponseErrorHandler;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisRestTemplateModifier;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisRestTemplateResponseErrorHandler;
import com.tencent.cloud.polaris.context.PolarisContextAutoConfiguration;
import com.tencent.polaris.api.core.ConsumerAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author : wh
* @date : 2022/6/21 21:34
* @description: Auto configuration PolarisRestTemplateAutoConfiguration
*/
@ConditionalOnProperty(value = "spring.cloud.polaris.circuitbreaker.enabled",
havingValue = "true", matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(PolarisContextAutoConfiguration.class)
public class PolarisRestTemplateAutoConfiguration {
@Bean
@ConditionalOnBean(RestTemplate.class)
public PolarisRestTemplateResponseErrorHandler polarisRestTemplateResponseErrorHandler(ConsumerAPI consumerAPI, @Autowired(required = false) PolarisResponseErrorHandler polarisResponseErrorHandler) {
return new PolarisRestTemplateResponseErrorHandler(consumerAPI, polarisResponseErrorHandler);
}
@Bean
@ConditionalOnBean(RestTemplate.class)
public PolarisRestTemplateModifier polarisRestTemplateBeanPostProcessor(PolarisRestTemplateResponseErrorHandler restTemplateResponseErrorHandler) {
return new PolarisRestTemplateModifier(restTemplateResponseErrorHandler);
}
}

@ -0,0 +1,29 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import org.springframework.web.client.ResponseErrorHandler;
/**
* @author : wh
* @date : 2022/6/21 19:12
* @description: errorHandler {@link ResponseErrorHandler}
*/
public interface PolarisResponseErrorHandler extends ResponseErrorHandler {
}

@ -0,0 +1,65 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.RestTemplate;
/**
* @author : wh
* @date : 2022/6/21 21:20
* @description: auto configuration RestTemplate Find the RestTemplate bean annotated with {@link LoadBalanced} and replace {@link org.springframework.web.client.ResponseErrorHandler}
* with {@link PolarisRestTemplateResponseErrorHandler}
*/
public class PolarisRestTemplateModifier implements ApplicationContextAware, SmartInitializingSingleton {
private ApplicationContext applicationContext;
private final PolarisRestTemplateResponseErrorHandler polarisRestTemplateResponseErrorHandler;
public PolarisRestTemplateModifier(PolarisRestTemplateResponseErrorHandler polarisRestTemplateResponseErrorHandler) {
this.polarisRestTemplateResponseErrorHandler = polarisRestTemplateResponseErrorHandler;
}
@Override
public void afterSingletonsInstantiated() {
Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(LoadBalanced.class);
if (!ObjectUtils.isEmpty(beans)) {
beans.forEach(this::initRestTemplate);
}
}
private void initRestTemplate(String beanName, Object bean) {
if (bean instanceof RestTemplate) {
RestTemplate restTemplate = (RestTemplate) bean;
restTemplate.setErrorHandler(polarisRestTemplateResponseErrorHandler);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

@ -0,0 +1,111 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Objects;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ReflectionUtils;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.RetStatus;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.api.rpc.ServiceCallResult;
import com.tencent.polaris.api.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
/**
* @author : wh
* @date : 2022/6/21 17:25
* @description: Extend ResponseErrorHandler to get request information
*/
public class PolarisRestTemplateResponseErrorHandler implements ResponseErrorHandler {
private static final Logger LOG = LoggerFactory.getLogger(PolarisRestTemplateResponseErrorHandler.class);
private static final String FileName = "connection";
private final ConsumerAPI consumerAPI;
private final PolarisResponseErrorHandler polarisResponseErrorHandler;
public PolarisRestTemplateResponseErrorHandler(ConsumerAPI consumerAPI, PolarisResponseErrorHandler polarisResponseErrorHandler) {
this.consumerAPI = consumerAPI;
this.polarisResponseErrorHandler = polarisResponseErrorHandler;
}
@Override
public boolean hasError(ClientHttpResponse response) {
return true;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (Objects.nonNull(polarisResponseErrorHandler)) {
if (polarisResponseErrorHandler.hasError(response)) {
polarisResponseErrorHandler.handleError(response);
}
}
}
public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
ServiceCallResult resultRequest = null;
try {
resultRequest = builderServiceCallResult(url, response);
}
catch (IOException e) {
LOG.error("Will report response of {} url {}", response, url, e);
throw e;
}
finally {
consumerAPI.updateServiceCallResult(resultRequest);
}
}
private ServiceCallResult builderServiceCallResult(URI uri, ClientHttpResponse response) throws IOException {
ServiceCallResult resultRequest = new ServiceCallResult();
String serviceName = uri.getHost();
resultRequest.setService(serviceName);
resultRequest.setNamespace(MetadataContext.LOCAL_NAMESPACE);
resultRequest.setMethod(uri.getPath());
resultRequest.setRetStatus(RetStatus.RetSuccess);
String sourceNamespace = MetadataContext.LOCAL_NAMESPACE;
String sourceService = MetadataContext.LOCAL_SERVICE;
if (StringUtils.isNotBlank(sourceNamespace) && StringUtils.isNotBlank(sourceService)) {
resultRequest.setCallerService(new ServiceKey(sourceNamespace, sourceService));
}
HttpURLConnection connection = (HttpURLConnection) ReflectionUtils.getFieldValue(response, FileName);
URL url = connection.getURL();
resultRequest.setHost(url.getHost());
resultRequest.setPort(url.getPort());
if (response.getStatusCode().value() > 500) {
resultRequest.setRetStatus(RetStatus.RetFail);
}
return resultRequest;
}
}

@ -1,4 +1,6 @@
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerBootstrapConfiguration
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tencent.cloud.polaris.circuitbreaker.PolarisFeignClientAutoConfiguration
com.tencent.cloud.polaris.circuitbreaker.config.PolarisFeignClientAutoConfiguration,\
com.tencent.cloud.polaris.circuitbreaker.config.PolarisRestTemplateAutoConfiguration

@ -17,6 +17,7 @@
package com.tencent.cloud.polaris.circuitbreaker;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerBootstrapConfiguration;
import org.junit.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;

@ -17,6 +17,7 @@
package com.tencent.cloud.polaris.circuitbreaker;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisFeignClientAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignBeanPostProcessor;
import com.tencent.cloud.polaris.context.PolarisContextAutoConfiguration;
import com.tencent.polaris.api.core.ConsumerAPI;

@ -0,0 +1,70 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisRestTemplateResponseErrorHandler;
import com.tencent.polaris.api.core.ConsumerAPI;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.junit4.SpringRunner;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author : wh
* @date : 2022/6/22 09:00
* @description: Test for {@link PolarisRestTemplateResponseErrorHandler}.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PolarisRestTemplateResponseErrorHandlerTest.TestApplication.class,
properties = {"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"})
public class PolarisRestTemplateResponseErrorHandlerTest {
@Test
public void handleError() throws Exception {
ConsumerAPI consumerAPI = mock(ConsumerAPI.class);
PolarisRestTemplateResponseErrorHandler polarisRestTemplateResponseErrorHandler = new PolarisRestTemplateResponseErrorHandler(consumerAPI, null);
URI uri = mock(URI.class);
when(uri.getPath()).thenReturn("/test");
when(uri.getHost()).thenReturn("host");
HttpURLConnection httpURLConnection = mock(HttpURLConnection.class);
URL url = mock(URL.class);
when(httpURLConnection.getURL()).thenReturn(url);
when(url.getHost()).thenReturn("127.0.0.1");
when(url.getPort()).thenReturn(8080);
when(httpURLConnection.getResponseCode()).thenReturn(200);
SimpleClientHttpResponseTest clientHttpResponse = new SimpleClientHttpResponseTest(httpURLConnection);
polarisRestTemplateResponseErrorHandler.handleError(uri, HttpMethod.GET, clientHttpResponse);
when(consumerAPI.unWatchService(null)).thenReturn(true);
}
@SpringBootApplication
protected static class TestApplication {
}
}

@ -0,0 +1,106 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.circuitbreaker;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.AbstractClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* @author : wh
* @date : 2022/6/22 09:00
* @description: mock {@link org.springframework.http.client.SimpleClientHttpResponse}
*/
public class SimpleClientHttpResponseTest extends AbstractClientHttpResponse {
private final HttpURLConnection connection;
@Nullable
private HttpHeaders headers;
@Nullable
private InputStream responseStream;
SimpleClientHttpResponseTest(HttpURLConnection connection) {
this.connection = connection;
}
@Override
public int getRawStatusCode() throws IOException {
return this.connection.getResponseCode();
}
@Override
public String getStatusText() throws IOException {
String result = this.connection.getResponseMessage();
return (result != null) ? result : "";
}
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders();
// Header field 0 is the status line for most HttpURLConnections, but not on GAE
String name = this.connection.getHeaderFieldKey(0);
if (StringUtils.hasLength(name)) {
this.headers.add(name, this.connection.getHeaderField(0));
}
int i = 1;
while (true) {
name = this.connection.getHeaderFieldKey(i);
if (!StringUtils.hasLength(name)) {
break;
}
this.headers.add(name, this.connection.getHeaderField(i));
i++;
}
}
return this.headers;
}
@Override
public InputStream getBody() throws IOException {
InputStream errorStream = this.connection.getErrorStream();
this.responseStream = (errorStream != null ? errorStream : this.connection.getInputStream());
return this.responseStream;
}
@Override
public void close() {
try {
if (this.responseStream == null) {
getBody();
}
StreamUtils.drain(this.responseStream);
this.responseStream.close();
}
catch (Exception ex) {
// ignore
}
}
}

@ -11,6 +11,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-starter-tencent-polaris-config</artifactId>
<name>Spring Cloud Starter Tencent Polaris Config</name>
<dependencies>
<!-- Spring Cloud Tencent dependencies start -->
@ -64,5 +65,28 @@
</dependency>
<!-- Spring cloud dependencies start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,37 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Whether to connect to a remote server, suitable for local development mode.
*
* @author lepdou 2022-06-11
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@ConditionalOnProperty(value = "spring.cloud.polaris.config.connect-remote-server", matchIfMissing = true)
public @interface ConditionalOnConnectRemoteServerEnabled {
}

@ -20,7 +20,9 @@ package com.tencent.cloud.polaris.config;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceAutoRefresher;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.listener.PolarisConfigChangeEventListener;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -46,4 +48,14 @@ public class PolarisConfigAutoConfiguration {
contextRefresher);
}
@Bean
public PolarisConfigAnnotationProcessor polarisConfigAnnotationProcessor() {
return new PolarisConfigAnnotationProcessor();
}
@Bean
public PolarisConfigChangeEventListener polarisConfigChangeEventListener() {
return new PolarisConfigChangeEventListener();
}
}

@ -50,24 +50,31 @@ public class PolarisConfigBootstrapAutoConfiguration {
}
@Bean
public ConfigFileService configFileService(SDKContext sdkContext) {
return ConfigFileServiceFactory.createConfigFileService(sdkContext);
public PolarisPropertySourceManager polarisPropertySourceManager() {
return new PolarisPropertySourceManager();
}
@Bean
public PolarisPropertySourceManager polarisPropertySourceManager() {
return new PolarisPropertySourceManager();
@ConditionalOnConnectRemoteServerEnabled
public ConfigFileService configFileService(SDKContext sdkContext) {
return ConfigFileServiceFactory.createConfigFileService(sdkContext);
}
@Bean
public PolarisConfigFileLocator polarisConfigFileLocator(PolarisConfigProperties polarisConfigProperties,
PolarisContextProperties polarisContextProperties, ConfigFileService configFileService,
PolarisPropertySourceManager polarisPropertySourceManager, Environment environment) {
return new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, configFileService,
@ConditionalOnConnectRemoteServerEnabled
public PolarisConfigFileLocator polarisConfigFileLocator(
PolarisConfigProperties polarisConfigProperties,
PolarisContextProperties polarisContextProperties,
ConfigFileService configFileService,
PolarisPropertySourceManager polarisPropertySourceManager,
Environment environment) {
return new PolarisConfigFileLocator(polarisConfigProperties,
polarisContextProperties, configFileService,
polarisPropertySourceManager, environment);
}
@Bean
@ConditionalOnConnectRemoteServerEnabled
public ConfigurationModifier configurationModifier(PolarisConfigProperties polarisConfigProperties,
PolarisContextProperties polarisContextProperties) {
return new ConfigurationModifier(polarisConfigProperties, polarisContextProperties);

@ -80,13 +80,14 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
CompositePropertySource compositePropertySource = new CompositePropertySource(
POLARIS_CONFIG_PROPERTY_SOURCE_NAME);
// load spring boot default config files
initInternalConfigFiles(compositePropertySource);
// load custom config files
List<ConfigFileGroup> configFileGroups = polarisConfigProperties.getGroups();
if (CollectionUtils.isEmpty(configFileGroups)) {
return compositePropertySource;
}
initInternalConfigFiles(compositePropertySource);
initCustomPolarisConfigFiles(compositePropertySource, configFileGroups);
return compositePropertySource;
@ -181,7 +182,8 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
private PolarisPropertySource loadPolarisPropertySource(String namespace, String group, String fileName) {
ConfigKVFile configKVFile;
// unknown extension is resolved as properties file
if (ConfigFileFormat.isPropertyFile(fileName) || ConfigFileFormat.isUnknownFile(fileName)) {
if (ConfigFileFormat.isPropertyFile(fileName)
|| ConfigFileFormat.isUnknownFile(fileName)) {
configKVFile = configFileService.getConfigPropertiesFile(namespace, group, fileName);
}
else if (ConfigFileFormat.isYamlFile(fileName)) {

@ -0,0 +1,105 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.annotation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent;
import com.tencent.cloud.polaris.config.listener.ConfigChangeListener;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;
import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.addChangeListener;
/**
* {@link PolarisConfigAnnotationProcessor} implementation for spring .
* <p>Refer to the Apollo project implementation
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java>
* ApolloAnnotationProcessor</a></code>
* @author <a href="mailto:iskp.me@gmail.com">Palmer Xu</a> 2022-06-07
*/
public class PolarisConfigAnnotationProcessor implements BeanPostProcessor, PriorityOrdered {
@Override
public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName)
throws BeansException {
Class<?> clazz = bean.getClass();
for (Method method : findAllMethod(clazz)) {
this.processPolarisConfigChangeListener(bean, method);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
return bean;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
private List<Method> findAllMethod(Class<?> clazz) {
final List<Method> res = new LinkedList<>();
ReflectionUtils.doWithMethods(clazz, res::add);
return res;
}
private void processPolarisConfigChangeListener(final Object bean, final Method method) {
PolarisConfigKVFileChangeListener annotation = AnnotationUtils
.findAnnotation(method, PolarisConfigKVFileChangeListener.class);
if (annotation == null) {
return;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
method);
Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
"Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
method);
ReflectionUtils.makeAccessible(method);
String[] annotatedInterestedKeys = annotation.interestedKeys();
String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
ConfigChangeListener configChangeListener = changeEvent -> ReflectionUtils.invokeMethod(method, bean, changeEvent);
Set<String> interestedKeys =
annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
Set<String> interestedKeyPrefixes =
annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes)
: null;
addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
}
}

@ -0,0 +1,58 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configuring the change listener annotation.
* <p>Refer to the Apollo project implementation
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java>
* ApolloAnnotationProcessor</a></code>
* @author Palmer Xu 2022-05-31
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface PolarisConfigKVFileChangeListener {
/**
* The keys interested in the listener, will only be notified if any of the interested keys is changed.
* <br />
* If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when any key is changed.
* @return interested keys in the listener
*/
String[] interestedKeys() default {};
/**
* The key prefixes that the listener is interested in, will be notified if and only if the changed keys start with anyone of the prefixes.
* The prefixes will simply be used to determine whether the {@code listener} should be notified or not using {@code changedKey.startsWith(prefix)}.
* e.g. "spring." means that {@code listener} is interested in keys that starts with "spring.", such as "spring.banner", "spring.jpa", etc.
* and "application" means that {@code listener} is interested in keys that starts with "application", such as "applicationName", "application.port", etc.
* <br />
* If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when whatever key is changed.
* @return interested key-prefixed in the listener
*/
String[] interestedKeyPrefixes() default {};
}

@ -0,0 +1,88 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.listener;
import java.util.Map;
import java.util.Set;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
/**
* A change event when config is changed .
*
* @author <a href="mailto:iskp.me@gmail.com">Palmer Xu</a> 2022-06-07
*/
public final class ConfigChangeEvent {
/**
* all changes keys map.
*/
private final Map<String, ConfigPropertyChangeInfo> changes;
/**
* all interested changed keys.
*/
private final Set<String> interestedChangedKeys;
/**
* Config Change Event Constructor.
* @param changes all changes keys map
* @param interestedChangedKeys all interested changed keys
*/
public ConfigChangeEvent(Map<String, ConfigPropertyChangeInfo> changes, Set<String> interestedChangedKeys) {
this.changes = changes;
this.interestedChangedKeys = interestedChangedKeys;
}
/**
* Get the keys changed.
* @return the list of the keys
*/
public Set<String> changedKeys() {
return changes.keySet();
}
/**
* Get a specific change instance for the key specified.
* @param key the changed key
* @return the change instance
*/
public ConfigPropertyChangeInfo getChange(String key) {
return changes.get(key);
}
/**
* Check whether the specified key is changed .
* @param key the key
* @return true if the key is changed, false otherwise.
*/
public boolean isChanged(String key) {
return changes.containsKey(key);
}
/**
* Maybe subclass override this method.
*
* @return interested and changed keys
*/
public Set<String> interestedChangedKeys() {
return interestedChangedKeys;
}
}

@ -0,0 +1,35 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.listener;
/**
* Configuring the change listener interface.
*
* @author Palmer Xu 2022-05-31
*/
public interface ConfigChangeListener {
/**
* Invoked when there is any config change for the namespace.
*
* @param changeEvent the event for this change
*/
void onChange(ConfigChangeEvent changeEvent);
}

@ -0,0 +1,111 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.listener;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.collect.Maps;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.lang.NonNull;
import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.fireConfigChange;
import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.initialize;
import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.merge;
/**
* Polaris Config Change Event Listener .
*
* @author <a href="mailto:iskp.me@gmail.com">Elve.Xu</a> 2022-06-08
*/
public final class PolarisConfigChangeEventListener implements ApplicationListener<ApplicationEvent> {
private static final AtomicBoolean started = new AtomicBoolean();
/**
* Handle an application event.
*
* @param event the event to respond to
*/
@Override
public void onApplicationEvent(@NonNull ApplicationEvent event) {
// Initialize application all environment properties .
if (event instanceof ApplicationStartedEvent && started.compareAndSet(false, true)) {
ApplicationStartedEvent applicationStartedEvent = (ApplicationStartedEvent) event;
ConfigurableEnvironment environment = applicationStartedEvent.getApplicationContext().getEnvironment();
Map<String, Object> ret = loadEnvironmentProperties(environment);
if (!ret.isEmpty()) {
initialize(ret);
}
}
// Process Environment Change Event .
if (event instanceof EnvironmentChangeEvent) {
EnvironmentChangeEvent environmentChangeEvent = (EnvironmentChangeEvent) event;
ConfigurableApplicationContext context = (ConfigurableApplicationContext) environmentChangeEvent.getSource();
ConfigurableEnvironment environment = context.getEnvironment();
Map<String, Object> ret = loadEnvironmentProperties(environment);
Map<String, ConfigPropertyChangeInfo> changes = merge(ret);
fireConfigChange(changes.keySet(), Maps.newHashMap(changes));
changes.clear();
}
}
/**
* Try load all application environment config properties .
* @param environment application environment instance of {@link Environment}
* @return properties
*/
@SuppressWarnings("unchecked")
private Map<String, Object> loadEnvironmentProperties(ConfigurableEnvironment environment) {
Map<String, Object> ret = Maps.newHashMap();
MutablePropertySources sources = environment.getPropertySources();
sources.iterator().forEachRemaining(propertySource -> {
Object o = propertySource.getSource();
if (o instanceof Map) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) o).entrySet()) {
String key = entry.getKey();
String value = environment.getProperty(key);
ret.put(key, value);
}
}
else if (o instanceof Collection) {
int count = 0;
Collection<Object> collection = (Collection<Object>) o;
for (Object object : collection) {
String key = "[" + (count++) + "]";
ret.put(key, object);
}
}
});
return ret;
}
}

@ -0,0 +1,277 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.listener;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import static com.tencent.polaris.configuration.api.core.ChangeType.ADDED;
import static com.tencent.polaris.configuration.api.core.ChangeType.DELETED;
import static com.tencent.polaris.configuration.api.core.ChangeType.MODIFIED;
/**
* Polaris Config Listener Context Defined .
* <p>Refer to the Apollo project implementation
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java>
* AbstractConfig</a></code>
* @author Palmer Xu 2022-06-06
*/
public final class PolarisConfigListenerContext {
/**
* Logger instance.
*/
private static final Logger LOG = LoggerFactory.getLogger(PolarisConfigListenerContext.class);
/**
* Execute service Atomic Reference Cache .
*/
private static final AtomicReference<ExecutorService> EAR = new AtomicReference<>();
/**
* All custom {@link ConfigChangeListener} instance defined in application .
*/
private static final List<ConfigChangeListener> listeners = Lists.newCopyOnWriteArrayList();
/**
* All custom interested keys defined in application .
*/
private static final Map<ConfigChangeListener, Set<String>> interestedKeys = Maps.newHashMap();
/**
* All custom interested key prefixes defined in application .
*/
private static final Map<ConfigChangeListener, Set<String>> interestedKeyPrefixes = Maps.newHashMap();
/**
* Cache all latest configuration information for users in the application environment .
*/
private static final Cache<String, Object> properties = CacheBuilder.newBuilder().build();
/**
* Get or Created new execute server .
* @return execute service instance of {@link ExecutorService}
*/
private static ExecutorService executor() {
if (EAR.get() == null) {
synchronized (PolarisConfigListenerContext.class) {
int coreThreadSize = Runtime.getRuntime().availableProcessors();
final ExecutorService service = new ThreadPoolExecutor(coreThreadSize, coreThreadSize,
0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(64),
new CustomizableThreadFactory("Config-Change-Notify-Thread-Pool-"));
// Register Jvm Shutdown Hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
LOG.info("Shutting down config change notify thread pool");
service.shutdown();
}
catch (Exception ignore) {
}
}));
EAR.set(service);
}
}
return EAR.get();
}
/**
* Initialize Environment Properties cache after listen <code>ApplicationStartedEvent</code> event .
* @param ret origin properties map
*/
static void initialize(Map<String, Object> ret) {
properties.putAll(ret);
}
/**
* Merge Changed Properties .
* @param ret current environment properties map
* @return merged properties result map
*/
static Map<String, ConfigPropertyChangeInfo> merge(Map<String, Object> ret) {
Map<String, ConfigPropertyChangeInfo> changes = Maps.newHashMap();
if (!ret.isEmpty()) {
Map<String, Object> origin = Maps.newHashMap(properties.asMap());
Map<String, ConfigPropertyChangeInfo> deleted = Maps.newHashMap();
origin.keySet().parallelStream().forEach(key -> {
if (!ret.containsKey(key)) {
deleted.put(key, new ConfigPropertyChangeInfo(key, String.valueOf(origin.get(key)), null, DELETED));
properties.invalidate(key);
}
});
changes.putAll(deleted);
ret.keySet().parallelStream().forEach(key -> {
Object oldValue = properties.getIfPresent(key);
Object newValue = ret.get(key);
if (oldValue != null) {
if (!newValue.equals(oldValue)) {
properties.put(key, newValue);
changes.put(key, new ConfigPropertyChangeInfo(key, String.valueOf(oldValue), String.valueOf(newValue), MODIFIED));
}
}
else {
properties.put(key, newValue);
changes.put(key, new ConfigPropertyChangeInfo(key, null, String.valueOf(newValue), ADDED));
}
});
}
return changes;
}
/**
* Adding a config file change listener, will trigger a callback when the config file is published .
* @param listener the listener will be added
* @param interestedKeys the keys interested in the listener, will only be notified if any of the interested keys is changed.
* @param interestedKeyPrefixes the key prefixes that the listener is interested in,
* will be notified if and only if the changed keys start with anyone of the prefixes.
*/
public static void addChangeListener(@NonNull ConfigChangeListener listener,
@Nullable Set<String> interestedKeys, @Nullable Set<String> interestedKeyPrefixes) {
if (!listeners.contains(listener)) {
listeners.add(listener);
PolarisConfigListenerContext.interestedKeys.put(listener, interestedKeys == null ? Sets.newHashSet() : interestedKeys);
PolarisConfigListenerContext.interestedKeyPrefixes.put(listener, interestedKeyPrefixes == null ? Sets.newHashSet() : interestedKeyPrefixes);
}
}
/**
* Fire config change event with {@link ConfigKVFileChangeListener} .
* @param changedKeys changed keys in listener
* @param changes target config file changes info
*/
public static void fireConfigChange(Set<String> changedKeys, Map<String, ConfigPropertyChangeInfo> changes) {
final List<ConfigChangeListener> listeners = findMatchedConfigChangeListeners(changedKeys);
for (ConfigChangeListener listener : listeners) {
Set<String> interestedChangedKeys = resolveInterestedChangedKeys(listener, changedKeys);
Map<String, ConfigPropertyChangeInfo> modifiedChanges = new HashMap<>(interestedChangedKeys.size());
interestedChangedKeys.parallelStream().forEach(key -> modifiedChanges.put(key, changes.get(key)));
ConfigChangeEvent event = new ConfigChangeEvent(modifiedChanges, interestedChangedKeys);
PolarisConfigListenerContext.executor().execute(() -> listener.onChange(event));
}
}
/**
* Try to find all matched config change listeners .
* @param changedKeys received changed keys
* @return list of matched {@link ConfigChangeListener}
*/
private static List<ConfigChangeListener> findMatchedConfigChangeListeners(Set<String> changedKeys) {
final List<ConfigChangeListener> configChangeListeners = Lists.newArrayList();
for (ConfigChangeListener listener : listeners) {
if (isConfigChangeListenerInterested(listener, changedKeys)) {
configChangeListeners.add(listener);
}
}
return configChangeListeners;
}
/**
* Check {@link ConfigChangeListener} is interested in custom keys.
* @param listener instance of {@link ConfigChangeListener}
* @param changedKeys received changed keys
* @return true is interested in custom keys
*/
private static boolean isConfigChangeListenerInterested(ConfigChangeListener listener, Set<String> changedKeys) {
Set<String> interestedKeys = PolarisConfigListenerContext.interestedKeys.get(listener);
Set<String> interestedKeyPrefixes = PolarisConfigListenerContext.interestedKeyPrefixes.get(listener);
if ((interestedKeys == null || interestedKeys.isEmpty())
&& (interestedKeyPrefixes == null || interestedKeyPrefixes.isEmpty())) {
return true;
}
if (interestedKeys != null) {
for (String interestedKey : interestedKeys) {
if (changedKeys.contains(interestedKey)) {
return true;
}
}
}
if (interestedKeyPrefixes != null) {
for (String prefix : interestedKeyPrefixes) {
for (final String changedKey : changedKeys) {
if (changedKey.startsWith(prefix)) {
return true;
}
}
}
}
return false;
}
/**
* Resolve all interested keys .
* @param listener instance of {@link ConfigChangeListener}
* @param changedKeys received changed keys
* @return set of all interested keys in listener
*/
private static Set<String> resolveInterestedChangedKeys(ConfigChangeListener listener, Set<String> changedKeys) {
Set<String> interestedChangedKeys = Sets.newHashSet();
if (interestedKeys.containsKey(listener)) {
Set<String> interestedKeys = PolarisConfigListenerContext.interestedKeys.get(listener);
for (String interestedKey : interestedKeys) {
if (changedKeys.contains(interestedKey)) {
interestedChangedKeys.add(interestedKey);
}
}
}
if (interestedKeyPrefixes.containsKey(listener)) {
Set<String> interestedKeyPrefixes = PolarisConfigListenerContext.interestedKeyPrefixes.get(listener);
for (String interestedKeyPrefix : interestedKeyPrefixes) {
for (String changedKey : changedKeys) {
if (changedKey.startsWith(interestedKeyPrefix)) {
interestedChangedKeys.add(changedKey);
}
}
}
}
return Collections.unmodifiableSet(interestedChangedKeys);
}
}

@ -34,6 +34,13 @@
"defaultValue": "",
"description": "List of imported config files.",
"sourceType": "com.tencent.cloud.polaris.config.config.PolarisConfigProperties"
},
{
"name": "spring.cloud.polaris.config.connect-remote-server",
"type": "java.lang.Boolean",
"defaultValue": "true",
"description": "Whether to connect to a remote server, suitable for local development mode.",
"sourceType": "com.tencent.cloud.polaris.config.config.PolarisConfigProperties"
}
]
}

@ -0,0 +1,170 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.adapter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.tencent.polaris.configuration.api.core.ConfigFileChangeListener;
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener;
/**
* Mock config kv file for test.
*@author lepdou 2022-06-11
*/
public class MockedConfigKVFile implements ConfigKVFile {
private final Map<String, Object> properties;
private final List<ConfigKVFileChangeListener> listeners = new ArrayList<>();
public MockedConfigKVFile(Map<String, Object> properties) {
this.properties = properties;
}
@Override
public String getProperty(String s, String s1) {
return String.valueOf(properties.get(s));
}
@Override
public Integer getIntProperty(String s, Integer integer) {
return null;
}
@Override
public Long getLongProperty(String s, Long aLong) {
return null;
}
@Override
public Short getShortProperty(String s, Short aShort) {
return null;
}
@Override
public Float getFloatProperty(String s, Float aFloat) {
return null;
}
@Override
public Double getDoubleProperty(String s, Double aDouble) {
return null;
}
@Override
public Byte getByteProperty(String s, Byte aByte) {
return null;
}
@Override
public Boolean getBooleanProperty(String s, Boolean aBoolean) {
return null;
}
@Override
public String[] getArrayProperty(String s, String s1, String[] strings) {
return new String[0];
}
@Override
public <T extends Enum<T>> T getEnumProperty(String s, Class<T> aClass, T t) {
return null;
}
@Override
public <T> T getJsonProperty(String s, Class<T> aClass, T t) {
return null;
}
@Override
public <T> T getJsonProperty(String s, Type type, T t) {
return null;
}
@Override
public Set<String> getPropertyNames() {
return properties.keySet();
}
@Override
public void addChangeListener(ConfigKVFileChangeListener configKVFileChangeListener) {
listeners.add(configKVFileChangeListener);
}
@Override
public void removeChangeListener(ConfigKVFileChangeListener configKVFileChangeListener) {
}
@Override
public String getContent() {
return null;
}
@Override
public <T> T asJson(Class<T> aClass, T t) {
return null;
}
@Override
public <T> T asJson(Type type, T t) {
return null;
}
@Override
public boolean hasContent() {
return false;
}
@Override
public void addChangeListener(ConfigFileChangeListener configFileChangeListener) {
}
@Override
public void removeChangeListener(ConfigFileChangeListener configFileChangeListener) {
}
public void fireChangeListener(ConfigKVFileChangeEvent event) {
for (ConfigKVFileChangeListener listener : listeners) {
listener.onChange(event);
}
}
@Override
public String getNamespace() {
return null;
}
@Override
public String getFileGroup() {
return null;
}
@Override
public String getFileName() {
return null;
}
}

@ -0,0 +1,188 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.adapter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.tencent.cloud.polaris.config.config.ConfigFileGroup;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.context.PolarisContextProperties;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import static org.mockito.Mockito.when;
/**
* test for {@link PolarisConfigFileLocator}
*@author lepdou 2022-06-11
*/
@RunWith(MockitoJUnitRunner.class)
public class PolarisConfigFileLocatorTest {
@Mock
private PolarisConfigProperties polarisConfigProperties;
@Mock
private PolarisContextProperties polarisContextProperties;
@Mock
private ConfigFileService configFileService;
@Mock
private PolarisPropertySourceManager polarisPropertySourceManager;
@Mock
private Environment environment;
private final String testNamespace = "testNamespace";
private final String testServiceName = "testServiceName";
@Test
public void testLoadApplicationPropertiesFile() {
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, polarisPropertySourceManager, environment);
when(polarisContextProperties.getNamespace()).thenReturn(testNamespace);
when(polarisContextProperties.getService()).thenReturn(testServiceName);
// application.properties
Map<String, Object> applicationProperties = new HashMap<>();
applicationProperties.put("k1", "v1");
applicationProperties.put("k2", "v2");
applicationProperties.put("k3", "v3");
ConfigKVFile propertiesFile = new MockedConfigKVFile(applicationProperties);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "application.properties"))
.thenReturn(propertiesFile);
Map<String, Object> emptyMap = new HashMap<>();
ConfigKVFile emptyConfigFile = new MockedConfigKVFile(emptyMap);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application.yml")).thenReturn(emptyConfigFile);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "bootstrap.properties")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap.yml")).thenReturn(emptyConfigFile);
when(polarisConfigProperties.getGroups()).thenReturn(null);
when(environment.getActiveProfiles()).thenReturn(new String[] {});
PropertySource<?> propertySource = locator.locate(environment);
Assert.assertEquals("v1", propertySource.getProperty("k1"));
Assert.assertEquals("v2", propertySource.getProperty("k2"));
Assert.assertEquals("v3", propertySource.getProperty("k3"));
}
@Test
public void testActiveProfileFilesPriorityBiggerThanDefault() {
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, polarisPropertySourceManager, environment);
when(polarisContextProperties.getNamespace()).thenReturn(testNamespace);
when(polarisContextProperties.getService()).thenReturn(testServiceName);
// application.properties
Map<String, Object> applicationProperties = new HashMap<>();
applicationProperties.put("k1", "v1");
applicationProperties.put("k2", "v2");
applicationProperties.put("k3", "v3");
ConfigKVFile propertiesFile = new MockedConfigKVFile(applicationProperties);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "application.properties"))
.thenReturn(propertiesFile);
// application-dev.properties
Map<String, Object> devProperties = new HashMap<>();
devProperties.put("k1", "v11");
ConfigKVFile devFile = new MockedConfigKVFile(devProperties);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "application-dev.properties"))
.thenReturn(devFile);
Map<String, Object> emptyMap = new HashMap<>();
ConfigKVFile emptyConfigFile = new MockedConfigKVFile(emptyMap);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application.yml")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application-dev.yml")).thenReturn(emptyConfigFile);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "bootstrap.properties")).thenReturn(emptyConfigFile);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "bootstrap-dev.properties")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap.yml")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap-dev.yml")).thenReturn(emptyConfigFile);
when(polarisConfigProperties.getGroups()).thenReturn(null);
when(environment.getActiveProfiles()).thenReturn(new String[] {"dev"});
PropertySource<?> propertySource = locator.locate(environment);
Assert.assertEquals("v11", propertySource.getProperty("k1"));
Assert.assertEquals("v2", propertySource.getProperty("k2"));
Assert.assertEquals("v3", propertySource.getProperty("k3"));
}
@Test
public void testGetCustomFiles() {
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, polarisPropertySourceManager, environment);
when(polarisContextProperties.getNamespace()).thenReturn(testNamespace);
when(polarisContextProperties.getService()).thenReturn(testServiceName);
Map<String, Object> emptyMap = new HashMap<>();
ConfigKVFile emptyConfigFile = new MockedConfigKVFile(emptyMap);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "application.properties")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application.yml")).thenReturn(emptyConfigFile);
when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "bootstrap.properties")).thenReturn(emptyConfigFile);
when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap.yml")).thenReturn(emptyConfigFile);
List<ConfigFileGroup> customFiles = new LinkedList<>();
ConfigFileGroup configFileGroup = new ConfigFileGroup();
String customGroup = "group1";
configFileGroup.setName(customGroup);
String customFile1 = "file1.properties";
String customFile2 = "file2.properties";
configFileGroup.setFiles(Lists.newArrayList(customFile1, customFile2));
customFiles.add(configFileGroup);
when(polarisConfigProperties.getGroups()).thenReturn(customFiles);
when(environment.getActiveProfiles()).thenReturn(new String[] {});
// file1.properties
Map<String, Object> file1Map = new HashMap<>();
file1Map.put("k1", "v1");
file1Map.put("k2", "v2");
ConfigKVFile file1 = new MockedConfigKVFile(file1Map);
when(configFileService.getConfigPropertiesFile(testNamespace, customGroup, customFile1)).thenReturn(file1);
// file2.properties
Map<String, Object> file2Map = new HashMap<>();
file2Map.put("k1", "v11");
file2Map.put("k3", "v3");
ConfigKVFile file2 = new MockedConfigKVFile(file2Map);
when(configFileService.getConfigPropertiesFile(testNamespace, customGroup, customFile2)).thenReturn(file2);
PropertySource<?> propertySource = locator.locate(environment);
Assert.assertEquals("v1", propertySource.getProperty("k1"));
Assert.assertEquals("v2", propertySource.getProperty("k2"));
Assert.assertEquals("v3", propertySource.getProperty("k3"));
}
}

@ -0,0 +1,122 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.adapter;
import java.util.HashMap;
import java.util.Map;
import com.google.common.collect.Lists;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.polaris.configuration.api.core.ChangeType;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cloud.context.refresh.ContextRefresher;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* test for {@link PolarisPropertySourceAutoRefresher}
*@author lepdou 2022-06-11
*/
@RunWith(MockitoJUnitRunner.class)
public class PolarisPropertiesSourceAutoRefresherTest {
@Mock
private PolarisConfigProperties polarisConfigProperties;
@Mock
private PolarisPropertySourceManager polarisPropertySourceManager;
@Mock
private ContextRefresher contextRefresher;
private final String testNamespace = "testNamespace";
private final String testServiceName = "testServiceName";
private final String testFileName = "application.properties";
@Test
public void testConfigFileChanged() {
PolarisPropertySourceAutoRefresher refresher = new PolarisPropertySourceAutoRefresher(polarisConfigProperties,
polarisPropertySourceManager, contextRefresher);
when(polarisConfigProperties.isAutoRefresh()).thenReturn(true);
Map<String, Object> content = new HashMap<>();
content.put("k1", "v1");
content.put("k2", "v2");
content.put("k3", "v3");
MockedConfigKVFile file = new MockedConfigKVFile(content);
PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testServiceName, testFileName,
file, content);
when(polarisPropertySourceManager.getAllPropertySources()).thenReturn(Lists.newArrayList(polarisPropertySource));
ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED);
ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED);
ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED);
Map<String, ConfigPropertyChangeInfo> changeInfos = new HashMap<>();
changeInfos.put("k1", changeInfo);
changeInfos.put("k2", changeInfo3);
changeInfos.put("k4", changeInfo2);
ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos);
refresher.onApplicationEvent(null);
file.fireChangeListener(event);
Assert.assertEquals("v11", polarisPropertySource.getProperty("k1"));
Assert.assertEquals("v3", polarisPropertySource.getProperty("k3"));
Assert.assertNull(polarisPropertySource.getProperty("k2"));
Assert.assertEquals("v4", polarisPropertySource.getProperty("k4"));
verify(contextRefresher).refresh();
}
@Test
public void testNewConfigFile() {
PolarisPropertySourceAutoRefresher refresher = new PolarisPropertySourceAutoRefresher(polarisConfigProperties,
polarisPropertySourceManager, contextRefresher);
when(polarisConfigProperties.isAutoRefresh()).thenReturn(true);
Map<String, Object> emptyContent = new HashMap<>();
MockedConfigKVFile file = new MockedConfigKVFile(emptyContent);
PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testServiceName, testFileName,
file, emptyContent);
when(polarisPropertySourceManager.getAllPropertySources()).thenReturn(Lists.newArrayList(polarisPropertySource));
ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", null, "v1", ChangeType.ADDED);
Map<String, ConfigPropertyChangeInfo> changeInfos = new HashMap<>();
changeInfos.put("k1", changeInfo);
ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos);
refresher.onApplicationEvent(null);
file.fireChangeListener(event);
Assert.assertEquals("v1", polarisPropertySource.getProperty("k1"));
verify(contextRefresher).refresh();
}
}

@ -0,0 +1,124 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.listener;
import com.google.common.collect.Sets;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Integration testing for change listener.
*@author lepdou 2022-06-11
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = DEFINED_PORT,
classes = ConfigChangeListenerTest.TestApplication.class,
properties = {"server.port=8081",
"spring.config.location = classpath:application-test.yml"})
public class ConfigChangeListenerTest {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private TestApplication.TestConfig testConfig;
@Test
public void test() throws InterruptedException {
//before change
Assert.assertEquals(1000, testConfig.getTimeout());
//submit change event
System.setProperty("timeout", "2000");
EnvironmentChangeEvent event = new EnvironmentChangeEvent(applicationContext,
Sets.newHashSet("timeout"));
applicationEventPublisher.publishEvent(event);
Thread.sleep(200);
//after change
Assert.assertEquals(2, testConfig.getChangeCnt());
Assert.assertEquals(2000, testConfig.getTimeout());
}
@SpringBootApplication
protected static class TestApplication {
@Component
protected static class TestConfig {
@Value("${timeout:1000}")
private int timeout;
private int changeCnt;
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getChangeCnt() {
return changeCnt;
}
@PolarisConfigKVFileChangeListener(interestedKeys = {"timeout"})
public void configChangedListener(ConfigChangeEvent event) {
ConfigPropertyChangeInfo changeInfo = event.getChange("timeout");
timeout = Integer.parseInt(changeInfo.getNewValue());
changeCnt++;
}
@PolarisConfigKVFileChangeListener(interestedKeyPrefixes = {"timeout"})
public void configChangedListener2(ConfigChangeEvent event) {
ConfigPropertyChangeInfo changeInfo = event.getChange("timeout");
timeout = Integer.parseInt(changeInfo.getNewValue());
changeCnt++;
}
}
@Component
protected static class EventPublisher implements ApplicationEventPublisher {
@Override
public void publishEvent(Object o) {
}
}
}
}

@ -0,0 +1,9 @@
spring:
application:
name: test
cloud:
polaris:
address: grpc://127.0.0.1:8091
namespace: default
config:
connect-remote-server: false

@ -19,6 +19,7 @@
package com.tencent.cloud.polaris.ratelimit.filter;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ -55,7 +56,7 @@ import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LAB
/**
* Reactive filter to check quota.
*
* @author Haotian Zhang, lepdou, cheese8
* @author Haotian Zhang, lepdou, kaiy, cheese8
*/
public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
@ -113,7 +114,7 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
}
// Unirate
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultOk && quotaResponse.getWaitMs() > 0) {
Thread.sleep(quotaResponse.getWaitMs());
return Mono.delay(Duration.ofMillis(quotaResponse.getWaitMs())).flatMap(e -> chain.filter(exchange));
}
}
catch (Throwable t) {

@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
@ -65,7 +66,7 @@ import static org.mockito.Mockito.when;
/**
* Test for {@link QuotaCheckReactiveFilter}.
*
* @author Haotian Zhang
* @author Haotian Zhang, kaiy
*/
@RunWith(MockitoJUnitRunner.class)
@SpringBootTest(classes = QuotaCheckReactiveFilterTest.TestApplication.class, properties = {
@ -201,7 +202,14 @@ public class QuotaCheckReactiveFilterTest {
// Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2";
long startTimestamp = System.currentTimeMillis();
quotaCheckReactiveFilter.filter(exchange, webFilterChain);
CountDownLatch countDownLatch = new CountDownLatch(1);
quotaCheckReactiveFilter.filter(exchange, webFilterChain).subscribe(e -> { }, t -> { }, countDownLatch::countDown);
try {
countDownLatch.await();
}
catch (InterruptedException e) {
fail("Exception encountered.", e);
}
assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L);
// Rate limited

@ -23,6 +23,7 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
/**
* Read file content from classpath resource.
@ -35,20 +36,15 @@ public final class ResourceFileUtils {
}
public static String readFile(String path) throws IOException {
StringBuilder sb = new StringBuilder();
ClassPathResource classPathResource = new ClassPathResource(path);
if (classPathResource.exists() && classPathResource.isReadable()) {
try (InputStream inputStream = classPathResource.getInputStream()) {
byte[] buffer = new byte[1024 * 10];
int len;
while ((len = inputStream.read(buffer)) != -1) {
sb.append(new String(buffer, 0, len, StandardCharsets.UTF_8));
}
return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
}
}
return sb.toString();
return "";
}
}

@ -35,7 +35,7 @@ public class ResourceFileUtilsTest {
@Test
public void testReadExistedFile() throws IOException {
String content = ResourceFileUtils.readFile("test.txt");
Assert.assertEquals("just for test\n", content);
Assert.assertEquals("just for test", content);
}
@Test

@ -24,6 +24,16 @@
<artifactId>spring-cloud-tencent-commons</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-context</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>
@ -49,10 +59,10 @@
<artifactId>spring-cloud-starter-tencent-polaris-router</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-context</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-config</artifactId>
</dependency>
</dependencies>
<build>
@ -75,4 +85,4 @@
</plugin>
</plugins>
</build>
</project>
</project>

@ -48,6 +48,11 @@ public class ServiceAController {
return polarisServiceB.info();
}
@GetMapping("/getBServiceInfoByRestTemplate")
public String getBServiceInfoByRestTemplate() {
return restTemplate.getForObject("http://polaris-circuitbreaker-example-b/example/service/b/info", String.class);
}
/**
* Get info of Service B by RestTemplate.
* @return info of Service B

@ -0,0 +1,49 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.example;
import java.util.Set;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener;
import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent;
import org.springframework.stereotype.Component;
/**
* Custom Config Listener Example .
*
* @author Palmer Xu 2022-06-06
*/
@Component
public final class PersonConfigChangeListener {
/**
* PolarisConfigKVFileChangeListener Example .
* @param event instance of {@link ConfigChangeEvent}
*/
@PolarisConfigKVFileChangeListener(interestedKeyPrefixes = "teacher")
public void onChange(ConfigChangeEvent event) {
Set<String> changedKeys = event.changedKeys();
for (String changedKey : changedKeys) {
System.out.printf("%s = %s \n", changedKey, event.getChange(changedKey));
}
}
}

@ -18,6 +18,11 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-ratelimit</artifactId>

Loading…
Cancel
Save