diff --git a/spring-cloud-alibaba-dependencies/pom.xml b/spring-cloud-alibaba-dependencies/pom.xml new file mode 100644 index 00000000..573dc2c4 --- /dev/null +++ b/spring-cloud-alibaba-dependencies/pom.xml @@ -0,0 +1,476 @@ + + + 4.0.0 + + spring-cloud-dependencies-parent + org.springframework.cloud + 2.1.6.RELEASE + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 0.9.1.BUILD-SNAPSHOT + pom + Spring Cloud Alibaba Dependencies + Spring Cloud Alibaba Dependencies + + + 1.6.2 + 3.1.0 + 0.5.2 + 1.1.1 + 0.8.0 + 1.0.9 + 1.0.1 + 4.4.1 + 1.0.5 + 2.44.0 + 2.0.2 + 2.1.6 + 2.7.1 + 2.7.1 + 2.7.1 + 1.1.0 + 1.1.8.6 + 1.1.1 + + + + + + + + com.alibaba.cloud + alicloud-context + ${alicloud.context.version} + + + com.aliyun + aliyun-java-sdk-edas + ${aliyun.sdk.edas.version} + + + com.aliyun + aliyun-java-sdk-core + + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun.sdk.version} + + + com.alibaba.ans + ans-sdk + ${ans.version} + + + com.alibaba.edas.acm + acm-sdk + ${acm.version} + + + com.alibaba.edas + schedulerX-client + ${schedulerX.client.version} + + + + + com.aliyun.mns + aliyun-sdk-mns + ${aliyun.sdk.mns} + + + + com.aliyun + aliyun-java-sdk-dysmsapi + ${aliyun.java.sdk.dysmsapi} + + + + + com.alibaba.nacos + nacos-client + ${nacos.client.version} + + + + com.alibaba.nacos + nacos-config + ${nacos.config.version} + + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + ${rocketmq.starter.version} + + + + + com.alibaba.csp + sentinel-core + ${sentinel.version} + + + com.alibaba.csp + sentinel-parameter-flow-control + ${sentinel.version} + + + com.alibaba.csp + sentinel-datasource-extension + ${sentinel.version} + + + com.alibaba.csp + sentinel-datasource-apollo + ${sentinel.version} + + + com.alibaba.csp + sentinel-datasource-zookeeper + ${sentinel.version} + + + com.alibaba.csp + sentinel-datasource-nacos + ${sentinel.version} + + + com.alibaba.csp + sentinel-web-servlet + ${sentinel.version} + + + com.alibaba.csp + sentinel-zuul-adapter + ${sentinel.version} + + + com.alibaba.csp + sentinel-spring-cloud-gateway-adapter + ${sentinel.version} + + + com.alibaba.csp + sentinel-transport-simple-http + ${sentinel.version} + + + com.alibaba.csp + sentinel-annotation-aspectj + ${sentinel.version} + + + com.alibaba.csp + sentinel-dubbo-adapter + ${sentinel.version} + + + com.alibaba.csp + sentinel-apache-dubbo-adapter + ${sentinel.version} + + + com.alibaba.cloud + sentinel-dubbo-api + ${project.version} + + + com.alibaba.csp + sentinel-cluster-server-default + ${sentinel.version} + + + com.alibaba.csp + sentinel-cluster-client-default + ${sentinel.version} + + + com.alibaba.csp + sentinel-spring-webflux-adapter + ${sentinel.version} + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + ${sentinel.version} + + + + + + + + io.seata + seata-spring + ${seata.version} + + + + + org.apache.dubbo + dubbo + ${dubbo.version} + + + org.springframework + spring-context + + + javax.servlet + servlet-api + + + log4j + log4j + + + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo-spring-boot.version} + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo-registry-nacos.version} + + + + + com.aliyun.oss + aliyun-sdk-oss + ${oss.version} + + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-datasource + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-gateway + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-oss + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-discovery + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-config + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-config-server + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-seata + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-acm + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-ans + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-schedulerx + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-sms + ${project.version} + + + com.alibaba.cloud + spring-cloud-alicloud-context + ${project.version} + + + com.alibaba.cloud + spring-cloud-stream-binder-rocketmq + ${project.version} + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + ${project.version} + + + com.alibaba.cloud + spring-cloud-starter-alicloud-oss + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + ${project.version} + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config-server + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alicloud-ans + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alicloud-acm + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-alicloud-schedulerx + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-stream-rocketmq + ${project.version} + + + + com.alibaba.cloud + spring-cloud-starter-bus-rocketmq + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alicloud-sms + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + ${project.version} + + + + + + + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + + + diff --git a/spring-cloud-alibaba-dubbo/README.md b/spring-cloud-alibaba-dubbo/README.md new file mode 100644 index 00000000..d2cdb64f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/README.md @@ -0,0 +1,6 @@ +## Info + + + +## Features + diff --git a/spring-cloud-alibaba-dubbo/README_CN.md b/spring-cloud-alibaba-dubbo/README_CN.md new file mode 100644 index 00000000..177caaa5 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/README_CN.md @@ -0,0 +1,49 @@ +## 简介 + +Dubbo Spring Cloud 基于 Dubbo Spring Boot 2.7.1[1] 和 Spring Cloud 2.x 开发,无论开发人员是 Dubbo 用户还是 Spring Cloud 用户, +都能轻松地驾驭,并以接近“零”成本的代价使应用向上迁移。Dubbo Spring Cloud 致力于简化 Cloud Native 开发成本,提高研发效能以及提升应用性能等目的。 + +Dubbo Spring Cloud 首个 Preview Release,随同 Spring Cloud Alibaba `0.2.2.RELEASE` 和 `0.9.0.RELEASE` 一同发布[2], +分别对应 Spring Cloud Finchley[3] 与 Greenwich[4] (下文分别简称为 “F” 版 和 “G” 版) 。 + + + + + +## 功能 + +由于 Dubbo Spring Cloud 构建在原生的 Spring Cloud 之上,其服务治理方面的能力可认为是 Spring Cloud Plus, +不仅完全覆盖 Spring Cloud 原生特性[5],而且提供更为稳定和成熟的实现,特性比对如下表所示: + +| 功能组件 | Spring Cloud | Dubbo Spring Cloud | +| ---------------------------------------------------- | -------------------------------------- | ------------------------------------------------------ | +| 分布式配置(Distributed configuration) | Git、Zookeeper、Consul、JDBC | Spring Cloud 分布式配置 + Dubbo 配置中心[6] | +| 服务注册与发现(Service registration and discovery) | Eureka、Zookeeper、Consul | Spring Cloud 原生注册中心[7] + Dubbo 原生注册中[8] | +| 负载均衡(Load balancing) | Ribbon(随机、轮询等算法) | Dubbo 内建实现(随机、轮询等算法 + 权重等特性) | +| 服务熔断(Circuit Breakers) | Spring Cloud Hystrix | Spring Cloud Hystrix + Alibaba Sentinel[9] 等 | +| 服务调用(Service-to-service calls) | Open Feign、`RestTemplate` | Spring Cloud 服务调用 + Dubbo `@Reference` | +| 链路跟踪(Tracing) | Spring Cloud Sleuth[10] + Zipkin[11] | Zipkin、opentracing 等 | + + + +[1]: 从 2.7.0 开始,Dubbo Spring Boot 与 Dubbo 在版本上保持一致 + +[2]: Preview releases of Spring Cloud Alibaba are available: 0.9.0, 0.2.2, and 0.1.2 - + +[3]: 目前最新的 Spring Cloud “F” 版的版本为:`Finchley.SR2` - + +[4]: 当前Spring Cloud “G” 版为 `Greenwich.RELEASE` + +[5]: Spring Cloud 特性列表 - + +[6]: Dubbo 2.7 开始支持配置中心,可自定义适配 - + +[7]: Spring Cloud 原生注册中心,除 Eureka、Zookeeper、Consul 之外,还包括 Spring Cloud Alibaba 中的 Nacos + +[8]: Dubbo 原生注册中心 - + +[9]: Alibaba Sentinel:Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性 - ,目前 Sentinel 已被 Spring Cloud 项目纳为 Circuit Breaker 的候选实现 - + +[10]:Spring Cloud Sleuth - + +[11]: Zipkin - \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/pom.xml b/spring-cloud-alibaba-dubbo/pom.xml new file mode 100644 index 00000000..36a9f4ac --- /dev/null +++ b/spring-cloud-alibaba-dubbo/pom.xml @@ -0,0 +1,262 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-alibaba-dubbo + Spring Cloud Alibaba Dubbo + + + 2.7.1 + 2.1.2.RELEASE + 2.1.2.RELEASE + 4.0.1 + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + org.apache.dubbo + dubbo-dependencies-bom + ${dubbo.version} + pom + import + + + + + + + + + + + org.springframework.boot + spring-boot-actuator + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + true + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot + true + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + + + org.springframework.cloud + spring-cloud-commons + true + + + + org.springframework.cloud + spring-cloud-context + true + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + true + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + ${spring-cloud-zookeeper.version} + true + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.zookeeper + zookeeper + 3.4.12 + true + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.curator + curator-framework + ${curator.version} + true + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + ${spring-cloud-consul.version} + true + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + true + + + + + org.springframework.cloud + spring-cloud-openfeign-core + + + + + org.apache.dubbo + dubbo + ${dubbo.version} + + + org.springframework + spring + + + javax.servlet + servlet-api + + + log4j + log4j + + + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + org.apache.dubbo + dubbo-spring-boot-actuator + ${dubbo.version} + + + + + io.netty + netty-all + + + + javax.servlet + javax.servlet-api + provided + + + + + io.github.openfeign + feign-jaxrs2 + 9.7.0 + + + + + junit + junit + test + + + + org.springframework + spring-test + test + + + + org.springframework.boot + spring-boot-starter-web + test + + + + org.springframework.boot + spring-boot-test + test + + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/DubboMetadataEndpointAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/DubboMetadataEndpointAutoConfiguration.java new file mode 100644 index 00000000..0cb46e70 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/DubboMetadataEndpointAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.actuate; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import com.alibaba.cloud.dubbo.actuate.endpoint.DubboRestMetadataEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/** + * Dubbo Metadata Endpoints Auto-{@link Configuration} + */ +@ConditionalOnClass(name = "org.springframework.boot.actuate.endpoint.annotation.Endpoint") +@PropertySource(value = "classpath:/META-INF/dubbo/default/actuator-endpoints.properties") +@ManagementContextConfiguration +public class DubboMetadataEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public DubboRestMetadataEndpoint dubboRestMetadataEndpoint() { + return new DubboRestMetadataEndpoint(); + } +} + + diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/endpoint/DubboRestMetadataEndpoint.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/endpoint/DubboRestMetadataEndpoint.java new file mode 100644 index 00000000..38794246 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/actuate/endpoint/DubboRestMetadataEndpoint.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.actuate.endpoint; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import com.alibaba.cloud.dubbo.service.DubboMetadataService; + +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; + +/** + * Dubbo Rest Metadata {@link Endpoint} + */ +@Endpoint(id = "dubborestmetadata") +public class DubboRestMetadataEndpoint { + + @Autowired + private DubboMetadataService dubboMetadataService; + + @ReadOperation(produces = APPLICATION_JSON_UTF8_VALUE) + public String get() { + return dubboMetadataService.getServiceRestMetadata(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/annotation/DubboTransported.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/annotation/DubboTransported.java new file mode 100644 index 00000000..d1236b3e --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/annotation/DubboTransported.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.annotation; + +import org.apache.dubbo.common.Constants; +import org.apache.dubbo.config.annotation.Reference; +import org.apache.dubbo.rpc.ExporterListener; +import org.apache.dubbo.rpc.Filter; + +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.client.RestTemplate; + +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; + +import static org.apache.dubbo.common.Constants.DEFAULT_RETRIES; + +/** + * {@link DubboTransported @DubboTransported} annotation indicates that the traditional Spring Cloud Service-to-Service call is transported + * by Dubbo under the hood, there are two main scenarios: + *
    + *
  1. {@link FeignClient @FeignClient} annotated classes: + *
      + * If {@link DubboTransported @DubboTransported} annotated classes, the invocation of all methods of + * {@link FeignClient @FeignClient} annotated classes. + *
    + *
      + * If {@link DubboTransported @DubboTransported} annotated methods of {@link FeignClient @FeignClient} annotated classes. + *
    + *
  2. + *
  3. {@link LoadBalanced @LoadBalanced} {@link RestTemplate} annotated field, method and parameters
  4. + *
+ *

+ * + * @see FeignClient + * @see LoadBalanced + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Documented +public @interface DubboTransported { + + /** + * The protocol of Dubbo transport whose value could be used the placeholder "dubbo.transport.protocol" + * + * @return the default protocol is "dubbo" + */ + String protocol() default "${dubbo.transport.protocol:dubbo}"; + + /** + * The cluster of Dubbo transport whose value could be used the placeholder "dubbo.transport.cluster" + * + * @return the default cluster is "failover" + */ + String cluster() default "${dubbo.transport.cluster:failover}"; + + /** + * Whether to reconnect if connection is lost, if not specify, reconnect is enabled by default, and the interval + * for retry connecting is 2000 ms + * + * @see Constants#DEFAULT_RECONNECT_PERIOD + * @see Reference#reconnect() + */ + String reconnect() default "${dubbo.transport.reconnect:2000}"; + + /** + * Maximum connections service provider can accept, default value is 0 - connection is shared + * + * @see Reference#connections() + */ + int connections() default 0; + + /** + * Service invocation retry times + * + * @see Constants#DEFAULT_RETRIES + * @see Reference#retries() + */ + int retries() default DEFAULT_RETRIES; + + /** + * Load balance strategy, legal values include: random, roundrobin, leastactive + * + * @see Constants#DEFAULT_LOADBALANCE + * @see Reference#loadbalance() + */ + String loadbalance() default "${dubbo.transport.loadbalance:}"; + + /** + * Maximum active requests allowed, default value is 0 + * + * @see Reference#actives() + */ + int actives() default 0; + + /** + * Timeout value for service invocation, default value is 0 + * + * @see Reference#timeout() + */ + int timeout() default 0; + + /** + * Specify cache implementation for service invocation, legal values include: lru, threadlocal, jcache + * + * @see Reference#cache() + */ + String cache() default "${dubbo.transport.cache:}"; + + /** + * Filters for service invocation + * + * @see Filter + * @see Reference#filter() + */ + String[] filter() default {}; + + /** + * Listeners for service exporting and unexporting + * + * @see ExporterListener + * @see Reference#listener() + */ + String[] listener() default {}; + + /** + * Customized parameter key-value pair, for example: {key1, value1, key2, value2} + * + * @see Reference#parameters() + */ + String[] parameters() default {}; +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboLoadBalancedRestTemplateAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboLoadBalancedRestTemplateAutoConfiguration.java new file mode 100644 index 00000000..5447c2b2 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboLoadBalancedRestTemplateAutoConfiguration.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import com.alibaba.cloud.dubbo.client.loadbalancer.DubboMetadataInitializerInterceptor; +import com.alibaba.cloud.dubbo.client.loadbalancer.DubboTransporterInterceptor; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.metadata.resolver.DubboTransportedAttributesResolver; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.RestTemplateCustomizer; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.core.type.MethodMetadata; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Dubbo Auto-{@link Configuration} for {@link LoadBalanced @LoadBalanced} {@link RestTemplate} + * + * @author Mercy + */ +@Configuration +@ConditionalOnClass(name = {"org.springframework.web.client.RestTemplate"}) +@AutoConfigureAfter(name = {"org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration"}) +public class DubboLoadBalancedRestTemplateAutoConfiguration implements BeanClassLoaderAware, SmartInitializingSingleton { + + private static final Class DUBBO_TRANSPORTED_CLASS = DubboTransported.class; + + private static final String DUBBO_TRANSPORTED_CLASS_NAME = DUBBO_TRANSPORTED_CLASS.getName(); + + @Autowired + private DubboServiceMetadataRepository repository; + + @Autowired(required = false) + private LoadBalancerInterceptor loadBalancerInterceptor; + + @Autowired(required = false) + private RetryLoadBalancerInterceptor retryLoadBalancerInterceptor; + + @Autowired + private ConfigurableListableBeanFactory beanFactory; + + @Autowired + private DubboGenericServiceFactory serviceFactory; + + @Autowired + private DubboGenericServiceExecutionContextFactory contextFactory; + + @Autowired + private Environment environment; + + @LoadBalanced + @Autowired(required = false) + private Map restTemplates = Collections.emptyMap(); + + private ClassLoader classLoader; + + /** + * The {@link ClientHttpRequestInterceptor} bean that may be {@link LoadBalancerInterceptor} or {@link RetryLoadBalancerInterceptor} + */ + private ClientHttpRequestInterceptor loadBalancerInterceptorBean; + + @Override + public void afterSingletonsInstantiated() { + loadBalancerInterceptorBean = retryLoadBalancerInterceptor != null ? + retryLoadBalancerInterceptor : + loadBalancerInterceptor; + } + + /** + * Adapt the {@link RestTemplate} beans that are annotated {@link LoadBalanced @LoadBalanced} and + * {@link LoadBalanced @LoadBalanced} when Spring Boot application started + * (after the callback of {@link SmartInitializingSingleton} beans or + * {@link RestTemplateCustomizer#customize(RestTemplate) customization}) + */ + @EventListener(ApplicationStartedEvent.class) + public void adaptRestTemplates() { + + DubboTransportedAttributesResolver attributesResolver = new DubboTransportedAttributesResolver(environment); + + for (Map.Entry entry : restTemplates.entrySet()) { + String beanName = entry.getKey(); + Map dubboTranslatedAttributes = getDubboTranslatedAttributes(beanName, attributesResolver); + if (!CollectionUtils.isEmpty(dubboTranslatedAttributes)) { + adaptRestTemplate(entry.getValue(), dubboTranslatedAttributes); + } + } + } + + /** + * Gets the annotation attributes {@link RestTemplate} bean being annotated + * {@link DubboTransported @DubboTransported} + * + * @param beanName the bean name of {@link LoadBalanced @LoadBalanced} {@link RestTemplate} + * @param attributesResolver {@link DubboTransportedAttributesResolver} + * @return non-null {@link Map} + */ + private Map getDubboTranslatedAttributes(String beanName, + DubboTransportedAttributesResolver attributesResolver) { + Map attributes = Collections.emptyMap(); + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + if (beanDefinition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition; + MethodMetadata factoryMethodMetadata = annotatedBeanDefinition.getFactoryMethodMetadata(); + attributes = factoryMethodMetadata != null ? + factoryMethodMetadata.getAnnotationAttributes(DUBBO_TRANSPORTED_CLASS_NAME) : Collections.emptyMap(); + } + return attributesResolver.resolve(attributes); + } + + + /** + * Adapt the instance of {@link DubboTransporterInterceptor} to the {@link LoadBalancerInterceptor} Bean. + * + * @param restTemplate {@link LoadBalanced @LoadBalanced} {@link RestTemplate} Bean + * @param dubboTranslatedAttributes the annotation dubboTranslatedAttributes {@link RestTemplate} bean being annotated + * {@link DubboTransported @DubboTransported} + */ + private void adaptRestTemplate(RestTemplate restTemplate, Map dubboTranslatedAttributes) { + + List interceptors = new ArrayList<>(restTemplate.getInterceptors()); + + int index = loadBalancerInterceptorBean == null ? -1 : interceptors.indexOf(loadBalancerInterceptorBean); + + index = index < 0 ? 0 : index; + + // Add ClientHttpRequestInterceptor instances before loadBalancerInterceptor + interceptors.add(index++, new DubboMetadataInitializerInterceptor(repository)); + + interceptors.add(index++, new DubboTransporterInterceptor(repository, restTemplate.getMessageConverters(), + classLoader, dubboTranslatedAttributes, serviceFactory, contextFactory)); + + restTemplate.setInterceptors(interceptors); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboMetadataAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboMetadataAutoConfiguration.java new file mode 100644 index 00000000..57a58176 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboMetadataAutoConfiguration.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import org.apache.dubbo.config.ProtocolConfig; +import org.apache.dubbo.config.spring.ServiceBean; +import org.apache.dubbo.config.spring.context.event.ServiceBeanExportedEvent; + +import feign.Contract; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import com.alibaba.cloud.dubbo.metadata.DubboProtocolConfigSupplier; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.metadata.resolver.DubboServiceBeanMetadataResolver; +import com.alibaba.cloud.dubbo.metadata.resolver.MetadataResolver; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceExporter; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy; +import com.alibaba.cloud.dubbo.service.IntrospectiveDubboMetadataService; +import com.alibaba.cloud.dubbo.util.JSONUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * Spring Boot Auto-Configuration class for Dubbo Metadata + * + * @author Mercy + */ +@Configuration +@Import({DubboServiceMetadataRepository.class, + IntrospectiveDubboMetadataService.class, + DubboMetadataServiceExporter.class, + JSONUtils.class}) +public class DubboMetadataAutoConfiguration { + + @Autowired + private ObjectProvider dubboServiceMetadataRepository; + + @Autowired + private MetadataResolver metadataResolver; + + @Autowired + private DubboMetadataServiceExporter dubboMetadataConfigServiceExporter; + + @Bean + @ConditionalOnMissingBean + public MetadataResolver metadataJsonResolver(ObjectProvider contract) { + return new DubboServiceBeanMetadataResolver(contract); + } + + @Bean + public Supplier dubboProtocolConfigSupplier(ObjectProvider> protocols) { + return new DubboProtocolConfigSupplier(protocols); + } + + @Bean + @ConditionalOnMissingBean + public DubboMetadataServiceProxy dubboMetadataConfigServiceProxy(DubboGenericServiceFactory factory) { + return new DubboMetadataServiceProxy(factory); + } + + // Event-Handling + + @EventListener(ServiceBeanExportedEvent.class) + public void onServiceBeanExported(ServiceBeanExportedEvent event) { + ServiceBean serviceBean = event.getServiceBean(); + publishServiceRestMetadata(serviceBean); + } + + @EventListener(ApplicationFailedEvent.class) + public void onApplicationFailed() { + unExportDubboMetadataConfigService(); + } + + @EventListener(ContextClosedEvent.class) + public void onContextClosed() { + unExportDubboMetadataConfigService(); + } + + private void publishServiceRestMetadata(ServiceBean serviceBean) { + dubboServiceMetadataRepository.getIfAvailable().publishServiceRestMetadata(metadataResolver.resolveServiceRestMetadata(serviceBean)); + } + + private void unExportDubboMetadataConfigService() { + dubboMetadataConfigServiceExporter.unexport(); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboOpenFeignAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboOpenFeignAutoConfiguration.java new file mode 100644 index 00000000..9f956e05 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboOpenFeignAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import com.alibaba.cloud.dubbo.openfeign.TargeterBeanPostProcessor; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import static com.alibaba.cloud.dubbo.autoconfigure.DubboOpenFeignAutoConfiguration.TARGETER_CLASS_NAME; + + +/** + * Dubbo Feign Auto-{@link Configuration Configuration} + * + * @author Mercy + */ +@ConditionalOnClass(name = {"feign.Feign", TARGETER_CLASS_NAME}) +@AutoConfigureAfter(name = {"org.springframework.cloud.openfeign.FeignAutoConfiguration"}) +@Configuration +public class DubboOpenFeignAutoConfiguration { + + public static final String TARGETER_CLASS_NAME = "org.springframework.cloud.openfeign.Targeter"; + + @Bean + public TargeterBeanPostProcessor targeterBeanPostProcessor(Environment environment, + DubboServiceMetadataRepository dubboServiceMetadataRepository, + DubboGenericServiceFactory dubboGenericServiceFactory, + DubboGenericServiceExecutionContextFactory contextFactory) { + return new TargeterBeanPostProcessor(environment, dubboServiceMetadataRepository, + dubboGenericServiceFactory, contextFactory); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceAutoConfiguration.java new file mode 100644 index 00000000..00af57ae --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceAutoConfiguration.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; +import com.alibaba.cloud.dubbo.service.parameter.PathVariableServiceParameterResolver; +import com.alibaba.cloud.dubbo.service.parameter.RequestBodyServiceParameterResolver; +import com.alibaba.cloud.dubbo.service.parameter.RequestHeaderServiceParameterResolver; +import com.alibaba.cloud.dubbo.service.parameter.RequestParamServiceParameterResolver; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import com.alibaba.cloud.dubbo.env.DubboCloudProperties; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; + +/** + * Spring Boot Auto-Configuration class for Dubbo Service + * + * @author Mercy + */ +@Configuration +@EnableConfigurationProperties(DubboCloudProperties.class) +public class DubboServiceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public DubboGenericServiceFactory dubboGenericServiceFactory() { + return new DubboGenericServiceFactory(); + } + + @Configuration + @Import(value = { + DubboGenericServiceExecutionContextFactory.class, + RequestParamServiceParameterResolver.class, + RequestBodyServiceParameterResolver.class, + RequestHeaderServiceParameterResolver.class, + PathVariableServiceParameterResolver.class + }) + static class ParameterResolversConfiguration { + } + + /** + * Build a primary {@link PropertyResolver} bean to {@link Autowired @Autowired} + * + * @param environment {@link Environment} + * @return alias bean for {@link Environment} + */ + @Bean + @Primary + public PropertyResolver primaryPropertyResolver(Environment environment) { + return environment; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfiguration.java new file mode 100644 index 00000000..d21e841f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfiguration.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.config.spring.ServiceBean; + +import com.ecwid.consul.v1.agent.model.NewService; +import com.netflix.appinfo.InstanceInfo; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import com.alibaba.cloud.dubbo.autoconfigure.condition.MissingSpringCloudRegistryConfigPropertyCondition; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.registry.DubboServiceRegistrationEventPublishingAspect; +import com.alibaba.cloud.dubbo.registry.event.ServiceInstancePreRegisteredEvent; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.consul.serviceregistry.ConsulRegistration; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.event.EventListener; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration.CONSUL_AUTO_CONFIGURATION_CLASS_NAME; +import static com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration.EUREKA_AUTO_CONFIGURATION_CLASS_NAME; +import static com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory.ADDRESS; +import static com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory.PROTOCOL; +import static org.springframework.util.ObjectUtils.isEmpty; + +/** + * Dubbo Service Registration Auto-{@link Configuration} + * + * @author Mercy + */ +@Configuration +@Import({DubboServiceRegistrationEventPublishingAspect.class}) +@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) +@AutoConfigureAfter(name = { + EUREKA_AUTO_CONFIGURATION_CLASS_NAME, + CONSUL_AUTO_CONFIGURATION_CLASS_NAME, + "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration" +}, value = { + DubboMetadataAutoConfiguration.class +}) +public class DubboServiceRegistrationAutoConfiguration { + + public static final String EUREKA_AUTO_CONFIGURATION_CLASS_NAME = + "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"; + + public static final String CONSUL_AUTO_CONFIGURATION_CLASS_NAME = + "org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationAutoConfiguration"; + + public static final String CONSUL_AUTO_REGISTRATION_CLASS_NAME = + "org.springframework.cloud.consul.serviceregistry.ConsulAutoRegistration"; + + public static final String ZOOKEEPER_AUTO_CONFIGURATION_CLASS_NAME = + "org.springframework.cloud.zookeeper.serviceregistry.ZookeeperAutoServiceRegistrationAutoConfiguration"; + + private static final Logger logger = LoggerFactory.getLogger(DubboServiceRegistrationAutoConfiguration.class); + + @Autowired + private DubboServiceMetadataRepository dubboServiceMetadataRepository; + + @Bean + @Conditional(value = { + MissingSpringCloudRegistryConfigPropertyCondition.class + }) + public RegistryConfig defaultSpringCloudRegistryConfig() { + return new RegistryConfig(ADDRESS, PROTOCOL); + } + + @EventListener(ServiceInstancePreRegisteredEvent.class) + public void onServiceInstancePreRegistered(ServiceInstancePreRegisteredEvent event) { + Registration registration = event.getSource(); + attachDubboMetadataServiceMetadata(registration); + } + + @Configuration + @ConditionalOnBean(name = EUREKA_AUTO_CONFIGURATION_CLASS_NAME) + @Aspect + class EurekaConfiguration implements SmartInitializingSingleton { + + @Autowired + private ObjectProvider> serviceBeans; + + @EventListener(ServiceInstancePreRegisteredEvent.class) + public void onServiceInstancePreRegistered(ServiceInstancePreRegisteredEvent event) { + Registration registration = event.getSource(); + EurekaRegistration eurekaRegistration = EurekaRegistration.class.cast(registration); + InstanceInfo instanceInfo = eurekaRegistration.getApplicationInfoManager().getInfo(); + attachDubboMetadataServiceMetadata(instanceInfo.getMetadata()); + } + + /** + * {@link EurekaServiceRegistry} will register current {@link ServiceInstance service instance} on + * {@link EurekaAutoServiceRegistration#start()} execution(in {@link SmartLifecycle#start() start phase}), + * thus this method must {@link ServiceBean#export() export} all {@link ServiceBean ServiceBeans} in advance. + */ + @Override + public void afterSingletonsInstantiated() { + Collection serviceBeans = this.serviceBeans.getIfAvailable(); + if (!isEmpty(serviceBeans)) { + serviceBeans.forEach(ServiceBean::export); + } + } + } + + @Configuration + @ConditionalOnBean(name = CONSUL_AUTO_CONFIGURATION_CLASS_NAME) + @AutoConfigureOrder + class ConsulConfiguration { + + /** + * Handle the pre-registered event of {@link ServiceInstance} for Consul + * + * @param event {@link ServiceInstancePreRegisteredEvent} + */ + @EventListener(ServiceInstancePreRegisteredEvent.class) + public void onServiceInstancePreRegistered(ServiceInstancePreRegisteredEvent event) { + Registration registration = event.getSource(); + Class registrationClass = AopUtils.getTargetClass(registration); + String registrationClassName = registrationClass.getName(); + if (CONSUL_AUTO_REGISTRATION_CLASS_NAME.equalsIgnoreCase(registrationClassName)) { + ConsulRegistration consulRegistration = (ConsulRegistration) registration; + attachURLsIntoMetadata(consulRegistration); + } + } + + private void attachURLsIntoMetadata(ConsulRegistration consulRegistration) { + NewService newService = consulRegistration.getService(); + Map serviceMetadata = dubboServiceMetadataRepository.getDubboMetadataServiceMetadata(); + if (!isEmpty(serviceMetadata)) { + List tags = newService.getTags(); + for (Map.Entry entry : serviceMetadata.entrySet()) { + tags.add(entry.getKey() + "=" + entry.getValue()); + } + } + } + } + + private void attachDubboMetadataServiceMetadata(Registration registration) { + if (registration == null) { + return; + } + synchronized (registration) { + Map metadata = registration.getMetadata(); + attachDubboMetadataServiceMetadata(metadata); + } + } + + private void attachDubboMetadataServiceMetadata(Map metadata) { + Map serviceMetadata = dubboServiceMetadataRepository.getDubboMetadataServiceMetadata(); + if (!isEmpty(serviceMetadata)) { + metadata.putAll(serviceMetadata); + } + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationNonWebApplicationAutoConfiguration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationNonWebApplicationAutoConfiguration.java new file mode 100644 index 00000000..6247a520 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationNonWebApplicationAutoConfiguration.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.spring.ServiceBean; + +import com.ecwid.consul.v1.agent.model.NewService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.SmartInitializingSingleton; +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.ConditionalOnNotWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.registry.event.ServiceInstancePreRegisteredEvent; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.cloud.consul.serviceregistry.ConsulAutoRegistration; +import org.springframework.cloud.consul.serviceregistry.ConsulRegistration; +import org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +import java.util.List; + +import static com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration.CONSUL_AUTO_CONFIGURATION_CLASS_NAME; +import static com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration.ZOOKEEPER_AUTO_CONFIGURATION_CLASS_NAME; + +/** + * Dubbo Service Registration Auto-{@link Configuration} for Non-Web application + * + * @author Mercy + */ +@Configuration +@ConditionalOnNotWebApplication +@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) +@AutoConfigureAfter(DubboServiceRegistrationAutoConfiguration.class) +@Aspect +public class DubboServiceRegistrationNonWebApplicationAutoConfiguration { + + private static final String REST_PROTOCOL = "rest"; + + @Autowired + private ServiceRegistry serviceRegistry; + + @Autowired + private Registration registration; + + private volatile Integer serverPort = null; + + private volatile boolean registered = false; + + @Autowired + private DubboServiceMetadataRepository repository; + + @Around("execution(* org.springframework.cloud.client.serviceregistry.Registration.getPort())") + public Object getPort(ProceedingJoinPoint pjp) throws Throwable { + return serverPort != null ? serverPort : pjp.proceed(); + } + + @EventListener(ApplicationStartedEvent.class) + public void onApplicationStarted() { + setServerPort(); + register(); + } + + private void register() { + if (registered) { + return; + } + serviceRegistry.register(registration); + registered = true; + } + + /** + * Set web port from {@link ServiceBean#getExportedUrls() exported URLs} if "rest" protocol is present. + */ + private void setServerPort() { + if (serverPort == null) { + for (List urls : repository.getAllExportedUrls().values()) { + urls.stream() + .filter(url -> REST_PROTOCOL.equalsIgnoreCase(url.getProtocol())) + .findFirst() + .ifPresent(url -> { + serverPort = url.getPort(); + }); + + // If REST protocol is not present, use any applied port. + if (serverPort == null) { + urls.stream() + .findAny().ifPresent(url -> { + serverPort = url.getPort(); + }); + } + } + } + } + + @Configuration + @ConditionalOnBean(name = ZOOKEEPER_AUTO_CONFIGURATION_CLASS_NAME) + class ZookeeperConfiguration implements SmartInitializingSingleton { + + @Autowired + private ServiceInstanceRegistration registration; + + @EventListener(ServiceInstancePreRegisteredEvent.class) + public void onServiceInstancePreRegistered(ServiceInstancePreRegisteredEvent event) { + registration.setPort(serverPort); + } + + @Override + public void afterSingletonsInstantiated() { + // invoke getServiceInstance() method to trigger the ServiceInstance building before register + registration.getServiceInstance(); + } + } + + @Configuration + @ConditionalOnBean(name = CONSUL_AUTO_CONFIGURATION_CLASS_NAME) + class ConsulConfiguration { + + /** + * Handle the pre-registered event of {@link ServiceInstance} for Consul + * + * @param event {@link ServiceInstancePreRegisteredEvent} + */ + @EventListener(ServiceInstancePreRegisteredEvent.class) + public void onServiceInstancePreRegistered(ServiceInstancePreRegisteredEvent event) { + Registration registration = event.getSource(); + ConsulAutoRegistration consulRegistration = (ConsulAutoRegistration) registration; + setPort(consulRegistration); + } + + /** + * Set port on Non-Web Application + * + * @param consulRegistration {@link ConsulRegistration} + */ + private void setPort(ConsulAutoRegistration consulRegistration) { + int port = consulRegistration.getPort(); + NewService newService = consulRegistration.getService(); + if (newService.getPort() == null) { + newService.setPort(port); + } + } + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/condition/MissingSpringCloudRegistryConfigPropertyCondition.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/condition/MissingSpringCloudRegistryConfigPropertyCondition.java new file mode 100644 index 00000000..66429440 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/autoconfigure/condition/MissingSpringCloudRegistryConfigPropertyCondition.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure.condition; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import com.alibaba.cloud.dubbo.registry.SpringCloudRegistry; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +import java.util.Map; + +import static org.apache.dubbo.config.spring.util.PropertySourcesUtils.getSubProperties; +import static com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory.PROTOCOL; + +/** + * Missing {@link SpringCloudRegistry} Property {@link Condition} + * + * @see SpringCloudRegistry + * @see Condition + */ +public class MissingSpringCloudRegistryConfigPropertyCondition extends SpringBootCondition { + + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConfigurableEnvironment environment = (ConfigurableEnvironment) context.getEnvironment(); + + String protocol = environment.getProperty("dubbo.registry.protocol"); + + if (PROTOCOL.equals(protocol)) { + return ConditionOutcome.noMatch("'spring-cloud' protocol was found from 'dubbo.registry.protocol'"); + } + + String address = environment.getProperty("dubbo.registry.address"); + + if (StringUtils.startsWithIgnoreCase(address, PROTOCOL)) { + return ConditionOutcome.noMatch("'spring-cloud' protocol was found from 'dubbo.registry.address'"); + } + + Map properties = getSubProperties(environment, "dubbo.registries."); + + boolean found = properties.entrySet().stream().anyMatch(entry -> { + String key = entry.getKey(); + String value = String.valueOf(entry.getValue()); + return (key.endsWith(".address") && value.startsWith(PROTOCOL)) || + (key.endsWith(".protocol") && PROTOCOL.equals(value)); + + }); + + return found ? ConditionOutcome.noMatch("'spring-cloud' protocol was found in 'dubbo.registries.*'") : ConditionOutcome.match(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponse.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponse.java new file mode 100644 index 00000000..c1458ed6 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponse.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.client.loadbalancer; + +import org.apache.dubbo.rpc.service.GenericException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Dubbo {@link ClientHttpResponse} implementation + * + * @author Mercy + * @see DubboTransporterInterceptor + */ +class DubboClientHttpResponse implements ClientHttpResponse { + + private final HttpStatus httpStatus; + + private final String statusText; + + private final HttpHeaders httpHeaders = new HttpHeaders(); + + private final DubboHttpOutputMessage httpOutputMessage; + + public DubboClientHttpResponse(DubboHttpOutputMessage httpOutputMessage, GenericException exception) { + this.httpStatus = exception != null ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK; + this.statusText = exception != null ? exception.getExceptionMessage() : httpStatus.getReasonPhrase(); + this.httpOutputMessage = httpOutputMessage; + this.httpHeaders.putAll(httpOutputMessage.getHeaders()); + } + + @Override + public HttpStatus getStatusCode() throws IOException { + return httpStatus; + } + + @Override + public int getRawStatusCode() throws IOException { + return httpStatus.value(); + } + + @Override + public String getStatusText() throws IOException { + return statusText; + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + return httpOutputMessage.getBody().getInputStream(); + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponseFactory.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponseFactory.java new file mode 100644 index 00000000..c503ac91 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboClientHttpResponseFactory.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.client.loadbalancer; + +import org.apache.dubbo.rpc.service.GenericException; +import com.alibaba.cloud.dubbo.http.converter.HttpMessageConverterHolder; +import com.alibaba.cloud.dubbo.http.util.HttpMessageConverterResolver; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; + +import java.io.IOException; +import java.util.List; + +/** + * Dubbo {@link ClientHttpResponse} Factory + * + * @author Mercy + */ +class DubboClientHttpResponseFactory { + + private final HttpMessageConverterResolver httpMessageConverterResolver; + + public DubboClientHttpResponseFactory(List> messageConverters, ClassLoader classLoader) { + this.httpMessageConverterResolver = new HttpMessageConverterResolver(messageConverters, classLoader); + } + + public ClientHttpResponse build(Object result, GenericException exception, + RequestMetadata requestMetadata, RestMethodMetadata restMethodMetadata) { + + DubboHttpOutputMessage httpOutputMessage = new DubboHttpOutputMessage(); + + HttpMessageConverterHolder httpMessageConverterHolder = httpMessageConverterResolver.resolve(requestMetadata, restMethodMetadata); + + if (httpMessageConverterHolder != null) { + MediaType mediaType = httpMessageConverterHolder.getMediaType(); + HttpMessageConverter converter = httpMessageConverterHolder.getConverter(); + try { + converter.write(result, mediaType, httpOutputMessage); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return new DubboClientHttpResponse(httpOutputMessage, exception); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboHttpOutputMessage.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboHttpOutputMessage.java new file mode 100644 index 00000000..63457902 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboHttpOutputMessage.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.client.loadbalancer; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.springframework.util.FastByteArrayOutputStream; + +import java.io.IOException; + +/** + * Dubbo {@link HttpOutputMessage} implementation + * + * @author Mercy + */ +class DubboHttpOutputMessage implements HttpOutputMessage { + + private final FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream(); + + private final HttpHeaders httpHeaders = new HttpHeaders(); + + @Override + public FastByteArrayOutputStream getBody() throws IOException { + return outputStream; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboMetadataInitializerInterceptor.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboMetadataInitializerInterceptor.java new file mode 100644 index 00000000..7abf5143 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboMetadataInitializerInterceptor.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.client.loadbalancer; + +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.net.URI; + +/** + * Dubbo Metadata {@link ClientHttpRequestInterceptor} Initializing Interceptor executes intercept before + * {@link DubboTransporterInterceptor} + * + * @author Mercy + */ +public class DubboMetadataInitializerInterceptor implements ClientHttpRequestInterceptor { + + private final DubboServiceMetadataRepository repository; + + public DubboMetadataInitializerInterceptor(DubboServiceMetadataRepository repository) { + this.repository = repository; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + + URI originalUri = request.getURI(); + + String serviceName = originalUri.getHost(); + + repository.initialize(serviceName); + + // Execute next + return execution.execute(request, body); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboTransporterInterceptor.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboTransporterInterceptor.java new file mode 100644 index 00000000..bec422ed --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/client/loadbalancer/DubboTransporterInterceptor.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.client.loadbalancer; + +import org.apache.dubbo.rpc.service.GenericException; +import org.apache.dubbo.rpc.service.GenericService; + +import com.alibaba.cloud.dubbo.http.MutableHttpServerRequest; +import com.alibaba.cloud.dubbo.metadata.DubboRestServiceMetadata; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContext; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.util.PathMatcher; +import org.springframework.web.util.UriComponents; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.springframework.web.util.UriComponentsBuilder.fromUri; + +/** + * Dubbo Transporter {@link ClientHttpRequestInterceptor} implementation + * + * @author Mercy + * @see LoadBalancerInterceptor + */ +public class DubboTransporterInterceptor implements ClientHttpRequestInterceptor { + + private final DubboServiceMetadataRepository repository; + + private final DubboClientHttpResponseFactory clientHttpResponseFactory; + + private final Map dubboTranslatedAttributes; + + private final DubboGenericServiceFactory serviceFactory; + + private final DubboGenericServiceExecutionContextFactory contextFactory; + + private final PathMatcher pathMatcher = new AntPathMatcher(); + + public DubboTransporterInterceptor(DubboServiceMetadataRepository dubboServiceMetadataRepository, + List> messageConverters, + ClassLoader classLoader, + Map dubboTranslatedAttributes, + DubboGenericServiceFactory serviceFactory, + DubboGenericServiceExecutionContextFactory contextFactory) { + this.repository = dubboServiceMetadataRepository; + this.dubboTranslatedAttributes = dubboTranslatedAttributes; + this.clientHttpResponseFactory = new DubboClientHttpResponseFactory(messageConverters, classLoader); + this.serviceFactory = serviceFactory; + this.contextFactory = contextFactory; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + + URI originalUri = request.getURI(); + + String serviceName = originalUri.getHost(); + + RequestMetadata clientMetadata = buildRequestMetadata(request); + + DubboRestServiceMetadata metadata = repository.get(serviceName, clientMetadata); + + if (metadata == null) { + // if DubboServiceMetadata is not found, executes next + return execution.execute(request, body); + } + + RestMethodMetadata dubboRestMethodMetadata = metadata.getRestMethodMetadata(); + + GenericService genericService = serviceFactory.create(metadata, dubboTranslatedAttributes); + + MutableHttpServerRequest httpServerRequest = new MutableHttpServerRequest(request, body); + + customizeRequest(httpServerRequest, dubboRestMethodMetadata, clientMetadata); + + DubboGenericServiceExecutionContext context = contextFactory.create(dubboRestMethodMetadata, httpServerRequest); + + Object result = null; + GenericException exception = null; + + try { + result = genericService.$invoke(context.getMethodName(), context.getParameterTypes(), context.getParameters()); + } catch (GenericException e) { + exception = e; + } + + return clientHttpResponseFactory.build(result, exception, clientMetadata, dubboRestMethodMetadata); + } + + protected void customizeRequest(MutableHttpServerRequest httpServerRequest, + RestMethodMetadata dubboRestMethodMetadata, RequestMetadata clientMetadata) { + + RequestMetadata dubboRequestMetadata = dubboRestMethodMetadata.getRequest(); + String pathPattern = dubboRequestMetadata.getPath(); + + Map pathVariables = pathMatcher.extractUriTemplateVariables(pathPattern, httpServerRequest.getPath()); + + if (!CollectionUtils.isEmpty(pathVariables)) { + // Put path variables Map into query parameters Map + httpServerRequest.params(pathVariables); + } + + } + + private RequestMetadata buildRequestMetadata(HttpRequest request) { + UriComponents uriComponents = fromUri(request.getURI()).build(true); + RequestMetadata requestMetadata = new RequestMetadata(); + requestMetadata.setPath(uriComponents.getPath()); + requestMetadata.setMethod(request.getMethod().name()); + requestMetadata.setParams(uriComponents.getQueryParams()); + requestMetadata.setHeaders(request.getHeaders()); + return requestMetadata; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/context/DubboServiceRegistrationApplicationContextInitializer.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/context/DubboServiceRegistrationApplicationContextInitializer.java new file mode 100644 index 00000000..83dd9831 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/context/DubboServiceRegistrationApplicationContextInitializer.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.context; + +import com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * The Dubbo services will be registered as the specified Spring cloud applications that will not be considered + * normal ones, but only are used to Dubbo's service discovery even if it is based on Spring Cloud Commons abstraction. + * However, current application will be registered by other DiscoveryClientAutoConfiguration. + * + * @author Mercy + */ +public class DubboServiceRegistrationApplicationContextInitializer implements + ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + // Set ApplicationContext into SpringCloudRegistryFactory before Dubbo Service Register + SpringCloudRegistryFactory.setApplicationContext(applicationContext); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboCloudProperties.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboCloudProperties.java new file mode 100644 index 00000000..dc349e58 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboCloudProperties.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.env; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.springframework.util.StringUtils.commaDelimitedListToStringArray; +import static org.springframework.util.StringUtils.hasText; +import static org.springframework.util.StringUtils.trimAllWhitespace; + +/** + * Dubbo Cloud {@link ConfigurationProperties Properties} + * + * @author Mercy + */ +@ConfigurationProperties(prefix = "dubbo.cloud") +public class DubboCloudProperties { + + /** + * All services of Dubbo + */ + public static final String ALL_DUBBO_SERVICES = "*"; + + /** + * The subscribed services, the default value is "*". The multiple value will use comma(",") as the separator. + * + * @see #ALL_DUBBO_SERVICES + */ + private String subscribedServices = ALL_DUBBO_SERVICES; + + public String getSubscribedServices() { + return subscribedServices; + } + + public void setSubscribedServices(String subscribedServices) { + this.subscribedServices = subscribedServices; + } + + /** + * Get the subscribed services as a {@link Set} with configuration order. + * + * @return non-null Read-only {@link Set} + */ + public Set subscribedServices() { + + String[] services = commaDelimitedListToStringArray(getSubscribedServices()); + + if (services.length < 1) { + return Collections.emptySet(); + } + + Set subscribedServices = new LinkedHashSet<>(); + + for (String service : services) { + if (hasText(service)) { // filter blank service name + // remove all whitespace + subscribedServices.add(trimAllWhitespace(service)); + } + } + + return Collections.unmodifiableSet(subscribedServices); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboNonWebApplicationEnvironmentPostProcessor.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboNonWebApplicationEnvironmentPostProcessor.java new file mode 100644 index 00000000..b7261e74 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/env/DubboNonWebApplicationEnvironmentPostProcessor.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.env; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.apache.dubbo.common.Constants.DEFAULT_PROTOCOL; +import static org.apache.dubbo.config.spring.util.PropertySourcesUtils.getSubProperties; + +/** + * Dubbo {@link WebApplicationType#NONE Non-Web Application} {@link EnvironmentPostProcessor} + * + * @author Mercy + */ +public class DubboNonWebApplicationEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + private static final String DOT = "."; + + /** + * The name of default {@link PropertySource} defined in SpringApplication#configurePropertySources method. + */ + private static final String PROPERTY_SOURCE_NAME = "defaultProperties"; + + private static final String SERVER_PORT_PROPERTY_NAME = "server.port"; + + private static final String PORT_PROPERTY_NAME = "port"; + + private static final String PROTOCOL_PROPERTY_NAME_PREFIX = "dubbo.protocol"; + + private static final String PROTOCOL_NAME_PROPERTY_NAME_SUFFIX = DOT + "name"; + + private static final String PROTOCOL_PORT_PROPERTY_NAME_SUFFIX = DOT + PORT_PROPERTY_NAME; + + private static final String PROTOCOL_PORT_PROPERTY_NAME = PROTOCOL_PROPERTY_NAME_PREFIX + PROTOCOL_PORT_PROPERTY_NAME_SUFFIX; + + private static final String PROTOCOL_NAME_PROPERTY_NAME = PROTOCOL_PROPERTY_NAME_PREFIX + PROTOCOL_NAME_PROPERTY_NAME_SUFFIX; + + private static final String PROTOCOLS_PROPERTY_NAME_PREFIX = "dubbo.protocols"; + + private static final String REST_PROTOCOL = "rest"; + + private final Logger logger = LoggerFactory.getLogger(DubboNonWebApplicationEnvironmentPostProcessor.class); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + WebApplicationType webApplicationType = application.getWebApplicationType(); + + if (!WebApplicationType.NONE.equals(webApplicationType)) { // Just works in Non-Web Application + if (logger.isDebugEnabled()) { + logger.debug("Current application is a Web Application, the process will be ignored."); + } + return; + } + + MutablePropertySources propertySources = environment.getPropertySources(); + Map defaultProperties = createDefaultProperties(environment); + if (!CollectionUtils.isEmpty(defaultProperties)) { + addOrReplace(propertySources, defaultProperties); + } + } + + private Map createDefaultProperties(ConfigurableEnvironment environment) { + Map defaultProperties = new HashMap(); + resetServerPort(environment, defaultProperties); + return defaultProperties; + } + + /** + * Reset server port property if it's absent, whose value is configured by "dubbbo.protocol.port" + * or "dubbo.protcols.rest.port" + * + * @param environment + * @param defaultProperties + */ + private void resetServerPort(ConfigurableEnvironment environment, Map defaultProperties) { + + String serverPort = environment.getProperty(SERVER_PORT_PROPERTY_NAME, environment.getProperty(PORT_PROPERTY_NAME)); + + if (serverPort != null) { + return; + } + + serverPort = getRestPortFromProtocolProperty(environment); + + if (serverPort == null) { + serverPort = getRestPortFromProtocolsProperties(environment); + } + + setServerPort(environment, serverPort, defaultProperties); + } + + private String getRestPortFromProtocolProperty(ConfigurableEnvironment environment) { + + String protocol = environment.getProperty(PROTOCOL_NAME_PROPERTY_NAME, DEFAULT_PROTOCOL); + + return isRestProtocol(protocol) ? + environment.getProperty(PROTOCOL_PORT_PROPERTY_NAME) : + null; + } + + private String getRestPortFromProtocolsProperties(ConfigurableEnvironment environment) { + + String restPort = null; + + Map subProperties = getSubProperties(environment, PROTOCOLS_PROPERTY_NAME_PREFIX); + + Properties properties = new Properties(); + + properties.putAll(subProperties); + + for (String propertyName : properties.stringPropertyNames()) { + if (propertyName.endsWith(PROTOCOL_NAME_PROPERTY_NAME_SUFFIX)) { // protocol name property + String protocol = properties.getProperty(propertyName); + if (isRestProtocol(protocol)) { + String beanName = resolveBeanName(propertyName); + if (StringUtils.hasText(beanName)) { + restPort = properties.getProperty(beanName + PROTOCOL_PORT_PROPERTY_NAME_SUFFIX); + break; + } + } + } + } + + return restPort; + } + + private String resolveBeanName(String propertyName) { + int index = propertyName.indexOf(DOT); + return index > -1 ? propertyName.substring(0, index) : null; + } + + private void setServerPort(ConfigurableEnvironment environment, String serverPort, + Map defaultProperties) { + if (serverPort == null) { + return; + } + + defaultProperties.put(SERVER_PORT_PROPERTY_NAME, serverPort); + + } + + /** + * Copy from BusEnvironmentPostProcessor#addOrReplace(MutablePropertySources, Map) + * + * @param propertySources {@link MutablePropertySources} + * @param map Default Dubbo Properties + */ + private void addOrReplace(MutablePropertySources propertySources, + Map map) { + MapPropertySource target = null; + if (propertySources.contains(PROPERTY_SOURCE_NAME)) { + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + for (String key : map.keySet()) { + if (!target.containsProperty(key)) { + target.getSource().put(key, map.get(key)); + } + } + } + } + if (target == null) { + target = new MapPropertySource(PROPERTY_SOURCE_NAME, map); + } + if (!propertySources.contains(PROPERTY_SOURCE_NAME)) { + propertySources.addLast(target); + } + } + + @Override + public int getOrder() { // Keep LOWEST_PRECEDENCE + return LOWEST_PRECEDENCE; + } + + private static boolean isRestProtocol(String protocol) { + return REST_PROTOCOL.equalsIgnoreCase(protocol); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/ByteArrayHttpInputMessage.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/ByteArrayHttpInputMessage.java new file mode 100644 index 00000000..83d0b1e6 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/ByteArrayHttpInputMessage.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http; + +import org.apache.dubbo.common.io.UnsafeByteArrayInputStream; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Byte array {@link HttpInputMessage} implementation + * + * @author Mercy + */ +class ByteArrayHttpInputMessage implements HttpInputMessage { + + private final HttpHeaders httpHeaders; + + private final InputStream inputStream; + + public ByteArrayHttpInputMessage(byte[] body) { + this(new HttpHeaders(), body); + } + + public ByteArrayHttpInputMessage(HttpHeaders httpHeaders, byte[] body) { + this.httpHeaders = httpHeaders; + this.inputStream = new UnsafeByteArrayInputStream(body); + } + + @Override + public InputStream getBody() throws IOException { + return inputStream; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/DefaultHttpRequest.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/DefaultHttpRequest.java new file mode 100644 index 00000000..228b8340 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/DefaultHttpRequest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.springframework.web.util.UriComponentsBuilder.fromPath; + +/** + * Default {@link HttpRequest} implementation + * + * @author Mercy + */ +public class DefaultHttpRequest implements HttpRequest { + + private final String method; + + private final URI uri; + + private final HttpHeaders headers = new HttpHeaders(); + + public DefaultHttpRequest(String method, String path, Map> params, + Map> headers) { + this.method = method == null ? HttpMethod.GET.name() : method.toUpperCase(); + this.uri = buildURI(path, params); + this.headers.putAll(headers); + } + + private URI buildURI(String path, Map> params) { + UriComponentsBuilder builder = fromPath(path) + .queryParams(new LinkedMultiValueMap<>(params)); + return builder.build().toUri(); + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.resolve(getMethodValue()); + } + + @Override + public String getMethodValue() { + return method; + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * {@link HttpRequest} Builder + */ + public static class Builder { + + String method; + + String path; + + MultiValueMap params = new LinkedMultiValueMap<>(); + + MultiValueMap headers = new LinkedMultiValueMap<>(); + + public Builder method(String method) { + this.method = method; + return this; + } + + public Builder path(String path) { + this.path = path; + return this; + } + + public Builder param(String name, String value) { + this.params.add(name, value); + return this; + } + + public Builder header(String name, String value) { + this.headers.add(name, value); + return this; + } + + public Builder params(Map> params) { + this.params.putAll(params); + return this; + } + + public Builder headers(Map> headers) { + this.headers.putAll(headers); + return this; + } + + public HttpRequest build() { + return new DefaultHttpRequest(method, path, params, headers); + } + } + + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/HttpServerRequest.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/HttpServerRequest.java new file mode 100644 index 00000000..00bcbf40 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/HttpServerRequest.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpRequest; +import org.springframework.util.MultiValueMap; + +/** + * HTTP Server Request + * + * @author Mercy + */ +public interface HttpServerRequest extends HttpRequest, HttpInputMessage { + + /** + * Return a path of current HTTP request + * + * @return + */ + String getPath(); + + /** + * Return a map with parsed and decoded query parameter values. + */ + MultiValueMap getQueryParams(); + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/MutableHttpServerRequest.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/MutableHttpServerRequest.java new file mode 100644 index 00000000..602c685a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/MutableHttpServerRequest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Map; + +import static com.alibaba.cloud.dubbo.http.util.HttpUtils.getParameters; + +/** + * Mutable {@link HttpServerRequest} implementation + * + * @author Mercy + */ +public class MutableHttpServerRequest implements HttpServerRequest { + + private final HttpMethod httpMethod; + + private final URI uri; + + private final String path; + + private final MultiValueMap queryParams; + + private final HttpHeaders httpHeaders; + + private final HttpInputMessage httpInputMessage; + + public MutableHttpServerRequest(HttpRequest httpRequest, byte[] body) { + this.httpMethod = httpRequest.getMethod(); + this.uri = httpRequest.getURI(); + this.path = uri.getPath(); + this.httpHeaders = httpRequest.getHeaders(); + this.queryParams = getParameters(httpRequest); + this.httpInputMessage = new ByteArrayHttpInputMessage(body); + } + + public MutableHttpServerRequest params(Map params) { + queryParams.setAll(params); + return this; + } + + @Override + public InputStream getBody() throws IOException { + return httpInputMessage.getBody(); + } + + @Override + public HttpMethod getMethod() { + return httpMethod; + } + + // Override method since Spring Framework 5.0 + @Override + public String getMethodValue() { + return httpMethod.name(); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } + + @Override + public String getPath() { + return path; + } + + @Override + public MultiValueMap getQueryParams() { + return queryParams; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/converter/HttpMessageConverterHolder.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/converter/HttpMessageConverterHolder.java new file mode 100644 index 00000000..2a242386 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/converter/HttpMessageConverterHolder.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.converter; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; + +/** + * {@link HttpMessageConverter} Holder with {@link MediaType}. + * + * @author Mercy + */ +public class HttpMessageConverterHolder { + + private final MediaType mediaType; + + private final HttpMessageConverter converter; + + public HttpMessageConverterHolder(MediaType mediaType, HttpMessageConverter converter) { + this.mediaType = mediaType; + this.converter = converter; + } + + public MediaType getMediaType() { + return mediaType; + } + + public HttpMessageConverter getConverter() { + return converter; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcher.java new file mode 100644 index 00000000..4bc1c281 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcher.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Abstract {@link HttpRequestMatcher} implementation + * + * @author Rossen Stoyanchev + * @author Mercy + */ +public abstract class AbstractHttpRequestMatcher implements HttpRequestMatcher { + + /** + * Return the discrete items a request condition is composed of. + *

+ * For example URL patterns, HTTP request methods, param expressions, etc. + * + * @return a collection of objects, never {@code null} + */ + protected abstract Collection getContent(); + + /** + * The notation to use when printing discrete items of content. + *

+ * For example {@code " || "} for URL patterns or {@code " && "} for param + * expressions. + */ + protected abstract String getToStringInfix(); + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return getContent().equals(((AbstractHttpRequestMatcher) other).getContent()); + } + + @Override + public int hashCode() { + return getContent().hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (Iterator iterator = getContent().iterator(); iterator.hasNext();) { + Object expression = iterator.next(); + builder.append(expression.toString()); + if (iterator.hasNext()) { + builder.append(getToStringInfix()); + } + } + builder.append("]"); + return builder.toString(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpression.java new file mode 100644 index 00000000..5f47f1bc --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpression.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.MediaType; + +/** + * The some source code is scratched from org.springframework.web.servlet.mvc.condition.AbstractMediaTypeExpression + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Mercy + */ +public class AbstractMediaTypeExpression implements MediaTypeExpression, Comparable { + + private final MediaType mediaType; + + private final boolean negated; + + AbstractMediaTypeExpression(String expression) { + if (expression.startsWith("!")) { + this.negated = true; + expression = expression.substring(1); + } else { + this.negated = false; + } + this.mediaType = MediaType.parseMediaType(expression); + } + + AbstractMediaTypeExpression(MediaType mediaType, boolean negated) { + this.mediaType = mediaType; + this.negated = negated; + } + + @Override + public MediaType getMediaType() { + return this.mediaType; + } + + @Override + public boolean isNegated() { + return this.negated; + } + + + @Override + public int compareTo(AbstractMediaTypeExpression other) { + return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + AbstractMediaTypeExpression otherExpr = (AbstractMediaTypeExpression) other; + return (this.mediaType.equals(otherExpr.mediaType) && this.negated == otherExpr.negated); + } + + @Override + public int hashCode() { + return this.mediaType.hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.negated) { + builder.append('!'); + } + builder.append(this.mediaType.toString()); + return builder.toString(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpression.java new file mode 100644 index 00000000..877a7aa0 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpression.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.springframework.util.StringUtils.trimWhitespace; + +/** + * The some source code is scratched from org.springframework.web.servlet.mvc.condition.AbstractNameValueExpression + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @author Mercy + */ +abstract class AbstractNameValueExpression implements NameValueExpression { + + protected final String name; + + protected final T value; + + protected final boolean negated; + + AbstractNameValueExpression(String expression) { + int separator = expression.indexOf('='); + if (separator == -1) { + this.negated = expression.startsWith("!"); + this.name = trimWhitespace((this.negated ? expression.substring(1) : expression)); + this.value = null; + } else { + this.negated = (separator > 0) && (expression.charAt(separator - 1) == '!'); + this.name = trimWhitespace((this.negated ? expression.substring(0, separator - 1) + : expression.substring(0, separator))); + String valueExpression = getValueExpression(expression, separator); + this.value = isExcludedValue(valueExpression) ? null : parseValue(valueExpression); + } + } + + private String getValueExpression(String expression, int separator) { + return trimWhitespace(expression.substring(separator + 1)); + } + + /** + * Exclude the pattern value Expression: "{value}", subclass could override this method. + * + * @param valueExpression + * @return + */ + protected boolean isExcludedValue(String valueExpression) { + return StringUtils.hasText(valueExpression) && + valueExpression.startsWith("{") + && valueExpression.endsWith("}"); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public T getValue() { + return this.value; + } + + @Override + public boolean isNegated() { + return this.negated; + } + + public final boolean match(HttpRequest request) { + boolean isMatch; + if (this.value != null) { + isMatch = matchValue(request); + } else { + isMatch = matchName(request); + } + return (this.negated ? !isMatch : isMatch); + } + + + protected abstract boolean isCaseSensitiveName(); + + protected abstract T parseValue(String valueExpression); + + protected abstract boolean matchName(HttpRequest request); + + protected abstract boolean matchValue(HttpRequest request); + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + AbstractNameValueExpression that = (AbstractNameValueExpression) other; + return ((isCaseSensitiveName() ? this.name.equals(that.name) : this.name.equalsIgnoreCase(that.name)) && + ObjectUtils.nullSafeEquals(this.value, that.value) && this.negated == that.negated); + } + + @Override + public int hashCode() { + int result = (isCaseSensitiveName() ? this.name.hashCode() : this.name.toLowerCase().hashCode()); + result = 31 * result + (this.value != null ? this.value.hashCode() : 0); + result = 31 * result + (this.negated ? 1 : 0); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.value != null) { + builder.append(this.name); + if (this.negated) { + builder.append('!'); + } + builder.append('='); + builder.append(this.value); + } else { + if (this.negated) { + builder.append('!'); + } + builder.append(this.name); + } + return builder.toString(); + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/CompositeHttpRequestMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/CompositeHttpRequestMatcher.java new file mode 100644 index 00000000..255a8829 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/CompositeHttpRequestMatcher.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * Composite {@link HttpRequestMatcher} implementation + * + * @author Mercy + */ +public abstract class CompositeHttpRequestMatcher extends AbstractHttpRequestMatcher { + + private final List matchers = new LinkedList<>(); + + public CompositeHttpRequestMatcher(HttpRequestMatcher... matchers) { + this.matchers.addAll(Arrays.asList(matchers)); + } + + public CompositeHttpRequestMatcher and(HttpRequestMatcher matcher) { + this.matchers.add(matcher); + return this; + } + + @Override + public boolean match(HttpRequest request) { + for (HttpRequestMatcher matcher : matchers) { + if (!matcher.match(request)) { + return false; + } + } + return true; + } + + protected List getMatchers() { + return this.matchers; + } + + @Override + protected Collection getContent() { + List content = new LinkedList<>(); + for (HttpRequestMatcher matcher : getMatchers()) { + if (matcher instanceof AbstractHttpRequestMatcher) { + content.addAll(((AbstractHttpRequestMatcher) matcher).getContent()); + } + } + return content; + } + + @Override + protected String getToStringInfix() { + return " && "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpression.java new file mode 100644 index 00000000..33f39a2a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpression.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.MediaType; + +/** + * Parses and matches a single media type expression to a request's 'Content-Type' header. + *

+ * The source code is scratched from + * org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition.ConsumeMediaTypeExpression + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + */ +class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression { + + ConsumeMediaTypeExpression(String expression) { + super(expression); + } + + ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + public final boolean match(MediaType contentType) { + boolean match = getMediaType().includes(contentType); + return (!isNegated() ? match : !match); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpression.java new file mode 100644 index 00000000..12a85bc0 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpression.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.util.ObjectUtils; + +/** + * Parses and matches a single header expression to a request. + *

+ * The some source code is scratched from org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Mercy + */ +class HeaderExpression extends AbstractNameValueExpression { + + HeaderExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return false; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(HttpRequest request) { + HttpHeaders httpHeaders = request.getHeaders(); + return httpHeaders.containsKey(this.name); + } + + @Override + protected boolean matchValue(HttpRequest request) { + HttpHeaders httpHeaders = request.getHeaders(); + String headerValue = httpHeaders.getFirst(this.name); + return ObjectUtils.nullSafeEquals(this.value, headerValue); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestConsumersMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestConsumersMatcher.java new file mode 100644 index 00000000..7117c891 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestConsumersMatcher.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * {@link HttpRequest} 'Content-Type' header {@link HttpRequestMatcher matcher} + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Mercy + */ +public class HttpRequestConsumersMatcher extends AbstractHttpRequestMatcher { + + private final List expressions; + + /** + * Creates a new instance from 0 or more "consumes" expressions. + * + * @param consumes consumes expressions if 0 expressions are provided, + * the condition will match to every request + */ + public HttpRequestConsumersMatcher(String... consumes) { + this(consumes, null); + } + + /** + * Creates a new instance with "consumes" and "header" expressions. + * "Header" expressions where the header name is not 'Content-Type' or have + * no header value defined are ignored. If 0 expressions are provided in + * total, the condition will match to every request + * + * @param consumes consumes expressions + * @param headers headers expressions + */ + public HttpRequestConsumersMatcher(String[] consumes, String[] headers) { + this(parseExpressions(consumes, headers)); + } + + /** + * Private constructor accepting parsed media type expressions. + */ + private HttpRequestConsumersMatcher(Collection expressions) { + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + } + + @Override + public boolean match(HttpRequest request) { + + if (expressions.isEmpty()) { + return true; + } + + HttpHeaders httpHeaders = request.getHeaders(); + + MediaType contentType = httpHeaders.getContentType(); + + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM; + } + + for (ConsumeMediaTypeExpression expression : expressions) { + if (!expression.match(contentType)) { + return false; + } + } + + return true; + } + + private static Set parseExpressions(String[] consumes, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeaderExpression expr = new HeaderExpression(header); + if ("Content-Type".equalsIgnoreCase(expr.name) && expr.value != null) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ConsumeMediaTypeExpression(mediaType, expr.negated)); + } + } + } + } + for (String consume : consumes) { + result.add(new ConsumeMediaTypeExpression(consume)); + } + return result; + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestHeadersMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestHeadersMatcher.java new file mode 100644 index 00000000..b83a02b0 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestHeadersMatcher.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * {@link HttpRequest} headers {@link HttpRequestMatcher matcher} + * + * @author Mercy + */ +public class HttpRequestHeadersMatcher extends AbstractHttpRequestMatcher { + + private final Set expressions; + + public HttpRequestHeadersMatcher(String... headers) { + this.expressions = parseExpressions(headers); + } + + private static Set parseExpressions(String... headers) { + Set expressions = new LinkedHashSet<>(); + for (String header : headers) { + HeaderExpression expr = new HeaderExpression(header); + if (HttpHeaders.ACCEPT.equalsIgnoreCase(expr.name) || + HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(expr.name)) { + continue; + } + expressions.add(expr); + } + return expressions; + } + + @Override + public boolean match(HttpRequest request) { + for (HeaderExpression expression : this.expressions) { + if (!expression.match(request)) { + return false; + } + } + return true; + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMatcher.java new file mode 100644 index 00000000..85606f2a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMatcher.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; + +/** + * {@link HttpRequest} Matcher + * + * @author Mercy + */ +public interface HttpRequestMatcher { + + /** + * Match {@link HttpRequest} or not + * + * @param request The {@link HttpRequest} instance + * @return if matched, return true, or false. + */ + boolean match(HttpRequest request); +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcher.java new file mode 100644 index 00000000..88a32394 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcher.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.springframework.http.HttpMethod.resolve; + +/** + * {@link HttpRequest} {@link HttpMethod methods} {@link HttpRequestMatcher matcher} + * + * @author Mercy + */ +public class HttpRequestMethodsMatcher extends AbstractHttpRequestMatcher { + + private final Set methods; + + public HttpRequestMethodsMatcher(String... methods) { + this.methods = resolveHttpMethods(methods); + } + + private Set resolveHttpMethods(String[] methods) { + Set httpMethods = new LinkedHashSet<>(methods.length); + for (String method : methods) { + if (!StringUtils.hasText(method)) { + continue; + } + HttpMethod httpMethod = resolve(method); + httpMethods.add(httpMethod); + } + return httpMethods; + } + + public Set getMethods() { + return methods; + } + + @Override + public boolean match(HttpRequest request) { + boolean matched = false; + HttpMethod httpMethod = request.getMethod(); + if (httpMethod != null) { + for (HttpMethod method : getMethods()) { + if (httpMethod.equals(method)) { + matched = true; + break; + } + } + } + return matched; + } + + @Override + protected Collection getContent() { + return methods; + } + + @Override + protected String getToStringInfix() { + return " || "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcher.java new file mode 100644 index 00000000..83fb599f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcher.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * {@link HttpRequest} parameters {@link HttpRequestMatcher matcher} + * + * @author Mercy + */ +public class HttpRequestParamsMatcher extends AbstractHttpRequestMatcher { + + private final Set expressions; + + /** + * @param params The pattern of params : + *

    + *
  • name=value
  • + *
  • name
  • + *
+ */ + public HttpRequestParamsMatcher(String... params) { + this.expressions = parseExpressions(params); + } + + @Override + public boolean match(HttpRequest request) { + if (CollectionUtils.isEmpty(expressions)) { + return true; + } + for (ParamExpression paramExpression : expressions) { + if (paramExpression.match(request)) { + return true; + } + } + return false; + } + + private static Set parseExpressions(String... params) { + Set expressions = new LinkedHashSet<>(); + for (String param : params) { + expressions.add(new ParamExpression(param)); + } + return expressions; + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestPathMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestPathMatcher.java new file mode 100644 index 00000000..07108e9f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestPathMatcher.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * {@link HttpRequest} {@link URI} {@link HttpRequestMatcher matcher} + * + * @author Mercy + */ +public class HttpRequestPathMatcher extends AbstractHttpRequestMatcher { + + private final Set patterns; + + private final PathMatcher pathMatcher; + + public HttpRequestPathMatcher(String... patterns) { + this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); + this.pathMatcher = new AntPathMatcher(); + } + + @Override + public boolean match(HttpRequest request) { + List matches = getMatchingPatterns(request); + return !matches.isEmpty(); + } + + public List getMatchingPatterns(HttpRequest request) { + String path = getPath(request); + List matches = getMatchingPatterns(path); + return matches; + } + + public List getMatchingPatterns(String lookupPath) { + List matches = new ArrayList<>(); + for (String pattern : this.patterns) { + String match = getMatchingPattern(pattern, lookupPath); + if (match != null) { + matches.add(match); + } + } + if (matches.size() > 1) { + matches.sort(this.pathMatcher.getPatternComparator(lookupPath)); + } + return matches; + } + + private String getMatchingPattern(String pattern, String lookupPath) { + if (pattern.equals(lookupPath)) { + return pattern; + } + boolean hasSuffix = pattern.indexOf('.') != -1; + if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { + return pattern + ".*"; + } + if (this.pathMatcher.match(pattern, lookupPath)) { + return pattern; + } + + if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { + return pattern + "/"; + } + return null; + } + + private String getPath(HttpRequest request) { + URI uri = request.getURI(); + return uri.getPath(); + } + + private static Set prependLeadingSlash(String[] patterns) { + Set result = new LinkedHashSet<>(patterns.length); + for (String pattern : patterns) { + if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { + pattern = "/" + pattern; + } + result.add(pattern); + } + return result; + } + + @Override + protected Collection getContent() { + return this.patterns; + } + + @Override + protected String getToStringInfix() { + return " || "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestProducesMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestProducesMatcher.java new file mode 100644 index 00000000..efc7300d --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestProducesMatcher.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * {@link HttpRequest} 'Accept' header {@link HttpRequestMatcher matcher} + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Mercy + */ +public class HttpRequestProducesMatcher extends AbstractHttpRequestMatcher { + + private final List expressions; + + /** + * Creates a new instance from "produces" expressions. If 0 expressions + * are provided in total, this condition will match to any request. + * + * @param produces produces expressions + */ + public HttpRequestProducesMatcher(String... produces) { + this(produces, null); + } + + /** + * Creates a new instance with "produces" and "header" expressions. "Header" + * expressions where the header name is not 'Accept' or have no header value + * defined are ignored. If 0 expressions are provided in total, this condition + * will match to any request. + * + * @param produces produces expressions + * @param headers headers expressions + */ + public HttpRequestProducesMatcher(String[] produces, String[] headers) { + this(parseExpressions(produces, headers)); + } + + /** + * Private constructor accepting parsed media type expressions. + */ + private HttpRequestProducesMatcher(Collection expressions) { + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + } + + @Override + public boolean match(HttpRequest request) { + + if (expressions.isEmpty()) { + return true; + } + + HttpHeaders httpHeaders = request.getHeaders(); + + List acceptedMediaTypes = httpHeaders.getAccept(); + + for (ProduceMediaTypeExpression expression : expressions) { + if (!expression.match(acceptedMediaTypes)) { + return false; + } + } + + return true; + } + + private static Set parseExpressions(String[] produces, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeaderExpression expr = new HeaderExpression(header); + if (HttpHeaders.ACCEPT.equalsIgnoreCase(expr.name) && expr.value != null) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ProduceMediaTypeExpression(mediaType, expr.negated)); + } + } + } + } + for (String produce : produces) { + result.add(new ProduceMediaTypeExpression(produce)); + } + return result; + } + + @Override + protected Collection getContent() { + return expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/MediaTypeExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/MediaTypeExpression.java new file mode 100644 index 00000000..61244f0a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/MediaTypeExpression.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.MediaType; + +/** + * A contract for media type expressions (e.g. "text/plain", "!text/plain") as + * defined in the for "consumes" and "produces". + *

+ * The source code is scratched from org.springframework.web.servlet.mvc.condition.MediaTypeExpression + * + * @author Rossen Stoyanchev + */ +interface MediaTypeExpression { + + MediaType getMediaType(); + + boolean isNegated(); + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/NameValueExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/NameValueExpression.java new file mode 100644 index 00000000..1da0b2fa --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/NameValueExpression.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + + +/** + * A contract for {@code "name!=value"} style expression used to specify request + * parameters and request header in HTTP request + *

+ * The some source code is scratched from org.springframework.web.servlet.mvc.condition.NameValueExpression + * + * @param the value type + * @author Rossen Stoyanchev + * @author Mercy + */ +interface NameValueExpression { + + String getName(); + + T getValue(); + + boolean isNegated(); + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpression.java new file mode 100644 index 00000000..8d14cd7b --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpression.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.HttpRequest; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +import static com.alibaba.cloud.dubbo.http.util.HttpUtils.getParameters; + +/** + * Parses and matches a single param expression to a request. + *

+ * The some source code is scratched from org.springframework.web.servlet.mvc.condition.ParamsRequestCondition.ParamExpression + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Mercy + */ +class ParamExpression extends AbstractNameValueExpression { + + ParamExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return true; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(HttpRequest request) { + MultiValueMap parametersMap = getParameters(request); + return parametersMap.containsKey(this.name); + } + + @Override + protected boolean matchValue(HttpRequest request) { + MultiValueMap parametersMap = getParameters(request); + String parameterValue = parametersMap.getFirst(this.name); + return ObjectUtils.nullSafeEquals(this.value, parameterValue); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpression.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpression.java new file mode 100644 index 00000000..d9830695 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpression.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.springframework.http.MediaType; + +import java.util.List; + +/** + * Parses and matches a single media type expression to a request's 'Accept' header. + *

+ * The source code is scratched from + * org.springframework.web.servlet.mvc.condition.ProducesRequestCondition.ProduceMediaTypeExpression + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + */ +class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { + + ProduceMediaTypeExpression(String expression) { + super(expression); + } + + ProduceMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + public final boolean match(List acceptedMediaTypes) { + boolean match = matchMediaType(acceptedMediaTypes); + return (!isNegated() ? match : !match); + } + + private boolean matchMediaType(List acceptedMediaTypes) { + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (getMediaType().isCompatibleWith(acceptedMediaType)) { + return true; + } + } + return false; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/RequestMetadataMatcher.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/RequestMetadataMatcher.java new file mode 100644 index 00000000..1f1b5b8e --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/matcher/RequestMetadataMatcher.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; + +import static com.alibaba.cloud.dubbo.http.util.HttpUtils.toNameAndValues; + +/** + * {@link RequestMetadata} {@link HttpRequestMatcher} implementation + * + * @author Mercy + */ +public class RequestMetadataMatcher extends CompositeHttpRequestMatcher { + + public RequestMetadataMatcher(RequestMetadata metadata) { + super( + // method + new HttpRequestMethodsMatcher(metadata.getMethod()), + // url + new HttpRequestPathMatcher(metadata.getPath()), + // params + new HttpRequestParamsMatcher(toNameAndValues(metadata.getParams())), + // headers + new HttpRequestHeadersMatcher(toNameAndValues(metadata.getHeaders())), + // consumes + new HttpRequestConsumersMatcher(metadata.getConsumes().toArray(new String[0])), + // produces + new HttpRequestProducesMatcher(metadata.getProduces().toArray(new String[0])) + ); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpMessageConverterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpMessageConverterResolver.java new file mode 100644 index 00000000..a5488f62 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpMessageConverterResolver.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.util; + +import com.alibaba.cloud.dubbo.http.converter.HttpMessageConverterHolder; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.unmodifiableList; + +/** + * {@link HttpMessageConverter} Resolver + * + * @author Mercy + */ +public class HttpMessageConverterResolver { + + private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); + + private final List> messageConverters; + + private final List allSupportedMediaTypes; + + private final ClassLoader classLoader; + + public HttpMessageConverterResolver(List> messageConverters, ClassLoader classLoader) { + this.messageConverters = messageConverters; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters); + this.classLoader = classLoader; + } + + public HttpMessageConverterHolder resolve(HttpRequest request, Class parameterType) { + + HttpMessageConverterHolder httpMessageConverterHolder = null; + + HttpHeaders httpHeaders = request.getHeaders(); + + MediaType contentType = httpHeaders.getContentType(); + + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM; + } + + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter) { + GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter; + if (genericConverter.canRead(parameterType, parameterType, contentType)) { + httpMessageConverterHolder = new HttpMessageConverterHolder(contentType, converter); + break; + } + } else { + if (converter.canRead(parameterType, contentType)) { + httpMessageConverterHolder = new HttpMessageConverterHolder(contentType, converter); + break; + } + } + + } + + return httpMessageConverterHolder; + } + + /** + * Resolve the most match {@link HttpMessageConverter} from {@link RequestMetadata} + * + * @param requestMetadata {@link RequestMetadata} + * @param restMethodMetadata {@link RestMethodMetadata} + * @return + */ + public HttpMessageConverterHolder resolve(RequestMetadata requestMetadata, RestMethodMetadata + restMethodMetadata) { + + HttpMessageConverterHolder httpMessageConverterHolder = null; + + Class returnValueClass = resolveReturnValueClass(restMethodMetadata); + + /** + * @see AbstractMessageConverterMethodProcessor#writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse) + */ + List requestedMediaTypes = getAcceptableMediaTypes(requestMetadata); + List producibleMediaTypes = getProducibleMediaTypes(restMethodMetadata, returnValueClass); + + Set compatibleMediaTypes = new LinkedHashSet(); + for (MediaType requestedType : requestedMediaTypes) { + for (MediaType producibleType : producibleMediaTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + + if (compatibleMediaTypes.isEmpty()) { + return httpMessageConverterHolder; + } + + List mediaTypes = new ArrayList<>(compatibleMediaTypes); + + MediaType.sortBySpecificityAndQuality(mediaTypes); + + MediaType selectedMediaType = null; + for (MediaType mediaType : mediaTypes) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; + } + } + + if (selectedMediaType != null) { + selectedMediaType = selectedMediaType.removeQualityValue(); + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { + httpMessageConverterHolder = new HttpMessageConverterHolder(selectedMediaType, messageConverter); + break; + } + } + } + + return httpMessageConverterHolder; + } + + public List getAllSupportedMediaTypes() { + return unmodifiableList(allSupportedMediaTypes); + } + + private Class resolveReturnValueClass(RestMethodMetadata restMethodMetadata) { + String returnClassName = restMethodMetadata.getMethod().getReturnType(); + return ClassUtils.resolveClassName(returnClassName, classLoader); + } + + /** + * Resolve the {@link MediaType media-types} + * + * @param requestMetadata {@link RequestMetadata} from client side + * @return non-null {@link List} + */ + private List getAcceptableMediaTypes(RequestMetadata requestMetadata) { + return requestMetadata.getProduceMediaTypes(); + } + + /** + * Returns + * the media types that can be produced:

  • The producible media types specified in the request mappings, or + *
  • Media types of configured converters that can write the specific return value, or
  • {@link MediaType#ALL} + *
+ * + * @param restMethodMetadata {@link RestMethodMetadata} from server side + * @param returnValueClass the class of return value + * @return non-null {@link List} + */ + private List getProducibleMediaTypes(RestMethodMetadata restMethodMetadata, Class + returnValueClass) { + RequestMetadata serverRequestMetadata = restMethodMetadata.getRequest(); + List mediaTypes = serverRequestMetadata.getProduceMediaTypes(); + if (!CollectionUtils.isEmpty(mediaTypes)) { // Empty + return mediaTypes; + } else if (!this.allSupportedMediaTypes.isEmpty()) { + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter.canWrite(returnValueClass, null)) { + result.addAll(converter.getSupportedMediaTypes()); + } + } + return result; + } else { + return Collections.singletonList(MediaType.ALL); + } + } + + /** + * Return the media types + * supported by all provided message converters sorted by specificity via {@link + * MediaType#sortBySpecificity(List)}. + * + * @param messageConverters + * @return + */ + private List getAllSupportedMediaTypes(List> messageConverters) { + Set allSupportedMediaTypes = new LinkedHashSet(); + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + } + List result = new ArrayList(allSupportedMediaTypes); + MediaType.sortBySpecificity(result); + return unmodifiableList(result); + } + + /** + * Return the more specific of the acceptable and the producible media types + * with the q-value of the former. + */ + private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { + MediaType produceTypeToUse = produceType.copyQualityValue(acceptType); + return (MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceTypeToUse) <= 0 ? acceptType : produceTypeToUse); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpUtils.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpUtils.java new file mode 100644 index 00000000..202509dc --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/http/util/HttpUtils.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.util; + +import org.springframework.http.HttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.springframework.util.StringUtils.delimitedListToStringArray; +import static org.springframework.util.StringUtils.hasText; +import static org.springframework.util.StringUtils.trimAllWhitespace; + +/** + * Http Utilities class + * + * @author Mercy + */ +public abstract class HttpUtils { + + private static final String UTF_8 = "UTF-8"; + + private static final String EQUAL = "="; + + private static final String AND = "&"; + + private static final String SEMICOLON = ";"; + + private static final String QUESTION_MASK = "?"; + + /** + * The empty value + */ + private static final String EMPTY_VALUE = ""; + + /** + * Normalize path: + *
    + *
  1. To remove query string if presents
  2. + *
  3. To remove duplicated slash("/") if exists
  4. + *
+ * + * @param path path to be normalized + * @return a normalized path if required + */ + public static String normalizePath(String path) { + if (!hasText(path)) { + return path; + } + String normalizedPath = path; + int index = normalizedPath.indexOf(QUESTION_MASK); + if (index > -1) { + normalizedPath = normalizedPath.substring(0, index); + } + return StringUtils.replace(normalizedPath, "//", "/"); + } + + + /** + * Get Parameters from the specified {@link HttpRequest request} + * + * @param request the specified {@link HttpRequest request} + * @return + */ + public static MultiValueMap getParameters(HttpRequest request) { + URI uri = request.getURI(); + return getParameters(uri.getQuery()); + } + + /** + * Get Parameters from the specified query string. + *

+ * + * @param queryString The query string + * @return The query parameters + */ + public static MultiValueMap getParameters(String queryString) { + return getParameters(delimitedListToStringArray(queryString, AND)); + } + + /** + * Get Parameters from the specified pairs of name-value. + *

+ * + * @param pairs The pairs of name-value + * @return The query parameters + */ + public static MultiValueMap getParameters(Iterable pairs) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + if (pairs != null) { + for (String pair : pairs) { + String[] nameAndValue = delimitedListToStringArray(pair, EQUAL); + String name = decode(nameAndValue[0]); + String value = nameAndValue.length < 2 ? null : nameAndValue[1]; + value = decode(value); + addParam(parameters, name, value); + } + } + return parameters; + } + + /** + * Get Parameters from the specified pairs of name-value. + *

+ * + * @param pairs The pairs of name-value + * @return The query parameters + */ + public static MultiValueMap getParameters(String... pairs) { + return getParameters(Arrays.asList(pairs)); + } + +// /** +// * Parse a read-only {@link MultiValueMap} of {@link HttpCookie} from {@link HttpHeaders} +// * +// * @param httpHeaders {@link HttpHeaders} +// * @return non-null, the key is a cookie name , the value is {@link HttpCookie} +// */ +// public static MultiValueMap parseCookies(HttpHeaders httpHeaders) { +// +// String cookie = httpHeaders.getFirst(COOKIE); +// +// String[] cookieNameAndValues = StringUtils.delimitedListToStringArray(cookie, SEMICOLON); +// +// MultiValueMap cookies = new LinkedMultiValueMap<>(cookieNameAndValues.length); +// +// for (String cookeNameAndValue : cookieNameAndValues) { +// String[] nameAndValue = delimitedListToStringArray(trimWhitespace(cookeNameAndValue), EQUAL); +// String name = nameAndValue[0]; +// String value = nameAndValue.length < 2 ? null : nameAndValue[1]; +// HttpCookie httpCookie = new HttpCookie(name, value); +// cookies.add(name, httpCookie); +// } +// +// return cookies; +// } + + /** + * To the name and value line sets + * + * @param nameAndValuesMap {@link MultiValueMap} the map of name and values + * @return non-null + */ + public static Set toNameAndValuesSet(Map> nameAndValuesMap) { + Set nameAndValues = new LinkedHashSet<>(); + for (Map.Entry> entry : nameAndValuesMap.entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + for (String value : values) { + String nameAndValue = name + EQUAL + value; + nameAndValues.add(nameAndValue); + } + } + return nameAndValues; + } + + public static String[] toNameAndValues(Map> nameAndValuesMap) { + return toNameAndValuesSet(nameAndValuesMap).toArray(new String[0]); + } + + /** + * Generate a string of query string from the specified request parameters {@link Map} + * + * @param params the specified request parameters {@link Map} + * @return non-null + */ + public static String toQueryString(Map> params) { + StringBuilder builder = new StringBuilder(); + for (String line : toNameAndValuesSet(params)) { + builder.append(line).append(AND); + } + return builder.toString(); + } + + /** + * Decode value + * + * @param value the value requires to decode + * @return the decoded value + */ + public static String decode(String value) { + if (value == null) { + return value; + } + String decodedValue = value; + try { + decodedValue = URLDecoder.decode(value, UTF_8); + } catch (UnsupportedEncodingException ex) { + } + return decodedValue; + } + + /** + * encode value + * + * @param value the value requires to encode + * @return the encoded value + */ + public static String encode(String value) { + String encodedValue = value; + try { + encodedValue = URLEncoder.encode(value, UTF_8); + } catch (UnsupportedEncodingException ex) { + } + return encodedValue; + } + + private static void addParam(MultiValueMap paramsMap, String name, String value) { + String paramValue = trimAllWhitespace(value); + if (!StringUtils.hasText(paramValue)) { + paramValue = EMPTY_VALUE; + } + paramsMap.add(trimAllWhitespace(name), paramValue); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboProtocolConfigSupplier.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboProtocolConfigSupplier.java new file mode 100644 index 00000000..8d4023ac --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboProtocolConfigSupplier.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import org.apache.dubbo.config.ProtocolConfig; + +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Supplier; + +import static org.apache.dubbo.common.Constants.DEFAULT_PROTOCOL; +import static org.springframework.util.CollectionUtils.isEmpty; + +/** + * Dubbo's {@link ProtocolConfig} {@link Supplier} + * + * @author Mercy + */ +public class DubboProtocolConfigSupplier implements Supplier { + + private final ObjectProvider> protocols; + + public DubboProtocolConfigSupplier(ObjectProvider> protocols) { + this.protocols = protocols; + } + + @Override + public ProtocolConfig get() { + ProtocolConfig protocolConfig = null; + Collection protocols = this.protocols.getIfAvailable(); + + if (!isEmpty(protocols)) { + for (ProtocolConfig protocol : protocols) { + String protocolName = protocol.getName(); + if (DEFAULT_PROTOCOL.equals(protocolName)) { + protocolConfig = protocol; + break; + } + } + + if (protocolConfig == null) { // If The ProtocolConfig bean named "dubbo" is absent, take first one of them + Iterator iterator = protocols.iterator(); + protocolConfig = iterator.hasNext() ? iterator.next() : null; + } + } + + if (protocolConfig == null) { + protocolConfig = new ProtocolConfig(); + protocolConfig.setName(DEFAULT_PROTOCOL); + protocolConfig.setPort(-1); + } + + return protocolConfig; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboRestServiceMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboRestServiceMetadata.java new file mode 100644 index 00000000..f689a499 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboRestServiceMetadata.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import java.util.Objects; + +/** + * Dubbo Rest Service Metadata + * + * @author Mercy + */ +public class DubboRestServiceMetadata { + + private final ServiceRestMetadata serviceRestMetadata; + + private final RestMethodMetadata restMethodMetadata; + + public DubboRestServiceMetadata(ServiceRestMetadata serviceRestMetadata, RestMethodMetadata restMethodMetadata) { + this.serviceRestMetadata = serviceRestMetadata; + this.restMethodMetadata = restMethodMetadata; + } + + public ServiceRestMetadata getServiceRestMetadata() { + return serviceRestMetadata; + } + + public RestMethodMetadata getRestMethodMetadata() { + return restMethodMetadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DubboRestServiceMetadata)) { + return false; + } + DubboRestServiceMetadata that = (DubboRestServiceMetadata) o; + return Objects.equals(serviceRestMetadata, that.serviceRestMetadata) && + Objects.equals(restMethodMetadata, that.restMethodMetadata); + } + + @Override + public int hashCode() { + return Objects.hash(serviceRestMetadata, restMethodMetadata); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboTransportedMethodMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboTransportedMethodMetadata.java new file mode 100644 index 00000000..22fdeb5e --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/DubboTransportedMethodMetadata.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.alibaba.cloud.dubbo.annotation.DubboTransported; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * {@link MethodMetadata} annotated {@link DubboTransported @DubboTransported} + * + * @author Mercy + */ +public class DubboTransportedMethodMetadata { + + private final MethodMetadata methodMetadata; + + private final Map attributes; + + public DubboTransportedMethodMetadata(Method method, Map attributes) { + this.methodMetadata = new MethodMetadata(method); + this.attributes = attributes; + } + + public String getName() { + return methodMetadata.getName(); + } + + public void setName(String name) { + methodMetadata.setName(name); + } + + public String getReturnType() { + return methodMetadata.getReturnType(); + } + + public void setReturnType(String returnType) { + methodMetadata.setReturnType(returnType); + } + + public List getParams() { + return methodMetadata.getParams(); + } + + public void setParams(List params) { + methodMetadata.setParams(params); + } + + public Method getMethod() { + return methodMetadata.getMethod(); + } + + public MethodMetadata getMethodMetadata() { + return methodMetadata; + } + + public Map getAttributes() { + return attributes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DubboTransportedMethodMetadata)) { + return false; + } + DubboTransportedMethodMetadata that = (DubboTransportedMethodMetadata) o; + return Objects.equals(methodMetadata, that.methodMetadata) && + Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(methodMetadata, attributes); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodMetadata.java new file mode 100644 index 00000000..34598677 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodMetadata.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +/** + * {@link Method} Metadata + * + * @author Mercy + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MethodMetadata { + + private String name; + + @JsonProperty("return-type") + private String returnType; + + private List params; + + @JsonIgnore + private Method method; + + public MethodMetadata() { + this.params = new LinkedList<>(); + } + + public MethodMetadata(Method method) { + this.name = method.getName(); + this.returnType = method.getReturnType().getName(); + this.params = initParameters(method); + this.method = method; + } + + private List initParameters(Method method) { + int parameterCount = method.getParameterCount(); + if (parameterCount < 1) { + return Collections.emptyList(); + } + List params = new ArrayList<>(parameterCount); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameterCount; i++) { + Parameter parameter = parameters[i]; + MethodParameterMetadata param = toMethodParameterMetadata(i, parameter); + params.add(param); + } + return params; + } + + private MethodParameterMetadata toMethodParameterMetadata(int index, Parameter parameter) { + MethodParameterMetadata metadata = new MethodParameterMetadata(); + metadata.setIndex(index); + metadata.setName(parameter.getName()); + metadata.setType(parameter.getType().getTypeName()); + return metadata; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getReturnType() { + return returnType; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public List getParams() { + return params; + } + + public void setParams(List params) { + this.params = params; + } + + public Method getMethod() { + return method; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodMetadata that = (MethodMetadata) o; + return Objects.equals(name, that.name) && + Objects.equals(returnType, that.returnType) && + Objects.equals(params, that.params); + } + + @Override + public int hashCode() { + return Objects.hash(name, returnType, params); + } + + @Override + public String toString() { + return "MethodMetadata{" + + "name='" + name + '\'' + + ", returnType='" + returnType + '\'' + + ", params=" + params + + ", method=" + method + + '}'; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodParameterMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodParameterMetadata.java new file mode 100644 index 00000000..39d4ac4a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/MethodParameterMetadata.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * {@link Method} Parameter Metadata + * + * @author Mercy + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MethodParameterMetadata { + + private int index; + + private String name; + + private String type; + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodParameterMetadata that = (MethodParameterMetadata) o; + return index == that.index && + Objects.equals(name, that.name) && + Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(index, name, type); + } + + @Override + public String toString() { + return "MethodParameterMetadata{" + + "index=" + index + + ", name='" + name + '\'' + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RequestMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RequestMetadata.java new file mode 100644 index 00000000..05b6b60d --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RequestMetadata.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import feign.RequestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; + +import static com.alibaba.cloud.dubbo.http.util.HttpUtils.normalizePath; +import static org.springframework.http.MediaType.parseMediaTypes; + +/** + * Request Metadata + * + * @author Mercy + */ +public class RequestMetadata { + + private String method; + + private String path; + + @JsonProperty("params") + private MultiValueMap params = new LinkedMultiValueMap<>(); + + @JsonProperty("headers") + private HttpHeaders headers = new HttpHeaders(); + + private Set consumes = new LinkedHashSet<>(); + + private Set produces = new LinkedHashSet<>(); + + public RequestMetadata() { + } + + public RequestMetadata(RequestTemplate requestTemplate) { + setMethod(requestTemplate.method()); + setPath(requestTemplate.url()); + params(requestTemplate.queries()); + headers(requestTemplate.headers()); + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method.toUpperCase(); + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = normalizePath(path); + } + + public MultiValueMap getParams() { + return params; + } + + public void setParams(Map> params) { + params(params); + } + + public Map> getHeaders() { + return headers; + } + + public void setHeaders(Map> headers) { + headers(headers); + } + + public Set getConsumes() { + return consumes; + } + + public void setConsumes(Set consumes) { + this.consumes = consumes; + } + + public Set getProduces() { + return produces; + } + + public void setProduces(Set produces) { + this.produces = produces; + } + + // @JsonIgnore properties + @JsonIgnore + public Set getParamNames() { + return params.keySet(); + } + + @JsonIgnore + public Set getHeaderNames() { + return headers.keySet(); + } + + @JsonIgnore + public List getConsumeMediaTypes() { + return toMediaTypes(consumes); + } + + @JsonIgnore + public List getProduceMediaTypes() { + return toMediaTypes(produces); + } + + public String getParameter(String name) { + return this.params.getFirst(name); + } + + public String getHeader(String name) { + return this.headers.getFirst(name); + } + + public RequestMetadata addParam(String name, String value) { + add(name, value, this.params); + return this; + } + + public RequestMetadata addHeader(String name, String value) { + add(name, value, this.headers); + return this; + } + + private > RequestMetadata params(Map params) { + addAll(params, this.params); + return this; + } + + private > RequestMetadata headers(Map headers) { + if (!CollectionUtils.isEmpty(headers)) { + HttpHeaders httpHeaders = new HttpHeaders(); + // Add all headers + addAll(headers, httpHeaders); + // Handles "Content-Type" and "Accept" headers if present + mediaTypes(httpHeaders, HttpHeaders.CONTENT_TYPE, this.consumes); + mediaTypes(httpHeaders, HttpHeaders.ACCEPT, this.produces); + this.headers.putAll(httpHeaders); + } + return this; + } + + /** + * Get the best matched {@link RequestMetadata} via specified {@link RequestMetadata} + * + * @param requestMetadataMap the source of {@link NavigableMap} + * @param requestMetadata the match object + * @return if not matched, return null + */ + public static RequestMetadata getBestMatch(NavigableMap requestMetadataMap, + RequestMetadata requestMetadata) { + + RequestMetadata key = requestMetadata; + + RequestMetadata result = requestMetadataMap.get(key); + + if (result == null) { + SortedMap headMap = requestMetadataMap.headMap(key, true); + result = headMap.isEmpty() ? null : requestMetadataMap.get(headMap.lastKey()); + } + + return result; + } + + private static void add(String key, String value, MultiValueMap destination) { + destination.add(key, value); + } + + private static > void addAll(Map source, + MultiValueMap destination) { + for (Map.Entry entry : source.entrySet()) { + String key = entry.getKey(); + for (String value : entry.getValue()) { + add(key, value, destination); + } + } + } + + private static void mediaTypes(HttpHeaders httpHeaders, String headerName, Collection destination) { + List value = httpHeaders.get(headerName); + List mediaTypes = parseMediaTypes(value); + destination.addAll(toMediaTypeValues(mediaTypes)); + } + + private static List toMediaTypeValues(List mediaTypes) { + List list = new ArrayList<>(mediaTypes.size()); + for (MediaType mediaType : mediaTypes) { + list.add(mediaType.toString()); + } + return list; + } + + private static List toMediaTypes(Collection mediaTypeValues) { + if (mediaTypeValues.isEmpty()) { + return Collections.singletonList(MediaType.ALL); + } + return parseMediaTypes(new LinkedList<>(mediaTypeValues)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RequestMetadata)) { + return false; + } + RequestMetadata that = (RequestMetadata) o; + return Objects.equals(method, that.method) && + Objects.equals(path, that.path) && + Objects.equals(consumes, that.consumes) && + Objects.equals(produces, that.produces) && + // Metadata should not compare the values + Objects.equals(getParamNames(), that.getParamNames()) && + Objects.equals(getHeaderNames(), that.getHeaderNames()); + + } + + @Override + public int hashCode() { + // The values of metadata should not use for the hashCode() method + return Objects.hash(method, path, consumes, produces, getParamNames(), getHeaderNames()); + } + + @Override + public String toString() { + return "RequestMetadata{" + + "method='" + method + '\'' + + ", path='" + path + '\'' + + ", params=" + params + + ", headers=" + headers + + ", consumes=" + consumes + + ", produces=" + produces + + '}'; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RestMethodMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RestMethodMetadata.java new file mode 100644 index 00000000..7840d448 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/RestMethodMetadata.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.core.ResolvableType; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Method Request Metadata + * + * @author Mercy + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RestMethodMetadata { + + private MethodMetadata method; + + private RequestMetadata request; + + @JsonProperty("url-index") + private Integer urlIndex; + + @JsonProperty("setBody-index") + private Integer bodyIndex; + + @JsonProperty("header-map-index") + private Integer headerMapIndex; + + @JsonProperty("query-map-index") + private Integer queryMapIndex; + + @JsonProperty("query-map-encoded") + private boolean queryMapEncoded; + + @JsonProperty("return-type") + private String returnType; + + @JsonProperty("setBody-type") + private String bodyType; + + @JsonProperty("index-to-name") + private Map> indexToName; + + @JsonProperty("form-params") + private List formParams; + + @JsonProperty("index-to-encoded") + private Map indexToEncoded; + + public RestMethodMetadata() { + } + + public RestMethodMetadata(feign.MethodMetadata methodMetadata) { + this.request = new RequestMetadata(methodMetadata.template()); + this.urlIndex = methodMetadata.urlIndex(); + this.bodyIndex = methodMetadata.bodyIndex(); + this.headerMapIndex = methodMetadata.headerMapIndex(); + this.queryMapEncoded = methodMetadata.queryMapEncoded(); + this.queryMapEncoded = methodMetadata.queryMapEncoded(); + this.returnType = getClassName(methodMetadata.returnType()); + this.bodyType = getClassName(methodMetadata.bodyType()); + this.indexToName = methodMetadata.indexToName(); + this.formParams = methodMetadata.formParams(); + this.indexToEncoded = methodMetadata.indexToEncoded(); + } + + public MethodMetadata getMethod() { + return method; + } + + public void setMethod(MethodMetadata method) { + this.method = method; + } + + public RequestMetadata getRequest() { + return request; + } + + public void setRequest(RequestMetadata request) { + this.request = request; + } + + public Map> getIndexToName() { + return indexToName; + } + + public void setIndexToName(Map> indexToName) { + this.indexToName = indexToName; + } + + public Integer getUrlIndex() { + return urlIndex; + } + + public void setUrlIndex(Integer urlIndex) { + this.urlIndex = urlIndex; + } + + public Integer getBodyIndex() { + return bodyIndex; + } + + public void setBodyIndex(Integer bodyIndex) { + this.bodyIndex = bodyIndex; + } + + public Integer getHeaderMapIndex() { + return headerMapIndex; + } + + public void setHeaderMapIndex(Integer headerMapIndex) { + this.headerMapIndex = headerMapIndex; + } + + public Integer getQueryMapIndex() { + return queryMapIndex; + } + + public void setQueryMapIndex(Integer queryMapIndex) { + this.queryMapIndex = queryMapIndex; + } + + public boolean isQueryMapEncoded() { + return queryMapEncoded; + } + + public void setQueryMapEncoded(boolean queryMapEncoded) { + this.queryMapEncoded = queryMapEncoded; + } + + public String getReturnType() { + return returnType; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public String getBodyType() { + return bodyType; + } + + public void setBodyType(String bodyType) { + this.bodyType = bodyType; + } + + public List getFormParams() { + return formParams; + } + + public void setFormParams(List formParams) { + this.formParams = formParams; + } + + public Map getIndexToEncoded() { + return indexToEncoded; + } + + public void setIndexToEncoded(Map indexToEncoded) { + this.indexToEncoded = indexToEncoded; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RestMethodMetadata)) { + return false; + } + RestMethodMetadata that = (RestMethodMetadata) o; + return queryMapEncoded == that.queryMapEncoded && + Objects.equals(method, that.method) && + Objects.equals(request, that.request) && + Objects.equals(urlIndex, that.urlIndex) && + Objects.equals(bodyIndex, that.bodyIndex) && + Objects.equals(headerMapIndex, that.headerMapIndex) && + Objects.equals(queryMapIndex, that.queryMapIndex) && + Objects.equals(returnType, that.returnType) && + Objects.equals(bodyType, that.bodyType) && + Objects.equals(indexToName, that.indexToName) && + Objects.equals(formParams, that.formParams) && + Objects.equals(indexToEncoded, that.indexToEncoded); + } + + @Override + public int hashCode() { + return Objects.hash(method, request, urlIndex, bodyIndex, headerMapIndex, queryMapIndex, queryMapEncoded, + returnType, bodyType, indexToName, formParams, indexToEncoded); + } + + private String getClassName(Type type) { + if (type == null) { + return null; + } + ResolvableType resolvableType = ResolvableType.forType(type); + return resolvableType.resolve().getName(); + } + + @Override + public String toString() { + return "RestMethodMetadata{" + + "method=" + method + + ", request=" + request + + ", urlIndex=" + urlIndex + + ", bodyIndex=" + bodyIndex + + ", headerMapIndex=" + headerMapIndex + + ", queryMapIndex=" + queryMapIndex + + ", queryMapEncoded=" + queryMapEncoded + + ", returnType='" + returnType + '\'' + + ", bodyType='" + bodyType + '\'' + + ", indexToName=" + indexToName + + ", formParams=" + formParams + + ", indexToEncoded=" + indexToEncoded + + '}'; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/ServiceRestMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/ServiceRestMetadata.java new file mode 100644 index 00000000..3a871b21 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/ServiceRestMetadata.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Objects; +import java.util.Set; + +/** + * Service Rest Metadata + * + * @author Mercy + * @see RestMethodMetadata + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ServiceRestMetadata { + + private String url; + + private Set meta; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Set getMeta() { + return meta; + } + + public void setMeta(Set meta) { + this.meta = meta; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ServiceRestMetadata)) { + return false; + } + ServiceRestMetadata that = (ServiceRestMetadata) o; + return Objects.equals(url, that.url) && + Objects.equals(meta, that.meta); + } + + @Override + public int hashCode() { + return Objects.hash(url, meta); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/repository/DubboServiceMetadataRepository.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/repository/DubboServiceMetadataRepository.java new file mode 100644 index 00000000..e14855df --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/repository/DubboServiceMetadataRepository.java @@ -0,0 +1,540 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.repository; + +import org.apache.dubbo.common.URL; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import com.alibaba.cloud.dubbo.env.DubboCloudProperties; +import com.alibaba.cloud.dubbo.http.matcher.RequestMetadataMatcher; +import com.alibaba.cloud.dubbo.metadata.DubboRestServiceMetadata; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; +import com.alibaba.cloud.dubbo.service.DubboMetadataService; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceExporter; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy; +import com.alibaba.cloud.dubbo.util.JSONUtils; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.http.HttpRequest; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.annotation.PostConstruct; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.lang.String.valueOf; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; +import static org.apache.dubbo.common.Constants.APPLICATION_KEY; +import static org.apache.dubbo.common.Constants.VERSION_KEY; +import static com.alibaba.cloud.dubbo.env.DubboCloudProperties.ALL_DUBBO_SERVICES; +import static com.alibaba.cloud.dubbo.http.DefaultHttpRequest.builder; +import static org.springframework.util.CollectionUtils.isEmpty; +import static org.springframework.util.StringUtils.hasText; + +/** + * Dubbo Service Metadata {@link Repository} + * + * @author Mercy + */ +@Repository +public class DubboServiceMetadataRepository { + + /** + * The prefix of {@link DubboMetadataService} : "dubbo.metadata-service." + */ + public static final String DUBBO_METADATA_SERVICE_PREFIX = "dubbo.metadata-service."; + + /** + * The {@link URL URLs} property name of {@link DubboMetadataService} : "dubbo.metadata-service.urls" + */ + public static final String DUBBO_METADATA_SERVICE_URLS_PROPERTY_NAME = DUBBO_METADATA_SERVICE_PREFIX + "urls"; + + /** + * The {@link String#format(String, Object...) pattern} of dubbo protocols port + */ + public static final String DUBBO_PROTOCOLS_PORT_PROPERTY_NAME_PATTERN = "dubbo.protocols.%s.port"; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // =================================== Registration =================================== // + + /** + * All exported {@link URL urls} {@link Map} whose key is the return value of {@link URL#getServiceKey()} method + * and value is the {@link List} of {@link URL URLs} + */ + private final MultiValueMap allExportedURLs = new LinkedMultiValueMap<>(); + + // ==================================================================================== // + + + // =================================== Subscription =================================== // + + private Set subscribedServices; + + /** + * The subscribed {@link URL urls} {@link Map} of {@link DubboMetadataService}, + * whose key is the return value of {@link URL#getServiceKey()} method and value is the {@link List} of + * {@link URL URLs} + */ + private final MultiValueMap subscribedDubboMetadataServiceURLs = new LinkedMultiValueMap<>(); + + // ==================================================================================== // + + + // =================================== REST Metadata ================================== // + + /** + * A Map to store REST metadata temporary, its' key is the special service name for a Dubbo service, + * the value is a JSON content of JAX-RS or Spring MVC REST metadata from the annotated methods. + */ + private final Set serviceRestMetadata = new LinkedHashSet<>(); + + /** + * Key is application name + * Value is Map + */ + private Map> dubboRestServiceMetadataRepository = newHashMap(); + + // ==================================================================================== // + + + // =================================== Dependencies =================================== // + + @Autowired + private DubboCloudProperties dubboCloudProperties; + + @Autowired + private DubboMetadataServiceProxy dubboMetadataConfigServiceProxy; + + @Autowired + private DiscoveryClient discoveryClient; + + @Autowired + private JSONUtils jsonUtils; + + @Autowired + private InetUtils inetUtils; + + @Value("${spring.application.name}") + private String currentApplicationName; + + @Autowired + private DubboMetadataServiceExporter dubboMetadataServiceExporter; + + // ==================================================================================== // + + + @PostConstruct + public void init() { + // Keep the order in following invocations + initSubscribedServices(); + initSubscribedDubboMetadataServices(); + initDubboRestServiceMetadataRepository(); + } + + /** + * Get the metadata {@link Map} of {@link DubboMetadataService} + * + * @return non-null read-only {@link Map} + */ + public Map getDubboMetadataServiceMetadata() { + + List dubboMetadataServiceURLs = dubboMetadataServiceExporter.export(); + + // remove the exported URLs of DubboMetadataService + removeDubboMetadataServiceURLs(dubboMetadataServiceURLs); + + Map metadata = newHashMap(); + + addDubboMetadataServiceURLsMetadata(metadata, dubboMetadataServiceURLs); + addDubboProtocolsPortMetadata(metadata); + + return Collections.unmodifiableMap(metadata); + } + + private void removeDubboMetadataServiceURLs(List dubboMetadataServiceURLs) { + dubboMetadataServiceURLs.forEach(this::unexportURL); + } + + private void addDubboMetadataServiceURLsMetadata(Map metadata, List dubboMetadataServiceURLs) { + String dubboMetadataServiceURLsJSON = jsonUtils.toJSON(dubboMetadataServiceURLs); + metadata.put(DUBBO_METADATA_SERVICE_URLS_PROPERTY_NAME, dubboMetadataServiceURLsJSON); + } + + private void addDubboProtocolsPortMetadata(Map metadata) { + + allExportedURLs.values() + .stream() + .flatMap(v -> v.stream()) + .forEach(url -> { + String protocol = url.getProtocol(); + String propertyName = getDubboProtocolPropertyName(protocol); + String propertyValue = valueOf(url.getPort()); + metadata.put(propertyName, propertyValue); + }); + } + + /** + * Get the property name of Dubbo Protocol + * + * @param protocol Dubbo Protocol + * @return non-null + */ + public String getDubboProtocolPropertyName(String protocol) { + return format(DUBBO_PROTOCOLS_PORT_PROPERTY_NAME_PATTERN, protocol); + } + + /** + * Publish the {@link Set} of {@link ServiceRestMetadata} + * + * @param serviceRestMetadataSet the {@link Set} of {@link ServiceRestMetadata} + */ + public void publishServiceRestMetadata(Set serviceRestMetadataSet) { + for (ServiceRestMetadata serviceRestMetadata : serviceRestMetadataSet) { + if (!isEmpty(serviceRestMetadata.getMeta())) { + this.serviceRestMetadata.add(serviceRestMetadata); + } + } + } + + /** + * Get the {@link Set} of {@link ServiceRestMetadata} + * + * @return non-null read-only {@link Set} + */ + public Set getServiceRestMetadata() { + return unmodifiableSet(serviceRestMetadata); + } + +// /** +// * Get The subscribed {@link DubboMetadataService}'s {@link URL URLs} +// * +// * @return non-null read-only {@link List} +// */ +// public List getSubscribedDubboMetadataServiceURLs() { +// return Collections.unmodifiableList(subscribedDubboMetadataServiceURLs); +// } + + public List findSubscribedDubboMetadataServiceURLs(String serviceName, String group, String version, + String protocol) { + String serviceKey = URL.buildKey(serviceName, group, version); + List urls = subscribedDubboMetadataServiceURLs.get(serviceKey); + + if (isEmpty(urls)) { + return emptyList(); + } + + return hasText(protocol) ? + urls.stream().filter(url -> url.getProtocol().equalsIgnoreCase(protocol)).collect(Collectors.toList()) : + unmodifiableList(urls) + ; + } + + /** + * The specified service is subscribe or not + * + * @param serviceName the service name + * @return + */ + public boolean isSubscribedService(String serviceName) { + return subscribedServices.contains(serviceName); + } + + public void exportURL(URL url) { + URL actualURL = url; + InetUtils.HostInfo hostInfo = inetUtils.findFirstNonLoopbackHostInfo(); + String ipAddress = hostInfo.getIpAddress(); + // To use InetUtils to set IP if they are different + // issue : https://github.com/spring-cloud-incubator/spring-cloud-alibaba/issues/589 + if (!Objects.equals(url.getHost(), ipAddress)) { + actualURL = url.setHost(ipAddress); + } + this.allExportedURLs.add(actualURL.getServiceKey(), actualURL); + } + + public void unexportURL(URL url) { + String key = url.getServiceKey(); + // NPE issue : https://github.com/spring-cloud-incubator/spring-cloud-alibaba/issues/591 + List urls = allExportedURLs.get(key); + if (!isEmpty(urls)) { + urls.remove(url); + allExportedURLs.addAll(key, urls); + } + } + + /** + * Get all exported {@link URL urls}. + * + * @return non-null read-only + */ + public Map> getAllExportedUrls() { + return unmodifiableMap(allExportedURLs); + } + + /** + * Get all exported {@link URL#getServiceKey() service keys} + * + * @return non-null read-only + */ + public Set getAllServiceKeys() { + return allExportedURLs.keySet(); + } + + /** + * Get the {@link URL urls} that {@link DubboMetadataService} exported by the specified {@link ServiceInstance} + * + * @param serviceInstance {@link ServiceInstance} + * @return the mutable {@link URL urls} + */ + public List getDubboMetadataServiceURLs(ServiceInstance serviceInstance) { + Map metadata = serviceInstance.getMetadata(); + String dubboURLsJSON = metadata.get(DUBBO_METADATA_SERVICE_URLS_PROPERTY_NAME); + return jsonUtils.toURLs(dubboURLsJSON); + } + + public Integer getDubboProtocolPort(ServiceInstance serviceInstance, String protocol) { + String protocolProperty = getDubboProtocolPropertyName(protocol); + Map metadata = serviceInstance.getMetadata(); + String protocolPort = metadata.get(protocolProperty); + return hasText(protocolPort) ? Integer.valueOf(protocolPort) : null; + } + + public List getExportedURLs(String serviceInterface, String group, String version) { + String serviceKey = URL.buildKey(serviceInterface, group, version); + return allExportedURLs.getOrDefault(serviceKey, Collections.emptyList()); + } + + /** + * Initialize the specified service's {@link ServiceRestMetadata} + * + * @param serviceName the service name + */ + public void initialize(String serviceName) { + + if (dubboRestServiceMetadataRepository.containsKey(serviceName)) { + return; + } + + Set serviceRestMetadataSet = getServiceRestMetadataSet(serviceName); + + if (isEmpty(serviceRestMetadataSet)) { + if (logger.isWarnEnabled()) { + logger.warn("The Spring application[name : {}] does not expose The REST metadata in the Dubbo services." + , serviceName); + } + return; + } + + Map metadataMap = getMetadataMap(serviceName); + + for (ServiceRestMetadata serviceRestMetadata : serviceRestMetadataSet) { + + serviceRestMetadata.getMeta().forEach(restMethodMetadata -> { + RequestMetadata requestMetadata = restMethodMetadata.getRequest(); + RequestMetadataMatcher matcher = new RequestMetadataMatcher(requestMetadata); + DubboRestServiceMetadata metadata = new DubboRestServiceMetadata(serviceRestMetadata, restMethodMetadata); + metadataMap.put(matcher, metadata); + }); + } + + if (logger.isInfoEnabled()) { + logger.info("The REST metadata in the dubbo services has been loaded in the Spring application[name : {}]", serviceName); + } + } + + /** + * Get a {@link DubboRestServiceMetadata} by the specified service name if {@link RequestMetadata} matched + * + * @param serviceName service name + * @param requestMetadata {@link RequestMetadata} to be matched + * @return {@link DubboRestServiceMetadata} if matched, or null + */ + public DubboRestServiceMetadata get(String serviceName, RequestMetadata requestMetadata) { + return match(dubboRestServiceMetadataRepository, serviceName, requestMetadata); + } + + public Set getSubscribedServices() { + return Collections.unmodifiableSet(subscribedServices); + } + + private T match(Map> repository, String serviceName, + RequestMetadata requestMetadata) { + + Map map = repository.get(serviceName); + + T object = null; + + if (!isEmpty(map)) { + RequestMetadataMatcher matcher = new RequestMetadataMatcher(requestMetadata); + object = map.get(matcher); + if (object == null) { // Can't match exactly + // Require to match one by one + HttpRequest request = builder() + .method(requestMetadata.getMethod()) + .path(requestMetadata.getPath()) + .params(requestMetadata.getParams()) + .headers(requestMetadata.getHeaders()) + .build(); + + for (Map.Entry entry : map.entrySet()) { + RequestMetadataMatcher possibleMatcher = entry.getKey(); + if (possibleMatcher.match(request)) { + object = entry.getValue(); + break; + } + } + } + } + + if (object == null) { + if (logger.isWarnEnabled()) { + logger.warn("DubboServiceMetadata can't be found in the Spring application [%s] and %s", + serviceName, requestMetadata); + } + } + + return object; + } + + private Map getMetadataMap(String serviceName) { + return getMap(dubboRestServiceMetadataRepository, serviceName); + } + + private Set getServiceRestMetadataSet(String serviceName) { + + Set metadata = emptySet(); + + DubboMetadataService dubboMetadataService = dubboMetadataConfigServiceProxy.getProxy(serviceName); + + if (dubboMetadataService != null) { + try { + String serviceRestMetadataJsonConfig = dubboMetadataService.getServiceRestMetadata(); + if (hasText(serviceRestMetadataJsonConfig)) { + metadata = objectMapper.readValue(serviceRestMetadataJsonConfig, + TypeFactory.defaultInstance().constructCollectionType(LinkedHashSet.class, ServiceRestMetadata.class)); + } + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error(e.getMessage(), e); + } + } + } + return metadata; + } + + private static Map getMap(Map> repository, String key) { + return getOrDefault(repository, key, newHashMap()); + } + + private static V getOrDefault(Map source, K key, V defaultValue) { + V value = source.get(key); + if (value == null) { + value = defaultValue; + source.put(key, value); + } + return value; + } + + private static Map newHashMap() { + return new LinkedHashMap<>(); + } + + private void initSubscribedServices() { + // If subscribes all services + if (ALL_DUBBO_SERVICES.equals(dubboCloudProperties.getSubscribedServices())) { + List services = discoveryClient.getServices(); + subscribedServices = new HashSet<>(services); + if (logger.isWarnEnabled()) { + logger.warn("Current application will subscribe all services(size:{}) in registry, " + + "a lot of memory and CPU cycles may be used, " + + "thus it's strongly recommend you using the externalized property '{}' " + + "to specify the services", + subscribedServices.size(), "dubbo.cloud.subscribed-services"); + } + } else { + subscribedServices = new HashSet<>(dubboCloudProperties.subscribedServices()); + } + + excludeSelf(subscribedServices); + } + + private void excludeSelf(Set subscribedServices) { + subscribedServices.remove(currentApplicationName); + } + + private void initSubscribedDubboMetadataServices() { + // clear subscribedDubboMetadataServiceURLs + subscribedDubboMetadataServiceURLs.clear(); + + subscribedServices.stream() + .map(discoveryClient::getInstances) + .filter(this::isNotEmpty) + .forEach(serviceInstances -> { + ServiceInstance serviceInstance = serviceInstances.get(0); + getDubboMetadataServiceURLs(serviceInstance).forEach(dubboMetadataServiceURL -> { + initSubscribedDubboMetadataServiceURLs(dubboMetadataServiceURL); + initDubboMetadataServiceProxy(dubboMetadataServiceURL); + }); + }); + } + + private void initSubscribedDubboMetadataServiceURLs(URL dubboMetadataServiceURL) { + // add subscriptions + String serviceKey = dubboMetadataServiceURL.getServiceKey(); + subscribedDubboMetadataServiceURLs.add(serviceKey, dubboMetadataServiceURL); + } + + private void initDubboMetadataServiceProxy(URL dubboMetadataServiceURL) { + String serviceName = dubboMetadataServiceURL.getParameter(APPLICATION_KEY); + String version = dubboMetadataServiceURL.getParameter(VERSION_KEY); + // Initialize DubboMetadataService with right version + dubboMetadataConfigServiceProxy.initProxy(serviceName, version); + } + + private void initDubboRestServiceMetadataRepository() { + subscribedServices.forEach(this::initialize); + } + + private boolean isNotEmpty(Collection collection) { + return !CollectionUtils.isEmpty(collection); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboServiceBeanMetadataResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboServiceBeanMetadataResolver.java new file mode 100644 index 00000000..9bf740c1 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboServiceBeanMetadataResolver.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.resolver; + +import feign.Contract; +import feign.Feign; +import feign.MethodMetadata; +import feign.Util; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.spring.ServiceBean; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The metadata resolver for {@link Feign} for {@link ServiceBean Dubbo Service Bean} in the provider side. + * + * @author Mercy + */ +public class DubboServiceBeanMetadataResolver implements BeanClassLoaderAware, SmartInitializingSingleton, + MetadataResolver { + + private static final String[] CONTRACT_CLASS_NAMES = { + "feign.jaxrs2.JAXRS2Contract", + "org.springframework.cloud.openfeign.support.SpringMvcContract", + }; + + private final ObjectProvider contractObjectProvider; + + private ClassLoader classLoader; + + /** + * Feign Contracts + */ + private Collection contracts; + + public DubboServiceBeanMetadataResolver(ObjectProvider contractObjectProvider) { + this.contractObjectProvider = contractObjectProvider; + } + + @Override + public void afterSingletonsInstantiated() { + + LinkedList contracts = new LinkedList<>(); + + // Add injected Contract if available, for example SpringMvcContract Bean under Spring Cloud Open Feign + Contract contract = contractObjectProvider.getIfAvailable(); + + if (contract != null) { + contracts.add(contract); + } + + Stream.of(CONTRACT_CLASS_NAMES) + .filter(this::isClassPresent) // filter the existed classes + .map(this::loadContractClass) // load Contract Class + .map(this::createContract) // createServiceInstance instance by the specified class + .forEach(contracts::add); // add the Contract instance into contracts + + this.contracts = Collections.unmodifiableCollection(contracts); + } + + private Contract createContract(Class contractClassName) { + return (Contract) BeanUtils.instantiateClass(contractClassName); + } + + private Class loadContractClass(String contractClassName) { + return ClassUtils.resolveClassName(contractClassName, classLoader); + } + + private boolean isClassPresent(String className) { + return ClassUtils.isPresent(className, classLoader); + } + + @Override + public Set resolveServiceRestMetadata(ServiceBean serviceBean) { + + Object bean = serviceBean.getRef(); + + Class beanType = bean.getClass(); + + Set serviceRestMetadata = new LinkedHashSet<>(); + + Set methodRestMetadata = resolveMethodRestMetadata(beanType); + + List urls = serviceBean.getExportedUrls(); + + urls.stream() + .map(URL::toString) + .forEach(url -> { + ServiceRestMetadata metadata = new ServiceRestMetadata(); + metadata.setUrl(url); + metadata.setMeta(methodRestMetadata); + serviceRestMetadata.add(metadata); + }); + + return serviceRestMetadata; + } + + @Override + public Set resolveMethodRestMetadata(Class targetType) { + List feignContractMethods = selectFeignContractMethods(targetType); + return contracts.stream() + .map(contract -> parseAndValidateMetadata(contract, targetType)) + .flatMap(v -> v.stream()) + .map(methodMetadata -> resolveMethodRestMetadata(methodMetadata, targetType, feignContractMethods)) + .collect(Collectors.toSet()); + } + + private List parseAndValidateMetadata(Contract contract, Class targetType) { + List methodMetadataList = Collections.emptyList(); + try { + methodMetadataList = contract.parseAndValidatateMetadata(targetType); + } catch (Throwable ignored) { + // ignore + } + return methodMetadataList; + } + + /** + * Select feign contract methods + *

+ * extract some code from {@link Contract.BaseContract#parseAndValidatateMetadata(java.lang.Class)} + * + * @param targetType + * @return non-null + */ + private List selectFeignContractMethods(Class targetType) { + List methods = new LinkedList<>(); + for (Method method : targetType.getMethods()) { + if (method.getDeclaringClass() == Object.class || + (method.getModifiers() & Modifier.STATIC) != 0 || + Util.isDefault(method)) { + continue; + } + methods.add(method); + } + return methods; + } + + protected RestMethodMetadata resolveMethodRestMetadata(MethodMetadata methodMetadata, Class targetType, + List feignContractMethods) { + String configKey = methodMetadata.configKey(); + Method feignContractMethod = getMatchedFeignContractMethod(targetType, feignContractMethods, configKey); + RestMethodMetadata metadata = new RestMethodMetadata(methodMetadata); + metadata.setMethod(new com.alibaba.cloud.dubbo.metadata.MethodMetadata(feignContractMethod)); + return metadata; + } + + private Method getMatchedFeignContractMethod(Class targetType, List methods, String expectedConfigKey) { + Method matchedMethod = null; + for (Method method : methods) { + String configKey = Feign.configKey(targetType, method); + if (expectedConfigKey.equals(configKey)) { + matchedMethod = method; + break; + } + } + return matchedMethod; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedAttributesResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedAttributesResolver.java new file mode 100644 index 00000000..21357661 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedAttributesResolver.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.resolver; + +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import org.springframework.core.env.PropertyResolver; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.springframework.core.annotation.AnnotationUtils.getAnnotationAttributes; + +/** + * {@link DubboTransported} annotation attributes resolver + * + * @author Mercy + */ +public class DubboTransportedAttributesResolver { + + private final PropertyResolver propertyResolver; + + public DubboTransportedAttributesResolver(PropertyResolver propertyResolver) { + this.propertyResolver = propertyResolver; + } + + public Map resolve(DubboTransported dubboTransported) { + Map attributes = getAnnotationAttributes(dubboTransported); + return resolve(attributes); + } + + public Map resolve(Map attributes) { + Map resolvedAttributes = new LinkedHashMap<>(); + for (Map.Entry entry : attributes.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + value = propertyResolver.resolvePlaceholders(value.toString()); + } + resolvedAttributes.put(entry.getKey(), value); + } + return resolvedAttributes; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolver.java new file mode 100644 index 00000000..1c09c0bc --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolver.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.resolver; + +import feign.Contract; +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import com.alibaba.cloud.dubbo.metadata.DubboTransportedMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.MethodMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.PropertyResolver; + +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static feign.Feign.configKey; + +/** + * {@link MethodMetadata} Resolver for the {@link DubboTransported} annotated classes or methods in client side. + * + * @author Mercy + * @see DubboTransportedMethodMetadata + */ +public class DubboTransportedMethodMetadataResolver { + + private static final Class DUBBO_TRANSPORTED_CLASS = DubboTransported.class; + + private final DubboTransportedAttributesResolver attributesResolver; + + private final Contract contract; + + public DubboTransportedMethodMetadataResolver(PropertyResolver propertyResolver, Contract contract) { + this.attributesResolver = new DubboTransportedAttributesResolver(propertyResolver); + this.contract = contract; + } + + public Map resolve(Class targetType) { + Set dubboTransportedMethodMetadataSet = + resolveDubboTransportedMethodMetadataSet(targetType); + Map restMethodMetadataMap = resolveRestRequestMetadataMap(targetType); + return dubboTransportedMethodMetadataSet + .stream() + .collect(Collectors.toMap(methodMetadata -> methodMetadata, methodMetadata -> { + RestMethodMetadata restMethodMetadata = restMethodMetadataMap.get(configKey(targetType, methodMetadata.getMethod())); + restMethodMetadata.setMethod(methodMetadata.getMethodMetadata()); + return restMethodMetadata; + } + )); + } + + protected Set resolveDubboTransportedMethodMetadataSet(Class targetType) { + // The public methods of target interface + Method[] methods = targetType.getMethods(); + + Set methodMetadataSet = new LinkedHashSet<>(); + + for (Method method : methods) { + DubboTransported dubboTransported = resolveDubboTransported(method); + if (dubboTransported != null) { + DubboTransportedMethodMetadata methodMetadata = createDubboTransportedMethodMetadata(method, dubboTransported); + methodMetadataSet.add(methodMetadata); + } + } + return methodMetadataSet; + } + + + private Map resolveRestRequestMetadataMap(Class targetType) { + return contract.parseAndValidatateMetadata(targetType) + .stream().collect(Collectors.toMap(feign.MethodMetadata::configKey, this::restMethodMetadata)); + } + + private RestMethodMetadata restMethodMetadata(feign.MethodMetadata methodMetadata) { + return new RestMethodMetadata(methodMetadata); + } + + private DubboTransportedMethodMetadata createDubboTransportedMethodMetadata(Method method, + DubboTransported dubboTransported) { + Map attributes = attributesResolver.resolve(dubboTransported); + return new DubboTransportedMethodMetadata(method, attributes); + } + + private DubboTransported resolveDubboTransported(Method method) { + DubboTransported dubboTransported = AnnotationUtils.findAnnotation(method, DUBBO_TRANSPORTED_CLASS); + if (dubboTransported == null) { // Attempt to find @DubboTransported in the declaring class + Class declaringClass = method.getDeclaringClass(); + dubboTransported = AnnotationUtils.findAnnotation(declaringClass, DUBBO_TRANSPORTED_CLASS); + } + return dubboTransported; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/MetadataResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/MetadataResolver.java new file mode 100644 index 00000000..52917c5d --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/metadata/resolver/MetadataResolver.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.resolver; + +import org.apache.dubbo.config.spring.ServiceBean; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; + +import java.util.Set; + +/** + * The REST metadata resolver + * + * @author Mercy + */ +public interface MetadataResolver { + + /** + * Resolve the {@link ServiceRestMetadata} {@link Set set} from {@link ServiceBean} + * + * @param serviceBean {@link ServiceBean} + * @return non-null {@link Set} + */ + Set resolveServiceRestMetadata(ServiceBean serviceBean); + + /** + * Resolve {@link RestMethodMetadata} {@link Set set} from {@link Class target type} + * + * @param targetType {@link Class target type} + * @return non-null {@link Set} + */ + Set resolveMethodRestMetadata(Class targetType); +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/DubboInvocationHandler.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/DubboInvocationHandler.java new file mode 100644 index 00000000..6bd21d06 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/DubboInvocationHandler.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.openfeign; + +import org.apache.dubbo.rpc.service.GenericService; + +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContext; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +import static org.apache.dubbo.common.utils.PojoUtils.realize; + +/** + * Dubbo {@link GenericService} for {@link InvocationHandler} + * + * @author Mercy + */ +public class DubboInvocationHandler implements InvocationHandler { + + private final Map feignMethodMetadataMap; + + private final InvocationHandler defaultInvocationHandler; + + private final DubboGenericServiceExecutionContextFactory contextFactory; + + private final ClassLoader classLoader; + + public DubboInvocationHandler(Map feignMethodMetadataMap, + InvocationHandler defaultInvocationHandler, + ClassLoader classLoader, + DubboGenericServiceExecutionContextFactory contextFactory) { + this.feignMethodMetadataMap = feignMethodMetadataMap; + this.defaultInvocationHandler = defaultInvocationHandler; + this.classLoader = classLoader; + this.contextFactory = contextFactory; + } + + @Override + public Object invoke(Object proxy, Method feignMethod, Object[] args) throws Throwable { + + FeignMethodMetadata feignMethodMetadata = feignMethodMetadataMap.get(feignMethod); + + if (feignMethodMetadata == null) { + return defaultInvocationHandler.invoke(proxy, feignMethod, args); + } + + GenericService dubboGenericService = feignMethodMetadata.getDubboGenericService(); + RestMethodMetadata dubboRestMethodMetadata = feignMethodMetadata.getDubboRestMethodMetadata(); + RestMethodMetadata feignRestMethodMetadata = feignMethodMetadata.getFeignMethodMetadata(); + + DubboGenericServiceExecutionContext context = contextFactory.create(dubboRestMethodMetadata, feignRestMethodMetadata, args); + + String methodName = context.getMethodName(); + String[] parameterTypes = context.getParameterTypes(); + Object[] parameters = context.getParameters(); + + Object result = dubboGenericService.$invoke(methodName, parameterTypes, parameters); + + Class returnType = getReturnType(dubboRestMethodMetadata); + + return realize(result, returnType); + } + + private Class getReturnType(RestMethodMetadata dubboRestMethodMetadata) { + String returnType = dubboRestMethodMetadata.getReturnType(); + return ClassUtils.resolveClassName(returnType, classLoader); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/FeignMethodMetadata.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/FeignMethodMetadata.java new file mode 100644 index 00000000..3c11c914 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/FeignMethodMetadata.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.openfeign; + +import org.apache.dubbo.rpc.service.GenericService; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; + +import java.lang.reflect.Method; + +/** + * Feign {@link Method} Metadata + * + * @author Mercy + */ +class FeignMethodMetadata { + + private final GenericService dubboGenericService; + + private final RestMethodMetadata dubboRestMethodMetadata; + + private final RestMethodMetadata feignMethodMetadata; + + + FeignMethodMetadata(GenericService dubboGenericService, RestMethodMetadata dubboRestMethodMetadata, + RestMethodMetadata feignMethodMetadata) { + this.dubboGenericService = dubboGenericService; + this.dubboRestMethodMetadata = dubboRestMethodMetadata; + this.feignMethodMetadata = feignMethodMetadata; + } + + GenericService getDubboGenericService() { + return dubboGenericService; + } + + RestMethodMetadata getDubboRestMethodMetadata() { + return dubboRestMethodMetadata; + } + + RestMethodMetadata getFeignMethodMetadata() { + return feignMethodMetadata; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterBeanPostProcessor.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterBeanPostProcessor.java new file mode 100644 index 00000000..289ffb33 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterBeanPostProcessor.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.openfeign; + +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; + +import org.springframework.core.env.Environment; + +import static java.lang.reflect.Proxy.newProxyInstance; +import static com.alibaba.cloud.dubbo.autoconfigure.DubboOpenFeignAutoConfiguration.TARGETER_CLASS_NAME; +import static org.springframework.util.ClassUtils.getUserClass; +import static org.springframework.util.ClassUtils.isPresent; +import static org.springframework.util.ClassUtils.resolveClassName; + +/** + * org.springframework.cloud.openfeign.Targeter {@link BeanPostProcessor} + * + * @author Mercy + */ +public class TargeterBeanPostProcessor implements BeanPostProcessor, BeanClassLoaderAware { + + private final Environment environment; + + private final DubboServiceMetadataRepository dubboServiceMetadataRepository; + + private final DubboGenericServiceFactory dubboGenericServiceFactory; + + private final DubboGenericServiceExecutionContextFactory contextFactory; + + private ClassLoader classLoader; + + public TargeterBeanPostProcessor(Environment environment, + DubboServiceMetadataRepository dubboServiceMetadataRepository, + DubboGenericServiceFactory dubboGenericServiceFactory, + DubboGenericServiceExecutionContextFactory contextFactory) { + this.environment = environment; + this.dubboServiceMetadataRepository = dubboServiceMetadataRepository; + this.dubboGenericServiceFactory = dubboGenericServiceFactory; + this.contextFactory = contextFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException { + if (isPresent(TARGETER_CLASS_NAME, classLoader)) { + Class beanClass = getUserClass(bean.getClass()); + Class targetClass = resolveClassName(TARGETER_CLASS_NAME, classLoader); + if (targetClass.isAssignableFrom(beanClass)) { + return newProxyInstance(classLoader, new Class[]{targetClass}, + new TargeterInvocationHandler(bean, environment, classLoader, dubboServiceMetadataRepository, + dubboGenericServiceFactory, contextFactory)); + } + } + return bean; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterInvocationHandler.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterInvocationHandler.java new file mode 100644 index 00000000..6ef5cb86 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/openfeign/TargeterInvocationHandler.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.openfeign; + + +import org.apache.dubbo.rpc.service.GenericService; + +import feign.Contract; +import feign.Target; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import com.alibaba.cloud.dubbo.metadata.DubboRestServiceMetadata; +import com.alibaba.cloud.dubbo.metadata.DubboTransportedMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.MethodMetadata; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.metadata.resolver.DubboTransportedMethodMetadataResolver; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.core.env.Environment; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.reflect.Proxy.newProxyInstance; + +/** + * org.springframework.cloud.openfeign.Targeter {@link InvocationHandler} + * + * @author Mercy + */ +class TargeterInvocationHandler implements InvocationHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Object bean; + + private final Environment environment; + + private final ClassLoader classLoader; + + private final DubboServiceMetadataRepository repository; + + private final DubboGenericServiceFactory dubboGenericServiceFactory; + + private final DubboGenericServiceExecutionContextFactory contextFactory; + + TargeterInvocationHandler(Object bean, Environment environment, + ClassLoader classLoader, + DubboServiceMetadataRepository repository, + DubboGenericServiceFactory dubboGenericServiceFactory, + DubboGenericServiceExecutionContextFactory contextFactory) { + this.bean = bean; + this.environment = environment; + this.classLoader = classLoader; + this.repository = repository; + this.dubboGenericServiceFactory = dubboGenericServiceFactory; + this.contextFactory = contextFactory; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + /** + * args[0]: FeignClientFactoryBean factory + * args[1]: Feign.Builder feign + * args[2]: FeignContext context + * args[3]: Target.HardCodedTarget target + */ + FeignContext feignContext = cast(args[2]); + Target.HardCodedTarget target = cast(args[3]); + + // Execute Targeter#target method first + method.setAccessible(true); + // Get the default proxy object + Object defaultProxy = method.invoke(bean, args); + // Create Dubbo Proxy if required + return createDubboProxyIfRequired(feignContext, target, defaultProxy); + } + + private Object createDubboProxyIfRequired(FeignContext feignContext, Target target, Object defaultProxy) { + + DubboInvocationHandler dubboInvocationHandler = createDubboInvocationHandler(feignContext, target, defaultProxy); + + if (dubboInvocationHandler == null) { + return defaultProxy; + } + + return newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, dubboInvocationHandler); + } + + private DubboInvocationHandler createDubboInvocationHandler(FeignContext feignContext, Target target, + Object defaultFeignClientProxy) { + + // Service name equals @FeignClient.name() + String serviceName = target.name(); + Class targetType = target.type(); + + // Get Contract Bean from FeignContext + Contract contract = feignContext.getInstance(serviceName, Contract.class); + + DubboTransportedMethodMetadataResolver resolver = + new DubboTransportedMethodMetadataResolver(environment, contract); + + Map feignRestMethodMetadataMap = resolver.resolve(targetType); + + if (feignRestMethodMetadataMap.isEmpty()) { // @DubboTransported method was not found from the Client interface + if (logger.isDebugEnabled()) { + logger.debug("@{} method was not found in the Feign target type[{}]", + DubboTransported.class.getSimpleName(), targetType.getName()); + } + return null; + } + + // Update Metadata + repository.initialize(serviceName); + + Map feignMethodMetadataMap = getFeignMethodMetadataMap(serviceName, feignRestMethodMetadataMap); + + InvocationHandler defaultFeignClientInvocationHandler = Proxy.getInvocationHandler(defaultFeignClientProxy); + + DubboInvocationHandler dubboInvocationHandler = new DubboInvocationHandler(feignMethodMetadataMap, + defaultFeignClientInvocationHandler, classLoader, contextFactory); + + return dubboInvocationHandler; + } + + private Map getFeignMethodMetadataMap(String serviceName, + Map + feignRestMethodMetadataMap) { + Map feignMethodMetadataMap = new HashMap<>(); + + for (Map.Entry entry : feignRestMethodMetadataMap.entrySet()) { + RestMethodMetadata feignRestMethodMetadata = entry.getValue(); + RequestMetadata feignRequestMetadata = feignRestMethodMetadata.getRequest(); + DubboRestServiceMetadata metadata = repository.get(serviceName, feignRequestMetadata); + if (metadata != null) { + DubboTransportedMethodMetadata dubboTransportedMethodMetadata = entry.getKey(); + Map dubboTranslatedAttributes = dubboTransportedMethodMetadata.getAttributes(); + Method method = dubboTransportedMethodMetadata.getMethod(); + GenericService dubboGenericService = dubboGenericServiceFactory.create(metadata, dubboTranslatedAttributes); + RestMethodMetadata dubboRestMethodMetadata = metadata.getRestMethodMetadata(); + MethodMetadata methodMetadata = dubboTransportedMethodMetadata.getMethodMetadata(); + FeignMethodMetadata feignMethodMetadata = new FeignMethodMetadata(dubboGenericService, + dubboRestMethodMetadata, feignRestMethodMetadata); + feignMethodMetadataMap.put(method, feignMethodMetadata); + } + } + + return feignMethodMetadataMap; + } + + private static T cast(Object object) { + return (T) object; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/AbstractSpringCloudRegistry.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/AbstractSpringCloudRegistry.java new file mode 100644 index 00000000..8fe3ee4a --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/AbstractSpringCloudRegistry.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry; + +import org.apache.dubbo.common.Constants; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.NotifyListener; +import org.apache.dubbo.registry.RegistryFactory; +import org.apache.dubbo.registry.support.FailbackRegistry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.service.DubboMetadataService; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy; +import com.alibaba.cloud.dubbo.util.JSONUtils; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static org.apache.dubbo.common.Constants.APPLICATION_KEY; +import static org.apache.dubbo.common.Constants.GROUP_KEY; +import static org.apache.dubbo.common.Constants.PROTOCOL_KEY; +import static org.apache.dubbo.common.Constants.PROVIDER_SIDE; +import static org.apache.dubbo.common.Constants.SIDE_KEY; +import static org.apache.dubbo.common.Constants.VERSION_KEY; +import static org.springframework.util.StringUtils.hasText; + +/** + * Abstract Dubbo {@link RegistryFactory} uses Spring Cloud Service Registration abstraction, whose protocol is "spring-cloud" + * + * @author Mercy + */ +public abstract class AbstractSpringCloudRegistry extends FailbackRegistry { + + /** + * The parameter name of {@link #servicesLookupInterval} + */ + public static final String SERVICES_LOOKUP_INTERVAL_PARAM_NAME = "dubbo.services.lookup.interval"; + + protected static final String DUBBO_METADATA_SERVICE_CLASS_NAME = DubboMetadataService.class.getName(); + + private static final Set SCHEDULER_TASKS = new HashSet<>(); + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * The interval in second of lookup service names(only for Dubbo-OPS) + */ + private final long servicesLookupInterval; + + private final DiscoveryClient discoveryClient; + + private final DubboServiceMetadataRepository repository; + + private final DubboMetadataServiceProxy dubboMetadataConfigServiceProxy; + + private final JSONUtils jsonUtils; + + + protected final ScheduledExecutorService servicesLookupScheduler; + + public AbstractSpringCloudRegistry(URL url, + DiscoveryClient discoveryClient, + DubboServiceMetadataRepository dubboServiceMetadataRepository, + DubboMetadataServiceProxy dubboMetadataConfigServiceProxy, + JSONUtils jsonUtils, + ScheduledExecutorService servicesLookupScheduler) { + super(url); + this.servicesLookupInterval = url.getParameter(SERVICES_LOOKUP_INTERVAL_PARAM_NAME, 60L); + this.discoveryClient = discoveryClient; + this.repository = dubboServiceMetadataRepository; + this.dubboMetadataConfigServiceProxy = dubboMetadataConfigServiceProxy; + this.jsonUtils = jsonUtils; + this.servicesLookupScheduler = servicesLookupScheduler; + } + + protected boolean shouldRegister(URL url) { + String side = url.getParameter(SIDE_KEY); + + boolean should = PROVIDER_SIDE.equals(side); // Only register the Provider. + + if (!should) { + if (logger.isDebugEnabled()) { + logger.debug("The URL[{}] should not be registered.", url.toString()); + } + } + + return should; + } + + @Override + public final void doRegister(URL url) { + if (!shouldRegister(url)) { + return; + } + doRegister0(url); + } + + /** + * The sub-type should implement to register + * + * @param url {@link URL} + */ + protected abstract void doRegister0(URL url); + + @Override + public final void doUnregister(URL url) { + if (!shouldRegister(url)) { + return; + } + doUnregister0(url); + } + + /** + * The sub-type should implement to unregister + * + * @param url {@link URL} + */ + protected abstract void doUnregister0(URL url); + + @Override + public final void doSubscribe(URL url, NotifyListener listener) { + + if (isAdminURL(url)) { + // TODO in future + } else if (isDubboMetadataServiceURL(url)) { // for DubboMetadataService + subscribeDubboMetadataServiceURLs(url, listener); + } else { // for general Dubbo Services + subscribeDubboServiceURLs(url, listener); + } + } + + protected void subscribeDubboServiceURLs(URL url, NotifyListener listener) { + + doSubscribeDubboServiceURLs(url, listener); + + submitSchedulerTaskIfAbsent(url, listener); + } + + private void submitSchedulerTaskIfAbsent(URL url, NotifyListener listener) { + String taskId = url.toIdentityString(); + if (SCHEDULER_TASKS.add(taskId)) { + schedule(() -> doSubscribeDubboServiceURLs(url, listener)); + } + } + + protected void doSubscribeDubboServiceURLs(URL url, NotifyListener listener) { + + Set subscribedServices = repository.getSubscribedServices(); + + subscribedServices.stream() + .map(dubboMetadataConfigServiceProxy::getProxy) + .filter(Objects::nonNull) + .forEach(dubboMetadataService -> { + List exportedURLs = getExportedURLs(dubboMetadataService, url); + List allSubscribedURLs = new LinkedList<>(); + for (URL exportedURL : exportedURLs) { + String serviceName = exportedURL.getParameter(APPLICATION_KEY); + List serviceInstances = getServiceInstances(serviceName); + String protocol = exportedURL.getProtocol(); + List subscribedURLs = new LinkedList<>(); + serviceInstances.forEach(serviceInstance -> { + Integer port = repository.getDubboProtocolPort(serviceInstance, protocol); + String host = serviceInstance.getHost(); + if (port == null) { + if (logger.isWarnEnabled()) { + logger.warn("The protocol[{}] port of Dubbo service instance[host : {}] " + + "can't be resolved", protocol, host); + } + } else { + URL subscribedURL = new URL(protocol, host, port, exportedURL.getParameters()); + subscribedURLs.add(subscribedURL); + } + }); + + if (logger.isDebugEnabled()) { + logger.debug("The subscribed URL[{}] will notify all URLs : {}", url, subscribedURLs); + } + + allSubscribedURLs.addAll(subscribedURLs); + } + + listener.notify(allSubscribedURLs); + }); + } + + private List getServiceInstances(String serviceName) { + return hasText(serviceName) ? doGetServiceInstances(serviceName) : emptyList(); + } + + private List doGetServiceInstances(String serviceName) { + List serviceInstances = emptyList(); + try { + serviceInstances = discoveryClient.getInstances(serviceName); + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error(e.getMessage(), e); + } + } + return serviceInstances; + } + + private List getExportedURLs(DubboMetadataService dubboMetadataService, URL url) { + String serviceInterface = url.getServiceInterface(); + String group = url.getParameter(GROUP_KEY); + String version = url.getParameter(VERSION_KEY); + // The subscribed protocol may be null + String subscribedProtocol = url.getParameter(PROTOCOL_KEY); + String exportedURLsJSON = dubboMetadataService.getExportedURLs(serviceInterface, group, version); + return jsonUtils + .toURLs(exportedURLsJSON) + .stream() + .filter(exportedURL -> + subscribedProtocol == null || subscribedProtocol.equalsIgnoreCase(exportedURL.getProtocol()) + ).collect(Collectors.toList()); + } + + private void subscribeDubboMetadataServiceURLs(URL url, NotifyListener listener) { + String serviceInterface = url.getServiceInterface(); + String group = url.getParameter(GROUP_KEY); + String version = url.getParameter(VERSION_KEY); + String protocol = url.getParameter(PROTOCOL_KEY); + List urls = repository.findSubscribedDubboMetadataServiceURLs(serviceInterface, group, version, protocol); + listener.notify(urls); + } + + @Override + public final void doUnsubscribe(URL url, NotifyListener listener) { + if (isAdminURL(url)) { + shutdownServiceNamesLookup(); + } + } + + @Override + public boolean isAvailable() { + return !discoveryClient.getServices().isEmpty(); + } + + protected void shutdownServiceNamesLookup() { + if (servicesLookupScheduler != null) { + servicesLookupScheduler.shutdown(); + } + } + + protected boolean isAdminURL(URL url) { + return Constants.ADMIN_PROTOCOL.equals(url.getProtocol()); + } + + protected boolean isDubboMetadataServiceURL(URL url) { + return DUBBO_METADATA_SERVICE_CLASS_NAME.equals(url.getServiceInterface()); + } + + protected ScheduledFuture schedule(Runnable runnable) { + return this.servicesLookupScheduler.scheduleAtFixedRate(runnable, servicesLookupInterval, + servicesLookupInterval, TimeUnit.SECONDS); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DelegatingRegistration.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DelegatingRegistration.java new file mode 100644 index 00000000..94491b48 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DelegatingRegistration.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.serviceregistry.Registration; + +import java.net.URI; +import java.util.Map; + +/** + * The {@link Registration} of Dubbo uses an external of {@link ServiceInstance} instance as the delegate. + * + * @author Mercy + */ +class DelegatingRegistration implements Registration { + + private final ServiceInstance delegate; + + public DelegatingRegistration(ServiceInstance delegate) { + this.delegate = delegate; + } + + @Override + public String getServiceId() { + return delegate.getServiceId(); + } + + @Override + public String getHost() { + return delegate.getHost(); + } + + @Override + public int getPort() { + return delegate.getPort(); + } + + @Override + public boolean isSecure() { + return delegate.isSecure(); + } + + @Override + public URI getUri() { + return delegate.getUri(); + } + + @Override + public Map getMetadata() { + return delegate.getMetadata(); + } + + @Override + public String getScheme() { + return delegate.getScheme(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DubboServiceRegistrationEventPublishingAspect.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DubboServiceRegistrationEventPublishingAspect.java new file mode 100644 index 00000000..c6c8c210 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/DubboServiceRegistrationEventPublishingAspect.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry; + +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import com.alibaba.cloud.dubbo.registry.event.ServiceInstancePreRegisteredEvent; +import com.alibaba.cloud.dubbo.registry.event.ServiceInstanceRegisteredEvent; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; + +/** + * Dubbo Service Registration Event-Publishing Aspect + * + * @author Mercy + * @see ServiceInstancePreRegisteredEvent + * @see ServiceInstanceRegisteredEvent + */ +@Aspect +public class DubboServiceRegistrationEventPublishingAspect implements ApplicationEventPublisherAware { + + /** + * The pointcut expression for {@link ServiceRegistry#register(Registration)} + */ + public static final String REGISTER_POINTCUT_EXPRESSION = + "execution(* org.springframework.cloud.client.serviceregistry.ServiceRegistry.register(*)) && args(registration)"; + + private ApplicationEventPublisher applicationEventPublisher; + + @Before(REGISTER_POINTCUT_EXPRESSION) + public void beforeRegister(Registration registration) { + applicationEventPublisher.publishEvent(new ServiceInstancePreRegisteredEvent(registration)); + } + + @After(REGISTER_POINTCUT_EXPRESSION) + public void afterRegister(Registration registration) { + applicationEventPublisher.publishEvent(new ServiceInstanceRegisteredEvent(registration)); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistry.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistry.java new file mode 100644 index 00000000..ce09de4e --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistry.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.RegistryFactory; + +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy; +import com.alibaba.cloud.dubbo.util.JSONUtils; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +import java.util.concurrent.ScheduledExecutorService; + +/** + * Dubbo {@link RegistryFactory} uses Spring Cloud Service Registration abstraction, whose protocol is "spring-cloud" + * + * @author Mercy + */ +public class SpringCloudRegistry extends AbstractSpringCloudRegistry { + + private final DubboServiceMetadataRepository dubboServiceMetadataRepository; + + public SpringCloudRegistry(URL url, DiscoveryClient discoveryClient, + DubboServiceMetadataRepository dubboServiceMetadataRepository, + DubboMetadataServiceProxy dubboMetadataConfigServiceProxy, + JSONUtils jsonUtils, + ScheduledExecutorService servicesLookupScheduler) { + super(url, discoveryClient, dubboServiceMetadataRepository, dubboMetadataConfigServiceProxy, jsonUtils, servicesLookupScheduler); + this.dubboServiceMetadataRepository = dubboServiceMetadataRepository; + } + + @Override + protected void doRegister0(URL url) { + dubboServiceMetadataRepository.exportURL(url); + } + + @Override + protected void doUnregister0(URL url) { + dubboServiceMetadataRepository.unexportURL(url); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistryFactory.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistryFactory.java new file mode 100644 index 00000000..9cdf099c --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/SpringCloudRegistryFactory.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.NamedThreadFactory; +import org.apache.dubbo.registry.Registry; +import org.apache.dubbo.registry.RegistryFactory; + +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.service.DubboMetadataServiceProxy; +import com.alibaba.cloud.dubbo.util.JSONUtils; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.concurrent.ScheduledExecutorService; + +import static java.lang.System.getProperty; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +/** + * Dubbo {@link RegistryFactory} uses Spring Cloud Service Registration abstraction, whose protocol is "spring-cloud" + * + * @author Mercy + * @see RegistryFactory + * @see SpringCloudRegistry + */ +public class SpringCloudRegistryFactory implements RegistryFactory { + + public static String PROTOCOL = "spring-cloud"; + + public static String ADDRESS = "localhost"; + + private static String SERVICES_LOOKUP_SCHEDULER_THREAD_NAME_PREFIX = + getProperty("dubbo.services.lookup.scheduler.thread.name.prefix ", "dubbo-services-lookup-"); + + private static ConfigurableApplicationContext applicationContext; + + private final ScheduledExecutorService servicesLookupScheduler; + + private DiscoveryClient discoveryClient; + + private DubboServiceMetadataRepository dubboServiceMetadataRepository; + + private DubboMetadataServiceProxy dubboMetadataConfigServiceProxy; + + private JSONUtils jsonUtils; + + private volatile boolean initialized = false; + + public SpringCloudRegistryFactory() { + servicesLookupScheduler = newSingleThreadScheduledExecutor( + new NamedThreadFactory(SERVICES_LOOKUP_SCHEDULER_THREAD_NAME_PREFIX)); + } + + protected void init() { + if (initialized || applicationContext == null) { + return; + } + this.discoveryClient = applicationContext.getBean(DiscoveryClient.class); + this.dubboServiceMetadataRepository = applicationContext.getBean(DubboServiceMetadataRepository.class); + this.dubboMetadataConfigServiceProxy = applicationContext.getBean(DubboMetadataServiceProxy.class); + this.jsonUtils = applicationContext.getBean(JSONUtils.class); + } + + @Override + public Registry getRegistry(URL url) { + init(); + return new SpringCloudRegistry(url, discoveryClient, dubboServiceMetadataRepository, + dubboMetadataConfigServiceProxy, jsonUtils, servicesLookupScheduler); + } + + public static void setApplicationContext(ConfigurableApplicationContext applicationContext) { + SpringCloudRegistryFactory.applicationContext = applicationContext; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstancePreRegisteredEvent.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstancePreRegisteredEvent.java new file mode 100644 index 00000000..08389694 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstancePreRegisteredEvent.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry.event; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.ApplicationEvent; + +/** + * The before-{@link ServiceRegistry#register(Registration) register} event for {@link ServiceInstance} + * + * @author Mercy + */ +public class ServiceInstancePreRegisteredEvent extends ApplicationEvent { + + public ServiceInstancePreRegisteredEvent(Registration source) { + super(source); + } + + @Override + public Registration getSource() { + return (Registration) super.getSource(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstanceRegisteredEvent.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstanceRegisteredEvent.java new file mode 100644 index 00000000..0022cfac --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/registry/event/ServiceInstanceRegisteredEvent.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.registry.event; + +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; + +import java.util.EventObject; + +/** + * The after-{@link ServiceRegistry#register(Registration) register} event for {@link Registration} + * + * @author Mercy + */ +public class ServiceInstanceRegisteredEvent extends EventObject { + + public ServiceInstanceRegisteredEvent(Registration source) { + super(source); + } + + @Override + public Registration getSource() { + return (Registration) super.getSource(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContext.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContext.java new file mode 100644 index 00000000..d8586933 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContext.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.rpc.service.GenericService; + +/** + * Dubbo {@link GenericService} execution context + * + * @author Mercy + */ +public class DubboGenericServiceExecutionContext { + + private final String methodName; + + private final String[] parameterTypes; + + private final Object[] parameters; + + public DubboGenericServiceExecutionContext(String methodName, String[] parameterTypes, Object[] parameters) { + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.parameters = parameters; + } + + public String getMethodName() { + return methodName; + } + + public String[] getParameterTypes() { + return parameterTypes; + } + + public Object[] getParameters() { + return parameters; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContextFactory.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContextFactory.java new file mode 100644 index 00000000..b40fe92c --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceExecutionContextFactory.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.springframework.beans.factory.annotation.Autowired; +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import com.alibaba.cloud.dubbo.metadata.MethodMetadata; +import com.alibaba.cloud.dubbo.metadata.MethodParameterMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.service.parameter.DubboGenericServiceParameterResolver; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link DubboGenericServiceExecutionContext} Factory + * + * @author Mercy + * @see DubboGenericServiceParameterResolver + */ +public class DubboGenericServiceExecutionContextFactory { + + @Autowired(required = false) + private final List resolvers = Collections.emptyList(); + + @PostConstruct + public void init() { + AnnotationAwareOrderComparator.sort(resolvers); + } + + public DubboGenericServiceExecutionContext create(RestMethodMetadata dubboRestMethodMetadata, + RestMethodMetadata clientMethodMetadata, Object[] arguments) { + + MethodMetadata dubboMethodMetadata = dubboRestMethodMetadata.getMethod(); + + String methodName = dubboMethodMetadata.getName(); + + String[] parameterTypes = resolveParameterTypes(dubboMethodMetadata); + + Object[] parameters = resolveParameters(dubboRestMethodMetadata, clientMethodMetadata, arguments); + + return new DubboGenericServiceExecutionContext(methodName, parameterTypes, parameters); + } + + public DubboGenericServiceExecutionContext create(RestMethodMetadata dubboRestMethodMetadata, + HttpServerRequest request) { + MethodMetadata methodMetadata = dubboRestMethodMetadata.getMethod(); + + String methodName = methodMetadata.getName(); + + String[] parameterTypes = resolveParameterTypes(methodMetadata); + + Object[] parameters = resolveParameters(dubboRestMethodMetadata, request); + + return new DubboGenericServiceExecutionContext(methodName, parameterTypes, parameters); + } + + private Map buildParamNameToIndex(List params) { + Map paramNameToIndex = new LinkedHashMap<>(); + for (MethodParameterMetadata param : params) { + paramNameToIndex.put(param.getName(), param.getIndex()); + } + return paramNameToIndex; + } + + protected String[] resolveParameterTypes(MethodMetadata methodMetadata) { + + List params = methodMetadata.getParams(); + + String[] parameterTypes = new String[params.size()]; + + for (MethodParameterMetadata parameterMetadata : params) { + int index = parameterMetadata.getIndex(); + String parameterType = parameterMetadata.getType(); + parameterTypes[index] = parameterType; + } + + return parameterTypes; + } + + protected Object[] resolveParameters(RestMethodMetadata dubboRestMethodMetadata, HttpServerRequest request) { + + MethodMetadata dubboMethodMetadata = dubboRestMethodMetadata.getMethod(); + + List params = dubboMethodMetadata.getParams(); + + Object[] parameters = new Object[params.size()]; + + for (MethodParameterMetadata parameterMetadata : params) { + + int index = parameterMetadata.getIndex(); + + for (DubboGenericServiceParameterResolver resolver : resolvers) { + Object parameter = resolver.resolve(dubboRestMethodMetadata, parameterMetadata, request); + if (parameter != null) { + parameters[index] = parameter; + break; + } + } + } + + return parameters; + } + + protected Object[] resolveParameters(RestMethodMetadata dubboRestMethodMetadata, + RestMethodMetadata clientRestMethodMetadata, Object[] arguments) { + + MethodMetadata dubboMethodMetadata = dubboRestMethodMetadata.getMethod(); + + List params = dubboMethodMetadata.getParams(); + + Object[] parameters = new Object[params.size()]; + + for (MethodParameterMetadata parameterMetadata : params) { + + int index = parameterMetadata.getIndex(); + + for (DubboGenericServiceParameterResolver resolver : resolvers) { + Object parameter = resolver.resolve(dubboRestMethodMetadata, parameterMetadata, clientRestMethodMetadata, arguments); + if (parameter != null) { + parameters[index] = parameter; + break; + } + } + } + + return parameters; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceFactory.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceFactory.java new file mode 100644 index 00000000..7c9ca828 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboGenericServiceFactory.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.config.spring.ReferenceBean; +import org.apache.dubbo.rpc.service.GenericService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import com.alibaba.cloud.dubbo.metadata.DubboRestServiceMetadata; +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; +import org.springframework.util.StringUtils; +import org.springframework.validation.DataBinder; + +import javax.annotation.PreDestroy; +import java.beans.PropertyEditorSupport; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static java.util.Collections.emptyMap; +import static org.apache.dubbo.common.Constants.GROUP_KEY; +import static org.apache.dubbo.common.Constants.VERSION_KEY; +import static org.springframework.util.StringUtils.commaDelimitedListToStringArray; + +/** + * Dubbo {@link GenericService} Factory + * + * @author Mercy + */ +public class DubboGenericServiceFactory { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ConcurrentMap> cache = new ConcurrentHashMap<>(); + + @Autowired + private ObjectProvider> registryConfigs; + + public GenericService create(DubboRestServiceMetadata dubboServiceMetadata, + Map dubboTranslatedAttributes) { + + ReferenceBean referenceBean = build(dubboServiceMetadata.getServiceRestMetadata(), dubboTranslatedAttributes); + + return referenceBean == null ? null : referenceBean.get(); + } + + public GenericService create(String serviceName, Class serviceClass, String version) { + String interfaceName = serviceClass.getName(); + ReferenceBean referenceBean = build(interfaceName, version, serviceName, emptyMap()); + return referenceBean.get(); + } + + + private ReferenceBean build(ServiceRestMetadata serviceRestMetadata, + Map dubboTranslatedAttributes) { + String urlValue = serviceRestMetadata.getUrl(); + URL url = URL.valueOf(urlValue); + String interfaceName = url.getServiceInterface(); + String version = url.getParameter(VERSION_KEY); + String group = url.getParameter(GROUP_KEY); + + return build(interfaceName, version, group, dubboTranslatedAttributes); + } + + private ReferenceBean build(String interfaceName, String version, String group, + Map dubboTranslatedAttributes) { + + Integer key = Objects.hash(interfaceName, version, group, dubboTranslatedAttributes); + + return cache.computeIfAbsent(key, k -> { + ReferenceBean referenceBean = new ReferenceBean<>(); + referenceBean.setGeneric(true); + referenceBean.setInterface(interfaceName); + referenceBean.setVersion(version); + referenceBean.setGroup(group); + bindReferenceBean(referenceBean, dubboTranslatedAttributes); + return referenceBean; + }); + } + + private void bindReferenceBean(ReferenceBean referenceBean, Map dubboTranslatedAttributes) { + DataBinder dataBinder = new DataBinder(referenceBean); + // Register CustomEditors for special fields + dataBinder.registerCustomEditor(String.class, "filter", new StringTrimmerEditor(true)); + dataBinder.registerCustomEditor(String.class, "listener", new StringTrimmerEditor(true)); + dataBinder.registerCustomEditor(Map.class, "parameters", new PropertyEditorSupport() { + + @Override + public void setAsText(String text) throws java.lang.IllegalArgumentException { + // Trim all whitespace + String content = StringUtils.trimAllWhitespace(text); + if (!StringUtils.hasText(content)) { // No content , ignore directly + return; + } + // replace "=" to "," + content = StringUtils.replace(content, "=", ","); + // replace ":" to "," + content = StringUtils.replace(content, ":", ","); + // String[] to Map + Map parameters = CollectionUtils.toStringMap(commaDelimitedListToStringArray(content)); + setValue(parameters); + } + }); + + // ignore "registries" field and then use RegistryConfig beans + dataBinder.setDisallowedFields("registries"); + + dataBinder.bind(new MutablePropertyValues(dubboTranslatedAttributes)); + + registryConfigs.ifAvailable(referenceBean::setRegistries); + } + + @PreDestroy + public void destroy() { + destroyReferenceBeans(); + cache.values(); + } + + private void destroyReferenceBeans() { + Collection> referenceBeans = cache.values(); + if (logger.isInfoEnabled()) { + logger.info("The Dubbo GenericService ReferenceBeans are destroying..."); + } + for (ReferenceBean referenceBean : referenceBeans) { + referenceBean.destroy(); // destroy ReferenceBean + if (logger.isInfoEnabled()) { + logger.info("Destroyed the ReferenceBean : {} ", referenceBean); + } + } + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataService.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataService.java new file mode 100644 index 00000000..0c491952 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataService.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.annotation.Service; + +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Dubbo Metadata Service is a core interface for service subscribers, + * it must keep the stable of structure in every evolution , makes sure all subscribers' compatibility. + *

+ * The interface contract's version must be {@link #VERSION} constant and group must be current Dubbo application name + * + * @author Mercy + */ +public interface DubboMetadataService { + + /** + * Current version of the interface contract + */ + String VERSION = "1.0.0"; + + /** + * Get the json content of {@link ServiceRestMetadata} {@link Set} + * + * @return null if present + */ + String getServiceRestMetadata(); + + + /** + * Get all exported {@link URL#getServiceKey() service keys} + * + * @return non-null read-only {@link Set} + */ + Set getAllServiceKeys(); + + /** + * Get all exported Dubbo's {@link URL URLs} {@link Map} whose key is the return value of + * {@link URL#getServiceKey()} method and value is the json content of List of {@link URL URLs} + * + * @return non-null read-only {@link Map} + */ + Map getAllExportedURLs(); + + /** + * Get the json content of an exported List of {@link URL URLs} by the serviceInterface , group and version + * + * @param serviceInterface The class name of service interface + * @param group {@link Service#group() the service group} (optional) + * @param version {@link Service#version() the service version} (optional) + * @return non-null read-only {@link List} + * @see URL + */ + String getExportedURLs(String serviceInterface, String group, String version); +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceExporter.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceExporter.java new file mode 100644 index 00000000..7c04abd7 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceExporter.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ProtocolConfig; +import org.apache.dubbo.config.ServiceConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.function.Supplier; + +/** + * {@link DubboMetadataService} exporter + * + * @author Mercy + */ +@Component +public class DubboMetadataServiceExporter { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private ApplicationConfig applicationConfig; + + @Autowired + private ObjectProvider dubboMetadataService; + + @Autowired + private Supplier protocolConfigSupplier; + + @Value("${spring.application.name:${dubbo.application.name:application}}") + private String currentApplicationName; + + /** + * The ServiceConfig of DubboMetadataConfigService to be exported, can be nullable. + */ + private ServiceConfig serviceConfig; + + /** + * export {@link DubboMetadataService} as Dubbo service + * + * @return the exported {@link URL URLs} + */ + public List export() { + + if (serviceConfig == null || !serviceConfig.isExported()) { + + serviceConfig = new ServiceConfig<>(); + + serviceConfig.setInterface(DubboMetadataService.class); + // Use DubboMetadataService.VERSION as the Dubbo Service version + serviceConfig.setVersion(DubboMetadataService.VERSION); + // Use current Spring application name as the Dubbo Service group + serviceConfig.setGroup(currentApplicationName); + serviceConfig.setRef(dubboMetadataService.getIfAvailable()); + serviceConfig.setApplication(applicationConfig); + serviceConfig.setProtocol(protocolConfigSupplier.get()); + + serviceConfig.export(); + + if (logger.isInfoEnabled()) { + logger.info("The Dubbo service[{}] has been exported.", serviceConfig.toString()); + } + } + + return serviceConfig.getExportedUrls(); + } + + + /** + * unexport {@link DubboMetadataService} + */ + @PreDestroy + public void unexport() { + + if (serviceConfig == null || serviceConfig.isUnexported()) { + return; + } + + serviceConfig.unexport(); + + if (logger.isInfoEnabled()) { + logger.info("The Dubbo service[{}] has been unexported.", serviceConfig.toString()); + } + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceInvocationHandler.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceInvocationHandler.java new file mode 100644 index 00000000..9312581f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceInvocationHandler.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.rpc.service.GenericService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.stream.Stream; + +/** + * {@link DubboMetadataService} {@link InvocationHandler} + * + * @author Mercy + */ +class DubboMetadataServiceInvocationHandler implements InvocationHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final GenericService genericService; + + public DubboMetadataServiceInvocationHandler(String serviceName, String version, DubboGenericServiceFactory dubboGenericServiceFactory) { + this.genericService = dubboGenericServiceFactory.create(serviceName, DubboMetadataService.class, version); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object returnValue = null; + try { + returnValue = genericService.$invoke(method.getName(), getParameterTypes(method), args); + } catch (Throwable e) { + if (logger.isErrorEnabled()) { + logger.error(e.getMessage(), e); + } + } + return returnValue; + } + + private String[] getParameterTypes(Method method) { + Class[] parameterTypes = method.getParameterTypes(); + return Stream.of(parameterTypes).map(Class::getName).toArray(length -> new String[length]); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceProxy.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceProxy.java new file mode 100644 index 00000000..da57fd55 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/DubboMetadataServiceProxy.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.lang.reflect.Proxy.newProxyInstance; + +/** + * The proxy of {@link DubboMetadataService} + */ +public class DubboMetadataServiceProxy implements BeanClassLoaderAware, DisposableBean { + + private final DubboGenericServiceFactory dubboGenericServiceFactory; + + private ClassLoader classLoader; + + private final Map dubboMetadataServiceCache = new ConcurrentHashMap<>(); + + public DubboMetadataServiceProxy(DubboGenericServiceFactory dubboGenericServiceFactory) { + this.dubboGenericServiceFactory = dubboGenericServiceFactory; + } + + /** + * Initializes {@link DubboMetadataService}'s Proxy + * + * @param serviceName the service name + * @param version the service version + * @return a {@link DubboMetadataService} proxy + */ + public DubboMetadataService initProxy(String serviceName, String version) { + return dubboMetadataServiceCache.computeIfAbsent(serviceName, name -> newProxy(name, version)); + } + + /** + * Get a proxy instance of {@link DubboMetadataService} via the specified service name + * + * @param serviceName the service name + * @return a {@link DubboMetadataService} proxy + */ + public DubboMetadataService getProxy(String serviceName) { + return dubboMetadataServiceCache.get(serviceName); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void destroy() throws Exception { + dubboMetadataServiceCache.clear(); + } + + /** + * New a proxy instance of {@link DubboMetadataService} via the specified service name + * + * @param serviceName the service name + * @param version the service version + * @return a {@link DubboMetadataService} proxy + */ + protected DubboMetadataService newProxy(String serviceName, String version) { + return (DubboMetadataService) newProxyInstance(classLoader, new Class[]{DubboMetadataService.class}, + new DubboMetadataServiceInvocationHandler(serviceName, version, dubboGenericServiceFactory)); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/IntrospectiveDubboMetadataService.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/IntrospectiveDubboMetadataService.java new file mode 100644 index 00000000..3e4e1a66 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/IntrospectiveDubboMetadataService.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import org.apache.dubbo.common.URL; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import com.alibaba.cloud.dubbo.metadata.ServiceRestMetadata; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.util.JSONUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.unmodifiableMap; +import static org.springframework.util.CollectionUtils.isEmpty; + +/** + * Introspective {@link DubboMetadataService} implementation + * + * @author Mercy + */ +public class IntrospectiveDubboMetadataService implements DubboMetadataService { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private ObjectProvider dubboServiceMetadataRepository; + + @Autowired + private JSONUtils jsonUtils; + + @Override + public String getServiceRestMetadata() { + Set serviceRestMetadata = getRepository().getServiceRestMetadata(); + String serviceRestMetadataJsonConfig = null; + if (!isEmpty(serviceRestMetadata)) { + serviceRestMetadataJsonConfig = jsonUtils.toJSON(serviceRestMetadata); + } + return serviceRestMetadataJsonConfig; + } + + @Override + public Set getAllServiceKeys() { + return getRepository().getAllServiceKeys(); + } + + @Override + public Map getAllExportedURLs() { + Map> allExportedUrls = getRepository().getAllExportedUrls(); + if (isEmpty(allExportedUrls)) { + if (logger.isDebugEnabled()) { + logger.debug("There is no registered URL."); + } + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + + allExportedUrls.forEach((serviceKey, urls) -> { + result.put(serviceKey, jsonUtils.toJSON(urls)); + }); + + return unmodifiableMap(result); + } + + @Override + public String getExportedURLs(String serviceInterface, String group, String version) { + List urls = getRepository().getExportedURLs(serviceInterface, group, version); + return jsonUtils.toJSON(urls); + } + + private DubboServiceMetadataRepository getRepository() { + return dubboServiceMetadataRepository.getIfAvailable(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractDubboGenericServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractDubboGenericServiceParameterResolver.java new file mode 100644 index 00000000..8b6b1d6b --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractDubboGenericServiceParameterResolver.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import static org.springframework.context.ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME; +import static org.springframework.util.ClassUtils.resolveClassName; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * Abstract {@link DubboGenericServiceParameterResolver} implementation + * + * @author Mercy + */ +public abstract class AbstractDubboGenericServiceParameterResolver + implements DubboGenericServiceParameterResolver, BeanClassLoaderAware { + + private int order; + + @Autowired(required = false) + @Qualifier(CONVERSION_SERVICE_BEAN_NAME) + private ConversionService conversionService = new DefaultFormattingConversionService(); + + private ClassLoader classLoader; + + public ConversionService getConversionService() { + return conversionService; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + + protected Class resolveClass(String className) { + return resolveClassName(className, classLoader); + } + + protected Object resolveValue(Object parameterValue, String parameterType) { + Class targetType = resolveClass(parameterType); + return resolveValue(parameterValue, targetType); + } + + protected Object resolveValue(Object parameterValue, Class parameterType) { + return conversionService.convert(parameterValue, parameterType); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractNamedValueServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractNamedValueServiceParameterResolver.java new file mode 100644 index 00000000..721109f3 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/AbstractNamedValueServiceParameterResolver.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import com.alibaba.cloud.dubbo.metadata.MethodParameterMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static org.springframework.util.ObjectUtils.isEmpty; + +/** + * Abstract HTTP Names Value {@link DubboGenericServiceParameterResolver Dubbo GenericService Parameter Resolver} + * + * @author Mercy + */ +public abstract class AbstractNamedValueServiceParameterResolver extends AbstractDubboGenericServiceParameterResolver { + + /** + * Get the {@link MultiValueMap} of names and values + * + * @param request + * @return + */ + protected abstract MultiValueMap getNameAndValuesMap(HttpServerRequest request); + + @Override + public Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + HttpServerRequest request) { + + Collection names = getNames(restMethodMetadata, methodParameterMetadata); + + if (isEmpty(names)) { // index can't match + return null; + } + + MultiValueMap nameAndValues = getNameAndValuesMap(request); + + String targetName = null; + + for (String name : names) { + if (nameAndValues.containsKey(name)) { + targetName = name; + break; + } + } + + if (targetName == null) { // request parameter is abstract + return null; + } + + Class parameterType = resolveClass(methodParameterMetadata.getType()); + + Object paramValue = null; + + if (parameterType.isArray()) { // Array type + paramValue = nameAndValues.get(targetName); + } else { + paramValue = nameAndValues.getFirst(targetName); + } + + return resolveValue(paramValue, parameterType); + } + + @Override + public Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + RestMethodMetadata clientRestMethodMetadata, Object[] arguments) { + + Collection names = getNames(restMethodMetadata, methodParameterMetadata); + + if (isEmpty(names)) { // index can't match + return null; + } + + Integer index = null; + + Map> clientIndexToName = clientRestMethodMetadata.getIndexToName(); + + for (Map.Entry> entry : clientIndexToName.entrySet()) { + + Collection clientParamNames = entry.getValue(); + + if (CollectionUtils.containsAny(names, clientParamNames)) { + index = entry.getKey(); + break; + } + } + + return index > -1 ? arguments[index] : null; + } + + protected Collection getNames(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata) { + + Map> indexToName = restMethodMetadata.getIndexToName(); + + int index = methodParameterMetadata.getIndex(); + + Collection paramNames = indexToName.get(index); + + return paramNames == null ? Collections.emptyList() : paramNames; + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/DubboGenericServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/DubboGenericServiceParameterResolver.java new file mode 100644 index 00000000..1700c303 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/DubboGenericServiceParameterResolver.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import com.alibaba.cloud.dubbo.metadata.MethodParameterMetadata; + +import org.apache.dubbo.rpc.service.GenericService; +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.core.Ordered; + +/** + * Dubbo {@link GenericService} Parameter Resolver + * + * @author Mercy + */ +public interface DubboGenericServiceParameterResolver extends Ordered { + + /** + * Resolves a method parameter into an argument value from a given request. + * + * @return + */ + Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + HttpServerRequest request); + + Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + RestMethodMetadata clientRestMethodMetadata, Object[] arguments); +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/PathVariableServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/PathVariableServiceParameterResolver.java new file mode 100644 index 00000000..e861fa60 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/PathVariableServiceParameterResolver.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import org.springframework.util.MultiValueMap; + +/** + * HTTP Request Path Variable {@link DubboGenericServiceParameterResolver Dubbo GenericService Parameter Resolver} + * + * @author Mercy + */ +public class PathVariableServiceParameterResolver extends AbstractNamedValueServiceParameterResolver { + + public static final int DEFAULT_ORDER = 3; + + public PathVariableServiceParameterResolver() { + super(); + setOrder(DEFAULT_ORDER); + } + + @Override + protected MultiValueMap getNameAndValuesMap(HttpServerRequest request) { + return request.getQueryParams(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestBodyServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestBodyServiceParameterResolver.java new file mode 100644 index 00000000..f9c2020c --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestBodyServiceParameterResolver.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import com.alibaba.cloud.dubbo.http.converter.HttpMessageConverterHolder; +import com.alibaba.cloud.dubbo.http.util.HttpMessageConverterResolver; +import com.alibaba.cloud.dubbo.metadata.MethodParameterMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.Collections; +import java.util.Objects; + +/** + * HTTP Request Body {@link DubboGenericServiceParameterResolver} + * + * @author Mercy + */ +public class RequestBodyServiceParameterResolver extends AbstractDubboGenericServiceParameterResolver { + + public static final int DEFAULT_ORDER = 7; + + @Autowired + private ObjectProvider httpMessageConverters; + + private HttpMessageConverterResolver httpMessageConverterResolver; + + public RequestBodyServiceParameterResolver() { + super(); + setOrder(DEFAULT_ORDER); + } + + @PostConstruct + public void init() { + HttpMessageConverters httpMessageConverters = this.httpMessageConverters.getIfAvailable(); + + httpMessageConverterResolver = new HttpMessageConverterResolver(httpMessageConverters == null ? + Collections.emptyList() : httpMessageConverters.getConverters(), + getClassLoader()); + } + + private boolean supportParameter(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata) { + + Integer index = methodParameterMetadata.getIndex(); + + Integer bodyIndex = restMethodMetadata.getBodyIndex(); + + if (!Objects.equals(index, bodyIndex)) { + return false; + } + + Class parameterType = resolveClass(methodParameterMetadata.getType()); + + Class bodyType = resolveClass(restMethodMetadata.getBodyType()); + + return Objects.equals(parameterType, bodyType); + } + + @Override + public Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + HttpServerRequest request) { + + if (!supportParameter(restMethodMetadata, methodParameterMetadata)) { + return null; + } + + Object result = null; + + Class parameterType = resolveClass(methodParameterMetadata.getType()); + + HttpMessageConverterHolder holder = httpMessageConverterResolver.resolve(request, parameterType); + + if (holder != null) { + HttpMessageConverter converter = holder.getConverter(); + try { + result = converter.read(parameterType, request); + } catch (IOException e) { + throw new HttpMessageNotReadableException("I/O error while reading input message", e); + } + } + + return result; + } + + @Override + public Object resolve(RestMethodMetadata restMethodMetadata, MethodParameterMetadata methodParameterMetadata, + RestMethodMetadata clientRestMethodMetadata, Object[] arguments) { + + if (!supportParameter(restMethodMetadata, methodParameterMetadata)) { + return null; + } + + Integer clientBodyIndex = clientRestMethodMetadata.getBodyIndex(); + return arguments[clientBodyIndex]; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestHeaderServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestHeaderServiceParameterResolver.java new file mode 100644 index 00000000..7581f466 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestHeaderServiceParameterResolver.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import org.springframework.util.MultiValueMap; + +/** + * HTTP Request Header {@link DubboGenericServiceParameterResolver Dubbo GenericService Parameter Resolver} + * + * @author Mercy + */ +public class RequestHeaderServiceParameterResolver extends AbstractNamedValueServiceParameterResolver { + + public static final int DEFAULT_ORDER = 9; + + public RequestHeaderServiceParameterResolver() { + super(); + setOrder(DEFAULT_ORDER); + } + + @Override + protected MultiValueMap getNameAndValuesMap(HttpServerRequest request) { + return request.getHeaders(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestParamServiceParameterResolver.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestParamServiceParameterResolver.java new file mode 100644 index 00000000..18cd92d2 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/service/parameter/RequestParamServiceParameterResolver.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service.parameter; + +import com.alibaba.cloud.dubbo.http.HttpServerRequest; +import org.springframework.util.MultiValueMap; + +/** + * HTTP Request Parameter {@link DubboGenericServiceParameterResolver Dubbo GenericService Parameter Resolver} + * + * @author Mercy + */ +public class RequestParamServiceParameterResolver extends AbstractNamedValueServiceParameterResolver { + + public static final int DEFAULT_ORDER = 1; + + public RequestParamServiceParameterResolver() { + super(); + setOrder(DEFAULT_ORDER); + } + + @Override + protected MultiValueMap getNameAndValuesMap(HttpServerRequest request) { + return request.getQueryParams(); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/util/JSONUtils.java b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/util/JSONUtils.java new file mode 100644 index 00000000..a1290a18 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/java/com/alibaba/cloud/dubbo/util/JSONUtils.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.util; + +import org.apache.dubbo.common.URL; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * JSON Utilities class + * + * @author Mercy + */ +public class JSONUtils { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @PostConstruct + public void init() { + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + public String toJSON(Collection urls) { + return toJSON(urls.stream().map(URL::toFullString).collect(Collectors.toSet())); + } + + public String toJSON(Object object) { + String jsonContent = null; + try { + jsonContent = objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + if (logger.isErrorEnabled()) { + logger.error(e.getMessage(), e); + } + } + return jsonContent; + } + + public List toURLs(String urlsJSON) { + List list = toList(urlsJSON); + return list.stream().map(URL::valueOf).collect(Collectors.toList()); + } + + public List toList(String json) { + List list = Collections.emptyList(); + try { + if (StringUtils.hasText(json)) { + list = objectMapper.readValue(json, List.class); + } + } catch (IOException e) { + if (logger.isErrorEnabled()) { + logger.error(e.getMessage(), e); + } + } + return list; + } +} diff --git a/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.registry.RegistryFactory b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.registry.RegistryFactory new file mode 100644 index 00000000..d8b83d57 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.registry.RegistryFactory @@ -0,0 +1 @@ +spring-cloud=com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/default/actuator-endpoints.properties b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/default/actuator-endpoints.properties new file mode 100644 index 00000000..4c9e0c57 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/dubbo/default/actuator-endpoints.properties @@ -0,0 +1,7 @@ +# Dubbo Endpoints Default Properties is loaded by @PropertySource with low order, +# Set enabled for Dubbo Endpoints +management.endpoint.dubborestmetadata.enabled = true + +# "management.endpoints.web.base-path" should not be configured in this file +# Re-defines path-mapping of Dubbo Web Endpoints +management.endpoints.web.path-mapping.dubborestmetadata = dubbo/rest/metadata \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..dbaa6c57 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,16 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.dubbo.autoconfigure.DubboMetadataAutoConfiguration,\ +com.alibaba.cloud.dubbo.autoconfigure.DubboOpenFeignAutoConfiguration,\ +com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration,\ +com.alibaba.cloud.dubbo.autoconfigure.DubboServiceRegistrationNonWebApplicationAutoConfiguration,\ +com.alibaba.cloud.dubbo.autoconfigure.DubboLoadBalancedRestTemplateAutoConfiguration,\ +com.alibaba.cloud.dubbo.autoconfigure.DubboServiceAutoConfiguration + +org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\ +com.alibaba.cloud.dubbo.actuate.DubboMetadataEndpointAutoConfiguration + +org.springframework.context.ApplicationContextInitializer=\ +com.alibaba.cloud.dubbo.context.DubboServiceRegistrationApplicationContextInitializer + +org.springframework.boot.env.EnvironmentPostProcessor=\ +com.alibaba.cloud.dubbo.env.DubboNonWebApplicationEnvironmentPostProcessor \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfigurationTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfigurationTest.java new file mode 100644 index 00000000..22dc2e02 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/autoconfigure/DubboServiceRegistrationAutoConfigurationTest.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.autoconfigure; + +import org.springframework.boot.test.context.SpringBootTest; + +/** + * {@link DubboServiceRegistrationAutoConfiguration} Test + * + * @author Mercy + */ +@SpringBootTest +public class DubboServiceRegistrationAutoConfigurationTest { +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcherTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcherTest.java new file mode 100644 index 00000000..5bdb881b --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractHttpRequestMatcherTest.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Test; + +/** + * {@link AbstractHttpRequestMatcher} Test + * + * @author Mercy + */ +public abstract class AbstractHttpRequestMatcherTest { + + @Test + public abstract void testEqualsAndHashCode(); + + @Test + public abstract void testGetContent(); + + @Test + public abstract void testGetToStringInfix(); + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpressionTest.java new file mode 100644 index 00000000..dcdc92ed --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractMediaTypeExpressionTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.BeanUtils; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +import java.lang.reflect.Constructor; + +/** + * {@link AbstractMediaTypeExpression} Test + * + * @author Mercy + */ +public abstract class AbstractMediaTypeExpressionTest { + + protected T createExpression(String expression) { + ResolvableType resolvableType = ResolvableType.forType(getClass().getGenericSuperclass()); + Class firstGenericType = (Class) resolvableType.resolveGeneric(0); + Constructor constructor = null; + try { + constructor = firstGenericType.getDeclaredConstructor(String.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + return BeanUtils.instantiateClass(constructor, expression); + } + + @Test + public void testGetMediaTypeAndNegated() { + // Normal + AbstractMediaTypeExpression expression = createExpression(MediaType.APPLICATION_JSON_VALUE); + Assert.assertEquals(MediaType.APPLICATION_JSON, expression.getMediaType()); + Assert.assertFalse(expression.isNegated()); + + // Negated + expression = createExpression("!" + MediaType.APPLICATION_JSON_VALUE); + Assert.assertEquals(MediaType.APPLICATION_JSON, expression.getMediaType()); + Assert.assertTrue(expression.isNegated()); + } + + @Test + public void testEqualsAndHashCode() { + Assert.assertEquals(createExpression(MediaType.APPLICATION_JSON_VALUE), createExpression(MediaType.APPLICATION_JSON_VALUE)); + Assert.assertEquals(createExpression(MediaType.APPLICATION_JSON_VALUE).hashCode(), + createExpression(MediaType.APPLICATION_JSON_VALUE).hashCode()); + } + + @Test + public void testCompareTo() { + Assert.assertEquals(0, + createExpression(MediaType.APPLICATION_JSON_VALUE).compareTo(createExpression(MediaType.APPLICATION_JSON_VALUE))); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpressionTest.java new file mode 100644 index 00000000..336d8843 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/AbstractNameValueExpressionTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.BeanUtils; +import org.springframework.core.ResolvableType; + +import java.lang.reflect.Constructor; + +/** + * {@link AbstractNameValueExpression} Test + * + * @author Mercy + */ +public abstract class AbstractNameValueExpressionTest { + + protected T createExpression(String expression) { + ResolvableType resolvableType = ResolvableType.forType(getClass().getGenericSuperclass()); + Class firstGenericType = (Class) resolvableType.resolveGeneric(0); + Constructor constructor = null; + try { + constructor = firstGenericType.getDeclaredConstructor(String.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + return BeanUtils.instantiateClass(constructor, expression); + } + + @Test + public void testGetNameAndValue() { + // Normal Name and value + AbstractNameValueExpression expression = createExpression("a=1"); + Assert.assertEquals("a", expression.getName()); + Assert.assertFalse(expression.isNegated()); + + expression = createExpression("a=1"); + Assert.assertEquals("a", expression.getName()); + Assert.assertEquals("1", expression.getValue()); + Assert.assertFalse(expression.isNegated()); + + // Negated Name + expression = createExpression("!a"); + Assert.assertEquals("a", expression.getName()); + Assert.assertTrue(expression.isNegated()); + + expression = createExpression("a!=1"); + Assert.assertEquals("a", expression.getName()); + Assert.assertEquals("1", expression.getValue()); + Assert.assertTrue(expression.isNegated()); + } + + @Test + public void testEqualsAndHashCode() { + Assert.assertEquals(createExpression("a"), createExpression("a")); + Assert.assertEquals(createExpression("a").hashCode(), createExpression("a").hashCode()); + Assert.assertEquals(createExpression("a=1"), createExpression("a = 1 ")); + Assert.assertEquals(createExpression("a=1").hashCode(), createExpression("a = 1 ").hashCode()); + Assert.assertNotEquals(createExpression("a"), createExpression("b")); + Assert.assertNotEquals(createExpression("a").hashCode(), createExpression("b").hashCode()); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpressionTest.java new file mode 100644 index 00000000..144332c8 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ConsumeMediaTypeExpressionTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.MediaType; + +/** + * {@link ConsumeMediaTypeExpression} Test + * + * @author Mercy + */ +public class ConsumeMediaTypeExpressionTest extends AbstractMediaTypeExpressionTest { + + @Test + public void testMatch() { + ConsumeMediaTypeExpression expression = createExpression(MediaType.ALL_VALUE); + + Assert.assertTrue(expression.match(MediaType.APPLICATION_JSON_UTF8)); + + expression = createExpression(MediaType.APPLICATION_JSON_VALUE); + Assert.assertTrue(expression.match(MediaType.APPLICATION_JSON_UTF8)); + + expression = createExpression(MediaType.APPLICATION_JSON_VALUE + ";q=0.7"); + Assert.assertTrue(expression.match(MediaType.APPLICATION_JSON_UTF8)); + + expression = createExpression(MediaType.TEXT_HTML_VALUE); + Assert.assertFalse(expression.match(MediaType.APPLICATION_JSON_UTF8)); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpressionTest.java new file mode 100644 index 00000000..69ff08ab --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HeaderExpressionTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.HttpRequest; + +import static com.alibaba.cloud.dubbo.http.DefaultHttpRequest.builder; + +/** + * {@link HeaderExpression} Test + * + * @author Mercy + */ +public class HeaderExpressionTest extends AbstractNameValueExpressionTest { + + @Test + public void testIsCaseSensitiveName() { + Assert.assertFalse(createExpression("a=1").isCaseSensitiveName()); + Assert.assertFalse(createExpression("a=!1").isCaseSensitiveName()); + Assert.assertFalse(createExpression("b=1").isCaseSensitiveName()); + } + + @Test + public void testMatch() { + + HeaderExpression expression = createExpression("a=1"); + HttpRequest request = builder().build(); + + Assert.assertFalse(expression.match(request)); + + request = builder().header("a", "").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().header("a", "2").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().header("", "1").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().header("a", "1").build(); + Assert.assertTrue(expression.match(request)); + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcherTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcherTest.java new file mode 100644 index 00000000..1da5db43 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestMethodsMatcherTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.springframework.http.HttpMethod; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * {@link HttpRequestMethodsMatcher} Test + * + * @author Mercy + */ +public class HttpRequestMethodsMatcherTest extends AbstractHttpRequestMatcherTest { + + HttpRequestMethodsMatcher matcher = new HttpRequestMethodsMatcher("GET"); + + @Override + public void testEqualsAndHashCode() { + Assert.assertEquals(new HashSet<>(Arrays.asList(HttpMethod.GET)), matcher.getMethods()); + } + + @Override + public void testGetContent() { + Assert.assertEquals(new HashSet<>(Arrays.asList(HttpMethod.GET)), matcher.getContent()); + } + + @Override + public void testGetToStringInfix() { + Assert.assertEquals(" || ", matcher.getToStringInfix()); + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcherTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcherTest.java new file mode 100644 index 00000000..38146434 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/HttpRequestParamsMatcherTest.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.http.client.MockClientHttpRequest; + +import java.net.URI; + +/** + * {@link HttpRequestParamsMatcher} Test + * + * @author Mercy + */ +public class HttpRequestParamsMatcherTest { + +// @Test +// public void testGetParams() { +// +// HttpRequestParamsMatcher matcher = new HttpRequestParamsMatcher( +// "a ", +// "a =1", +// "b = 2", +// "b = 3 ", +// " c = 4 ", +// " d" +// ); +// +// Map> params = matcher.getParams(); +// Assert.assertEquals(4, params.size()); +// Assert.assertTrue(params.containsKey("a")); +// Assert.assertTrue(params.containsKey("b")); +// Assert.assertTrue(params.containsKey("c")); +// Assert.assertTrue(params.containsKey("d")); +// Assert.assertFalse(params.containsKey("e")); +// +// List values = params.get("a"); +// Assert.assertEquals(2, values.size()); +// Assert.assertEquals("", values.get(0)); +// Assert.assertEquals("1", values.get(1)); +// +// values = params.get("b"); +// Assert.assertEquals(2, values.size()); +// Assert.assertEquals("2", values.get(0)); +// Assert.assertEquals("3", values.get(1)); +// +// values = params.get("c"); +// Assert.assertEquals(1, values.size()); +// Assert.assertEquals("4", values.get(0)); +// +// values = params.get("d"); +// Assert.assertEquals(1, values.size()); +// Assert.assertEquals("", values.get(0)); +// } + + @Test + public void testEquals() { + + HttpRequestParamsMatcher matcher = new HttpRequestParamsMatcher("a ", "a = 1"); + + MockClientHttpRequest request = new MockClientHttpRequest(); + + request.setURI(URI.create("http://dummy/?a")); + Assert.assertTrue(matcher.match(request)); + request.setURI(URI.create("http://dummy/?a&a=1")); + Assert.assertTrue(matcher.match(request)); + + matcher = new HttpRequestParamsMatcher("a ", "a =1", "b", "b=2"); + request.setURI(URI.create("http://dummy/?a&a=1&b")); + Assert.assertTrue(matcher.match(request)); + request.setURI(URI.create("http://dummy/?a&a=1&b&b=2")); + Assert.assertTrue(matcher.match(request)); + + matcher = new HttpRequestParamsMatcher("a ", "a =1", "b", "b=2", "b = 3 "); + request.setURI(URI.create("http://dummy/?a&a=1&b&b=2&b=3")); + Assert.assertTrue(matcher.match(request)); + + request.setURI(URI.create("http://dummy/?d=1")); + Assert.assertFalse(matcher.match(request)); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpressionTest.java new file mode 100644 index 00000000..f512bdd9 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ParamExpressionTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.HttpRequest; + +import static com.alibaba.cloud.dubbo.http.DefaultHttpRequest.builder; + +/** + * {@link ParamExpression} Test + * + * @author Mercy + */ +public class ParamExpressionTest extends AbstractNameValueExpressionTest { + + @Test + public void testIsCaseSensitiveName() { + Assert.assertTrue(createExpression("a=1").isCaseSensitiveName()); + Assert.assertTrue(createExpression("a=!1").isCaseSensitiveName()); + Assert.assertTrue(createExpression("b=1").isCaseSensitiveName()); + } + + @Test + public void testMatch() { + + ParamExpression expression = createExpression("a=1"); + HttpRequest request = builder().build(); + + Assert.assertFalse(expression.match(request)); + + request = builder().param("a", "").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().param("a", "2").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().param("", "1").build(); + Assert.assertFalse(expression.match(request)); + + request = builder().param("a", "1").build(); + Assert.assertTrue(expression.match(request)); + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpressionTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpressionTest.java new file mode 100644 index 00000000..1df8fdd8 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/matcher/ProduceMediaTypeExpressionTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.matcher; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.MediaType; + +import java.util.Arrays; + +/** + * {@link ProduceMediaTypeExpression} Test + * + * @author Mercy + */ +public class ProduceMediaTypeExpressionTest extends AbstractMediaTypeExpressionTest { + + @Test + public void testMatch() { + ProduceMediaTypeExpression expression = createExpression(MediaType.APPLICATION_JSON_VALUE); + Assert.assertTrue(expression.match(Arrays.asList(MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON))); + + expression = createExpression(MediaType.APPLICATION_JSON_VALUE); + Assert.assertFalse(expression.match(Arrays.asList(MediaType.APPLICATION_XML))); + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/util/HttpUtilsTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/util/HttpUtilsTest.java new file mode 100644 index 00000000..322d6e1f --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/http/util/HttpUtilsTest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.http.util; + +import org.junit.Assert; +import org.junit.Test; + +/** + * {@link HttpUtils} Test + * + * @author Mercy + */ +public class HttpUtilsTest { + + @Test + public void testEncodeAndDecode() { + + String whitespace = " "; + + String encodedValue = HttpUtils.encode(" "); + + String decodedValue = HttpUtils.decode(encodedValue); + + Assert.assertEquals(whitespace, decodedValue); + } + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/RequestMetadataTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/RequestMetadataTest.java new file mode 100644 index 00000000..84992422 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/RequestMetadataTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * {@link RequestMetadata} Test + * + * @author Mercy + */ +public class RequestMetadataTest { + + private String method = "GET"; + + private String url = "/param"; + + private Set paramNames = new LinkedHashSet<>(Arrays.asList("a", "b", "c")); + + private Set headerNames = new LinkedHashSet<>(Arrays.asList("d", "e", "f")); + + @Test + public void testEqualsAndHashCodeAndCompareTo() { + + RequestMetadata metadata = new RequestMetadata(); + RequestMetadata metadata2 = new RequestMetadata(); + + Assert.assertEquals(metadata, metadata2); + Assert.assertEquals(metadata.hashCode(), metadata2.hashCode()); + + metadata.setMethod(method); + metadata2.setMethod(method); + + Assert.assertEquals(metadata, metadata2); + Assert.assertEquals(metadata.hashCode(), metadata2.hashCode()); + + metadata.setPath(url); + metadata2.setPath(url); + + Assert.assertEquals(metadata, metadata2); + Assert.assertEquals(metadata.hashCode(), metadata2.hashCode()); + + metadata.addParam("a", "1").addParam("b", "2").addParam("c", "3"); + metadata2.addParam("a", "1a").addParam("b", "2b").addParam("c", "3c"); + + Assert.assertEquals(metadata, metadata2); + Assert.assertEquals(metadata.hashCode(), metadata2.hashCode()); + + metadata.addHeader("d", "1").addHeader("e", "2").addHeader("f", "3"); + metadata2.addHeader("d", "1").addHeader("e", "2"); + + Assert.assertNotEquals(metadata, metadata2); + Assert.assertNotEquals(metadata.hashCode(), metadata2.hashCode()); + } + +// @Test +// public void testBestMatch() { +// +// NavigableMap requestMetadataMap = new TreeMap<>(); +// +// RequestMetadata metadata = new RequestMetadata(); +// metadata.setMethod(method); +// +// RequestMetadata metadata1 = new RequestMetadata(); +// metadata1.setMethod(method); +// metadata1.setPath(url); +// +// RequestMetadata metadata2 = new RequestMetadata(); +// metadata2.setMethod(method); +// metadata2.setPath(url); +// metadata2.setParams(paramNames); +// +// RequestMetadata metadata3 = new RequestMetadata(); +// metadata3.setMethod(method); +// metadata3.setPath(url); +// metadata3.setParams(paramNames); +// metadata3.setHeaders(headerNames); +// +// requestMetadataMap.put(metadata, metadata); +// requestMetadataMap.put(metadata1, metadata1); +// requestMetadataMap.put(metadata2, metadata2); +// requestMetadataMap.put(metadata3, metadata3); +// +// RequestMetadata result = getBestMatch(requestMetadataMap, metadata); +// Assert.assertEquals(result, metadata); +// +// result = getBestMatch(requestMetadataMap, metadata1); +// Assert.assertEquals(result, metadata1); +// +// result = getBestMatch(requestMetadataMap, metadata2); +// Assert.assertEquals(result, metadata2); +// +// result = getBestMatch(requestMetadataMap, metadata3); +// Assert.assertEquals(result, metadata3); +// +// // REDO +// requestMetadataMap.clear(); +// +// requestMetadataMap.put(metadata1, metadata1); +// +// result = getBestMatch(requestMetadataMap, metadata2); +// Assert.assertEquals(metadata1, result); +// +// requestMetadataMap.put(metadata2, metadata2); +// +// result = getBestMatch(requestMetadataMap, metadata3); +// Assert.assertEquals(metadata2, result); +// +// result = getBestMatch(requestMetadataMap, new RequestMetadata()); +// Assert.assertNull(result); +// +// result = getBestMatch(requestMetadataMap, metadata); +// Assert.assertNull(result); +// +// } + +} diff --git a/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolverTest.java b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolverTest.java new file mode 100644 index 00000000..b1cc3a74 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/java/com/alibaba/cloud/dubbo/metadata/resolver/DubboTransportedMethodMetadataResolverTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.metadata.resolver; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import com.alibaba.cloud.dubbo.metadata.DubboTransportedMethodMetadata; +import org.springframework.cloud.openfeign.support.SpringMvcContract; +import org.springframework.mock.env.MockEnvironment; + +import java.util.Set; + +/** + * {@link DubboTransportedMethodMetadataResolver} Test + * + * @author Mercy + */ +public class DubboTransportedMethodMetadataResolverTest { + + private DubboTransportedMethodMetadataResolver resolver; + + private MockEnvironment environment; + + @Before + public void init() { + environment = new MockEnvironment(); + resolver = new DubboTransportedMethodMetadataResolver(environment, new SpringMvcContract()); + } + + @Test + public void testResolve() { + Set metadataSet = resolver.resolveDubboTransportedMethodMetadataSet(TestDefaultService.class); + Assert.assertEquals(1, metadataSet.size()); + } + + + @DubboTransported + interface TestDefaultService { + + String test(String message); + + } +} diff --git a/spring-cloud-alibaba-dubbo/src/test/resources/application.yaml b/spring-cloud-alibaba-dubbo/src/test/resources/application.yaml new file mode 100644 index 00000000..455d0b31 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/resources/application.yaml @@ -0,0 +1,20 @@ +dubbo: + scan: + base-packages: com.alibaba.cloud.dubbo.service + protocols: + dubbo: + name: dubbo + port: 12345 + rest: + name: rest + port: 8081 + server: netty + registry: + address: spring-cloud://nacos + +feign: + hystrix: + enabled: true + +server: + port: 8080 \ No newline at end of file diff --git a/spring-cloud-alibaba-dubbo/src/test/resources/bootstrap.yaml b/spring-cloud-alibaba-dubbo/src/test/resources/bootstrap.yaml new file mode 100644 index 00000000..9b1aad42 --- /dev/null +++ b/spring-cloud-alibaba-dubbo/src/test/resources/bootstrap.yaml @@ -0,0 +1,47 @@ +spring: + application: + name: spring-cloud-alibaba-dubbo + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 + config: + server-addr: 127.0.0.1:8848 + zookeeper: + enabled: false + + main: + allow-bean-definition-overriding: true + +eureka: + client: + enabled: false + +--- +spring: + profiles: eureka + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + +eureka: + client: + enabled: true + service-url: + defaultZone: http://localhost:9090/eureka/ + +--- +spring: + profiles: zookeeper + + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + + zookeeper: + enabled: true + connect-string: localhost:2181 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/nacos-discovery-spring-cloud-config-client/src/main/java/com/alibaba/cloud/examples/SpringCloudConfigClientApplication.java b/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/nacos-discovery-spring-cloud-config-client/src/main/java/com/alibaba/cloud/examples/SpringCloudConfigClientApplication.java new file mode 100644 index 00000000..4968266c --- /dev/null +++ b/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/nacos-discovery-spring-cloud-config-client/src/main/java/com/alibaba/cloud/examples/SpringCloudConfigClientApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.examples; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author JevonYang + */ +@SpringBootApplication +@EnableDiscoveryClient +public class SpringCloudConfigClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringCloudConfigClientApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsApplication.java b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsApplication.java new file mode 100644 index 00000000..07c0d68f --- /dev/null +++ b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * + */ +@SpringBootApplication +public class SmsApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SmsApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsController.java b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsController.java new file mode 100644 index 00000000..b34862ce --- /dev/null +++ b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsController.java @@ -0,0 +1,145 @@ +package com.alibaba.cloud.example; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.alibaba.alicloud.sms.ISmsService; + +import com.aliyun.mns.model.Message; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; + +@RestController +public class SmsController { + + @Autowired + private Environment environment; + + @Autowired + private ISmsService smsService; + + @Autowired + private SmsReportMessageListener smsReportMessageListener; + + @GetMapping("/report-queue.do") + public String getSmsReportQueuename() { + + return environment.getProperty("spring.cloud.alicloud.sms.up-queue-name"); + } + + /** + * 短信发送 Example + * @param code + * @return + */ + @RequestMapping("/batch-sms-send.do") + public SendBatchSmsResponse batchsendCheckCode( + @RequestParam(name = "code") String code) { + // 组装请求对象 + SendBatchSmsRequest request = new SendBatchSmsRequest(); + // 使用post提交 + request.setMethod(MethodType.GET); + // 必填:待发送手机号。支持JSON格式的批量调用,批量上限为100个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式 + request.setPhoneNumberJson("[\"177********\",\"130********\"]"); + // 必填:短信签名-支持不同的号码发送不同的短信签名 + request.setSignNameJson("[\"*******\",\"*******\"]"); + // 必填:短信模板-可在短信控制台中找到 + request.setTemplateCode("******"); + // 必填:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为 + // 友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败 + request.setTemplateParamJson( + "[{\"code\":\"" + code + "\"},{\"code\":\"" + code + "\"}]"); + // 可选-上行短信扩展码(扩展码字段控制在7位或以下,无特殊需求用户请忽略此字段) + // request.setSmsUpExtendCodeJson("[\"90997\",\"90998\"]"); + try { + SendBatchSmsResponse sendSmsResponse = smsService + .sendSmsBatchRequest(request); + return sendSmsResponse; + } + catch (ClientException e) { + e.printStackTrace(); + } + return new SendBatchSmsResponse(); + } + + /** + * 短信发送 Example + * @param code + * @return + */ + @RequestMapping("/sms-send.do") + public SendSmsResponse sendCheckCode(@RequestParam(name = "code") String code) { + // 组装请求对象-具体描述见控制台-文档部分内容 + SendSmsRequest request = new SendSmsRequest(); + // 必填:待发送手机号 + request.setPhoneNumbers("******"); + // 必填:短信签名-可在短信控制台中找到 + request.setSignName("******"); + // 必填:短信模板-可在短信控制台中找到 + request.setTemplateCode("******"); + // 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为 + request.setTemplateParam("{\"code\":\"" + code + "\"}"); + + // 选填-上行短信扩展码(无特殊需求用户请忽略此字段) + // request.setSmsUpExtendCode("90997"); + + // 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者 + request.setOutId("****TraceId"); + try { + SendSmsResponse sendSmsResponse = smsService.sendSmsRequest(request); + return sendSmsResponse; + } + catch (ClientException e) { + e.printStackTrace(); + } + return new SendSmsResponse(); + } + + /** + * + * 短信查询 Example + * @param telephone + * @return + */ + @RequestMapping("/query.do") + public QuerySendDetailsResponse querySendDetailsResponse( + @RequestParam(name = "tel") String telephone) { + // 组装请求对象 + QuerySendDetailsRequest request = new QuerySendDetailsRequest(); + // 必填-号码 + request.setPhoneNumber(telephone); + // 必填-短信发送的日期 支持30天内记录查询(可查其中一天的发送数据),格式yyyyMMdd + request.setSendDate("20190103"); + // 必填-页大小 + request.setPageSize(10L); + // 必填-当前页码从1开始计数 + request.setCurrentPage(1L); + try { + QuerySendDetailsResponse response = smsService.querySendDetails(request); + return response; + } + catch (ClientException e) { + e.printStackTrace(); + } + + return new QuerySendDetailsResponse(); + } + + @RequestMapping("/sms-report.do") + public List smsReport() { + + return smsReportMessageListener.getSmsReportMessageSet(); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsReportMessageListener.java b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsReportMessageListener.java new file mode 100644 index 00000000..a94b6bcc --- /dev/null +++ b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsReportMessageListener.java @@ -0,0 +1,29 @@ +package com.alibaba.cloud.example; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.aliyun.mns.model.Message; + +/** + * @author 如果需要监听短信是否被对方成功接收,只需实现这个接口并初始化一个 Spring Bean 即可。 + */ +@Component +public class SmsReportMessageListener + implements com.alibaba.alicloud.sms.SmsReportMessageListener { + private List smsReportMessageSet = new LinkedList<>(); + + @Override + public boolean dealMessage(Message message) { + smsReportMessageSet.add(message); + System.err.println(this.getClass().getName() + "; " + message.toString()); + return true; + } + + public List getSmsReportMessageSet() { + + return smsReportMessageSet; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsUpMessageListener.java b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsUpMessageListener.java new file mode 100644 index 00000000..df8e971e --- /dev/null +++ b/spring-cloud-alibaba-examples/sms-example/src/main/java/com/alibaba/cloud/example/SmsUpMessageListener.java @@ -0,0 +1,19 @@ +package com.alibaba.cloud.example; + +import org.springframework.stereotype.Component; + +import com.aliyun.mns.model.Message; + +/** + * @author 如果发送的短信需要接收对方回复的状态消息,只需实现该接口并初始化一个 Spring Bean 即可。 + */ +@Component +public class SmsUpMessageListener + implements com.alibaba.alicloud.sms.SmsUpMessageListener { + + @Override + public boolean dealMessage(Message message) { + System.err.println(this.getClass().getName() + "; " + message.toString()); + return true; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/README_CN.md b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/README_CN.md new file mode 100644 index 00000000..30c2d3a6 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/README_CN.md @@ -0,0 +1,348 @@ +# Dubbo Spring Cloud 示例工程 + + +## 快速开始 + +### 定义 Dubbo 服务接口 + +Dubbo 服务接口是服务提供方与消费方的远程通讯契约,通常由普通的 Java 接口(interface)来声明,如 `EchoService` 接口: + +```java +public interface EchoService { + + String echo(String message); +} +``` + +为了确保契约的一致性,推荐的做法是将 Dubbo 服务接口打包在第二方或者第三方的 artifact(jar)中,如以上接口就存放在 + artifact [spring-cloud-dubbo-sample-api](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api) 之中。 +对于服务提供方而言,不仅通过依赖 artifact 的形式引入 Dubbo 服务接口,而且需要将其实现。对应的服务消费端,同样地需要依赖该 artifact, +并以接口调用的方式执行远程方法。接下来进一步讨论怎样实现 Dubbo 服务提供方和消费方。 + + +### 实现 Dubbo 服务提供方 + + + +#### 初始化 `spring-cloud-dubbo-server-sample` Maven 工程 + +首先,创建 `artifactId` 名为 `spring-cloud-dubbo-server-sample` 的 Maven 工程,并在其 `pom.xml` 文件中增添 +Dubbo Spring Cloud 必要的依赖: + +```xml + + + + org.springframework.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + org.springframework.boot + spring-boot-actuator + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + +``` + +以上依赖 artifact 说明如下: + +- `spring-cloud-dubbo-sample-api` : 提供 `EchoService` 接口的 artifact +- `spring-boot-actuator` : Spring Boot Production-Ready artifact,间接引入 `spring-boot` artifact +- `spring-cloud-starter-dubbo` : Dubbo Spring Cloud Starter `artifact`,间接引入 `dubbo-spring-boot-starter` 等 artifact +- `spring-cloud-starter-alibaba-nacos-discovery` : Nacos Spring Cloud 服务注册与发现 `artifact` + + +值得注意的是,以上 artifact 未指定版本(version),因此,还需显示地声明 `` : + +```xml + + + + + org.springframework.cloud + spring-cloud-alibaba-dependencies + 0.9.0.RELEASE + pom + import + + + +``` + +> 以上完整的 Maven 依赖配置,请参考 `spring-cloud-dubbo-server-sample` [`pom.xml`](spring-cloud-dubbo-server-sample/pom.xml) 文件 + +完成以上步骤之后,下一步则是实现 Dubbo 服务 + + +#### 实现 Dubbo 服务 + +`EchoService` 作为暴露的 Dubbo 服务接口,服务提供方 `spring-cloud-dubbo-server-sample` 需要将其实现: + +```java +@org.apache.dubbo.config.annotation.Service +class EchoServiceImpl implements EchoService { + + @Override + public String echo(String message) { + return "[echo] Hello, " + message; + } +} +``` + +其中,`@org.apache.dubbo.config.annotation.Service` 是 Dubbo 服务注解,仅声明该 Java 服务(本地)实现为 Dubbo 服务。 +因此,下一步需要将其配置 Dubbo 服务(远程)。 + + + +#### 配置 Dubbo 服务提供方 + +在暴露 Dubbo 服务方面,推荐开发人员外部化配置的方式,即指定 Java 服务实现类的扫描基准包。 +> Dubbo Spring Cloud 继承了 Dubbo Spring Boot 的外部化配置特性,也可以通过标注 `@DubboComponentScan` 来实现基准包扫描。 + +同时,Dubbo 远程服务需要暴露网络端口,并设定通讯协议,完整的 YAML 配置如下所示: + +```yaml +dubbo: + scan: + # dubbo 服务扫描基准包 + base-packages: com.alibaba.cloud.dubbo.bootstrap + protocol: + # dubbo 协议 + name: dubbo + # dubbo 协议端口( -1 表示自增端口,从 20880 开始) + port: -1 + registry: + # 挂载到 Spring Cloud 注册中心 + address: spring-cloud://localhost + +spring: + application: + # Dubbo 应用名称 + name: spring-cloud-alibaba-dubbo-server + main: + # Spring Boot 2.1 需要设定 + allow-bean-definition-overriding: true + cloud: + nacos: + # Nacos 服务发现与注册配置 + discovery: + server-addr: 127.0.0.1:8848 +``` + +以上 YAML 内容,上半部分为 Dubbo 的配置: + +- `dubbo.scan.base-packages` : 指定 Dubbo 服务实现类的扫描基准包 +- `dubbo.protocol` : Dubbo 服务暴露的协议配置,其中子属性 `name` 为协议名称,`port` 为协议端口( -1 表示自增端口,从 20880 开始) +- `dubbo.registry` : Dubbo 服务注册中心配置,其中子属性 `address` 的值 "spring-cloud://localhost",说明挂载到 Spring Cloud 注册中心 +> 当前 Dubbo Spring Cloud 实现必须配置 `dubbo.registry.address = spring-cloud://localhost`,下一个版本将其配置变为可选 +(参考 [issue #592](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/issues/592)), +> 并且支持传统 Dubbo 协议的支持(参考 [issue #588](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/issues/588)) + +下半部分则是 Spring Cloud 相关配置: + +- `spring.application.name` : Spring 应用名称,用于 Spring Cloud 服务注册和发现。 +> 该值在 Dubbo Spring Cloud 加持下被视作 `dubbo.application.name`,因此,无需再显示地配置 `dubbo.application.name` +- `spring.main.allow-bean-definition-overriding` : 在 Spring Boot 2.1 以及更高的版本增加该设定, +因为 Spring Boot 默认调整了 Bean 定义覆盖行为。(推荐一个好的 Dubbo 讨论 [issue #3193](https://github.com/apache/dubbo/issues/3193#issuecomment-474340165)) +- `spring.cloud.nacos.discovery` : Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口 + +> 以上完整的 YAML 配置文件,请参考 `spring-cloud-dubbo-server-sample` [`bootstrap.yaml`](spring-cloud-dubbo-server-sample/src/main/resources/bootstrap.yaml) 文件 + + +完成以上步骤后,还需编写一个 Dubbo Spring Cloud 引导类。 + + +#### 引导 Dubbo Spring Cloud 服务提供方应用 + +Dubbo Spring Cloud 引导类与普通 Spring Cloud 应用并无差别,如下所示: +```java +@EnableDiscoveryClient +@EnableAutoConfiguration +public class DubboSpringCloudServerBootstrap { + + public static void main(String[] args) { + SpringApplication.run(DubboSpringCloudServerBootstrap.class); + } +} +``` + +在引导 `DubboSpringCloudServerBootstrap` 之前,请提前启动 Nacos 服务器。 +当 `DubboSpringCloudServerBootstrap` 启动后,将应用 `spring-cloud-dubbo-server-sample` 将出现在 Nacos 控制台界面。 + + +当 Dubbo 服务提供方启动后,下一步实现一个 Dubbo 服务消费方。 + + + +### 实现 Dubbo 服务消费方 + +由于 Java 服务就 `EchoService`、服务提供方应用 `spring-cloud-dubbo-server-sample` 以及 Nacos 服务器均已准备完毕。Dubbo 服务消费方 +只需初始化服务消费方 Maven 工程 `spring-cloud-dubbo-client-sample` 以及消费 Dubbo 服务。 + + + +#### 初始化 `spring-cloud-dubbo-client-sample` Maven 工程 + +与服务提供方 Maven 工程类,需添加相关 Maven 依赖: + +```xml + + + + + org.springframework.cloud + spring-cloud-alibaba-dependencies + 0.9.0.RELEASE + pom + import + + + + + + + + org.springframework.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-actuator + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + +``` + +与应用 `spring-cloud-dubbo-server-sample` 不同的是,当前应用依赖 `spring-boot-starter-web`,表明它属于 Web Servlet 应用。 + +> 以上完整的 Maven 依赖配置,请参考 `spring-cloud-dubbo-client-sample` [`pom.xml`](spring-cloud-dubbo-client-sample/pom.xml) 文件 + + +#### 配置 Dubbo 服务消费方 + +Dubbo 服务消费方配置与服务提供方类似,当前应用 `spring-cloud-dubbo-client-sample` 属于纯服务消费方,因此,所需的外部化配置更精简: + +```yaml +dubbo: + registry: + # 挂载到 Spring Cloud 注册中心 + address: spring-cloud://localhost + cloud: + subscribed-services: spring-cloud-alibaba-dubbo-server + +spring: + application: + # Dubbo 应用名称 + name: spring-cloud-alibaba-dubbo-client + main: + # Spring Boot 2.1 需要设定 + allow-bean-definition-overriding: true + cloud: + nacos: + # Nacos 服务发现与注册配置 + discovery: + server-addr: 127.0.0.1:8848 +``` + +对比应用 `spring-cloud-dubbo-server-sample`,除应用名称 `spring.application.name` 存在差异外,`spring-cloud-dubbo-client-sample` +新增了属性 `dubbo.cloud.subscribed-services` 的设置。并且该值为服务提供方应用 "spring-cloud-dubbo-server-sample"。 + +- `dubbo.cloud.subscribed-services` : 用于服务消费方订阅服务提供方的应用名称的列表,若需订阅多应用,使用 "," 分割。 +不推荐使用默认值为 "*",它将订阅所有应用。 +> 当应用使用属性 `dubbo.cloud.subscribed-services` 默认值时,日志中将会输出一行警告: +> > Current application will subscribe all services(size:x) in registry, a lot of memory and CPU cycles may be used, +> > thus it's strongly recommend you using the externalized property 'dubbo.cloud.subscribed-services' to specify the services + +由于当前应用属于 Web 应用,它会默认地使用 8080 作为 Web 服务端口,如果需要自定义,可通过属性 `server.port` 调整。 + +> 以上完整的 YAML 配置文件,请参考 `spring-cloud-dubbo-client-sample` [`bootstrap.yaml`](spring-cloud-dubbo-client-sample/src/main/resources/bootstrap.yaml) 文件 + + + +#### 引导 Dubbo Spring Cloud 服务消费方应用 + +为了减少实现步骤,以下引导类将 Dubbo 服务消费以及引导功能合二为一: + +```java +@EnableDiscoveryClient +@EnableAutoConfiguration +@RestController +public class DubboSpringCloudClientBootstrap { + + @Reference + private EchoService echoService; + + @GetMapping("/echo") + public String echo(String message) { + return echoService.echo(message); + } + + public static void main(String[] args) { + SpringApplication.run(DubboSpringCloudClientBootstrap.class); + } +} +``` + +不仅如此,`DubboSpringCloudClientBootstrap` 也作为 REST Endpoint,通过暴露 `/echo` Web 服务,消费 Dubbo `EchoService` 服务。因此, +可通过 `curl` 命令执行 HTTP GET 方法: + +``` +$ curl http://127.0.0.1:8080/echo?message=%E5%B0%8F%E9%A9%AC%E5%93%A5%EF%BC%88mercyblitz%EF%BC%89 +``` + +HTTP 响应为: + +``` +[echo] Hello, 小马哥(mercyblitz) +``` + +以上结果说明应用 `spring-cloud-dubbo-client-sample` 通过消费 Dubbo 服务,返回服务提供方 `spring-cloud-dubbo-server-sample` +运算后的内容。 + +以上操作就一套完整的 Dubbo 服务提供方和消费方的运用,更多的详情请直接参考模块: +- [`spring-cloud-dubbo-server-sample` ](spring-cloud-dubbo-server-sample) +- [`spring-cloud-dubbo-client-sample`](spring-cloud-dubbo-client-sample) + + + + +## 模块说明 + +- [spring-cloud-dubbo-sample-api](spring-cloud-dubbo-sample-api):API 模块,存放 Dubbo 服务接口和模型定义 +- [spring-cloud-dubbo-provider-web-sample](spring-cloud-dubbo-provider-web-sample):Dubbo Spring Cloud 服务提供方示例(Web 应用) +- [spring-cloud-dubbo-provider-sample](spring-cloud-dubbo-provider-sample):Dubbo Spring Cloud 服务提供方示例(非 Web 应用) +- [spring-cloud-dubbo-consumer-sample](spring-cloud-dubbo-consumer-sample):Dubbo Spring Cloud 服务消费方示例 +- [spring-cloud-dubbo-servlet-gateway](spring-cloud-dubbo-servlet-gateway)-sample:Dubbo Spring Cloud Servlet 网关简易实现示例 diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/pom.xml new file mode 100644 index 00000000..66a1b5bf --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/pom.xml @@ -0,0 +1,104 @@ + + + + org.springframework.cloud + spring-cloud-build + 2.1.3.RELEASE + + + 4.0.0 + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + Spring Cloud Alibaba Dubbo Examples + pom + + + spring-cloud-dubbo-sample-api + spring-cloud-dubbo-server-sample + spring-cloud-dubbo-client-sample + spring-cloud-dubbo-provider-sample + spring-cloud-dubbo-consumer-sample + spring-cloud-dubbo-provider-web-sample + spring-cloud-dubbo-servlet-gateway-sample + + + + 2.7.1 + 2.1.2.RELEASE + 2.1.2.RELEASE + 2.1.2.RELEASE + 2.1.2.RELEASE + 2.1.2.RELEASE + 4.0.1 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${project.version} + pom + import + + + + org.springframework.cloud + spring-cloud-netflix + ${spring-cloud-netflix.version} + pom + import + + + + org.springframework.cloud + spring-cloud-openfeign-dependencies + ${spring-cloud-openfeign.version} + pom + import + + + + org.apache.dubbo + dubbo-bom + ${dubbo.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/pom.xml new file mode 100644 index 00000000..adc03a2f --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/pom.xml @@ -0,0 +1,71 @@ + + + + org.springframework.cloud + spring-cloud-build + 2.1.3.RELEASE + + + 4.0.0 + + com.alibaba.cloud + spring-cloud-dubbo-client-sample + Spring Cloud Dubbo Client Sample + 0.9.1.BUILD-SNAPSHOT + + + + + + org.springframework.cloud + spring-cloud-alibaba-dependencies + 0.9.0.RELEASE + pom + import + + + + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-actuator + + + + + org.springframework.cloud + spring-cloud-starter-dubbo + + + + + org.springframework.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudClientBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudClientBootstrap.java new file mode 100644 index 00000000..fdf38eb1 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudClientBootstrap.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.bootstrap; + +import org.apache.dubbo.config.annotation.Reference; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.alibaba.cloud.dubbo.service.EchoService; + +/** + * Dubbo Spring Cloud Client Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +@RestController +public class DubboSpringCloudClientBootstrap { + + @Reference + private EchoService echoService; + + @GetMapping("/echo") + public String echo(String message) { + return echoService.echo(message); + } + + public static void main(String[] args) { + SpringApplication.run(DubboSpringCloudClientBootstrap.class); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..42e49bcb --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-client-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,15 @@ +dubbo: + registry: + address: spring-cloud://localhost + cloud: + subscribed-services: spring-cloud-alibaba-dubbo-server + +spring: + application: + name: spring-cloud-alibaba-dubbo-client + main: + allow-bean-definition-overriding: true + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/pom.xml new file mode 100644 index 00000000..cb575169 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/pom.xml @@ -0,0 +1,115 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-dubbo-consumer-sample + Spring Cloud Dubbo Consumer Sample + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-actuator + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springframework.retry + spring-retry + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + ${spring-cloud-zookeeper.version} + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.zookeeper + zookeeper + 3.4.12 + true + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.curator + curator-framework + ${curator.version} + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + ${spring-cloud-consul.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudConsumerBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudConsumerBootstrap.java new file mode 100644 index 00000000..809ebc7d --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudConsumerBootstrap.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.bootstrap; + +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.dubbo.config.annotation.Reference; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.dubbo.annotation.DubboTransported; +import com.alibaba.cloud.dubbo.service.RestService; +import com.alibaba.cloud.dubbo.service.User; +import com.alibaba.cloud.dubbo.service.UserService; + +/** + * Dubbo Spring Cloud Consumer Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +@EnableFeignClients +public class DubboSpringCloudConsumerBootstrap { + + @Reference + private UserService userService; + + @Reference(version = "1.0.0", protocol = "dubbo") + private RestService restService; + + @Autowired + @Lazy + private FeignRestService feignRestService; + + @Autowired + @Lazy + private DubboFeignRestService dubboFeignRestService; + + @Value("${provider.application.name}") + private String providerApplicationName; + + @Autowired + @LoadBalanced + private RestTemplate restTemplate; + + @FeignClient("${provider.application.name}") + public interface FeignRestService { + + @GetMapping(value = "/param") + String param(@RequestParam("param") String param); + + @PostMapping("/params") + public String params(@RequestParam("b") String b, @RequestParam("a") int a); + + @PostMapping(value = "/request/body/map", produces = APPLICATION_JSON_UTF8_VALUE) + User requestBody(@RequestParam("param") String param, + @RequestBody Map data); + + @GetMapping("/headers") + public String headers(@RequestHeader("h2") String header2, + @RequestHeader("h") String header, @RequestParam("v") Integer value); + + @GetMapping("/path-variables/{p1}/{p2}") + public String pathVariables(@PathVariable("p2") String path2, + @PathVariable("p1") String path1, @RequestParam("v") String param); + } + + @FeignClient("${provider.application.name}") + @DubboTransported(protocol = "dubbo") + public interface DubboFeignRestService { + + @GetMapping(value = "/param") + String param(@RequestParam("param") String param); + + @PostMapping("/params") + String params(@RequestParam("b") String paramB, @RequestParam("a") int paramA); + + @PostMapping(value = "/request/body/map", produces = APPLICATION_JSON_UTF8_VALUE) + User requestBody(@RequestParam("param") String param, + @RequestBody Map data); + + @GetMapping("/headers") + public String headers(@RequestHeader("h2") String header2, + @RequestParam("v") Integer value, @RequestHeader("h") String header); + + @GetMapping("/path-variables/{p1}/{p2}") + public String pathVariables(@RequestParam("v") String param, + @PathVariable("p2") String path2, @PathVariable("p1") String path1); + } + + @Bean + public ApplicationRunner userServiceRunner() { + return arguments -> { + + User user = new User(); + user.setId(1L); + user.setName("小马哥"); + user.setAge(33); + + // save User + System.out.printf("UserService.save(%s) : %s\n", user, + userService.save(user)); + + // find all Users + System.out.printf("UserService.findAll() : %s\n", user, + userService.findAll()); + + // remove User + System.out.printf("UserService.remove(%d) : %s\n", user.getId(), + userService.remove(user.getId())); + + }; + } + + @Bean + public ApplicationRunner callRunner() { + return arguments -> { + + // To call /path-variables + callPathVariables(); + + // To call /headers + callHeaders(); + + // To call /param + callParam(); + + // To call /params + callParams(); + + // To call /request/body/map + callRequestBodyMap(); + + }; + } + + private void callPathVariables() { + // Dubbo Service call + System.out.println(restService.pathVariables("a", "b", "c")); + // Spring Cloud Open Feign REST Call (Dubbo Transported) + System.out.println(dubboFeignRestService.pathVariables("c", "b", "a")); + // Spring Cloud Open Feign REST Call + System.out.println(feignRestService.pathVariables("b", "a", "c")); + + // RestTemplate call + System.out.println(restTemplate.getForEntity( + "http://" + providerApplicationName + "//path-variables/{p1}/{p2}?v=c", + String.class, "a", "b")); + } + + private void callHeaders() { + // Dubbo Service call + System.out.println(restService.headers("a", "b", 10)); + // Spring Cloud Open Feign REST Call (Dubbo Transported) + System.out.println(dubboFeignRestService.headers("b", 10, "a")); + // Spring Cloud Open Feign REST Call + System.out.println(feignRestService.headers("b", "a", 10)); + } + + private void callParam() { + // Dubbo Service call + System.out.println(restService.param("mercyblitz")); + // Spring Cloud Open Feign REST Call (Dubbo Transported) + System.out.println(dubboFeignRestService.param("mercyblitz")); + // Spring Cloud Open Feign REST Call + System.out.println(feignRestService.param("mercyblitz")); + } + + private void callParams() { + // Dubbo Service call + System.out.println(restService.params(1, "1")); + // Spring Cloud Open Feign REST Call (Dubbo Transported) + System.out.println(dubboFeignRestService.params("1", 1)); + // Spring Cloud Open Feign REST Call + System.out.println(feignRestService.params("1", 1)); + + // RestTemplate call + System.out.println(restTemplate.getForEntity( + "http://" + providerApplicationName + "/param?param=小马哥", String.class)); + } + + private void callRequestBodyMap() { + + Map data = new HashMap<>(); + data.put("id", 1); + data.put("name", "小马哥"); + data.put("age", 33); + + // Dubbo Service call + System.out.println(restService.requestBodyMap(data, "Hello,World")); + // Spring Cloud Open Feign REST Call (Dubbo Transported) + System.out.println(dubboFeignRestService.requestBody("Hello,World", data)); + // Spring Cloud Open Feign REST Call + System.out.println(feignRestService.requestBody("Hello,World", data)); + + // RestTemplate call + System.out.println(restTemplate.postForObject( + "http://" + providerApplicationName + "/request/body/map?param=小马哥", data, + User.class)); + } + + @Bean + @LoadBalanced + @DubboTransported + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + public static void main(String[] args) { + new SpringApplicationBuilder(DubboSpringCloudConsumerBootstrap.class) + .properties("spring.profiles.active=nacos").run(args); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/application.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/application.yaml new file mode 100644 index 00000000..7244e451 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/application.yaml @@ -0,0 +1,18 @@ +dubbo: + registry: + # The Spring Cloud Dubbo's registry extension + ## the default value of dubbo-provider-services is "*", that means to subscribe all providers, + ## thus it's optimized if subscriber specifies the required providers. + address: spring-cloud://localhost +# The traditional Dubbo's registry also is supported +# address: zookeeper://127.0.0.1:2181 + cloud: + # The subscribed services in consumer side + subscribed-services: ${provider.application.name} + +server: + port: 0 + +provider: + application: + name: spring-cloud-alibaba-dubbo-provider \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..dca555cf --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-consumer-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,70 @@ +spring: + application: + name: spring-cloud-alibaba-dubbo-consumer + main: + allow-bean-definition-overriding: true + + + # default disable all + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + zookeeper: + enabled: false + consul: + enabled: false + +eureka: + client: + enabled: false + +ribbon: + nacos: + enabled: false + +--- +spring: + profiles: nacos + + cloud: + nacos: + discovery: + enabled: true + register-enabled: true + server-addr: 127.0.0.1:8848 + +ribbon: + nacos: + enabled: true + +--- +spring: + profiles: eureka + +eureka: + client: + enabled: true + service-url: + defaultZone: http://127.0.0.1:8761/eureka/ + + +--- +spring: + profiles: zookeeper + cloud: + zookeeper: + enabled: true + connect-string: 127.0.0.1:2181 + + +--- +spring: + profiles: consul + + cloud: + consul: + enabled: true + host: 127.0.0.1 + port: 8500 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/pom.xml new file mode 100644 index 00000000..47de4040 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/pom.xml @@ -0,0 +1,155 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-dubbo-provider-sample + Spring Cloud Dubbo Provider Sample + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-actuator + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + ${spring-cloud-zookeeper.version} + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.zookeeper + zookeeper + 3.4.12 + true + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.curator + curator-framework + ${curator.version} + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + ${spring-cloud-consul.version} + + + + + io.netty + netty-all + + + + org.jboss.resteasy + resteasy-jaxrs + + + + org.jboss.resteasy + resteasy-client + + + + org.jboss.resteasy + resteasy-netty4 + + + + javax.validation + validation-api + + + + org.jboss.resteasy + resteasy-jackson-provider + + + + org.jboss.resteasy + resteasy-jaxb-provider + + + + org.hibernate.validator + hibernate-validator + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudProviderBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudProviderBootstrap.java new file mode 100644 index 00000000..94b4b750 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudProviderBootstrap.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.bootstrap; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Dubbo Spring Cloud Provider Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +public class DubboSpringCloudProviderBootstrap { + + public static void main(String[] args) { + new SpringApplicationBuilder(DubboSpringCloudProviderBootstrap.class) + .properties("spring.profiles.active=nacos").web(WebApplicationType.NONE) + .run(args); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java new file mode 100644 index 00000000..d254196d --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.dubbo.config.annotation.Service; + +/** + * In-Memory {@link UserService} implementation + */ +@Service(protocol = "dubbo") +public class InMemoryUserService implements UserService { + + private Map usersRepository = new HashMap<>(); + + @Override + public boolean save(User user) { + return usersRepository.put(user.getId(), user) == null; + } + + @Override + public boolean remove(Long userId) { + return usersRepository.remove(userId) != null; + } + + @Override + public Collection findAll() { + return usersRepository.values(); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/StandardRestService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/StandardRestService.java new file mode 100644 index 00000000..e30a4cb3 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/java/com/alibaba/cloud/dubbo/service/StandardRestService.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import static com.alibaba.cloud.dubbo.util.LoggerUtils.log; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.apache.dubbo.config.annotation.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default {@link RestService} + * + * @author Mercy + */ +@Service(version = "1.0.0", protocol = { "dubbo", "rest" }) +@Path("/") +public class StandardRestService implements RestService { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + @Path("param") + @GET + public String param(@QueryParam("param") String param) { + log("/param", param); + return param; + } + + @Override + @Path("params") + @POST + public String params(@QueryParam("a") int a, @QueryParam("b") String b) { + log("/params", a + b); + return a + b; + } + + @Override + @Path("headers") + @GET + public String headers(@HeaderParam("h") String header, + @HeaderParam("h2") String header2, @QueryParam("v") Integer param) { + String result = header + " , " + header2 + " , " + param; + log("/headers", result); + return result; + } + + @Override + @Path("path-variables/{p1}/{p2}") + @GET + public String pathVariables(@PathParam("p1") String path1, + @PathParam("p2") String path2, @QueryParam("v") String param) { + String result = path1 + " , " + path2 + " , " + param; + log("/path-variables", result); + return result; + } + + // @CookieParam does not support : https://github.com/OpenFeign/feign/issues/913 + // @CookieValue also does not support + + @Override + @Path("form") + @POST + public String form(@FormParam("f") String form) { + return String.valueOf(form); + } + + @Override + @Path("request/body/map") + @POST + @Produces(APPLICATION_JSON_VALUE) + public User requestBodyMap(Map data, + @QueryParam("param") String param) { + User user = new User(); + user.setId(((Integer) data.get("id")).longValue()); + user.setName((String) data.get("name")); + user.setAge((Integer) data.get("age")); + log("/request/body/map", param); + return user; + } + + @Path("request/body/user") + @POST + @Override + @Consumes(MediaType.APPLICATION_JSON) + public Map requestBodyUser(User user) { + Map map = new HashMap<>(); + map.put("id", user.getId()); + map.put("name", user.getName()); + map.put("age", user.getAge()); + return map; + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/application.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/application.yaml new file mode 100644 index 00000000..1a7ec7c5 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +dubbo: + scan: + base-packages: com.alibaba.cloud.dubbo.service + protocols: + dubbo: + name: dubbo + port: -1 + rest: + name: rest + port: 9090 + server: netty + registry: +# The Spring Cloud Dubbo's registry extension + address: spring-cloud://localhost +# The traditional Dubbo's registry +# address: zookeeper://127.0.0.1:2181 +feign: + hystrix: + enabled: true \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..0d8dfca9 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,64 @@ +spring: + application: + name: spring-cloud-alibaba-dubbo-provider + main: + allow-bean-definition-overriding: true + + + # default disable all + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + zookeeper: + enabled: false + consul: + enabled: false + +eureka: + client: + enabled: false + + +--- +spring: + profiles: nacos + + cloud: + nacos: + discovery: + enabled: true + register-enabled: true + server-addr: 127.0.0.1:8848 + + +--- +spring: + profiles: eureka + +eureka: + client: + enabled: true + service-url: + defaultZone: http://127.0.0.1:8761/eureka/ + + +--- +spring: + profiles: zookeeper + cloud: + zookeeper: + enabled: true + connect-string: 127.0.0.1:2181 + + +--- +spring: + profiles: consul + + cloud: + consul: + enabled: true + host: 127.0.0.1 + port: 8500 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/pom.xml new file mode 100644 index 00000000..648c064e --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/pom.xml @@ -0,0 +1,107 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-dubbo-provider-web-sample + Spring Cloud Dubbo Provider Web Sample + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + ${spring-cloud-zookeeper.version} + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.zookeeper + zookeeper + 3.4.12 + true + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.curator + curator-framework + ${curator.version} + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + ${spring-cloud-consul.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudWebProviderBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudWebProviderBootstrap.java new file mode 100644 index 00000000..18f859af --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudWebProviderBootstrap.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.bootstrap; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Dubbo Spring Cloud Provider Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +public class DubboSpringCloudWebProviderBootstrap { + + public static void main(String[] args) { + new SpringApplicationBuilder(DubboSpringCloudWebProviderBootstrap.class) + .properties("spring.profiles.active=nacos").run(args); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java new file mode 100644 index 00000000..d254196d --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/InMemoryUserService.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.dubbo.config.annotation.Service; + +/** + * In-Memory {@link UserService} implementation + */ +@Service(protocol = "dubbo") +public class InMemoryUserService implements UserService { + + private Map usersRepository = new HashMap<>(); + + @Override + public boolean save(User user) { + return usersRepository.put(user.getId(), user) == null; + } + + @Override + public boolean remove(Long userId) { + return usersRepository.remove(userId) != null; + } + + @Override + public Collection findAll() { + return usersRepository.values(); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/SpringRestService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/SpringRestService.java new file mode 100644 index 00000000..cdbefcd8 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/java/com/alibaba/cloud/dubbo/service/SpringRestService.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import static com.alibaba.cloud.dubbo.util.LoggerUtils.log; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.dubbo.config.annotation.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Spring MVC {@link RestService} + * + * @author Mercy + */ +@Service(version = "1.0.0") +@RestController +public class SpringRestService implements RestService { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + @GetMapping(value = "/param") + public String param(@RequestParam String param) { + log("/param", param); + return param; + } + + @Override + @PostMapping("/params") + public String params(@RequestParam int a, @RequestParam String b) { + log("/params", a + b); + return a + b; + } + + @Override + @GetMapping("/headers") + public String headers(@RequestHeader("h") String header, + @RequestHeader("h2") String header2, @RequestParam("v") Integer param) { + String result = header + " , " + header2 + " , " + param; + log("/headers", result); + return result; + } + + @Override + @GetMapping("/path-variables/{p1}/{p2}") + public String pathVariables(@PathVariable("p1") String path1, + @PathVariable("p2") String path2, @RequestParam("v") String param) { + String result = path1 + " , " + path2 + " , " + param; + log("/path-variables", result); + return result; + } + + @Override + @PostMapping("/form") + public String form(@RequestParam("f") String form) { + return String.valueOf(form); + } + + @Override + @PostMapping(value = "/request/body/map", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public User requestBodyMap(@RequestBody Map data, + @RequestParam("param") String param) { + User user = new User(); + user.setId(((Integer) data.get("id")).longValue()); + user.setName((String) data.get("name")); + user.setAge((Integer) data.get("age")); + log("/request/body/map", param); + return user; + } + + @PostMapping(value = "/request/body/user", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) + @Override + public Map requestBodyUser(@RequestBody User user) { + Map map = new HashMap<>(); + map.put("id", user.getId()); + map.put("name", user.getName()); + map.put("age", user.getAge()); + return map; + } + +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/application.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/application.yaml new file mode 100644 index 00000000..b6858c74 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/application.yaml @@ -0,0 +1,28 @@ +dubbo: + scan: + base-packages: com.alibaba.cloud.dubbo.service + protocols: + dubbo: + name: dubbo + port: -1 + registries: + new: + address: spring-cloud://localhost +# registry: +# The Spring Cloud Dubbo's registry extension +# address: spring-cloud://localhost +# The traditional Dubbo's registry +# address: nacos://127.0.0.1:8848 + +feign: + hystrix: + enabled: true + +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: dubborestmetadata \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..0d8dfca9 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-provider-web-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,64 @@ +spring: + application: + name: spring-cloud-alibaba-dubbo-provider + main: + allow-bean-definition-overriding: true + + + # default disable all + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + zookeeper: + enabled: false + consul: + enabled: false + +eureka: + client: + enabled: false + + +--- +spring: + profiles: nacos + + cloud: + nacos: + discovery: + enabled: true + register-enabled: true + server-addr: 127.0.0.1:8848 + + +--- +spring: + profiles: eureka + +eureka: + client: + enabled: true + service-url: + defaultZone: http://127.0.0.1:8761/eureka/ + + +--- +spring: + profiles: zookeeper + cloud: + zookeeper: + enabled: true + connect-string: 127.0.0.1:2181 + + +--- +spring: + profiles: consul + + cloud: + consul: + enabled: true + host: 127.0.0.1 + port: 8500 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/pom.xml new file mode 100644 index 00000000..6ed389d1 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/pom.xml @@ -0,0 +1,32 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + Spring Cloud Dubbo Sample API + + + + + org.slf4j + slf4j-api + 1.7.25 + + + + + org.apache.dubbo + dubbo + ${dubbo.version} + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/EchoService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/EchoService.java new file mode 100644 index 00000000..d9fb9eb2 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/EchoService.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +/** + * Echo Service + */ +public interface EchoService { + + String echo(String message); +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/RestService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/RestService.java new file mode 100644 index 00000000..1bfe4b91 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/RestService.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import java.util.Map; + +/** + * Rest Service + * + * @author Mercy + */ +public interface RestService { + + String param(String param); + + String params(int a, String b); + + String headers(String header, String header2, Integer param); + + String pathVariables(String path1, String path2, String param); + + String form(String form); + + User requestBodyMap(Map data, String param); + + Map requestBodyUser(User user); +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/User.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/User.java new file mode 100644 index 00000000..14055cd3 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/User.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import java.io.Serializable; + +/** + * User Entity + * + * @author Mercy + */ +public class User implements Serializable { + + private Long id; + + private String name; + + private Integer age; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + @Override + public String toString() { + return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/UserService.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/UserService.java new file mode 100644 index 00000000..81df4787 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/service/UserService.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.service; + +import java.util.Collection; + +/** + * {@link User} Service + * + * @author Mercy + */ +public interface UserService { + + boolean save(User user); + + boolean remove(Long userId); + + Collection findAll(); +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/util/LoggerUtils.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/util/LoggerUtils.java new file mode 100644 index 00000000..31c9af2c --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-sample-api/src/main/java/com/alibaba/cloud/dubbo/util/LoggerUtils.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.util; + +import org.apache.dubbo.rpc.RpcContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Logger Utilities + */ +public abstract class LoggerUtils { + + private static final Logger logger = LoggerFactory.getLogger(LoggerUtils.class); + + public static void log(String url, Object result) { + String message = String + .format("The client[%s] uses '%s' protocol to call %s : %s", + RpcContext.getContext().getRemoteHostName(), + RpcContext.getContext().getUrl() == null ? "N/A" + : RpcContext.getContext().getUrl().getProtocol(), + url, result); + if (logger.isInfoEnabled()) { + logger.info(message); + } + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/pom.xml new file mode 100644 index 00000000..c829aaf7 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/pom.xml @@ -0,0 +1,68 @@ + + + + org.springframework.cloud + spring-cloud-build + 2.1.3.RELEASE + + + + 4.0.0 + + com.alibaba.cloud + spring-cloud-dubbo-server-sample + Spring Cloud Dubbo Server Sample + 0.9.1.BUILD-SNAPSHOT + + + + + + org.springframework.cloud + spring-cloud-alibaba-dependencies + 0.9.0.RELEASE + pom + import + + + + + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + org.springframework.boot + spring-boot-actuator + + + + + org.springframework.cloud + spring-cloud-starter-dubbo + + + + + org.springframework.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServerBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServerBootstrap.java new file mode 100644 index 00000000..8a7661ae --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServerBootstrap.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.dubbo.bootstrap; + +import org.apache.dubbo.config.annotation.Service; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +import com.alibaba.cloud.dubbo.service.EchoService; + +/** + * Dubbo Spring Cloud Server Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +public class DubboSpringCloudServerBootstrap { + + public static void main(String[] args) { + SpringApplication.run(DubboSpringCloudServerBootstrap.class); + } +} + +@Service +class EchoServiceImpl implements EchoService { + + @Override + public String echo(String message) { + return "[echo] Hello, " + message; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..394df7ec --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-server-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,18 @@ +dubbo: + scan: + base-packages: com.alibaba.cloud.dubbo.bootstrap + protocol: + name: dubbo + port: -1 + registry: + address: spring-cloud://localhost + +spring: + application: + name: spring-cloud-alibaba-dubbo-server + main: + allow-bean-definition-overriding: true + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/pom.xml new file mode 100644 index 00000000..eae78ce1 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/pom.xml @@ -0,0 +1,57 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba-dubbo-examples + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-dubbo-servlet-gateway-sample + Spring Cloud Dubbo Servlet Gateway Sample + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.alibaba.cloud + spring-cloud-dubbo-sample-api + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-dubbo + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServletGatewayBootstrap.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServletGatewayBootstrap.java new file mode 100644 index 00000000..6eecaf1a --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/bootstrap/DubboSpringCloudServletGatewayBootstrap.java @@ -0,0 +1,22 @@ +package com.alibaba.cloud.dubbo.bootstrap; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +/** + * Dubbo Spring Cloud Servlet Gateway Bootstrap + */ +@EnableDiscoveryClient +@EnableAutoConfiguration +@EnableFeignClients +@ServletComponentScan(basePackages = "com.alibaba.cloud.dubbo.gateway") +public class DubboSpringCloudServletGatewayBootstrap { + + public static void main(String[] args) { + new SpringApplicationBuilder(DubboSpringCloudServletGatewayBootstrap.class) + .properties("spring.profiles.active=nacos").run(args); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/gateway/DubboGatewayServlet.java b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/gateway/DubboGatewayServlet.java new file mode 100644 index 00000000..5ca9451f --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/java/com/alibaba/cloud/dubbo/gateway/DubboGatewayServlet.java @@ -0,0 +1,194 @@ +package com.alibaba.cloud.dubbo.gateway; + +import static org.apache.commons.lang3.StringUtils.substringAfter; +import static org.apache.commons.lang3.StringUtils.substringBetween; +import static org.springframework.web.util.UriComponentsBuilder.fromUriString; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.dubbo.rpc.service.GenericException; +import org.apache.dubbo.rpc.service.GenericService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.util.StreamUtils; +import org.springframework.web.servlet.HttpServletBean; +import org.springframework.web.util.UriComponents; + +import com.alibaba.cloud.dubbo.http.MutableHttpServerRequest; +import com.alibaba.cloud.dubbo.metadata.DubboRestServiceMetadata; +import com.alibaba.cloud.dubbo.metadata.RequestMetadata; +import com.alibaba.cloud.dubbo.metadata.RestMethodMetadata; +import com.alibaba.cloud.dubbo.metadata.repository.DubboServiceMetadataRepository; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContext; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceExecutionContextFactory; +import com.alibaba.cloud.dubbo.service.DubboGenericServiceFactory; + +@WebServlet(urlPatterns = "/dsc/*") +public class DubboGatewayServlet extends HttpServletBean { + + private final DubboServiceMetadataRepository repository; + + private final DubboGenericServiceFactory serviceFactory; + + private final DubboGenericServiceExecutionContextFactory contextFactory; + + private final PathMatcher pathMatcher = new AntPathMatcher(); + + private final Map dubboTranslatedAttributes = new HashMap<>(); + + public DubboGatewayServlet(DubboServiceMetadataRepository repository, + DubboGenericServiceFactory serviceFactory, + DubboGenericServiceExecutionContextFactory contextFactory) { + this.repository = repository; + this.serviceFactory = serviceFactory; + this.contextFactory = contextFactory; + dubboTranslatedAttributes.put("protocol", "dubbo"); + dubboTranslatedAttributes.put("cluster", "failover"); + } + + private String resolveServiceName(HttpServletRequest request) { + + // /g/{app-name}/{rest-path} + String requestURI = request.getRequestURI(); + // /g/ + String servletPath = request.getServletPath(); + + String part = substringAfter(requestURI, servletPath); + + String serviceName = substringBetween(part, "/", "/"); + + return serviceName; + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + String serviceName = resolveServiceName(request); + + String restPath = substringAfter(request.getRequestURI(), serviceName); + + // 初始化 serviceName 的 REST 请求元数据 + repository.initialize(serviceName); + // 将 HttpServletRequest 转化为 RequestMetadata + RequestMetadata clientMetadata = buildRequestMetadata(request, restPath); + + DubboRestServiceMetadata dubboRestServiceMetadata = repository.get(serviceName, + clientMetadata); + + if (dubboRestServiceMetadata == null) { + // if DubboServiceMetadata is not found, executes next + throw new ServletException("DubboServiceMetadata can't be found!"); + } + + RestMethodMetadata dubboRestMethodMetadata = dubboRestServiceMetadata + .getRestMethodMetadata(); + + GenericService genericService = serviceFactory.create(dubboRestServiceMetadata, + dubboTranslatedAttributes); + + // TODO: Get the Request Body from HttpServletRequest + byte[] body = getRequestBody(request); + + MutableHttpServerRequest httpServerRequest = new MutableHttpServerRequest( + new HttpRequestAdapter(request), body); + + DubboGenericServiceExecutionContext context = contextFactory + .create(dubboRestMethodMetadata, httpServerRequest); + + Object result = null; + GenericException exception = null; + + try { + result = genericService.$invoke(context.getMethodName(), + context.getParameterTypes(), context.getParameters()); + } + catch (GenericException e) { + exception = e; + } + response.getWriter().println(result); + } + + private byte[] getRequestBody(HttpServletRequest request) throws IOException { + ServletInputStream inputStream = request.getInputStream(); + return StreamUtils.copyToByteArray(inputStream); + } + + private static class HttpRequestAdapter implements HttpRequest { + + private final HttpServletRequest request; + + private HttpRequestAdapter(HttpServletRequest request) { + this.request = request; + } + + @Override + public String getMethodValue() { + return request.getMethod(); + } + + @Override + public URI getURI() { + try { + return new URI(request.getRequestURL().toString() + "?" + + request.getQueryString()); + } + catch (URISyntaxException e) { + e.printStackTrace(); + } + throw new RuntimeException(); + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + } + + private RequestMetadata buildRequestMetadata(HttpServletRequest request, + String restPath) { + UriComponents uriComponents = fromUriString(request.getRequestURI()).build(true); + RequestMetadata requestMetadata = new RequestMetadata(); + requestMetadata.setPath(restPath); + requestMetadata.setMethod(request.getMethod()); + requestMetadata.setParams(getParams(request)); + requestMetadata.setHeaders(getHeaders(request)); + return requestMetadata; + } + + private Map> getHeaders(HttpServletRequest request) { + Map> map = new LinkedHashMap<>(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(headerName); + map.put(headerName, Collections.list(headerValues)); + } + return map; + } + + private Map> getParams(HttpServletRequest request) { + Map> map = new LinkedHashMap<>(); + for (Map.Entry entry : request.getParameterMap().entrySet()) { + map.put(entry.getKey(), Arrays.asList(entry.getValue())); + } + return map; + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/application.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/application.yaml new file mode 100644 index 00000000..263a5699 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/application.yaml @@ -0,0 +1,12 @@ +dubbo: + registry: + # The Spring Cloud Dubbo's registry extension + address: spring-cloud://localhost +# The traditional Dubbo's registry +# address: zookeeper://127.0.0.1:2181 +server: + port: 0 + +provider: + application: + name: spring-cloud-alibaba-dubbo-web-provider \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/bootstrap.yaml b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/bootstrap.yaml new file mode 100644 index 00000000..a86acd15 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-alibaba-dubbo-examples/spring-cloud-dubbo-servlet-gateway-sample/src/main/resources/bootstrap.yaml @@ -0,0 +1,70 @@ +spring: + application: + name: spring-cloud-alibaba-dubbo-servlet-gateway + main: + allow-bean-definition-overriding: true + + + # default disable all + cloud: + nacos: + discovery: + enabled: false + register-enabled: false + zookeeper: + enabled: false + consul: + enabled: false + +eureka: + client: + enabled: false + +ribbon: + nacos: + enabled: false + +--- +spring: + profiles: nacos + + cloud: + nacos: + discovery: + enabled: true + register-enabled: true + server-addr: 127.0.0.1:8848 + +ribbon: + nacos: + enabled: true + +--- +spring: + profiles: eureka + +eureka: + client: + enabled: true + service-url: + defaultZone: http://127.0.0.1:8761/eureka/ + + +--- +spring: + profiles: zookeeper + cloud: + zookeeper: + enabled: true + connect-string: 127.0.0.1:2181 + + +--- +spring: + profiles: consul + + cloud: + consul: + enabled: true + host: 127.0.0.1 + port: 8500 \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/pom.xml new file mode 100644 index 00000000..62e2580a --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/pom.xml @@ -0,0 +1,53 @@ + + + + spring-cloud-alibaba-examples + com.alibaba.cloud + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-bus-rocketmq-example + Spring Cloud Bus RocketMQ Example + + + + + + com.alibaba.cloud + spring-cloud-starter-bus-rocketmq + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java new file mode 100644 index 00000000..6a8eee88 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.examples.rocketmq; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.bus.event.AckRemoteApplicationEvent; +import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * RocketMQ Bus Spring Application + * + * @author Mercy + * @since 0.2.1 + */ +@RestController +@EnableAutoConfiguration +@RemoteApplicationEventScan(basePackages = "com.alibaba.cloud.examples.rocketmq") +public class RocketMQBusApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(RocketMQBusApplication.class) + .properties("server.port=0") // Random server port + .properties("management.endpoints.web.exposure.include=*") // exposure + // includes + // all + .properties("spring.cloud.bus.trace.enabled=true") // Enable trace + .run(args); + } + + @Autowired + private ApplicationEventPublisher publisher; + + @Value("${spring.cloud.bus.id}") + private String originService; + + @Value("${server.port}") + private int localServerPort; + + @Autowired + private ObjectMapper objectMapper; + + /** + * Publish the {@link UserRemoteApplicationEvent} + * + * @param name the user name + * @param destination the destination + * @return If published + */ + @GetMapping("/bus/event/publish/user") + public boolean publish(@RequestParam String name, + @RequestParam(required = false) String destination) { + User user = new User(); + user.setId(System.currentTimeMillis()); + user.setName(name); + publisher.publishEvent( + new UserRemoteApplicationEvent(this, user, originService, destination)); + return true; + } + + /** + * Listener on the {@link UserRemoteApplicationEvent} + * + * @param event {@link UserRemoteApplicationEvent} + */ + @EventListener + public void onEvent(UserRemoteApplicationEvent event) { + System.out.printf("Server [port : %d] listeners on %s\n", localServerPort, + event.getUser()); + } + + @EventListener + public void onAckEvent(AckRemoteApplicationEvent event) + throws JsonProcessingException { + System.out.printf("Server [port : %d] listeners on %s\n", localServerPort, + objectMapper.writeValueAsString(event)); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java new file mode 100644 index 00000000..2c9dae87 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.examples.rocketmq; + +/** + * User Domain + * + * @author Mercy + * @since 0.2.1 + */ +public class User { + + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java new file mode 100644 index 00000000..9979e190 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.examples.rocketmq; + +import org.springframework.cloud.bus.event.RemoteApplicationEvent; + +/** + * {@link User} {@link RemoteApplicationEvent} + * + * @author Mercy + * @since 0.2.1 + */ +public class UserRemoteApplicationEvent extends RemoteApplicationEvent { + + private User user; + + public UserRemoteApplicationEvent() { + } + + public UserRemoteApplicationEvent(Object source, User user, String originService, + String destinationService) { + super(source, originService, destinationService); + this.user = user; + } + + public void setUser(User user) { + this.user = user; + } + + public User getUser() { + return user; + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/resources/bootstrap.properties b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/resources/bootstrap.properties new file mode 100644 index 00000000..9e36d301 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/resources/bootstrap.properties @@ -0,0 +1,4 @@ +spring.application.name=spring-cloud-bus-rocketmq-example +spring.cloud.stream.rocketmq.binder.name-server=127.0.0.1:9876 +server.port=8080 +spring.cloud.bus.id=${spring.application.name}:${server.port} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-config-server/pom.xml b/spring-cloud-alibaba-nacos-config-server/pom.xml new file mode 100644 index 00000000..9c9c33c2 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/pom.xml @@ -0,0 +1,87 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-alibaba-nacos-config-server + Spring Cloud Alibaba Nacos Config Server + + + + + + com.alibaba.nacos + nacos-config + + + + + org.springframework.cloud + spring-cloud-config-server + + + + + org.springframework.boot + spring-boot-starter + true + + + + org.springframework.boot + spring-boot-starter-actuator + true + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.cloud + spring-cloud-test-support + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/NacosConfigServerAutoConfiguration.java b/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/NacosConfigServerAutoConfiguration.java new file mode 100644 index 00000000..884a3cff --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/NacosConfigServerAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.nacos.config.server; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import com.alibaba.cloud.nacos.config.server.environment.NacosEnvironmentRepository; +import org.springframework.cloud.config.server.EnableConfigServer; +import org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Nacos Config Server Auto-Configuration + * + * @author Mercy + * @since 0.2.0 + */ +@ConditionalOnClass(EnableConfigServer.class) // If class of @EnableConfigServer is present in class-path +@ComponentScan(basePackages = { + "com.alibaba.nacos.config.server", +}) +@AutoConfigureBefore(ConfigServerAutoConfiguration.class) +@Configuration +public class NacosConfigServerAutoConfiguration { + + @Bean + public NacosEnvironmentRepository nacosEnvironmentRepository() { + return new NacosEnvironmentRepository(); + } + +} diff --git a/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/environment/NacosEnvironmentRepository.java b/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/environment/NacosEnvironmentRepository.java new file mode 100644 index 00000000..8f36b9b7 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/src/main/java/com/alibaba/cloud/nacos/config/server/environment/NacosEnvironmentRepository.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.nacos.config.server.environment; + +import com.alibaba.nacos.config.server.model.ConfigInfo; +import com.alibaba.nacos.config.server.service.PersistService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; + +import static com.alibaba.nacos.config.server.constant.Constants.DEFAULT_GROUP; + +/** + * Nacos {@link EnvironmentRepository} + * + * @author Mercy + * @since 0.2.0 + */ +public class NacosEnvironmentRepository implements EnvironmentRepository { + + @Autowired + private PersistService persistService; + + @Override + public Environment findOne(String application, String profile, String label) { + + String dataId = application + "-" + profile + ".properties"; + + ConfigInfo configInfo = persistService.findConfigInfo(dataId, DEFAULT_GROUP, label); + + return createEnvironment(configInfo, application, profile); + } + + private Environment createEnvironment(ConfigInfo configInfo, String application, String profile) { + + Environment environment = new Environment(application, profile); + + Properties properties = createProperties(configInfo); + + String propertySourceName = String.format("Nacos[application : %s , profile : %s]", application, profile); + + PropertySource propertySource = new PropertySource(propertySourceName, properties); + + environment.add(propertySource); + + return environment; + } + + private Properties createProperties(ConfigInfo configInfo) { + Properties properties = new Properties(); + String content = configInfo == null ? null : configInfo.getContent(); + if (StringUtils.hasText(content)) { + try { + properties.load(new StringReader(content)); + } catch (IOException e) { + throw new IllegalStateException("The format of content is a properties"); + } + } + return properties; + } + + private static String[] of(String... values) { + return values; + } +} diff --git a/spring-cloud-alibaba-nacos-config-server/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-nacos-config-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..aef71745 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto-Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.nacos.config.server.NacosConfigServerAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-config-server/src/test/java/com/alibaba/cloud/nacos/config/server/bootstrap/NacosConfigServerBootstrap.java b/spring-cloud-alibaba-nacos-config-server/src/test/java/com/alibaba/cloud/nacos/config/server/bootstrap/NacosConfigServerBootstrap.java new file mode 100644 index 00000000..8bab6235 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/src/test/java/com/alibaba/cloud/nacos/config/server/bootstrap/NacosConfigServerBootstrap.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.cloud.nacos.config.server.bootstrap; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.config.server.EnableConfigServer; +import org.springframework.context.annotation.Bean; + +/** + * Nacos Config Server Bootstrap + * + * @author Mercy + * @since 0.2.0 + */ +@EnableAutoConfiguration +@EnableConfigServer +public class NacosConfigServerBootstrap { + + public static void main(String[] args) { + System.setProperty("nacos.standalone", "true"); + SpringApplication.run(NacosConfigServerBootstrap.class); + } + + @Bean + public ApplicationRunner applicationRunner() { + + return args -> { + System.out.println("Running..."); + }; + } +} diff --git a/spring-cloud-alibaba-nacos-config-server/src/test/resources/application.properties b/spring-cloud-alibaba-nacos-config-server/src/test/resources/application.properties new file mode 100644 index 00000000..dcecabe7 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config-server/src/test/resources/application.properties @@ -0,0 +1,2 @@ +spring.application.name=nacos-config-server +management.endpoints.web.exposure.include=* \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-config/pom.xml b/spring-cloud-alibaba-nacos-config/pom.xml new file mode 100644 index 00000000..26115f00 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/pom.xml @@ -0,0 +1,122 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-nacos-config + Spring Cloud Alibaba Nacos Config + + + + + com.alibaba.nacos + nacos-client + + + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + + + + org.springframework.boot + spring-boot-actuator + provided + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + + org.powermock + powermock-module-junit4 + 2.0.0 + test + + + org.powermock + powermock-api-mockito2 + 2.0.0 + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java new file mode 100644 index 00000000..cab15a4e --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.refresh.NacosContextRefresher; +import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; +import com.alibaba.cloud.nacos.refresh.NacosRefreshProperties; + +/** + * @author juven.xuxb + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true) +public class NacosConfigAutoConfiguration { + + @Bean + public NacosConfigProperties nacosConfigProperties(ApplicationContext context) { + if (context.getParent() != null + && BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + context.getParent(), NacosConfigProperties.class).length > 0) { + return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(), + NacosConfigProperties.class); + } + NacosConfigProperties nacosConfigProperties = new NacosConfigProperties(); + return nacosConfigProperties; + } + + @Bean + public NacosRefreshProperties nacosRefreshProperties() { + return new NacosRefreshProperties(); + } + + @Bean + public NacosRefreshHistory nacosRefreshHistory() { + return new NacosRefreshHistory(); + } + + @Bean + public NacosContextRefresher nacosContextRefresher( + NacosConfigProperties nacosConfigProperties, + NacosRefreshProperties nacosRefreshProperties, + NacosRefreshHistory refreshHistory) { + return new NacosContextRefresher(nacosRefreshProperties, refreshHistory, + nacosConfigProperties.configServiceInstance()); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigBootstrapConfiguration.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigBootstrapConfiguration.java new file mode 100644 index 00000000..081464cd --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigBootstrapConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator; + +/** + * @author xiaojing + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true) +public class NacosConfigBootstrapConfiguration { + + @Bean + @ConditionalOnMissingBean + public NacosConfigProperties nacosConfigProperties() { + return new NacosConfigProperties(); + } + + @Bean + public NacosPropertySourceLocator nacosPropertySourceLocator( + NacosConfigProperties nacosConfigProperties) { + return new NacosPropertySourceLocator(nacosConfigProperties); + } + +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigProperties.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigProperties.java new file mode 100644 index 00000000..82ab1fac --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigProperties.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static com.alibaba.nacos.api.PropertyKeyConst.ACCESS_KEY; +import static com.alibaba.nacos.api.PropertyKeyConst.CLUSTER_NAME; +import static com.alibaba.nacos.api.PropertyKeyConst.CONTEXT_PATH; +import static com.alibaba.nacos.api.PropertyKeyConst.ENCODE; +import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT; +import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT_PORT; +import static com.alibaba.nacos.api.PropertyKeyConst.NAMESPACE; +import static com.alibaba.nacos.api.PropertyKeyConst.SECRET_KEY; +import static com.alibaba.nacos.api.PropertyKeyConst.SERVER_ADDR; + +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.nacos.api.NacosFactory; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * nacos properties + * + * @author leijuan + * @author xiaojing + * @author pbting + */ +@ConfigurationProperties(NacosConfigProperties.PREFIX) +public class NacosConfigProperties { + + public static final String PREFIX = "spring.cloud.nacos.config"; + + private static final Logger log = LoggerFactory + .getLogger(NacosConfigProperties.class); + + /** + * nacos config server address + */ + private String serverAddr; + + /** + * encode for nacos config content. + */ + private String encode; + + /** + * nacos config group, group is config data meta info. + */ + private String group = "DEFAULT_GROUP"; + + /** + * nacos config dataId prefix + */ + private String prefix; + /** + * the suffix of nacos config dataId, also the file extension of config content. + */ + private String fileExtension = "properties"; + + /** + * timeout for get config from nacos. + */ + private int timeout = 3000; + + /** + * endpoint for Nacos, the domain name of a service, through which the server address + * can be dynamically obtained. + */ + private String endpoint; + + /** + * namespace, separation configuration of different environments. + */ + private String namespace; + + /** + * access key for namespace. + */ + private String accessKey; + + /** + * secret key for namespace. + */ + private String secretKey; + + /** + * context path for nacos config server. + */ + private String contextPath; + + /** + * nacos config cluster name + */ + private String clusterName; + + private String name; + + /** + * the dataids for configurable multiple shared configurations , multiple separated by + * commas . + */ + private String sharedDataids; + + /** + * refreshable dataids , multiple separated by commas . + */ + private String refreshableDataids; + + /** + * a set of extended configurations . + */ + private List extConfig; + + private ConfigService configService; + + // todo sts support + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getFileExtension() { + return fileExtension; + } + + public void setFileExtension(String fileExtension) { + this.fileExtension = fileExtension; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getEncode() { + return encode; + } + + public void setEncode(String encode) { + this.encode = encode; + } + + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getName() { + return name; + } + + public String getSharedDataids() { + return sharedDataids; + } + + public void setSharedDataids(String sharedDataids) { + this.sharedDataids = sharedDataids; + } + + public String getRefreshableDataids() { + return refreshableDataids; + } + + public void setRefreshableDataids(String refreshableDataids) { + this.refreshableDataids = refreshableDataids; + } + + public List getExtConfig() { + return extConfig; + } + + public void setExtConfig(List extConfig) { + this.extConfig = extConfig; + } + + public void setName(String name) { + this.name = name; + } + + public static class Config { + /** + * the data id of extended configuration + */ + private String dataId; + /** + * the group of extended configuration, the default value is DEFAULT_GROUP + */ + private String group = "DEFAULT_GROUP"; + /** + * whether to support dynamic refresh, the default does not support . + */ + private boolean refresh = false; + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public boolean isRefresh() { + return refresh; + } + + public void setRefresh(boolean refresh) { + this.refresh = refresh; + } + } + + @Override + public String toString() { + return "NacosConfigProperties{" + "serverAddr='" + serverAddr + '\'' + + ", encode='" + encode + '\'' + ", group='" + group + '\'' + ", prefix='" + + prefix + '\'' + ", fileExtension='" + fileExtension + '\'' + + ", timeout=" + timeout + ", endpoint='" + endpoint + '\'' + + ", namespace='" + namespace + '\'' + ", accessKey='" + accessKey + '\'' + + ", secretKey='" + secretKey + '\'' + ", contextPath='" + contextPath + + '\'' + ", clusterName='" + clusterName + '\'' + ", name='" + name + '\'' + + ", sharedDataids='" + sharedDataids + '\'' + ", refreshableDataids='" + + refreshableDataids + '\'' + ", extConfig=" + extConfig + '}'; + } + + public ConfigService configServiceInstance() { + + if (null != configService) { + return configService; + } + + Properties properties = new Properties(); + properties.put(SERVER_ADDR, Objects.toString(this.serverAddr, "")); + properties.put(ENCODE, Objects.toString(this.encode, "")); + properties.put(NAMESPACE, Objects.toString(this.namespace, "")); + properties.put(ACCESS_KEY, Objects.toString(this.accessKey, "")); + properties.put(SECRET_KEY, Objects.toString(this.secretKey, "")); + properties.put(CONTEXT_PATH, Objects.toString(this.contextPath, "")); + properties.put(CLUSTER_NAME, Objects.toString(this.clusterName, "")); + + String endpoint = Objects.toString(this.endpoint, ""); + if (endpoint.contains(":")) { + int index = endpoint.indexOf(":"); + properties.put(ENDPOINT, endpoint.substring(0, index)); + properties.put(ENDPOINT_PORT, endpoint.substring(index + 1)); + } + else { + properties.put(ENDPOINT, endpoint); + } + + try { + configService = NacosFactory.createConfigService(properties); + return configService; + } + catch (Exception e) { + log.error("create config service error!properties={},e=,", this, e); + return null; + } + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosPropertySourceRepository.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosPropertySourceRepository.java new file mode 100644 index 00000000..ea011b00 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosPropertySourceRepository.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.cloud.nacos.client.NacosPropertySource; + +/** + * @author xiaojing + * @author pbting + */ +public class NacosPropertySourceRepository { + + private final static ConcurrentHashMap NACOS_PROPERTY_SOURCE_REPOSITORY = new ConcurrentHashMap<>(); + + /** + * @return all nacos properties from application context + */ + public static List getAll() { + List result = new ArrayList<>(); + result.addAll(NACOS_PROPERTY_SOURCE_REPOSITORY.values()); + return result; + } + + public static void collectNacosPropertySources( + NacosPropertySource nacosPropertySource) { + NACOS_PROPERTY_SOURCE_REPOSITORY.putIfAbsent(nacosPropertySource.getDataId(), + nacosPropertySource); + } + + public static NacosPropertySource getNacosPropertySource(String dataId) { + + return NACOS_PROPERTY_SOURCE_REPOSITORY.get(dataId); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySource.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySource.java new file mode 100644 index 00000000..d932f2b3 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySource.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.client; + +import java.util.Date; +import java.util.Map; + +import org.springframework.core.env.MapPropertySource; + +/** + * @author xiaojing + * @author pbting + */ +public class NacosPropertySource extends MapPropertySource { + + /** + * Nacos Group + */ + private final String group; + + /** + * Nacos dataID + */ + private final String dataId; + + /** + * timestamp the property get + */ + private final Date timestamp; + + /** + * Whether to support dynamic refresh for this Property Source + */ + private final boolean isRefreshable; + + NacosPropertySource(String group, String dataId, Map source, + Date timestamp, boolean isRefreshable) { + super(dataId, source); + this.group = group; + this.dataId = dataId; + this.timestamp = timestamp; + this.isRefreshable = isRefreshable; + } + + public String getGroup() { + return this.group; + } + + public String getDataId() { + return dataId; + } + + public Date getTimestamp() { + return timestamp; + } + + public boolean isRefreshable() { + return isRefreshable; + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceBuilder.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceBuilder.java new file mode 100644 index 00000000..e10c9a30 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceBuilder.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.client; + +import java.io.StringReader; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosPropertySourceRepository; +import com.alibaba.nacos.api.config.ConfigService; +import com.alibaba.nacos.api.exception.NacosException; + +/** + * @author xiaojing + * @author pbting + */ +public class NacosPropertySourceBuilder { + private static final Logger log = LoggerFactory + .getLogger(NacosPropertySourceBuilder.class); + private static final Properties EMPTY_PROPERTIES = new Properties(); + + private ConfigService configService; + private long timeout; + + public NacosPropertySourceBuilder(ConfigService configService, long timeout) { + this.configService = configService; + this.timeout = timeout; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public ConfigService getConfigService() { + return configService; + } + + public void setConfigService(ConfigService configService) { + this.configService = configService; + } + + /** + * @param dataId Nacos dataId + * @param group Nacos group + */ + NacosPropertySource build(String dataId, String group, String fileExtension, + boolean isRefreshable) { + Properties p = loadNacosData(dataId, group, fileExtension); + NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId, + propertiesToMap(p), new Date(), isRefreshable); + NacosPropertySourceRepository.collectNacosPropertySources(nacosPropertySource); + return nacosPropertySource; + } + + private Properties loadNacosData(String dataId, String group, String fileExtension) { + String data = null; + try { + data = configService.getConfig(dataId, group, timeout); + if (!StringUtils.isEmpty(data)) { + log.info(String.format("Loading nacos data, dataId: '%s', group: '%s'", + dataId, group)); + + if (fileExtension.equalsIgnoreCase("properties")) { + Properties properties = new Properties(); + + properties.load(new StringReader(data)); + return properties; + } + else if (fileExtension.equalsIgnoreCase("yaml") + || fileExtension.equalsIgnoreCase("yml")) { + YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean(); + yamlFactory.setResources(new ByteArrayResource(data.getBytes())); + return yamlFactory.getObject(); + } + + } + } + catch (NacosException e) { + log.error("get data from Nacos error,dataId:{}, ", dataId, e); + } + catch (Exception e) { + log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e); + } + return EMPTY_PROPERTIES; + } + + @SuppressWarnings("unchecked") + private Map propertiesToMap(Properties properties) { + Map result = new HashMap<>(16); + Enumeration keys = (Enumeration) properties.propertyNames(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + Object value = properties.getProperty(key); + if (value != null) { + result.put(key, ((String) value).trim()); + } + else { + result.put(key, null); + } + } + return result; + } + +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceLocator.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceLocator.java new file mode 100644 index 00000000..51541602 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/client/NacosPropertySourceLocator.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.client; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosConfigProperties; +import com.alibaba.cloud.nacos.NacosPropertySourceRepository; +import com.alibaba.cloud.nacos.refresh.NacosContextRefresher; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * @author xiaojing + * @author pbting + */ +@Order(0) +public class NacosPropertySourceLocator implements PropertySourceLocator { + + private static final Logger log = LoggerFactory + .getLogger(NacosPropertySourceLocator.class); + private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS"; + private static final String SEP1 = "-"; + private static final String DOT = "."; + private static final String SHARED_CONFIG_SEPARATOR_CHAR = "[,]"; + private static final List SUPPORT_FILE_EXTENSION = Arrays.asList("properties", + "yaml", "yml"); + + private NacosPropertySourceBuilder nacosPropertySourceBuilder; + + private NacosConfigProperties nacosConfigProperties; + + public NacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) { + this.nacosConfigProperties = nacosConfigProperties; + } + + @Override + public PropertySource locate(Environment env) { + + ConfigService configService = nacosConfigProperties.configServiceInstance(); + + if (null == configService) { + log.warn("no instance of config service found, can't load config from nacos"); + return null; + } + long timeout = nacosConfigProperties.getTimeout(); + nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, + timeout); + String name = nacosConfigProperties.getName(); + + String dataIdPrefix = nacosConfigProperties.getPrefix(); + if (StringUtils.isEmpty(dataIdPrefix)) { + dataIdPrefix = name; + } + + if (StringUtils.isEmpty(dataIdPrefix)) { + dataIdPrefix = env.getProperty("spring.application.name"); + } + + CompositePropertySource composite = new CompositePropertySource( + NACOS_PROPERTY_SOURCE_NAME); + + loadSharedConfiguration(composite); + loadExtConfiguration(composite); + loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env); + + return composite; + } + + private void loadSharedConfiguration( + CompositePropertySource compositePropertySource) { + String sharedDataIds = nacosConfigProperties.getSharedDataids(); + String refreshDataIds = nacosConfigProperties.getRefreshableDataids(); + + if (sharedDataIds == null || sharedDataIds.trim().length() == 0) { + return; + } + + String[] sharedDataIdArry = sharedDataIds.split(SHARED_CONFIG_SEPARATOR_CHAR); + checkDataIdFileExtension(sharedDataIdArry); + + for (int i = 0; i < sharedDataIdArry.length; i++) { + String dataId = sharedDataIdArry[i]; + String fileExtension = dataId.substring(dataId.lastIndexOf(".") + 1); + boolean isRefreshable = checkDataIdIsRefreshbable(refreshDataIds, + sharedDataIdArry[i]); + + loadNacosDataIfPresent(compositePropertySource, dataId, "DEFAULT_GROUP", + fileExtension, isRefreshable); + } + } + + private void loadExtConfiguration(CompositePropertySource compositePropertySource) { + if (nacosConfigProperties.getExtConfig() == null + || nacosConfigProperties.getExtConfig().isEmpty()) { + return; + } + + List extConfigs = nacosConfigProperties + .getExtConfig(); + checkExtConfiguration(extConfigs); + + for (NacosConfigProperties.Config config : extConfigs) { + String dataId = config.getDataId(); + String fileExtension = dataId.substring(dataId.lastIndexOf(".") + 1); + loadNacosDataIfPresent(compositePropertySource, dataId, config.getGroup(), + fileExtension, config.isRefresh()); + } + } + + private void checkExtConfiguration(List extConfigs) { + String[] dataIds = new String[extConfigs.size()]; + for (int i = 0; i < extConfigs.size(); i++) { + String dataId = extConfigs.get(i).getDataId(); + if (dataId == null || dataId.trim().length() == 0) { + throw new IllegalStateException(String.format( + "the [ spring.cloud.nacos.config.ext-config[%s] ] must give a dataid", + i)); + } + dataIds[i] = dataId; + } + checkDataIdFileExtension(dataIds); + } + + private void loadApplicationConfiguration( + CompositePropertySource compositePropertySource, String dataIdPrefix, + NacosConfigProperties properties, Environment environment) { + + String fileExtension = properties.getFileExtension(); + String nacosGroup = properties.getGroup(); + + loadNacosDataIfPresent(compositePropertySource, + dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true); + for (String profile : environment.getActiveProfiles()) { + String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension; + loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, + fileExtension, true); + } + } + + private void loadNacosDataIfPresent(final CompositePropertySource composite, + final String dataId, final String group, String fileExtension, + boolean isRefreshable) { + if (NacosContextRefresher.getRefreshCount() != 0) { + NacosPropertySource ps; + if (!isRefreshable) { + ps = NacosPropertySourceRepository.getNacosPropertySource(dataId); + } + else { + ps = nacosPropertySourceBuilder.build(dataId, group, fileExtension, true); + } + + composite.addFirstPropertySource(ps); + } + else { + NacosPropertySource ps = nacosPropertySourceBuilder.build(dataId, group, + fileExtension, isRefreshable); + composite.addFirstPropertySource(ps); + } + } + + private static void checkDataIdFileExtension(String[] dataIdArray) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < dataIdArray.length; i++) { + boolean isLegal = false; + for (String fileExtension : SUPPORT_FILE_EXTENSION) { + if (dataIdArray[i].indexOf(fileExtension) > 0) { + isLegal = true; + break; + } + } + // add tips + if (!isLegal) { + stringBuilder.append(dataIdArray[i] + ","); + } + } + + if (stringBuilder.length() > 0) { + String result = stringBuilder.substring(0, stringBuilder.length() - 1); + throw new IllegalStateException(String.format( + "[%s] must contains file extension with properties|yaml|yml", + result)); + } + } + + private boolean checkDataIdIsRefreshbable(String refreshDataIds, + String sharedDataId) { + if (refreshDataIds == null || "".equals(refreshDataIds)) { + return false; + } + + String[] refreshDataIdArry = refreshDataIds.split(SHARED_CONFIG_SEPARATOR_CHAR); + for (String refreshDataId : refreshDataIdArry) { + if (refreshDataId.equals(sharedDataId)) { + return true; + } + } + + return false; + } + +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureAnalyzer.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureAnalyzer.java new file mode 100644 index 00000000..10b1e20a --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureAnalyzer.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.diagnostics.analyzer; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * A {@code FailureAnalyzer} that performs analysis of failures caused by a + * {@code NacosConnectionFailureException}. + * + * @author juven.xuxb + */ +public class NacosConnectionFailureAnalyzer + extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + NacosConnectionFailureException cause) { + return new FailureAnalysis("Application failed to connect to Nacos server", + "check your nacos server config", cause); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureException.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureException.java new file mode 100644 index 00000000..f01458d4 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/diagnostics/analyzer/NacosConnectionFailureException.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.diagnostics.analyzer; + +/** + * A {@code NacosConnectionFailureException} is thrown when the application fails to + * connect to Nacos Server. + * + * @author juven.xuxb + */ +public class NacosConnectionFailureException extends RuntimeException { + + private final String domain; + + private final String port; + + public NacosConnectionFailureException(String domain, String port, String message) { + super(message); + this.domain = domain; + this.port = port; + } + + public NacosConnectionFailureException(String domain, String port, String message, + Throwable cause) { + super(message, cause); + this.domain = domain; + this.port = port; + } + + String getDomain() { + return domain; + } + + String getPort() { + return port; + } + +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpoint.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpoint.java new file mode 100644 index 00000000..e42a5421 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpoint.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.cloud.nacos.NacosConfigProperties; +import com.alibaba.cloud.nacos.NacosPropertySourceRepository; +import com.alibaba.cloud.nacos.client.NacosPropertySource; +import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; + +/** + * Endpoint for Nacos, contains config data and refresh history + * @author xiaojing + */ +@Endpoint(id = "nacos-config") +public class NacosConfigEndpoint { + + private final NacosConfigProperties properties; + + private final NacosRefreshHistory refreshHistory; + + private ThreadLocal dateFormat = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public NacosConfigEndpoint(NacosConfigProperties properties, + NacosRefreshHistory refreshHistory) { + this.properties = properties; + this.refreshHistory = refreshHistory; + } + + @ReadOperation + public Map invoke() { + Map result = new HashMap<>(16); + result.put("NacosConfigProperties", properties); + + List all = NacosPropertySourceRepository.getAll(); + + List> sources = new ArrayList<>(); + for (NacosPropertySource ps : all) { + Map source = new HashMap<>(16); + source.put("dataId", ps.getDataId()); + source.put("lastSynced", dateFormat.get().format(ps.getTimestamp())); + sources.add(source); + } + result.put("Sources", sources); + result.put("RefreshHistory", refreshHistory.getRecords()); + + return result; + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointAutoConfiguration.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointAutoConfiguration.java new file mode 100644 index 00000000..2ebeeb5e --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; + +import com.alibaba.cloud.nacos.NacosConfigProperties; +import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; + +/** + * @author xiaojing + */ +@ConditionalOnWebApplication +@ConditionalOnClass(value = Endpoint.class) +@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true) +public class NacosConfigEndpointAutoConfiguration { + + @Autowired + private NacosConfigProperties nacosConfigProperties; + + @Autowired + private NacosRefreshHistory nacosRefreshHistory; + + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + @Bean + public NacosConfigEndpoint nacosConfigEndpoint() { + return new NacosConfigEndpoint(nacosConfigProperties, nacosRefreshHistory); + } + + @Bean + public NacosConfigHealthIndicator nacosConfigHealthIndicator() { + return new NacosConfigHealthIndicator(nacosConfigProperties, + nacosConfigProperties.configServiceInstance()); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigHealthIndicator.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigHealthIndicator.java new file mode 100644 index 00000000..4954378c --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosConfigHealthIndicator.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosConfigProperties; +import com.alibaba.cloud.nacos.NacosPropertySourceRepository; +import com.alibaba.cloud.nacos.client.NacosPropertySource; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * @author xiaojing + */ +public class NacosConfigHealthIndicator extends AbstractHealthIndicator { + + private final NacosConfigProperties nacosConfigProperties; + + private final List dataIds; + + private final ConfigService configService; + + public NacosConfigHealthIndicator(NacosConfigProperties nacosConfigProperties, + ConfigService configService) { + this.nacosConfigProperties = nacosConfigProperties; + this.configService = configService; + + this.dataIds = new ArrayList<>(); + for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository + .getAll()) { + this.dataIds.add(nacosPropertySource.getDataId()); + } + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + for (String dataId : dataIds) { + try { + String config = configService.getConfig(dataId, + nacosConfigProperties.getGroup(), + nacosConfigProperties.getTimeout()); + if (StringUtils.isEmpty(config)) { + builder.down().withDetail(String.format("dataId: '%s', group: '%s'", + dataId, nacosConfigProperties.getGroup()), "config is empty"); + } + } + catch (Exception e) { + builder.down().withDetail(String.format("dataId: '%s', group: '%s'", + dataId, nacosConfigProperties.getGroup()), e.getMessage()); + } + } + builder.up().withDetail("dataIds", dataIds); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosContextRefresher.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosContextRefresher.java new file mode 100644 index 00000000..7512b283 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosContextRefresher.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.refresh; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cloud.endpoint.event.RefreshEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosPropertySourceRepository; +import com.alibaba.cloud.nacos.client.NacosPropertySource; +import com.alibaba.nacos.api.config.ConfigService; +import com.alibaba.nacos.api.config.listener.Listener; +import com.alibaba.nacos.api.exception.NacosException; + +/** + * On application start up, NacosContextRefresher add nacos listeners to all application + * level dataIds, when there is a change in the data, listeners will refresh + * configurations. + * + * @author juven.xuxb + * @author pbting + */ +public class NacosContextRefresher + implements ApplicationListener, ApplicationContextAware { + + private final static Logger log = LoggerFactory + .getLogger(NacosContextRefresher.class); + + private static final AtomicLong REFRESH_COUNT = new AtomicLong(0); + + private final NacosRefreshProperties refreshProperties; + + private final NacosRefreshHistory refreshHistory; + + private final ConfigService configService; + + private ApplicationContext applicationContext; + + private AtomicBoolean ready = new AtomicBoolean(false); + + private Map listenerMap = new ConcurrentHashMap<>(16); + + public NacosContextRefresher(NacosRefreshProperties refreshProperties, + NacosRefreshHistory refreshHistory, ConfigService configService) { + this.refreshProperties = refreshProperties; + this.refreshHistory = refreshHistory; + this.configService = configService; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + // many Spring context + if (this.ready.compareAndSet(false, true)) { + this.registerNacosListenersForApplications(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + private void registerNacosListenersForApplications() { + if (refreshProperties.isEnabled()) { + for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository + .getAll()) { + + if (!nacosPropertySource.isRefreshable()) { + continue; + } + + String dataId = nacosPropertySource.getDataId(); + registerNacosListener(nacosPropertySource.getGroup(), dataId); + } + } + } + + private void registerNacosListener(final String group, final String dataId) { + + Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() { + @Override + public void receiveConfigInfo(String configInfo) { + refreshCountIncrement(); + String md5 = ""; + if (!StringUtils.isEmpty(configInfo)) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8"))) + .toString(16); + } + catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e); + } + } + refreshHistory.add(dataId, md5); + applicationContext.publishEvent( + new RefreshEvent(this, null, "Refresh Nacos config")); + if (log.isDebugEnabled()) { + log.debug("Refresh Nacos config group " + group + ",dataId" + dataId); + } + } + + @Override + public Executor getExecutor() { + return null; + } + }); + + try { + configService.addListener(dataId, group, listener); + } + catch (NacosException e) { + e.printStackTrace(); + } + } + + public static long getRefreshCount() { + return REFRESH_COUNT.get(); + } + + public static void refreshCountIncrement() { + REFRESH_COUNT.incrementAndGet(); + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshHistory.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshHistory.java new file mode 100644 index 00000000..0778fb34 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshHistory.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.refresh; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; + +public class NacosRefreshHistory { + + private static final int MAX_SIZE = 20; + + private LinkedList records = new LinkedList<>(); + + private ThreadLocal dateFormat = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public void add(String dataId, String md5) { + records.addFirst(new Record(dateFormat.get().format(new Date()), dataId, md5)); + if (records.size() > MAX_SIZE) { + records.removeLast(); + } + } + + public LinkedList getRecords() { + return records; + } +} + +class Record { + + private final String timestamp; + + private final String dataId; + + private final String md5; + + public Record(String timestamp, String dataId, String md5) { + this.timestamp = timestamp; + this.dataId = dataId; + this.md5 = md5; + } + + public String getTimestamp() { + return timestamp; + } + + public String getDataId() { + return dataId; + } + + public String getMd5() { + return md5; + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshProperties.java b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshProperties.java new file mode 100644 index 00000000..6d07e794 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/NacosRefreshProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.refresh; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author xiaojing + */ +@Component +public class NacosRefreshProperties { + + @Value("${spring.cloud.nacos.config.refresh.enabled:true}") + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..597126e8 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,43 @@ +{ + "properties": [ + { + "name": "spring.cloud.nacos.config.encode", + "type": "java.lang.String", + "defaultValue": "UTF-8", + "description": "default encode for nacos config content." + }, + { + "name": "spring.cloud.nacos.config.prefix", + "type": "java.lang.String", + "defaultValue": "${spring.application.name}", + "description": "the prefix of dataId, nacos config data meta info. dataId = prefix + '-' + ${spring.active.profile} + `.` + ${spring.cloud.nacos.config.file-extension}." + }, + { + "name": "spring.cloud.nacos.config.file-extension", + "type": "java.lang.String", + "defaultValue": "properties", + "description": "the suffix of nacos config dataId, also the file extension of config content, only support properties now." + }, + { + "name": "spring.cloud.nacos.config.shared-dataids", + "type": "java.lang.String", + "description": "the dataids for configurable multiple shared configurations , multiple separated by commas ." + }, + { + "name": "spring.cloud.nacos.config.refreshable-dataids", + "type": "java.lang.String", + "description": "refreshable dataids , multiple separated by commas ." + }, + { + "name": "spring.cloud.nacos.config.ext-config", + "type": "java.util.List", + "description": "a set of extended configurations ." + }, + { + "name": "spring.cloud.nacos.config.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "enable nacos config or not." + } + ] +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..9977a854 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\ +com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration +org.springframework.boot.diagnostics.FailureAnalyzer=\ +com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationExtConfigTests.java b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationExtConfigTests.java new file mode 100644 index 00000000..46760321 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationExtConfigTests.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator; +import com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration; +import com.alibaba.nacos.client.config.NacosConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore("javax.management.*") +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ NacosConfigService.class }) +@SpringBootTest(classes = NacosConfigurationExtConfigTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", "spring.profiles.active=dev,test", + "spring.cloud.nacos.config.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.config.encode=utf-8", + "spring.cloud.nacos.config.timeout=1000", + "spring.cloud.nacos.config.file-extension=properties", + "spring.cloud.nacos.config.ext-config[0].data-id=ext-config-common01.properties", + "spring.cloud.nacos.config.ext-config[1].data-id=ext-config-common02.properties", + "spring.cloud.nacos.config.ext-config[1].group=GLOBAL_GROUP", + "spring.cloud.nacos.config.shared-dataids=common1.properties,common2.properties", + "spring.cloud.nacos.config.accessKey=test-accessKey", + "spring.cloud.nacos.config.secretKey=test-secretKey" }, webEnvironment = NONE) +public class NacosConfigurationExtConfigTests { + + static { + + try { + // when(any(ConfigService.class).getConfig(eq("test-name.properties"), + // eq("test-group"), any())).thenReturn("user.name=hello"); + + Method method = PowerMockito.method(NacosConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if ("test-name.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user.name=hello\nuser.age=12"; + } + + if ("test-name-dev.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user.name=dev"; + } + + if ("ext-config-common01.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-ext-config1=config1\ntest-ext-config2=config1"; + } + if ("ext-config-common02.properties".equals(args[0]) + && "GLOBAL_GROUP".equals(args[1])) { + return "test-ext-config2=config2"; + } + + if ("common1.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-common1=common1\ntest-common2=common1"; + } + + if ("common2.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-common2=common2"; + } + + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Autowired + private NacosPropertySourceLocator locator; + + @Autowired + private NacosConfigProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosPropertySourceLocator was not created", locator); + assertNotNull("NacosConfigProperties was not created", properties); + + Assert.assertEquals(environment.getProperty("test-ext-config1"), "config1"); + Assert.assertEquals(environment.getProperty("test-ext-config2"), "config2"); + Assert.assertEquals(environment.getProperty("test-common1"), "common1"); + Assert.assertEquals(environment.getProperty("test-common2"), "common2"); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ NacosConfigEndpointAutoConfiguration.class, + NacosConfigAutoConfiguration.class, NacosConfigBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationTests.java b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationTests.java new file mode 100644 index 00000000..1f80136c --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosConfigurationTests.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator; +import com.alibaba.cloud.nacos.endpoint.NacosConfigEndpoint; +import com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration; +import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; +import com.alibaba.nacos.client.config.NacosConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore("javax.management.*") +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ NacosConfigService.class }) +@SpringBootTest(classes = NacosConfigurationTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", "spring.profiles.active=dev,test", + "spring.cloud.nacos.config.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.config.namespace=test-namespace", + "spring.cloud.nacos.config.encode=utf-8", + "spring.cloud.nacos.config.timeout=1000", + "spring.cloud.nacos.config.group=test-group", + "spring.cloud.nacos.config.name=test-name", + "spring.cloud.nacos.config.cluster-name=test-cluster", + "spring.cloud.nacos.config.file-extension=properties", + "spring.cloud.nacos.config.contextPath=test-contextpath", + "spring.cloud.nacos.config.ext-config[0].data-id=ext-config-common01.properties", + "spring.cloud.nacos.config.ext-config[1].data-id=ext-config-common02.properties", + "spring.cloud.nacos.config.ext-config[1].group=GLOBAL_GROUP", + "spring.cloud.nacos.config.shared-dataids=common1.properties,common2.properties", + "spring.cloud.nacos.config.accessKey=test-accessKey", + "spring.cloud.nacos.config.secretKey=test-secretKey" }, webEnvironment = NONE) +public class NacosConfigurationTests { + + static { + + try { + + Method method = PowerMockito.method(NacosConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if ("test-name.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "user.name=hello\nuser.age=12"; + } + + if ("test-name-dev.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "user.name=dev"; + } + + if ("ext-config-common01.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-ext-config1=config1\ntest-ext-config2=config1"; + } + if ("ext-config-common02.properties".equals(args[0]) + && "GLOBAL_GROUP".equals(args[1])) { + return "test-ext-config2=config2"; + } + + if ("common1.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-common1=common1\ntest-common2=common1"; + } + + if ("common2.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "test-common2=common2"; + } + + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Autowired + private NacosPropertySourceLocator locator; + + @Autowired + private NacosConfigProperties properties; + + @Autowired + private NacosRefreshHistory refreshHistory; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosPropertySourceLocator was not created", locator); + assertNotNull("NacosConfigProperties was not created", properties); + + checkoutNacosConfigServerAddr(); + checkoutNacosConfigNamespace(); + checkoutNacosConfigClusterName(); + checkoutNacosConfigAccessKey(); + checkoutNacosConfigSecrectKey(); + checkoutNacosConfigName(); + checkoutNacosConfigGroup(); + checkoutNacosConfigContextPath(); + checkoutNacosConfigFileExtension(); + checkoutNacosConfigTimeout(); + checkoutNacosConfigEncode(); + + checkoutEndpoint(); + checkoutDataLoad(); + + } + + private void checkoutNacosConfigServerAddr() { + assertEquals("NacosConfigProperties server address is wrong", "127.0.0.1:8848", + properties.getServerAddr()); + } + + private void checkoutNacosConfigNamespace() { + assertEquals("NacosConfigProperties namespace is wrong", "test-namespace", + properties.getNamespace()); + } + + private void checkoutNacosConfigClusterName() { + assertEquals("NacosConfigProperties' cluster is wrong", "test-cluster", + properties.getClusterName()); + } + + private void checkoutNacosConfigAccessKey() { + assertEquals("NacosConfigProperties' is access key is wrong", "test-accessKey", + properties.getAccessKey()); + } + + private void checkoutNacosConfigSecrectKey() { + assertEquals("NacosConfigProperties' is secret key is wrong", "test-secretKey", + properties.getSecretKey()); + } + + private void checkoutNacosConfigContextPath() { + assertEquals("NacosConfigProperties' context path is wrong", "test-contextpath", + properties.getContextPath()); + } + + private void checkoutNacosConfigName() { + assertEquals("NacosConfigProperties' name is wrong", "test-name", + properties.getName()); + } + + private void checkoutNacosConfigGroup() { + assertEquals("NacosConfigProperties' group is wrong", "test-group", + properties.getGroup()); + } + + private void checkoutNacosConfigFileExtension() { + assertEquals("NacosConfigProperties' file extension is wrong", "properties", + properties.getFileExtension()); + } + + private void checkoutNacosConfigTimeout() { + assertEquals("NacosConfigProperties' timeout is wrong", 1000, + properties.getTimeout()); + } + + private void checkoutNacosConfigEncode() { + assertEquals("NacosConfigProperties' encode is wrong", "utf-8", + properties.getEncode()); + } + + private void checkoutDataLoad() { + + Assert.assertEquals("dev", environment.getProperty("user.name")); + Assert.assertEquals("12", environment.getProperty("user.age")); + } + + private void checkoutEndpoint() throws Exception { + NacosConfigEndpoint nacosConfigEndpoint = new NacosConfigEndpoint(properties, + refreshHistory); + Map map = nacosConfigEndpoint.invoke(); + assertEquals(map.get("NacosConfigProperties"), properties); + assertEquals(map.get("RefreshHistory"), refreshHistory.getRecords()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ NacosConfigEndpointAutoConfiguration.class, + NacosConfigAutoConfiguration.class, NacosConfigBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosFileExtensionTest.java b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosFileExtensionTest.java new file mode 100644 index 00000000..e4806e66 --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/NacosFileExtensionTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration; +import com.alibaba.nacos.client.config.NacosConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore("javax.management.*") +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ NacosConfigService.class }) +@SpringBootTest(classes = NacosFileExtensionTest.TestConfig.class, properties = { + "spring.application.name=test-name", + "spring.cloud.nacos.config.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.config.file-extension=yaml" }, webEnvironment = NONE) +public class NacosFileExtensionTest { + + static { + + try { + Method method = PowerMockito.method(NacosConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if ("test-name.yaml".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user:\n name: hello\n age: 12"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Test + public void contextLoads() throws Exception { + + Assert.assertEquals(environment.getProperty("user.name"), "hello"); + Assert.assertEquals(environment.getProperty("user.age"), "12"); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ NacosConfigEndpointAutoConfiguration.class, + NacosConfigAutoConfiguration.class, NacosConfigBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointTests.java b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointTests.java new file mode 100644 index 00000000..78b5c13a --- /dev/null +++ b/spring-cloud-alibaba-nacos-config/src/test/java/com/alibaba/cloud/nacos/endpoint/NacosConfigEndpointTests.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import static org.junit.Assert.assertEquals; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosConfigAutoConfiguration; +import com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration; +import com.alibaba.cloud.nacos.NacosConfigProperties; +import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; +import com.alibaba.nacos.client.config.NacosConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore("javax.management.*") +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ NacosConfigService.class }) +@SpringBootTest(classes = NacosConfigEndpointTests.TestConfig.class, properties = { + "spring.application.name=test-name", + "spring.cloud.nacos.config.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.config.file-extension=properties" }, webEnvironment = NONE) +public class NacosConfigEndpointTests { + + static { + + try { + + Method method = PowerMockito.method(NacosConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if ("test-name.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user.name=hello\nuser.age=12"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private NacosConfigProperties properties; + + @Autowired + private NacosRefreshHistory refreshHistory; + + @Test + public void contextLoads() throws Exception { + + checkoutEndpoint(); + checkoutAcmHealthIndicator(); + + } + + private void checkoutAcmHealthIndicator() { + try { + Builder builder = new Builder(); + + NacosConfigHealthIndicator healthIndicator = new NacosConfigHealthIndicator( + properties, properties.configServiceInstance()); + healthIndicator.doHealthCheck(builder); + + Builder builder1 = new Builder(); + List dataIds = new ArrayList<>(); + dataIds.add("test-name.properties"); + builder1.up().withDetail("dataIds", dataIds); + + Assert.assertTrue(builder.build().equals(builder1.build())); + + } + catch (Exception ignoreE) { + + } + + } + + private void checkoutEndpoint() throws Exception { + NacosConfigEndpoint endpoint = new NacosConfigEndpoint(properties, + refreshHistory); + Map map = endpoint.invoke(); + assertEquals(map.get("NacosConfigProperties"), properties); + assertEquals(map.get("RefreshHistory"), refreshHistory.getRecords()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ NacosConfigEndpointAutoConfiguration.class, + NacosConfigAutoConfiguration.class, NacosConfigBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/pom.xml b/spring-cloud-alibaba-nacos-discovery/pom.xml new file mode 100644 index 00000000..edcd04c2 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/pom.xml @@ -0,0 +1,120 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-nacos-discovery + Spring Cloud Alibaba Nacos Discovery + + + + + com.alibaba.nacos + nacos-client + + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + + + org.springframework.cloud + spring-cloud-config-client + true + + + + org.springframework.cloud + spring-cloud-config-server + true + + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ConditionalOnNacosDiscoveryEnabled.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ConditionalOnNacosDiscoveryEnabled.java new file mode 100644 index 00000000..d576b879 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ConditionalOnNacosDiscoveryEnabled.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.enabled", matchIfMissing = true) +public @interface ConditionalOnNacosDiscoveryEnabled { + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java new file mode 100644 index 00000000..180f1fa8 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration; +import com.alibaba.cloud.nacos.registry.NacosRegistration; +import com.alibaba.cloud.nacos.registry.NacosServiceRegistry; + +/** + * @author xiaojing + * @author Mercy + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnNacosDiscoveryEnabled +@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) +@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class, + AutoServiceRegistrationAutoConfiguration.class }) +public class NacosDiscoveryAutoConfiguration { + + @Bean + public NacosServiceRegistry nacosServiceRegistry( + NacosDiscoveryProperties nacosDiscoveryProperties) { + return new NacosServiceRegistry(nacosDiscoveryProperties); + } + + @Bean + @ConditionalOnBean(AutoServiceRegistrationProperties.class) + public NacosRegistration nacosRegistration( + NacosDiscoveryProperties nacosDiscoveryProperties, + ApplicationContext context) { + return new NacosRegistration(nacosDiscoveryProperties, context); + } + + @Bean + @ConditionalOnBean(AutoServiceRegistrationProperties.class) + public NacosAutoServiceRegistration nacosAutoServiceRegistration( + NacosServiceRegistry registry, + AutoServiceRegistrationProperties autoServiceRegistrationProperties, + NacosRegistration registration) { + return new NacosAutoServiceRegistration(registry, + autoServiceRegistrationProperties, registration); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryProperties.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryProperties.java new file mode 100644 index 00000000..30379591 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosDiscoveryProperties.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static com.alibaba.nacos.api.PropertyKeyConst.ACCESS_KEY; +import static com.alibaba.nacos.api.PropertyKeyConst.CLUSTER_NAME; +import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT; +import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT_PORT; +import static com.alibaba.nacos.api.PropertyKeyConst.NAMESPACE; +import static com.alibaba.nacos.api.PropertyKeyConst.NAMING_LOAD_CACHE_AT_START; +import static com.alibaba.nacos.api.PropertyKeyConst.SECRET_KEY; +import static com.alibaba.nacos.api.PropertyKeyConst.SERVER_ADDR; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.alibaba.nacos.api.NacosFactory; +import com.alibaba.nacos.api.naming.NamingMaintainFactory; +import com.alibaba.nacos.api.naming.NamingMaintainService; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.PreservedMetadataKeys; +import com.alibaba.nacos.client.naming.utils.UtilAndComs; + +/** + * @author dungu.zpf + * @author xiaojing + * @author Mercy + */ + +@ConfigurationProperties("spring.cloud.nacos.discovery") +public class NacosDiscoveryProperties { + + private static final Logger log = LoggerFactory + .getLogger(NacosDiscoveryProperties.class); + + /** + * nacos discovery server address + */ + private String serverAddr; + + /** + * the domain name of a service, through which the server address can be dynamically + * obtained. + */ + private String endpoint; + + /** + * namespace, separation registry of different environments. + */ + private String namespace; + + /** + * watch delay,duration to pull new service from nacos server. + */ + private long watchDelay = 30000; + + /** + * nacos naming log file name + */ + private String logName; + + /** + * service name to registry + */ + @Value("${spring.cloud.nacos.discovery.service:${spring.application.name:}}") + private String service; + + /** + * weight for service instance, the larger the value, the larger the weight. + */ + private float weight = 1; + + /** + * cluster name for nacos server. + */ + private String clusterName = "DEFAULT"; + + /** + * naming load from local cache at application start. true is load + */ + private String namingLoadCacheAtStart = "false"; + + /** + * extra metadata to register. + */ + private Map metadata = new HashMap<>(); + + /** + * if you just want to subscribe, but don't want to register your service, set it to + * false. + */ + private boolean registerEnabled = true; + + /** + * The ip address your want to register for your service instance, needn't to set it + * if the auto detect ip works well. + */ + private String ip; + + /** + * which network interface's ip you want to register + */ + private String networkInterface = ""; + + /** + * The port your want to register for your service instance, needn't to set it if the + * auto detect port works well + */ + private int port = -1; + + /** + * whether your service is a https service + */ + private boolean secure = false; + + /** + * access key for namespace. + */ + private String accessKey; + + /** + * secret key for namespace. + */ + private String secretKey; + + @Autowired + private InetUtils inetUtils; + + @Autowired + private Environment environment; + + private NamingService namingService; + + private NamingMaintainService namingMaintainService; + + @PostConstruct + public void init() throws SocketException { + + metadata.put(PreservedMetadataKeys.REGISTER_SOURCE, "SPRING_CLOUD"); + if (secure) { + metadata.put("secure", "true"); + } + + serverAddr = Objects.toString(serverAddr, ""); + if (serverAddr.lastIndexOf("/") != -1) { + serverAddr = serverAddr.substring(0, serverAddr.length() - 1); + } + endpoint = Objects.toString(endpoint, ""); + namespace = Objects.toString(namespace, ""); + logName = Objects.toString(logName, ""); + + if (StringUtils.isEmpty(ip)) { + // traversing network interfaces if didn't specify a interface + if (StringUtils.isEmpty(networkInterface)) { + ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + } + else { + NetworkInterface netInterface = NetworkInterface + .getByName(networkInterface); + if (null == netInterface) { + throw new IllegalArgumentException( + "no such interface " + networkInterface); + } + + Enumeration inetAddress = netInterface.getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + ip = currentAddress.getHostAddress(); + break; + } + } + + if (StringUtils.isEmpty(ip)) { + throw new RuntimeException("cannot find available ip from" + + " network interface " + networkInterface); + } + + } + } + + this.overrideFromEnv(environment); + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getLogName() { + return logName; + } + + public void setLogName(String logName) { + this.logName = logName; + } + + public void setInetUtils(InetUtils inetUtils) { + this.inetUtils = inetUtils; + } + + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public boolean isRegisterEnabled() { + return registerEnabled; + } + + public void setRegisterEnabled(boolean registerEnabled) { + this.registerEnabled = registerEnabled; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getNetworkInterface() { + return networkInterface; + } + + public void setNetworkInterface(String networkInterface) { + this.networkInterface = networkInterface; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean isSecure() { + return secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getNamingLoadCacheAtStart() { + return namingLoadCacheAtStart; + } + + public void setNamingLoadCacheAtStart(String namingLoadCacheAtStart) { + this.namingLoadCacheAtStart = namingLoadCacheAtStart; + } + + public long getWatchDelay() { + return watchDelay; + } + + public void setWatchDelay(long watchDelay) { + this.watchDelay = watchDelay; + } + + @Override + public String toString() { + return "NacosDiscoveryProperties{" + "serverAddr='" + serverAddr + '\'' + + ", endpoint='" + endpoint + '\'' + ", namespace='" + namespace + '\'' + + ", watchDelay=" + watchDelay + ", logName='" + logName + '\'' + + ", service='" + service + '\'' + ", weight=" + weight + + ", clusterName='" + clusterName + '\'' + ", namingLoadCacheAtStart='" + + namingLoadCacheAtStart + '\'' + ", metadata=" + metadata + + ", registerEnabled=" + registerEnabled + ", ip='" + ip + '\'' + + ", networkInterface='" + networkInterface + '\'' + ", port=" + port + + ", secure=" + secure + ", accessKey='" + accessKey + '\'' + + ", secretKey='" + secretKey + '\'' + '}'; + } + + public void overrideFromEnv(Environment env) { + + if (StringUtils.isEmpty(this.getServerAddr())) { + this.setServerAddr(env + .resolvePlaceholders("${spring.cloud.nacos.discovery.server-addr:}")); + } + if (StringUtils.isEmpty(this.getNamespace())) { + this.setNamespace(env + .resolvePlaceholders("${spring.cloud.nacos.discovery.namespace:}")); + } + if (StringUtils.isEmpty(this.getAccessKey())) { + this.setAccessKey(env + .resolvePlaceholders("${spring.cloud.nacos.discovery.access-key:}")); + } + if (StringUtils.isEmpty(this.getSecretKey())) { + this.setSecretKey(env + .resolvePlaceholders("${spring.cloud.nacos.discovery.secret-key:}")); + } + if (StringUtils.isEmpty(this.getLogName())) { + this.setLogName( + env.resolvePlaceholders("${spring.cloud.nacos.discovery.log-name:}")); + } + if (StringUtils.isEmpty(this.getClusterName())) { + this.setClusterName(env.resolvePlaceholders( + "${spring.cloud.nacos.discovery.cluster-name:}")); + } + if (StringUtils.isEmpty(this.getEndpoint())) { + this.setEndpoint( + env.resolvePlaceholders("${spring.cloud.nacos.discovery.endpoint:}")); + } + } + + public NamingService namingServiceInstance() { + + if (null != namingService) { + return namingService; + } + + try { + namingService = NacosFactory.createNamingService(getNacosProperties()); + } + catch (Exception e) { + log.error("create naming service error!properties={},e=,", this, e); + return null; + } + return namingService; + } + + public NamingMaintainService namingMaintainServiceInstance() { + + if (null != namingMaintainService) { + return namingMaintainService; + } + + try { + namingMaintainService = NamingMaintainFactory + .createMaintainService(getNacosProperties()); + } + catch (Exception e) { + log.error("create naming service error!properties={},e=,", this, e); + return null; + } + return namingMaintainService; + } + + private Properties getNacosProperties() { + Properties properties = new Properties(); + properties.put(SERVER_ADDR, serverAddr); + properties.put(NAMESPACE, namespace); + properties.put(UtilAndComs.NACOS_NAMING_LOG_NAME, logName); + + if (endpoint.contains(":")) { + int index = endpoint.indexOf(":"); + properties.put(ENDPOINT, endpoint.substring(0, index)); + properties.put(ENDPOINT_PORT, endpoint.substring(index + 1)); + } + else { + properties.put(ENDPOINT, endpoint); + } + + properties.put(ACCESS_KEY, accessKey); + properties.put(SECRET_KEY, secretKey); + properties.put(CLUSTER_NAME, clusterName); + properties.put(NAMING_LOAD_CACHE_AT_START, namingLoadCacheAtStart); + return properties; + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosServiceInstance.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosServiceInstance.java new file mode 100644 index 00000000..407a1560 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/NacosServiceInstance.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import java.net.URI; +import java.util.Map; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; + +/** + * @author xiaojing + */ +public class NacosServiceInstance implements ServiceInstance { + private String serviceId; + private String host; + private int port; + private boolean secure; + private Map metadata; + + @Override + public String getServiceId() { + return serviceId; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public URI getUri() { + return DefaultServiceInstance.getUri(this); + } + + @Override + public Map getMetadata() { + return metadata; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClient.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClient.java new file mode 100644 index 00000000..941a2319 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClient.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.discovery; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.NacosServiceInstance; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.api.naming.pojo.ListView; + +/** + * @author xiaojing + * @author renhaojun + */ +public class NacosDiscoveryClient implements DiscoveryClient { + + private static final Logger log = LoggerFactory.getLogger(NacosDiscoveryClient.class); + public static final String DESCRIPTION = "Spring Cloud Nacos Discovery Client"; + + private NacosDiscoveryProperties discoveryProperties; + + public NacosDiscoveryClient(NacosDiscoveryProperties discoveryProperties) { + this.discoveryProperties = discoveryProperties; + } + + @Override + public String description() { + return DESCRIPTION; + } + + @Override + public List getInstances(String serviceId) { + try { + List instances = discoveryProperties.namingServiceInstance() + .selectInstances(serviceId, true); + return hostToServiceInstanceList(instances, serviceId); + } + catch (Exception e) { + throw new RuntimeException( + "Can not get hosts from nacos server. serviceId: " + serviceId, e); + } + } + + private static ServiceInstance hostToServiceInstance(Instance instance, + String serviceId) { + NacosServiceInstance nacosServiceInstance = new NacosServiceInstance(); + nacosServiceInstance.setHost(instance.getIp()); + nacosServiceInstance.setPort(instance.getPort()); + nacosServiceInstance.setServiceId(serviceId); + + Map metadata = new HashMap<>(); + metadata.put("nacos.instanceId", instance.getInstanceId()); + metadata.put("nacos.weight", instance.getWeight() + ""); + metadata.put("nacos.healthy", instance.isHealthy() + ""); + metadata.put("nacos.cluster", instance.getClusterName() + ""); + metadata.putAll(instance.getMetadata()); + nacosServiceInstance.setMetadata(metadata); + + if (metadata.containsKey("secure")) { + boolean secure = Boolean.parseBoolean(metadata.get("secure")); + nacosServiceInstance.setSecure(secure); + } + return nacosServiceInstance; + } + + private static List hostToServiceInstanceList( + List instances, String serviceId) { + List result = new ArrayList<>(instances.size()); + for (Instance instance : instances) { + result.add(hostToServiceInstance(instance, serviceId)); + } + return result; + } + + @Override + public List getServices() { + + try { + ListView services = discoveryProperties.namingServiceInstance() + .getServicesOfServer(1, Integer.MAX_VALUE); + return services.getData(); + } + catch (Exception e) { + log.error("get service name from nacos server fail,", e); + return Collections.emptyList(); + } + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClientAutoConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..8de3bbae --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryClientAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.discovery; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.CommonsClientAutoConfiguration; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; + +/** + * @author xiaojing + */ +@Configuration +@ConditionalOnNacosDiscoveryEnabled +@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, + CommonsClientAutoConfiguration.class }) +public class NacosDiscoveryClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public NacosDiscoveryProperties nacosProperties() { + return new NacosDiscoveryProperties(); + } + + @Bean + public DiscoveryClient nacosDiscoveryClient( + NacosDiscoveryProperties discoveryProperties) { + return new NacosDiscoveryClient(discoveryProperties); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled", matchIfMissing = true) + public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties) { + return new NacosWatch(nacosDiscoveryProperties); + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosWatch.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosWatch.java new file mode 100644 index 00000000..d0c45a76 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/NacosWatch.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.nacos.discovery; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.listener.EventListener; + +/** + * @author xiaojing + */ +public class NacosWatch implements ApplicationEventPublisherAware, SmartLifecycle { + + private static final Logger log = LoggerFactory.getLogger(NacosWatch.class); + + private final NacosDiscoveryProperties properties; + + private final TaskScheduler taskScheduler; + + private final AtomicLong nacosWatchIndex = new AtomicLong(0); + + private final AtomicBoolean running = new AtomicBoolean(false); + + private ApplicationEventPublisher publisher; + + private ScheduledFuture watchFuture; + + private Set cacheServices = new HashSet<>(); + + private HashMap subscribeListeners = new HashMap<>(); + + public NacosWatch(NacosDiscoveryProperties properties) { + this(properties, getTaskScheduler()); + } + + public NacosWatch(NacosDiscoveryProperties properties, TaskScheduler taskScheduler) { + this.properties = properties; + this.taskScheduler = taskScheduler; + } + + private static ThreadPoolTaskScheduler getTaskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.initialize(); + return taskScheduler; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + this.stop(); + callback.run(); + } + + @Override + public void start() { + if (this.running.compareAndSet(false, true)) { + this.watchFuture = this.taskScheduler.scheduleWithFixedDelay( + this::nacosServicesWatch, this.properties.getWatchDelay()); + } + } + + @Override + public void stop() { + if (this.running.compareAndSet(true, false) && this.watchFuture != null) { + this.watchFuture.cancel(true); + } + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + public int getPhase() { + return 0; + } + + public void nacosServicesWatch() { + + // nacos doesn't support watch now , publish an event every 30 seconds. + this.publisher.publishEvent( + new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement())); + + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosConfigServerAutoConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosConfigServerAutoConfiguration.java new file mode 100644 index 00000000..0cccdcfd --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosConfigServerAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.discovery.configclient; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.config.ConfigServerProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; + +/** + * Extra configuration for config server if it happens to be registered with Nacos. + * + * @author JevonYang + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnClass({ NacosDiscoveryProperties.class, ConfigServerProperties.class }) +public class NacosConfigServerAutoConfiguration { + + @Autowired(required = false) + private NacosDiscoveryProperties properties; + + @Autowired(required = false) + private ConfigServerProperties server; + + @PostConstruct + public void init() { + if (this.properties == null || this.server == null) { + return; + } + String prefix = this.server.getPrefix(); + if (StringUtils.hasText(prefix) && !StringUtils + .hasText(this.properties.getMetadata().get("configPath"))) { + this.properties.getMetadata().put("configPath", prefix); + } + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosDiscoveryClientConfigServiceBootstrapConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosDiscoveryClientConfigServiceBootstrapConfiguration.java new file mode 100644 index 00000000..309439ae --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/discovery/configclient/NacosDiscoveryClientConfigServiceBootstrapConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.discovery.configclient; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +/** + * Helper for config client that wants to lookup the config server via discovery. + * + * @author JevonYang + */ +@ConditionalOnClass(ConfigServicePropertySourceLocator.class) +@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false) +@Configuration +@ImportAutoConfiguration({ NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) +public class NacosDiscoveryClientConfigServiceBootstrapConfiguration { + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpoint.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpoint.java new file mode 100644 index 00000000..6ea62bbb --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpoint.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.ServiceInfo; + +/** + * Endpoint for nacos discovery, get nacos properties and subscribed services + * @author xiaojing + */ +@Endpoint(id = "nacos-discovery") +public class NacosDiscoveryEndpoint { + + private static final Logger log = LoggerFactory + .getLogger(NacosDiscoveryEndpoint.class); + + private NacosDiscoveryProperties nacosDiscoveryProperties; + + public NacosDiscoveryEndpoint(NacosDiscoveryProperties nacosDiscoveryProperties) { + this.nacosDiscoveryProperties = nacosDiscoveryProperties; + } + + /** + * @return nacos discovery endpoint + */ + @ReadOperation + public Map nacosDiscovery() { + Map result = new HashMap<>(); + result.put("NacosDiscoveryProperties", nacosDiscoveryProperties); + + NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); + List subscribe = Collections.emptyList(); + + try { + subscribe = namingService.getSubscribeServices(); + } + catch (Exception e) { + log.error("get subscribe services from nacos fail,", e); + } + result.put("subscribe", subscribe); + return result; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpointAutoConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpointAutoConfiguration.java new file mode 100644 index 00000000..efb4b0f8 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/endpoint/NacosDiscoveryEndpointAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.endpoint; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; + +/** + * @author xiaojing + */ +@Configuration +@ConditionalOnClass(Endpoint.class) +@ConditionalOnNacosDiscoveryEnabled +public class NacosDiscoveryEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public NacosDiscoveryEndpoint nacosDiscoveryEndpoint( + NacosDiscoveryProperties nacosDiscoveryProperties) { + return new NacosDiscoveryEndpoint(nacosDiscoveryProperties); + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistration.java new file mode 100644 index 00000000..3dbf37c2 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistration.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author xiaojing + * @author Mercy + */ +public class NacosAutoServiceRegistration + extends AbstractAutoServiceRegistration { + private static final Logger log = LoggerFactory + .getLogger(NacosAutoServiceRegistration.class); + + private NacosRegistration registration; + + public NacosAutoServiceRegistration(ServiceRegistry serviceRegistry, + AutoServiceRegistrationProperties autoServiceRegistrationProperties, + NacosRegistration registration) { + super(serviceRegistry, autoServiceRegistrationProperties); + this.registration = registration; + } + + @Deprecated + public void setPort(int port) { + getPort().set(port); + } + + @Override + protected NacosRegistration getRegistration() { + if (this.registration.getPort() < 0 && this.getPort().get() > 0) { + this.registration.setPort(this.getPort().get()); + } + Assert.isTrue(this.registration.getPort() > 0, "service.port has not been set"); + return this.registration; + } + + @Override + protected NacosRegistration getManagementRegistration() { + return null; + } + + @Override + protected void register() { + if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) { + log.debug("Registration disabled."); + return; + } + if (this.registration.getPort() < 0) { + this.registration.setPort(getPort().get()); + } + super.register(); + } + + @Override + protected void registerManagement() { + if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) { + return; + } + super.registerManagement(); + + } + + @Override + protected Object getConfiguration() { + return this.registration.getNacosDiscoveryProperties(); + } + + @Override + protected boolean isEnabled() { + return this.registration.getNacosDiscoveryProperties().isRegisterEnabled(); + } + + @Override + @SuppressWarnings("deprecation") + protected String getAppName() { + String appName = registration.getNacosDiscoveryProperties().getService(); + return StringUtils.isEmpty(appName) ? super.getAppName() : appName; + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosRegistration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosRegistration.java new file mode 100644 index 00000000..2ad8783f --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosRegistration.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import java.net.URI; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ManagementServerPortUtils; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.NamingService; + +/** + * @author xiaojing + */ +public class NacosRegistration implements Registration, ServiceInstance { + + public static final String MANAGEMENT_PORT = "management.port"; + public static final String MANAGEMENT_CONTEXT_PATH = "management.context-path"; + public static final String MANAGEMENT_ADDRESS = "management.address"; + public static final String MANAGEMENT_ENDPOINT_BASE_PATH = "management.endpoints.web.base-path"; + + private NacosDiscoveryProperties nacosDiscoveryProperties; + + private ApplicationContext context; + + public NacosRegistration(NacosDiscoveryProperties nacosDiscoveryProperties, + ApplicationContext context) { + this.nacosDiscoveryProperties = nacosDiscoveryProperties; + this.context = context; + } + + @PostConstruct + public void init() { + + Map metadata = nacosDiscoveryProperties.getMetadata(); + Environment env = context.getEnvironment(); + + String endpointBasePath = env.getProperty(MANAGEMENT_ENDPOINT_BASE_PATH); + if (!StringUtils.isEmpty(endpointBasePath)) { + metadata.put(MANAGEMENT_ENDPOINT_BASE_PATH, endpointBasePath); + } + + Integer managementPort = ManagementServerPortUtils.getPort(context); + if (null != managementPort) { + metadata.put(MANAGEMENT_PORT, managementPort.toString()); + String contextPath = env + .getProperty("management.server.servlet.context-path"); + String address = env.getProperty("management.server.address"); + if (!StringUtils.isEmpty(contextPath)) { + metadata.put(MANAGEMENT_CONTEXT_PATH, contextPath); + } + if (!StringUtils.isEmpty(address)) { + metadata.put(MANAGEMENT_ADDRESS, address); + } + } + } + + @Override + public String getServiceId() { + return nacosDiscoveryProperties.getService(); + } + + @Override + public String getHost() { + return nacosDiscoveryProperties.getIp(); + } + + @Override + public int getPort() { + return nacosDiscoveryProperties.getPort(); + } + + public void setPort(int port) { + this.nacosDiscoveryProperties.setPort(port); + } + + @Override + public boolean isSecure() { + return nacosDiscoveryProperties.isSecure(); + } + + @Override + public URI getUri() { + return DefaultServiceInstance.getUri(this); + } + + @Override + public Map getMetadata() { + return nacosDiscoveryProperties.getMetadata(); + } + + public boolean isRegisterEnabled() { + return nacosDiscoveryProperties.isRegisterEnabled(); + } + + public String getCluster() { + return nacosDiscoveryProperties.getClusterName(); + } + + public float getRegisterWeight() { + return nacosDiscoveryProperties.getWeight(); + } + + public NacosDiscoveryProperties getNacosDiscoveryProperties() { + return nacosDiscoveryProperties; + } + + public NamingService getNacosNamingService() { + return nacosDiscoveryProperties.namingServiceInstance(); + } + + @Override + public String toString() { + return "NacosRegistration{" + "nacosDiscoveryProperties=" + + nacosDiscoveryProperties + '}'; + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosServiceRegistry.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosServiceRegistry.java new file mode 100644 index 00000000..6a88b1d1 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/registry/NacosServiceRegistry.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; + +/** + * @author xiaojing + * @author Mercy + */ +public class NacosServiceRegistry implements ServiceRegistry { + + private static final Logger log = LoggerFactory.getLogger(NacosServiceRegistry.class); + + private final NacosDiscoveryProperties nacosDiscoveryProperties; + + private final NamingService namingService; + + public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) { + this.nacosDiscoveryProperties = nacosDiscoveryProperties; + this.namingService = nacosDiscoveryProperties.namingServiceInstance(); + } + + @Override + public void register(Registration registration) { + + if (StringUtils.isEmpty(registration.getServiceId())) { + log.warn("No service to register for nacos client..."); + return; + } + + String serviceId = registration.getServiceId(); + + Instance instance = getNacosInstanceFromRegistration(registration); + + try { + namingService.registerInstance(serviceId, instance); + log.info("nacos registry, {} {}:{} register finished", serviceId, + instance.getIp(), instance.getPort()); + } + catch (Exception e) { + log.error("nacos registry, {} register failed...{},", serviceId, + registration.toString(), e); + } + } + + @Override + public void deregister(Registration registration) { + + log.info("De-registering from Nacos Server now..."); + + if (StringUtils.isEmpty(registration.getServiceId())) { + log.warn("No dom to de-register for nacos client..."); + return; + } + + NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); + String serviceId = registration.getServiceId(); + + try { + namingService.deregisterInstance(serviceId, registration.getHost(), + registration.getPort(), nacosDiscoveryProperties.getClusterName()); + } + catch (Exception e) { + log.error("ERR_NACOS_DEREGISTER, de-register failed...{},", + registration.toString(), e); + } + + log.info("De-registration finished."); + } + + @Override + public void close() { + + } + + @Override + public void setStatus(Registration registration, String status) { + + if (!status.equalsIgnoreCase("UP") && !status.equalsIgnoreCase("DOWN")) { + log.warn("can't support status {},please choose UP or DOWN", status); + return; + } + + String serviceId = registration.getServiceId(); + + Instance instance = getNacosInstanceFromRegistration(registration); + + if (status.equalsIgnoreCase("DOWN")) { + instance.setEnabled(false); + } + else { + instance.setEnabled(true); + } + + try { + nacosDiscoveryProperties.namingMaintainServiceInstance() + .updateInstance(serviceId, instance); + } + catch (Exception e) { + throw new RuntimeException("update nacos instance status fail", e); + } + + } + + @Override + public Object getStatus(Registration registration) { + + String serviceName = registration.getServiceId(); + try { + List instances = nacosDiscoveryProperties.namingServiceInstance() + .getAllInstances(serviceName); + for (Instance instance : instances) { + if (instance.getIp().equalsIgnoreCase(nacosDiscoveryProperties.getIp()) + && instance.getPort() == nacosDiscoveryProperties.getPort()) + return instance.isEnabled() ? "UP" : "DOWN"; + } + } + catch (Exception e) { + log.error("get all instance of {} error,", serviceName, e); + } + return null; + } + + private Instance getNacosInstanceFromRegistration(Registration registration) { + Instance instance = new Instance(); + instance.setIp(registration.getHost()); + instance.setPort(registration.getPort()); + instance.setWeight(nacosDiscoveryProperties.getWeight()); + instance.setClusterName(nacosDiscoveryProperties.getClusterName()); + instance.setMetadata(registration.getMetadata()); + return instance; + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ConditionalOnRibbonNacos.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ConditionalOnRibbonNacos.java new file mode 100644 index 00000000..1e25ec47 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ConditionalOnRibbonNacos.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnProperty(value = "ribbon.nacos.enabled", matchIfMissing = true) +public @interface ConditionalOnRibbonNacos { + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ExtendBalancer.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ExtendBalancer.java new file mode 100644 index 00000000..06111263 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/ExtendBalancer.java @@ -0,0 +1,21 @@ +package com.alibaba.cloud.nacos.ribbon; + +import java.util.List; + +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.client.naming.core.Balancer; + +/** + * @author itmuch.com + */ +public class ExtendBalancer extends Balancer { + /** + * 根据权重,随机选择实例 + * + * @param instances 实例列表 + * @return 选择的实例 + */ + public static Instance getHostByRandomWeight2(List instances) { + return getHostByRandomWeight(instances); + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfiguration.java new file mode 100644 index 00000000..86d56f24 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ServerList; + +/** + * integrated Ribbon by default + * @author xiaojing + */ +@Configuration +@ConditionalOnRibbonNacos +public class NacosRibbonClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public ServerList ribbonServerList(IClientConfig config, + NacosDiscoveryProperties nacosDiscoveryProperties) { + NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); + serverList.initWithNiwsConfig(config); + return serverList; + } + + @Bean + @ConditionalOnMissingBean + public NacosServerIntrospector nacosServerIntrospector() { + return new NacosServerIntrospector(); + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRule.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRule.java new file mode 100644 index 00000000..95bd7e6a --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosRule.java @@ -0,0 +1,78 @@ +package com.alibaba.cloud.nacos.ribbon; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AbstractLoadBalancerRule; +import com.netflix.loadbalancer.DynamicServerListLoadBalancer; +import com.netflix.loadbalancer.Server; + +/** + * Supports preferentially calling the ribbon load balancing rules of the same cluster + * instance. + * + * @author itmuch.com + */ +public class NacosRule extends AbstractLoadBalancerRule { + private static final Logger LOGGER = LoggerFactory.getLogger(NacosRule.class); + + @Autowired + private NacosDiscoveryProperties nacosDiscoveryProperties; + + @Override + public Server choose(Object key) { + try { + String clusterName = this.nacosDiscoveryProperties.getClusterName(); + DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer(); + String name = loadBalancer.getName(); + + NamingService namingService = this.nacosDiscoveryProperties + .namingServiceInstance(); + List instances = namingService.selectInstances(name, true); + if (CollectionUtils.isEmpty(instances)) { + LOGGER.warn("no instance in service {}", name); + return null; + } + + List instancesToChoose = instances; + if (StringUtils.isNotBlank(clusterName)) { + List sameClusterInstances = instances.stream() + .filter(instance -> Objects.equals(clusterName, + instance.getClusterName())) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(sameClusterInstances)) { + instancesToChoose = sameClusterInstances; + } + else { + LOGGER.warn( + "A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", + name, clusterName, instances); + } + } + + Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose); + + return new NacosServer(instance); + } + catch (Exception e) { + LOGGER.warn("NacosRule error", e); + return null; + } + } + + @Override + public void initWithNiwsConfig(IClientConfig iClientConfig) { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServer.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServer.java new file mode 100644 index 00000000..73d9579e --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import java.util.Map; + +import com.alibaba.nacos.api.naming.pojo.Instance; + +import com.netflix.loadbalancer.Server; + +/** + * @author xiaojing + * @author pbting + */ +public class NacosServer extends Server { + + private final MetaInfo metaInfo; + private final Instance instance; + private final Map metadata; + + public NacosServer(final Instance instance) { + super(instance.getIp(), instance.getPort()); + this.instance = instance; + this.metaInfo = new MetaInfo() { + @Override + public String getAppName() { + return instance.getServiceName(); + } + + @Override + public String getServerGroup() { + return null; + } + + @Override + public String getServiceIdForDiscovery() { + return null; + } + + @Override + public String getInstanceId() { + return instance.getInstanceId(); + } + }; + this.metadata = instance.getMetadata(); + } + + @Override + public MetaInfo getMetaInfo() { + return metaInfo; + } + + public Instance getInstance() { + return instance; + } + + public Map getMetadata() { + return metadata; + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerIntrospector.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerIntrospector.java new file mode 100644 index 00000000..481c2d4a --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerIntrospector.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import java.util.Map; + +import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; + +import com.netflix.loadbalancer.Server; + +/** + * @author xiaojing + */ +public class NacosServerIntrospector extends DefaultServerIntrospector { + + @Override + public Map getMetadata(Server server) { + if (server instanceof NacosServer) { + return ((NacosServer) server).getMetadata(); + } + return super.getMetadata(server); + } + + @Override + public boolean isSecure(Server server) { + if (server instanceof NacosServer) { + return Boolean.valueOf(((NacosServer) server).getMetadata().get("secure")); + } + + return super.isSecure(server); + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerList.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerList.java new file mode 100644 index 00000000..7da981a3 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/NacosServerList.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.nacos.api.naming.pojo.Instance; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AbstractServerList; + +/** + * @author xiaojing + * @author renhaojun + */ +public class NacosServerList extends AbstractServerList { + + private NacosDiscoveryProperties discoveryProperties; + + private String serviceId; + + public NacosServerList(NacosDiscoveryProperties discoveryProperties) { + this.discoveryProperties = discoveryProperties; + } + + @Override + public List getInitialListOfServers() { + return getServers(); + } + + @Override + public List getUpdatedListOfServers() { + return getServers(); + } + + private List getServers() { + try { + List instances = discoveryProperties.namingServiceInstance() + .selectInstances(serviceId, true); + return instancesToServerList(instances); + } + catch (Exception e) { + throw new IllegalStateException( + "Can not get service instances from nacos, serviceId=" + serviceId, + e); + } + } + + private List instancesToServerList(List instances) { + List result = new ArrayList<>(); + if (null == instances) { + return result; + } + for (Instance instance : instances) { + result.add(new NacosServer(instance)); + } + + return result; + } + + public String getServiceId() { + return serviceId; + } + + @Override + public void initWithNiwsConfig(IClientConfig iClientConfig) { + this.serviceId = iClientConfig.getClientName(); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/RibbonNacosAutoConfiguration.java b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/RibbonNacosAutoConfiguration.java new file mode 100644 index 00000000..64a30167 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/ribbon/RibbonNacosAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonClients; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that sets up Ribbon for Nacos. + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnBean(SpringClientFactory.class) +@ConditionalOnRibbonNacos +@ConditionalOnNacosDiscoveryEnabled +@AutoConfigureAfter(RibbonAutoConfiguration.class) +@RibbonClients(defaultConfiguration = NacosRibbonClientConfiguration.class) +public class RibbonNacosAutoConfiguration { +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..54c3f79f --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,26 @@ +{"properties": [ + { + "name": "spring.cloud.nacos.discovery.service", + "type": "java.lang.String", + "defaultValue": "${spring.application.name}", + "description": "the service name to register, default value is ${spring.application.name}." + }, + { + "name": "spring.cloud.nacos.discovery.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "enable nacos discovery or not." + }, + { + "name": "spring.cloud.nacos.discovery.namingLoadCacheAtStart", + "type": "java.lang.Boolean", + "defaultValue": "false", + "description": "naming load from local cache at application start ." + }, + { + "name": "spring.cloud.nacos.discovery.watch.enabled", + "type": "java.lang.Boolean", + "defaultValue": "true", + "description": "enable nacos discovery watch or not ." + } +]} diff --git a/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..409c2720 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration,\ + com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\ + com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\ + com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration,\ + com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryClientTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryClientTests.java new file mode 100644 index 00000000..260f46ff --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryClientTests.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +import static com.alibaba.cloud.nacos.test.NacosMockTest.serviceInstance; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import org.junit.Test; +import org.springframework.cloud.client.ServiceInstance; + +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClient; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.api.naming.pojo.ListView; + +/** + * @author xiaojing + */ +public class NacosDiscoveryClientTests { + + private String host = "123.123.123.123"; + private int port = 8888; + private String serviceName = "test-service"; + + @Test + public void testGetServers() throws Exception { + + ArrayList instances = new ArrayList<>(); + + HashMap map = new HashMap<>(); + map.put("test-key", "test-value"); + map.put("secure", "true"); + + instances.add(serviceInstance(serviceName, false, host, port, map)); + + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.selectInstances(eq(serviceName), eq(true))) + .thenReturn(instances); + + NacosDiscoveryClient discoveryClient = new NacosDiscoveryClient( + nacosDiscoveryProperties); + + List serviceInstances = discoveryClient + .getInstances(serviceName); + + assertThat(serviceInstances.size()).isEqualTo(1); + + ServiceInstance serviceInstance = serviceInstances.get(0); + + assertThat(serviceInstance.getServiceId()).isEqualTo(serviceName); + assertThat(serviceInstance.getHost()).isEqualTo(host); + assertThat(serviceInstance.getPort()).isEqualTo(port); + assertThat(serviceInstance.isSecure()).isEqualTo(true); + assertThat(serviceInstance.getUri().toString()) + .isEqualTo(getUri(serviceInstance)); + assertThat(serviceInstance.getMetadata().get("test-key")).isEqualTo("test-value"); + + } + + @Test + public void testGetAllService() throws Exception { + + ListView nacosServices = new ListView<>(); + + nacosServices.setData(new LinkedList<>()); + + nacosServices.getData().add(serviceName + "1"); + nacosServices.getData().add(serviceName + "2"); + nacosServices.getData().add(serviceName + "3"); + + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + NacosDiscoveryClient discoveryClient = new NacosDiscoveryClient( + nacosDiscoveryProperties); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.getServicesOfServer(eq(1), eq(Integer.MAX_VALUE))) + .thenReturn(nacosServices); + + List services = discoveryClient.getServices(); + + assertThat(services.size()).isEqualTo(3); + assertThat(services.contains(serviceName + "1")); + assertThat(services.contains(serviceName + "2")); + assertThat(services.contains(serviceName + "3")); + + } + + private String getUri(ServiceInstance instance) { + + if (instance.isSecure()) { + return "https://" + host + ":" + port; + } + + return "http://" + host + ":" + port; + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpNetworkInterfaceTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpNetworkInterfaceTests.java new file mode 100644 index 00000000..0486b851 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpNetworkInterfaceTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NacosAutoServiceRegistrationIpNetworkInterfaceTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848" }, webEnvironment = RANDOM_PORT) +public class NacosAutoServiceRegistrationIpNetworkInterfaceTests { + + @Autowired + private NacosRegistration registration; + + @Autowired + private NacosAutoServiceRegistration nacosAutoServiceRegistration; + + @Autowired + private NacosDiscoveryProperties properties; + + @Autowired + private InetUtils inetUtils; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosRegistration was not created", registration); + assertNotNull("NacosDiscoveryProperties was not created", properties); + assertNotNull("NacosAutoServiceRegistration was not created", + nacosAutoServiceRegistration); + + checkoutNacosDiscoveryServiceIP(); + + } + + private void checkoutNacosDiscoveryServiceIP() { + assertEquals("NacosDiscoveryProperties service IP was wrong", + getIPFromNetworkInterface(TestConfig.netWorkInterfaceName), + registration.getHost()); + + } + + private String getIPFromNetworkInterface(String networkInterface) { + + if (!TestConfig.hasValidNetworkInterface) { + return inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + } + + try { + NetworkInterface netInterface = NetworkInterface.getByName(networkInterface); + + Enumeration inetAddress = netInterface.getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + return currentAddress.getHostAddress(); + } + } + return networkInterface; + } + catch (Exception e) { + return networkInterface; + } + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) + public static class TestConfig { + + static boolean hasValidNetworkInterface = false; + static String netWorkInterfaceName; + + static { + + try { + Enumeration enumeration = NetworkInterface + .getNetworkInterfaces(); + while (enumeration.hasMoreElements() && !hasValidNetworkInterface) { + NetworkInterface networkInterface = enumeration.nextElement(); + Enumeration inetAddress = networkInterface + .getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + hasValidNetworkInterface = true; + netWorkInterfaceName = networkInterface.getName(); + System.setProperty( + "spring.cloud.nacos.discovery.network-interface", + networkInterface.getName()); + break; + } + } + } + + } + catch (Exception e) { + + } + } + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpTests.java new file mode 100644 index 00000000..2f0944eb --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationIpTests.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NacosAutoServiceRegistrationIpTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.discovery.ip=123.123.123.123" }, webEnvironment = RANDOM_PORT) +public class NacosAutoServiceRegistrationIpTests { + + @Autowired + private NacosRegistration registration; + + @Autowired + private NacosAutoServiceRegistration nacosAutoServiceRegistration; + + @Autowired + private NacosDiscoveryProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosRegistration was not created", registration); + assertNotNull("NacosDiscoveryProperties was not created", properties); + assertNotNull("NacosAutoServiceRegistration was not created", + nacosAutoServiceRegistration); + + checkoutNacosDiscoveryServiceIP(); + + } + + private void checkoutNacosDiscoveryServiceIP() { + assertEquals("NacosDiscoveryProperties service IP was wrong", "123.123.123.123", + registration.getHost()); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationManagementPortTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationManagementPortTests.java new file mode 100644 index 00000000..6fca25f8 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationManagementPortTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NacosAutoServiceRegistrationManagementPortTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", "management.server.port=8888", + "management.server.servlet.context-path=/test-context-path", + "spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.discovery.port=8888" }, webEnvironment = RANDOM_PORT) +public class NacosAutoServiceRegistrationManagementPortTests { + + @Autowired + private NacosRegistration registration; + + @Autowired + private NacosAutoServiceRegistration nacosAutoServiceRegistration; + + @Autowired + private NacosDiscoveryProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosRegistration was not created", registration); + assertNotNull("NacosDiscoveryProperties was not created", properties); + assertNotNull("NacosAutoServiceRegistration was not created", + nacosAutoServiceRegistration); + + checkoutNacosDiscoveryManagementData(); + + } + + private void checkoutNacosDiscoveryManagementData() { + assertEquals("NacosDiscoveryProperties management port was wrong", "8888", + properties.getMetadata().get(NacosRegistration.MANAGEMENT_PORT)); + + assertEquals("NacosDiscoveryProperties management context path was wrong", + "/test-context-path", + properties.getMetadata().get(NacosRegistration.MANAGEMENT_CONTEXT_PATH)); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationPortTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationPortTests.java new file mode 100644 index 00000000..25483a82 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationPortTests.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NacosAutoServiceRegistrationPortTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.discovery.port=8888" }, webEnvironment = RANDOM_PORT) +public class NacosAutoServiceRegistrationPortTests { + + @Autowired + private NacosRegistration registration; + + @Autowired + private NacosAutoServiceRegistration nacosAutoServiceRegistration; + + @Autowired + private NacosDiscoveryProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosRegistration was not created", registration); + assertNotNull("NacosDiscoveryProperties was not created", properties); + assertNotNull("NacosAutoServiceRegistration was not created", + nacosAutoServiceRegistration); + + checkoutNacosDiscoveryServicePort(); + + } + + private void checkoutNacosDiscoveryServicePort() { + assertEquals("NacosDiscoveryProperties service Port was wrong", 8888, + registration.getPort()); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationTests.java new file mode 100644 index 00000000..214b569a --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/registry/NacosAutoServiceRegistrationTests.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; +import com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpoint; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NacosAutoServiceRegistrationTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848", + "spring.cloud.nacos.discovery.endpoint=test-endpoint", + "spring.cloud.nacos.discovery.namespace=test-namespace", + "spring.cloud.nacos.discovery.log-name=test-logName", + "spring.cloud.nacos.discovery.weight=2", + "spring.cloud.nacos.discovery.clusterName=test-cluster", + "spring.cloud.nacos.discovery.namingLoadCacheAtStart=true", + "spring.cloud.nacos.discovery.secure=true", + "spring.cloud.nacos.discovery.accessKey=test-accessKey", + "spring.cloud.nacos.discovery.secretKey=test-secretKey" }, webEnvironment = RANDOM_PORT) +public class NacosAutoServiceRegistrationTests { + + @Autowired + private NacosRegistration registration; + + @Autowired + private NacosAutoServiceRegistration nacosAutoServiceRegistration; + + @LocalServerPort + private int port; + + @Autowired + private NacosDiscoveryProperties properties; + + @Autowired + private InetUtils inetUtils; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("NacosRegistration was not created", registration); + assertNotNull("NacosDiscoveryProperties was not created", properties); + assertNotNull("NacosAutoServiceRegistration was not created", + nacosAutoServiceRegistration); + + checkoutNacosDiscoveryServerAddr(); + checkoutNacosDiscoveryEndpoint(); + checkoutNacosDiscoveryNamespace(); + checkoutNacosDiscoveryLogName(); + checkoutNacosDiscoveryWeight(); + checkoutNacosDiscoveryClusterName(); + checkoutNacosDiscoveryCache(); + checkoutNacosDiscoverySecure(); + checkoutNacosDiscoveryAccessKey(); + checkoutNacosDiscoverySecrectKey(); + + checkoutNacosDiscoveryServiceName(); + checkoutNacosDiscoveryServiceIP(); + checkoutNacosDiscoveryServicePort(); + + checkAutoRegister(); + + checkoutEndpoint(); + + } + + private void checkAutoRegister() { + assertTrue("Nacos Auto Registration was not start", + nacosAutoServiceRegistration.isRunning()); + } + + private void checkoutNacosDiscoveryServerAddr() { + assertEquals("NacosDiscoveryProperties server address was wrong", + "127.0.0.1:8848", properties.getServerAddr()); + + } + + private void checkoutNacosDiscoveryEndpoint() { + assertEquals("NacosDiscoveryProperties endpoint was wrong", "test-endpoint", + properties.getEndpoint()); + + } + + private void checkoutNacosDiscoveryNamespace() { + assertEquals("NacosDiscoveryProperties namespace was wrong", "test-namespace", + properties.getNamespace()); + + } + + private void checkoutNacosDiscoveryLogName() { + assertEquals("NacosDiscoveryProperties logName was wrong", "test-logName", + properties.getLogName()); + } + + private void checkoutNacosDiscoveryWeight() { + assertEquals(2, properties.getWeight(), 0.00000001); + } + + private void checkoutNacosDiscoveryClusterName() { + assertEquals("NacosDiscoveryProperties cluster was wrong", "test-cluster", + properties.getClusterName()); + } + + private void checkoutNacosDiscoveryCache() { + assertEquals("NacosDiscoveryProperties naming load cache was wrong", "true", + properties.getNamingLoadCacheAtStart()); + } + + private void checkoutNacosDiscoverySecure() { + assertEquals("NacosDiscoveryProperties is secure was wrong", true, + properties.isSecure()); + assertEquals("NacosDiscoveryProperties is secure was wrong", "true", + properties.getMetadata().get("secure")); + } + + private void checkoutNacosDiscoveryAccessKey() { + assertEquals("NacosDiscoveryProperties is access key was wrong", "test-accessKey", + properties.getAccessKey()); + } + + private void checkoutNacosDiscoverySecrectKey() { + assertEquals("NacosDiscoveryProperties is secret key was wrong", "test-secretKey", + properties.getSecretKey()); + } + + private void checkoutNacosDiscoveryServiceName() { + assertEquals("NacosDiscoveryProperties service name was wrong", "myTestService1", + properties.getService()); + + } + + private void checkoutNacosDiscoveryServiceIP() { + assertEquals("NacosDiscoveryProperties service IP was wrong", + inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(), + registration.getHost()); + + } + + private void checkoutNacosDiscoveryServicePort() { + assertEquals("NacosDiscoveryProperties service Port was wrong", port, + registration.getPort()); + + } + + private void checkoutEndpoint() throws Exception { + NacosDiscoveryEndpoint nacosDiscoveryEndpoint = new NacosDiscoveryEndpoint( + properties); + Map map = nacosDiscoveryEndpoint.nacosDiscovery(); + assertEquals(map.get("NacosDiscoveryProperties"), properties); + assertEquals(map.get("subscribe").toString(), + properties.namingServiceInstance().getSubscribeServices().toString()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + NacosDiscoveryAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfigurationTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfigurationTests.java new file mode 100644 index 00000000..2dd70b04 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosRibbonClientConfigurationTests.java @@ -0,0 +1,63 @@ +package com.alibaba.cloud.nacos.ribbon; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration; + +import com.netflix.client.config.DefaultClientConfigImpl; +import com.netflix.client.config.IClientConfig; + +/** + * @author xiaojing + */ +public class NacosRibbonClientConfigurationTests { + + private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(NacosRibbonTestConfiguration.class, + NacosRibbonClientConfiguration.class, + NacosDiscoveryClientAutoConfiguration.class, + RibbonNacosAutoConfiguration.class)) + .withPropertyValues("spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848") + .withPropertyValues("spring.cloud.nacos.discovery.port=18080") + .withPropertyValues("spring.cloud.nacos.discovery.service=myapp"); + + @Test + public void testProperties() { + + this.contextRunner.run(context -> { + NacosServerList serverList = context.getBean(NacosServerList.class); + assertThat(serverList.getServiceId()).isEqualTo("myapp"); + }); + } + + @Configuration + @EnableAutoConfiguration + @EnableDiscoveryClient + static class NacosRibbonTestConfiguration { + + @Bean + IClientConfig iClientConfig() { + DefaultClientConfigImpl config = new DefaultClientConfigImpl(); + config.setClientName("myapp"); + return config; + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplate(); + } + + } + +} diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosServerListTests.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosServerListTests.java new file mode 100644 index 00000000..2732f2b4 --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/ribbon/NacosServerListTests.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.ribbon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.test.NacosMockTest; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; + +import com.netflix.client.config.IClientConfig; + +/** + * @author xiaojing + */ + +public class NacosServerListTests { + + @Test + @SuppressWarnings("unchecked") + public void testEmptyInstancesReturnsEmptyList() throws Exception { + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.selectInstances(anyString(), eq(true))).thenReturn(null); + + NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); + List servers = serverList.getInitialListOfServers(); + assertThat(servers).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetServers() throws Exception { + + ArrayList instances = new ArrayList<>(); + instances.add(NacosMockTest.serviceInstance("test-service", false, + Collections.emptyMap())); + + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.selectInstances(eq("test-service"), eq(true))) + .thenReturn(instances); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); + serverList.initWithNiwsConfig(clientConfig); + List servers = serverList.getInitialListOfServers(); + assertThat(servers).hasSize(1); + + servers = serverList.getUpdatedListOfServers(); + assertThat(servers).hasSize(1); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetServersWithInstanceStatus() throws Exception { + ArrayList instances = new ArrayList<>(); + + HashMap map1 = new HashMap<>(); + map1.put("instanceNum", "1"); + HashMap map2 = new HashMap<>(); + map2.put("instanceNum", "2"); + instances.add(NacosMockTest.serviceInstance("test-service", false, map1)); + instances.add(NacosMockTest.serviceInstance("test-service", true, map2)); + + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.selectInstances(eq("test-service"), eq(true))) + .thenReturn(instances.stream().filter(Instance::isHealthy) + .collect(Collectors.toList())); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); + serverList.initWithNiwsConfig(clientConfig); + List servers = serverList.getInitialListOfServers(); + assertThat(servers).hasSize(1); + + NacosServer nacosServer = servers.get(0); + + assertThat(nacosServer.getMetaInfo().getInstanceId()) + .isEqualTo(instances.get(1).getInstanceId()); + assertThat(nacosServer.getMetadata()).isEqualTo(map2); + assertThat(nacosServer.getInstance().isHealthy()).isEqualTo(true); + assertThat(nacosServer.getInstance().getServiceName()).isEqualTo("test-service"); + assertThat(nacosServer.getInstance().getMetadata().get("instanceNum")) + .isEqualTo("2"); + + } + + @Test + public void testUpdateServers() throws Exception { + ArrayList instances = new ArrayList<>(); + + HashMap map = new HashMap<>(); + map.put("instanceNum", "1"); + instances.add(NacosMockTest.serviceInstance("test-service", true, map)); + + NacosDiscoveryProperties nacosDiscoveryProperties = mock( + NacosDiscoveryProperties.class); + + NamingService namingService = mock(NamingService.class); + + when(nacosDiscoveryProperties.namingServiceInstance()).thenReturn(namingService); + when(namingService.selectInstances(eq("test-service"), eq(true))) + .thenReturn(instances.stream().filter(Instance::isHealthy) + .collect(Collectors.toList())); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); + serverList.initWithNiwsConfig(clientConfig); + + List servers = serverList.getUpdatedListOfServers(); + assertThat(servers).hasSize(1); + + assertThat(servers.get(0).getInstance().isHealthy()).isEqualTo(true); + assertThat(servers.get(0).getInstance().getMetadata().get("instanceNum")) + .isEqualTo("1"); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/CommonTestConfig.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/CommonTestConfig.java new file mode 100644 index 00000000..7e7b51ab --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/CommonTestConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.test; + +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * @author xiaojing + */ + +@Configuration +public class CommonTestConfig { + + @Bean + @LoadBalanced + RestTemplate loadBalancedRestTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/NacosMockTest.java b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/NacosMockTest.java new file mode 100644 index 00000000..f97870de --- /dev/null +++ b/spring-cloud-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/test/NacosMockTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos.test; + +import java.util.Map; +import java.util.UUID; + +import com.alibaba.nacos.api.naming.pojo.Instance; + +/** + * @author xiaojing + */ +public class NacosMockTest { + + public static Instance serviceInstance(String serviceName, boolean isHealthy, + Map metadata) { + Instance instance = new Instance(); + instance.setInstanceId(UUID.randomUUID().toString()); + instance.setServiceName(serviceName); + instance.setHealthy(isHealthy); + instance.setMetadata(metadata); + return instance; + } + + public static Instance serviceInstance(String serviceName, boolean isHealthy, + String host, int port, Map metadata) { + Instance instance = new Instance(); + instance.setIp(host); + instance.setPort(port); + instance.setServiceName(serviceName); + instance.setHealthy(isHealthy); + instance.setMetadata(metadata); + return instance; + } +} diff --git a/spring-cloud-alibaba-seata/pom.xml b/spring-cloud-alibaba-seata/pom.xml new file mode 100644 index 00000000..3938654f --- /dev/null +++ b/spring-cloud-alibaba-seata/pom.xml @@ -0,0 +1,128 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-seata + Spring Cloud Alibaba Seata + + + + + io.seata + seata-spring + + + + org.springframework.cloud + spring-cloud-starter-openfeign + provided + true + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + provided + true + + + + org.springframework.cloud + spring-cloud-commons + true + + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + provided + true + + + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-actuator + provided + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-starter-web + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/GlobalTransactionAutoConfiguration.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/GlobalTransactionAutoConfiguration.java new file mode 100644 index 00000000..a26aa578 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/GlobalTransactionAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import io.seata.spring.annotation.GlobalTransactionScanner; + +/** + * @author xiaojing + */ + +@Configuration +@EnableConfigurationProperties(SeataProperties.class) +public class GlobalTransactionAutoConfiguration { + + private final ApplicationContext applicationContext; + + private final SeataProperties seataProperties; + + public GlobalTransactionAutoConfiguration(ApplicationContext applicationContext, + SeataProperties seataProperties) { + this.applicationContext = applicationContext; + this.seataProperties = seataProperties; + } + + @Bean + public GlobalTransactionScanner globalTransactionScanner() { + + String applicationName = applicationContext.getEnvironment() + .getProperty("spring.application.name"); + + String txServiceGroup = seataProperties.getTxServiceGroup(); + + if (StringUtils.isEmpty(txServiceGroup)) { + txServiceGroup = applicationName + "-fescar-service-group"; + seataProperties.setTxServiceGroup(txServiceGroup); + } + + return new GlobalTransactionScanner(applicationName, txServiceGroup); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/SeataProperties.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/SeataProperties.java new file mode 100644 index 00000000..492d054d --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/SeataProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author xiaojing + */ +@ConfigurationProperties("spring.cloud.alibaba.seata") +public class SeataProperties { + + // todo support config Fescar server information + + /** + * Seata tx service group.default is ${spring.application.name}-fescar-service-group. + */ + private String txServiceGroup; + + public String getTxServiceGroup() { + return txServiceGroup; + } + + public void setTxServiceGroup(String txServiceGroup) { + this.txServiceGroup = txServiceGroup; + } + +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataBeanPostProcessor.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataBeanPostProcessor.java new file mode 100644 index 00000000..4218c767 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataBeanPostProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * @author xiaojing + */ +final class SeataBeanPostProcessor implements BeanPostProcessor { + + private final SeataFeignObjectWrapper seataFeignObjectWrapper; + + SeataBeanPostProcessor(SeataFeignObjectWrapper seataFeignObjectWrapper) { + this.seataFeignObjectWrapper = seataFeignObjectWrapper; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return this.seataFeignObjectWrapper.wrap(bean); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } +} \ No newline at end of file diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataContextBeanPostProcessor.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataContextBeanPostProcessor.java new file mode 100644 index 00000000..e214bddd --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataContextBeanPostProcessor.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.openfeign.FeignContext; + +/** + * @author xiaojing + */ +public class SeataContextBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + private SeataFeignObjectWrapper seataFeignObjectWrapper; + + SeataContextBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof FeignContext && !(bean instanceof SeataFeignContext)) { + return new SeataFeignContext(getSeataFeignObjectWrapper(), + (FeignContext) bean); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + private SeataFeignObjectWrapper getSeataFeignObjectWrapper() { + if (this.seataFeignObjectWrapper == null) { + this.seataFeignObjectWrapper = this.beanFactory + .getBean(SeataFeignObjectWrapper.class); + } + return this.seataFeignObjectWrapper; + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignBuilder.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignBuilder.java new file mode 100644 index 00000000..0855e358 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.factory.BeanFactory; + +import feign.Feign; + +/** + * @author xiaojing + */ +final class SeataFeignBuilder { + + private SeataFeignBuilder() { + } + + static Feign.Builder builder(BeanFactory beanFactory) { + return Feign.builder().client(new SeataFeignClient(beanFactory)); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClient.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClient.java new file mode 100644 index 00000000..9e486120 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClient.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.util.StringUtils; + +import feign.Client; +import feign.Request; +import feign.Response; +import io.seata.core.context.RootContext; + +/** + * @author xiaojing + */ +public class SeataFeignClient implements Client { + + private final Client delegate; + private final BeanFactory beanFactory; + private static final int MAP_SIZE = 16; + + SeataFeignClient(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + this.delegate = new Client.Default(null, null); + } + + SeataFeignClient(BeanFactory beanFactory, Client delegate) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + + Request modifiedRequest = getModifyRequest(request); + return this.delegate.execute(modifiedRequest, options); + } + + private Request getModifyRequest(Request request) { + + String xid = RootContext.getXID(); + + if (StringUtils.isEmpty(xid)) { + return request; + } + + Map> headers = new HashMap<>(MAP_SIZE); + headers.putAll(request.headers()); + + List fescarXid = new ArrayList<>(); + fescarXid.add(xid); + headers.put(RootContext.KEY_XID, fescarXid); + + return Request.create(request.method(), request.url(), headers, request.body(), + request.charset()); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClientAutoConfiguration.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClientAutoConfiguration.java new file mode 100644 index 00000000..3602e23d --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignClientAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import feign.Client; +import feign.Feign; + +/** + * @author xiaojing + */ + +@Configuration +@ConditionalOnClass(Client.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +public class SeataFeignClientAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnClass(name = "com.netflix.hystrix.HystrixCommand") + @ConditionalOnProperty(name = "feign.hystrix.enabled", havingValue = "true") + Feign.Builder feignHystrixBuilder(BeanFactory beanFactory) { + return SeataHystrixFeignBuilder.builder(beanFactory); + } + + @Bean + @Scope("prototype") + @ConditionalOnClass(name = "com.alibaba.csp.sentinel.SphU") + @ConditionalOnProperty(name = "feign.sentinel.enabled", havingValue = "true") + Feign.Builder feignSentinelBuilder(BeanFactory beanFactory) { + return SeataSentinelFeignBuilder.builder(beanFactory); + } + + @Bean + @ConditionalOnMissingBean + @Scope("prototype") + Feign.Builder feignBuilder(BeanFactory beanFactory) { + return SeataFeignBuilder.builder(beanFactory); + } + + @Configuration + protected static class FeignBeanPostProcessorConfiguration { + + @Bean + SeataBeanPostProcessor seataBeanPostProcessor( + SeataFeignObjectWrapper seataFeignObjectWrapper) { + return new SeataBeanPostProcessor(seataFeignObjectWrapper); + } + + @Bean + SeataContextBeanPostProcessor seataContextBeanPostProcessor( + BeanFactory beanFactory) { + return new SeataContextBeanPostProcessor(beanFactory); + } + + @Bean + SeataFeignObjectWrapper seataFeignObjectWrapper(BeanFactory beanFactory) { + return new SeataFeignObjectWrapper(beanFactory); + } + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignContext.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignContext.java new file mode 100644 index 00000000..86b017f6 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignContext.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignContext; + +import feign.Client; + +/** + * + * @author xiaojing + */ +public class SeataFeignContext extends FeignContext { + + private final SeataFeignObjectWrapper seataFeignObjectWrapper; + private final FeignContext delegate; + + SeataFeignContext(SeataFeignObjectWrapper seataFeignObjectWrapper, + FeignContext delegate) { + this.seataFeignObjectWrapper = seataFeignObjectWrapper; + this.delegate = delegate; + } + + @Override + public T getInstance(String name, Class type) { + T object = this.delegate.getInstance(name, type); + if (object instanceof Client) { + return object; + } + return (T) this.seataFeignObjectWrapper.wrap(object); + } + + @Override + public Map getInstances(String name, Class type) { + Map instances = this.delegate.getInstances(name, type); + if (instances == null) { + return null; + } + Map convertedInstances = new HashMap<>(); + for (Map.Entry entry : instances.entrySet()) { + if (entry.getValue() instanceof Client) { + convertedInstances.put(entry.getKey(), entry.getValue()); + } + else { + convertedInstances.put(entry.getKey(), + (T) this.seataFeignObjectWrapper.wrap(entry.getValue())); + } + } + return convertedInstances; + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignObjectWrapper.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignObjectWrapper.java new file mode 100644 index 00000000..374b8367 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataFeignObjectWrapper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory; +import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient; + +import feign.Client; + +/** + * @author xiaojing + */ +public class SeataFeignObjectWrapper { + + private final BeanFactory beanFactory; + + private CachingSpringLoadBalancerFactory cachingSpringLoadBalancerFactory; + private SpringClientFactory springClientFactory; + + SeataFeignObjectWrapper(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + Object wrap(Object bean) { + if (bean instanceof Client && !(bean instanceof SeataFeignClient)) { + if (bean instanceof LoadBalancerFeignClient) { + LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); + return new SeataLoadBalancerFeignClient(client.getDelegate(), factory(), + clientFactory(), this.beanFactory); + } + return new SeataFeignClient(this.beanFactory, (Client) bean); + } + return bean; + } + + CachingSpringLoadBalancerFactory factory() { + if (this.cachingSpringLoadBalancerFactory == null) { + this.cachingSpringLoadBalancerFactory = this.beanFactory + .getBean(CachingSpringLoadBalancerFactory.class); + } + return this.cachingSpringLoadBalancerFactory; + } + + SpringClientFactory clientFactory() { + if (this.springClientFactory == null) { + this.springClientFactory = this.beanFactory + .getBean(SpringClientFactory.class); + } + return this.springClientFactory; + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataHystrixFeignBuilder.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataHystrixFeignBuilder.java new file mode 100644 index 00000000..f226244a --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataHystrixFeignBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.factory.BeanFactory; + +import feign.Feign; +import feign.Retryer; +import feign.hystrix.HystrixFeign; + +/** + * @author xiaojing + */ +final class SeataHystrixFeignBuilder { + + private SeataHystrixFeignBuilder() { + } + + static Feign.Builder builder(BeanFactory beanFactory) { + return HystrixFeign.builder().retryer(Retryer.NEVER_RETRY) + .client(new SeataFeignClient(beanFactory)); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataLoadBalancerFeignClient.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataLoadBalancerFeignClient.java new file mode 100644 index 00000000..912e66c8 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataLoadBalancerFeignClient.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory; +import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient; +import org.springframework.util.StringUtils; + +import feign.Client; +import feign.Request; +import feign.Response; +import io.seata.core.context.RootContext; + +/** + * @author xiaojing + */ +public class SeataLoadBalancerFeignClient extends LoadBalancerFeignClient { + + private static final int MAP_SIZE = 16; + + private final BeanFactory beanFactory; + + SeataLoadBalancerFeignClient(Client delegate, + CachingSpringLoadBalancerFactory lbClientFactory, + SpringClientFactory clientFactory, BeanFactory beanFactory) { + super(wrap(delegate, beanFactory), lbClientFactory, clientFactory); + this.beanFactory = beanFactory; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + Request modifiedRequest = getModifyRequest(request); + return super.execute(request, options); + } + + private static Client wrap(Client delegate, BeanFactory beanFactory) { + return (Client) new SeataFeignObjectWrapper(beanFactory).wrap(delegate); + } + + private Request getModifyRequest(Request request) { + + String xid = RootContext.getXID(); + + if (StringUtils.isEmpty(xid)) { + return request; + } + + Map> headers = new HashMap<>(MAP_SIZE); + headers.putAll(request.headers()); + + List fescarXid = new ArrayList<>(); + fescarXid.add(xid); + headers.put(RootContext.KEY_XID, fescarXid); + + return Request.create(request.method(), request.url(), headers, request.body(), + request.charset()); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataSentinelFeignBuilder.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataSentinelFeignBuilder.java new file mode 100644 index 00000000..0f185c2a --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/SeataSentinelFeignBuilder.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign; + +import org.springframework.beans.factory.BeanFactory; + +import com.alibaba.cloud.sentinel.feign.SentinelFeign; + +import feign.Feign; +import feign.Retryer; + +/** + * @author xiaojing + */ +final class SeataSentinelFeignBuilder { + + private SeataSentinelFeignBuilder() { + } + + static Feign.Builder builder(BeanFactory beanFactory) { + return SentinelFeign.builder().retryer(Retryer.NEVER_RETRY) + .client(new SeataFeignClient(beanFactory)); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixAutoConfiguration.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixAutoConfiguration.java new file mode 100644 index 00000000..465ca3e6 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign.hystrix; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.hystrix.HystrixCommand; + +/** + * @author xiaojing + */ + +@Configuration +@ConditionalOnClass(HystrixCommand.class) +public class SeataHystrixAutoConfiguration { + + @Bean + SeataHystrixConcurrencyStrategy seataHystrixConcurrencyStrategy() { + return new SeataHystrixConcurrencyStrategy(); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixConcurrencyStrategy.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixConcurrencyStrategy.java new file mode 100644 index 00000000..0bff8173 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/feign/hystrix/SeataHystrixConcurrencyStrategy.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.feign.hystrix; + +import java.util.concurrent.Callable; + +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; + +import io.seata.core.context.RootContext; + +/** + * @author xiaojing + */ +public class SeataHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { + + private HystrixConcurrencyStrategy delegate; + + public SeataHystrixConcurrencyStrategy() { + this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerConcurrencyStrategy(this); + } + + @Override + public Callable wrapCallable(Callable c) { + if (c instanceof SeataContextCallable) { + return c; + } + + Callable wrappedCallable; + if (this.delegate != null) { + wrappedCallable = this.delegate.wrapCallable(c); + } + else { + wrappedCallable = c; + } + if (wrappedCallable instanceof SeataContextCallable) { + return wrappedCallable; + } + + return new SeataContextCallable<>(wrappedCallable); + } + + private static class SeataContextCallable implements Callable { + + private final Callable actual; + private final String xid; + + SeataContextCallable(Callable actual) { + this.actual = actual; + this.xid = RootContext.getXID(); + } + + @Override + public K call() throws Exception { + try { + RootContext.bind(xid); + return actual.call(); + } + finally { + RootContext.unbind(); + } + } + + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateAutoConfiguration.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateAutoConfiguration.java new file mode 100644 index 00000000..7dc05fde --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateAutoConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 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 + * + * http://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. + */ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.rest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +/** + * @author xiaojing + */ + +@Configuration +public class SeataRestTemplateAutoConfiguration { + + @Bean + public SeataRestTemplateInterceptor seataRestTemplateInterceptor() { + return new SeataRestTemplateInterceptor(); + } + + @Autowired(required = false) + private Collection restTemplates; + + @Autowired + private SeataRestTemplateInterceptor seataRestTemplateInterceptor; + + @PostConstruct + public void init() { + if (this.restTemplates != null) { + for (RestTemplate restTemplate : restTemplates) { + List interceptors = new ArrayList( + restTemplate.getInterceptors()); + interceptors.add(this.seataRestTemplateInterceptor); + restTemplate.setInterceptors(interceptors); + } + } + } + +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateInterceptor.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateInterceptor.java new file mode 100644 index 00000000..d90f1255 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/rest/SeataRestTemplateInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.rest; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.support.HttpRequestWrapper; +import org.springframework.util.StringUtils; + +import io.seata.core.context.RootContext; + +/** + * @author xiaojing + */ +public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor { + @Override + public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, + ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { + HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest); + + String xid = RootContext.getXID(); + + if (!StringUtils.isEmpty(xid)) { + requestWrapper.getHeaders().add(RootContext.KEY_XID, xid); + } + return clientHttpRequestExecution.execute(requestWrapper, bytes); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptor.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptor.java new file mode 100644 index 00000000..9f50352a --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptor.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.web; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import io.seata.core.context.RootContext; + +/** + * @author xiaojing + * + * Seata HandlerInterceptor, Convert Seata information into + * @see io.seata.core.context.RootContext from http request's header in + * {@link org.springframework.web.servlet.HandlerInterceptor#preHandle(HttpServletRequest , HttpServletResponse , Object )}, + * And clean up Seata information after servlet method invocation in + * {@link org.springframework.web.servlet.HandlerInterceptor#afterCompletion(HttpServletRequest, HttpServletResponse, Object, Exception)} + */ +public class SeataHandlerInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory + .getLogger(SeataHandlerInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) { + + String xid = RootContext.getXID(); + String rpcXid = request.getHeader(RootContext.KEY_XID); + if (log.isDebugEnabled()) { + log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid); + } + + if (xid == null && rpcXid != null) { + RootContext.bind(rpcXid); + if (log.isDebugEnabled()) { + log.debug("bind {} to RootContext", rpcXid); + } + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception e) { + + String rpcXid = request.getHeader(RootContext.KEY_XID); + + if (StringUtils.isEmpty(rpcXid)) { + return; + } + + String unbindXid = RootContext.unbind(); + if (log.isDebugEnabled()) { + log.debug("unbind {} from RootContext", unbindXid); + } + if (!rpcXid.equalsIgnoreCase(unbindXid)) { + log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid); + if (unbindXid != null) { + RootContext.bind(unbindXid); + log.warn("bind {} back to RootContext", unbindXid); + } + } + } + +} diff --git a/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptorConfiguration.java b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptorConfiguration.java new file mode 100644 index 00000000..51757bf4 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/java/com/alibaba/cloud/seata/web/SeataHandlerInterceptorConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.cloud.seata.web; + +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author xiaojing + */ +public class SeataHandlerInterceptorConfiguration implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SeataHandlerInterceptor()).addPathPatterns("/**"); + } +} diff --git a/spring-cloud-alibaba-seata/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-seata/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..ae993684 --- /dev/null +++ b/spring-cloud-alibaba-seata/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.seata.rest.SeataRestTemplateAutoConfiguration,\ +com.alibaba.cloud.seata.web.SeataHandlerInterceptorConfiguration,\ +com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration,\ +com.alibaba.cloud.seata.feign.SeataFeignClientAutoConfiguration,\ +com.alibaba.cloud.seata.feign.hystrix.SeataHystrixAutoConfiguration + diff --git a/spring-cloud-alibaba-sentinel-datasource/pom.xml b/spring-cloud-alibaba-sentinel-datasource/pom.xml new file mode 100644 index 00000000..ad4b67a9 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/pom.xml @@ -0,0 +1,131 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-sentinel-datasource + Spring Cloud Alibaba Sentinel DataSource + + + + + com.alibaba.csp + sentinel-datasource-extension + true + + + + com.alibaba.csp + sentinel-parameter-flow-control + true + + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + true + + + + com.alibaba.csp + sentinel-datasource-nacos + provided + true + + + + com.alibaba.csp + sentinel-datasource-zookeeper + provided + true + + + + com.alibaba.csp + sentinel-datasource-apollo + provided + true + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + + org.hibernate.validator + hibernate-validator + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + + diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/RuleType.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/RuleType.java new file mode 100644 index 00000000..4a376fd2 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/RuleType.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import java.util.Arrays; +import java.util.Optional; + +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.config.AbstractDataSourceProperties; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +/** + * Enum for {@link AbstractRule} class, using in + * {@link AbstractDataSourceProperties#ruleType} + * + * @author Jim + */ +public enum RuleType { + + /** + * flow + */ + FLOW("flow", FlowRule.class), + /** + * degrade + */ + DEGRADE("degrade", DegradeRule.class), + /** + * param flow + */ + PARAM_FLOW("param-flow", ParamFlowRule.class), + /** + * system + */ + SYSTEM("system", SystemRule.class), + /** + * authority + */ + AUTHORITY("authority", AuthorityRule.class), + /** + * gateway flow + */ + GW_FLOW("gw-flow", + "com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule"), + /** + * api + */ + GW_API_GROUP("gw-api-group", + "com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition"); + + /** + * alias for {@link AbstractRule} + */ + private final String name; + + /** + * concrete {@link AbstractRule} class + */ + private Class clazz; + + /** + * concrete {@link AbstractRule} class name + */ + private String clazzName; + + RuleType(String name, Class clazz) { + this.name = name; + this.clazz = clazz; + } + + RuleType(String name, String clazzName) { + this.name = name; + this.clazzName = clazzName; + } + + public String getName() { + return name; + } + + public Class getClazz() { + if (clazz != null) { + return clazz; + } + else { + try { + return Class.forName(clazzName); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + public static Optional getByName(String name) { + if (StringUtils.isEmpty(name)) { + return Optional.empty(); + } + return Arrays.stream(RuleType.values()) + .filter(ruleType -> name.equals(ruleType.getName())).findFirst(); + } + + public static Optional getByClass(Class clazz) { + return Arrays.stream(RuleType.values()) + .filter(ruleType -> clazz == ruleType.getClazz()).findFirst(); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/SentinelDataSourceConstants.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/SentinelDataSourceConstants.java new file mode 100644 index 00000000..5240ba26 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/SentinelDataSourceConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +/** + * @author Jim + */ +public interface SentinelDataSourceConstants { + + String PROPERTY_PREFIX = "spring.cloud.sentinel"; + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/AbstractDataSourceProperties.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/AbstractDataSourceProperties.java new file mode 100644 index 00000000..df89dbd9 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/AbstractDataSourceProperties.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.springframework.core.env.Environment; + +import com.alibaba.cloud.sentinel.datasource.RuleType; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Abstract class Using by {@link DataSourcePropertiesConfiguration} + * + * @author Jim + */ +public class AbstractDataSourceProperties { + + @NotEmpty + private String dataType = "json"; + @NotNull + private RuleType ruleType; + private String converterClass; + @JsonIgnore + private final String factoryBeanName; + @JsonIgnore + private Environment env; + + public AbstractDataSourceProperties(String factoryBeanName) { + this.factoryBeanName = factoryBeanName; + } + + public String getDataType() { + return dataType; + } + + public void setDataType(String dataType) { + this.dataType = dataType; + } + + public RuleType getRuleType() { + return ruleType; + } + + public void setRuleType(RuleType ruleType) { + this.ruleType = ruleType; + } + + public String getConverterClass() { + return converterClass; + } + + public void setConverterClass(String converterClass) { + this.converterClass = converterClass; + } + + public String getFactoryBeanName() { + return factoryBeanName; + } + + protected Environment getEnv() { + return env; + } + + public void setEnv(Environment env) { + this.env = env; + } + + public void preCheck(String dataSourceName) { + + } + + public void postRegister(AbstractDataSource dataSource) { + switch (this.getRuleType()) { + case FLOW: + FlowRuleManager.register2Property(dataSource.getProperty()); + break; + case DEGRADE: + DegradeRuleManager.register2Property(dataSource.getProperty()); + break; + case PARAM_FLOW: + ParamFlowRuleManager.register2Property(dataSource.getProperty()); + break; + case SYSTEM: + SystemRuleManager.register2Property(dataSource.getProperty()); + break; + case AUTHORITY: + AuthorityRuleManager.register2Property(dataSource.getProperty()); + break; + case GW_FLOW: + GatewayRuleManager.register2Property(dataSource.getProperty()); + break; + case GW_API_GROUP: + GatewayApiDefinitionManager.register2Property(dataSource.getProperty()); + break; + default: + break; + } + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ApolloDataSourceProperties.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ApolloDataSourceProperties.java new file mode 100644 index 00000000..d67baf04 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ApolloDataSourceProperties.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import javax.validation.constraints.NotEmpty; + +import com.alibaba.cloud.sentinel.datasource.factorybean.ApolloDataSourceFactoryBean; + +/** + * Apollo Properties class Using by {@link DataSourcePropertiesConfiguration} and + * {@link ApolloDataSourceFactoryBean} + * + * @author Jim + */ +public class ApolloDataSourceProperties extends AbstractDataSourceProperties { + + @NotEmpty + private String namespaceName; + @NotEmpty + private String flowRulesKey; + private String defaultFlowRuleValue; + + public ApolloDataSourceProperties() { + super(ApolloDataSourceFactoryBean.class.getName()); + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getFlowRulesKey() { + return flowRulesKey; + } + + public void setFlowRulesKey(String flowRulesKey) { + this.flowRulesKey = flowRulesKey; + } + + public String getDefaultFlowRuleValue() { + return defaultFlowRuleValue; + } + + public void setDefaultFlowRuleValue(String defaultFlowRuleValue) { + this.defaultFlowRuleValue = defaultFlowRuleValue; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/DataSourcePropertiesConfiguration.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/DataSourcePropertiesConfiguration.java new file mode 100644 index 00000000..12dde119 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/DataSourcePropertiesConfiguration.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.util.ObjectUtils; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Using By ConfigurationProperties. + * + * @author Jim + * @see NacosDataSourceProperties + * @see ApolloDataSourceProperties + * @see ZookeeperDataSourceProperties + * @see FileDataSourceProperties + */ +public class DataSourcePropertiesConfiguration { + + private FileDataSourceProperties file; + + private NacosDataSourceProperties nacos; + + private ZookeeperDataSourceProperties zk; + + private ApolloDataSourceProperties apollo; + + public DataSourcePropertiesConfiguration() { + } + + public DataSourcePropertiesConfiguration(FileDataSourceProperties file) { + this.file = file; + } + + public DataSourcePropertiesConfiguration(NacosDataSourceProperties nacos) { + this.nacos = nacos; + } + + public DataSourcePropertiesConfiguration(ZookeeperDataSourceProperties zk) { + this.zk = zk; + } + + public DataSourcePropertiesConfiguration(ApolloDataSourceProperties apollo) { + this.apollo = apollo; + } + + public FileDataSourceProperties getFile() { + return file; + } + + public void setFile(FileDataSourceProperties file) { + this.file = file; + } + + public NacosDataSourceProperties getNacos() { + return nacos; + } + + public void setNacos(NacosDataSourceProperties nacos) { + this.nacos = nacos; + } + + public ZookeeperDataSourceProperties getZk() { + return zk; + } + + public void setZk(ZookeeperDataSourceProperties zk) { + this.zk = zk; + } + + public ApolloDataSourceProperties getApollo() { + return apollo; + } + + public void setApollo(ApolloDataSourceProperties apollo) { + this.apollo = apollo; + } + + @JsonIgnore + public List getValidField() { + return Arrays.stream(this.getClass().getDeclaredFields()).map(field -> { + try { + if (!ObjectUtils.isEmpty(field.get(this))) { + return field.getName(); + } + return null; + } + catch (IllegalAccessException e) { + // won't happen + } + return null; + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @JsonIgnore + public AbstractDataSourceProperties getValidDataSourceProperties() { + List invalidFields = getValidField(); + if (invalidFields.size() == 1) { + try { + this.getClass().getDeclaredField(invalidFields.get(0)) + .setAccessible(true); + return (AbstractDataSourceProperties) this.getClass() + .getDeclaredField(invalidFields.get(0)).get(this); + } + catch (IllegalAccessException e) { + // won't happen + } + catch (NoSuchFieldException e) { + // won't happen + } + } + return null; + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/FileDataSourceProperties.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/FileDataSourceProperties.java new file mode 100644 index 00000000..c8551c24 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/FileDataSourceProperties.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import java.io.IOException; + +import javax.validation.constraints.NotEmpty; + +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.factorybean.FileRefreshableDataSourceFactoryBean; + +/** + * File Properties class Using by {@link DataSourcePropertiesConfiguration} and + * {@link FileRefreshableDataSourceFactoryBean} + * + * @author Jim + */ +public class FileDataSourceProperties extends AbstractDataSourceProperties { + + @NotEmpty + private String file; + private String charset = "utf-8"; + private long recommendRefreshMs = 3000L; + private int bufSize = 1024 * 1024; + + public FileDataSourceProperties() { + super(FileRefreshableDataSourceFactoryBean.class.getName()); + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public long getRecommendRefreshMs() { + return recommendRefreshMs; + } + + public void setRecommendRefreshMs(long recommendRefreshMs) { + this.recommendRefreshMs = recommendRefreshMs; + } + + public int getBufSize() { + return bufSize; + } + + public void setBufSize(int bufSize) { + this.bufSize = bufSize; + } + + @Override + public void preCheck(String dataSourceName) { + super.preCheck(dataSourceName); + try { + this.setFile( + ResourceUtils.getFile(StringUtils.trimAllWhitespace(this.getFile())) + .getAbsolutePath()); + } + catch (IOException e) { + throw new RuntimeException("[Sentinel Starter] DataSource " + dataSourceName + + " handle file [" + this.getFile() + "] error: " + e.getMessage(), + e); + } + + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/NacosDataSourceProperties.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/NacosDataSourceProperties.java new file mode 100644 index 00000000..f8673d0b --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/NacosDataSourceProperties.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import javax.validation.constraints.NotEmpty; + +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean; + +/** + * Nacos Properties class Using by {@link DataSourcePropertiesConfiguration} and + * {@link NacosDataSourceFactoryBean} + * + * @author Jim + */ +public class NacosDataSourceProperties extends AbstractDataSourceProperties { + + private String serverAddr; + + @NotEmpty + private String groupId = "DEFAULT_GROUP"; + + @NotEmpty + private String dataId; + + private String endpoint; + private String namespace; + private String accessKey; + private String secretKey; + + public NacosDataSourceProperties() { + super(NacosDataSourceFactoryBean.class.getName()); + } + + @Override + public void preCheck(String dataSourceName) { + if (StringUtils.isEmpty(serverAddr)) { + serverAddr = this.getEnv().getProperty( + "spring.cloud.sentinel.datasource.nacos.server-addr", ""); + if (StringUtils.isEmpty(serverAddr)) { + throw new IllegalArgumentException( + "NacosDataSource server-addr is empty"); + } + } + } + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ZookeeperDataSourceProperties.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ZookeeperDataSourceProperties.java new file mode 100644 index 00000000..4d1755bf --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/config/ZookeeperDataSourceProperties.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.config; + +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.factorybean.ZookeeperDataSourceFactoryBean; + +/** + * Zookeeper Properties class Using by {@link DataSourcePropertiesConfiguration} and + * {@link ZookeeperDataSourceFactoryBean} + * + * @author Jim + */ +public class ZookeeperDataSourceProperties extends AbstractDataSourceProperties { + + public ZookeeperDataSourceProperties() { + super(ZookeeperDataSourceFactoryBean.class.getName()); + } + + private String serverAddr; + + private String path; + + private String groupId; + + private String dataId; + + @Override + public void preCheck(String dataSourceName) { + if (StringUtils.isEmpty(serverAddr)) { + serverAddr = this.getEnv() + .getProperty("spring.cloud.sentinel.datasource.zk.server-addr", ""); + if (StringUtils.isEmpty(serverAddr)) { + throw new IllegalArgumentException( + "ZookeeperDataSource server-addr is empty"); + } + } + } + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/JsonConverter.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/JsonConverter.java new file mode 100644 index 00000000..7ffd4ef7 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/JsonConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.converter; + +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Convert sentinel rules for json array Using strict mode to parse json + * + * @author Jim + * @see FlowRule + * @see DegradeRule + * @see SystemRule + * @see AuthorityRule + * @see ParamFlowRule + * @see ObjectMapper + */ +public class JsonConverter extends SentinelConverter { + + public JsonConverter(ObjectMapper objectMapper, Class ruleClass) { + super(objectMapper, ruleClass); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/SentinelConverter.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/SentinelConverter.java new file mode 100644 index 00000000..1d8269d7 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/SentinelConverter.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.converter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.RuleType; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleUtil; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Convert sentinel rules for json or xml array Using strict mode to parse json or xml + * + * @author Jim + * @see FlowRule + * @see DegradeRule + * @see SystemRule + * @see AuthorityRule + * @see ParamFlowRule + * @see ObjectMapper + */ +public abstract class SentinelConverter + implements Converter> { + + private static final Logger log = LoggerFactory.getLogger(SentinelConverter.class); + + private final ObjectMapper objectMapper; + + private final Class ruleClass; + + public SentinelConverter(ObjectMapper objectMapper, Class ruleClass) { + this.objectMapper = objectMapper; + this.ruleClass = ruleClass; + } + + @Override + public Collection convert(String source) { + Collection ruleCollection; + + // hard code + if (ruleClass == FlowRule.class || ruleClass == DegradeRule.class + || ruleClass == SystemRule.class || ruleClass == AuthorityRule.class + || ruleClass == ParamFlowRule.class) { + ruleCollection = new ArrayList<>(); + } + else { + ruleCollection = new HashSet<>(); + } + + if (StringUtils.isEmpty(source)) { + log.warn("converter can not convert rules because source is empty"); + return ruleCollection; + } + try { + List sourceArray = objectMapper.readValue(source, + new TypeReference>() { + }); + sourceArray.stream().forEach(obj -> { + + String item = null; + try { + item = objectMapper.writeValueAsString(obj); + } + catch (JsonProcessingException e) { + // won't be happen + } + + Optional.ofNullable(convertRule(item)) + .ifPresent(convertRule -> ruleCollection.add(convertRule)); + }); + + if (ruleCollection.size() != sourceArray.size()) { + throw new IllegalArgumentException("convert " + ruleCollection.size() + + " rules but there are " + sourceArray.size() + + " rules from datasource. RuleClass: " + + ruleClass.getSimpleName()); + } + } + catch (Exception e) { + throw new RuntimeException("convert error: " + e.getMessage(), e); + } + return ruleCollection; + } + + private Object convertRule(String ruleStr) { + try { + final Object rule = objectMapper.readValue(ruleStr, ruleClass); + RuleType ruleType = RuleType.getByClass(ruleClass).get(); + switch (ruleType) { + case FLOW: + if (!FlowRuleUtil.isValidRule((FlowRule) rule)) { + return null; + } + break; + case DEGRADE: + if (!DegradeRuleManager.isValidRule((DegradeRule) rule)) { + return null; + } + default: + break; + } + return rule; + } + catch (Exception e) { + // ignore + } + return null; + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/XmlConverter.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/XmlConverter.java new file mode 100644 index 00000000..770413c1 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/converter/XmlConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.converter; + +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * Convert sentinel rules for xml array Using strict mode to parse xml + * + * @author Jim + * @see FlowRule + * @see DegradeRule + * @see SystemRule + * @see AuthorityRule + * @see ParamFlowRule + * @see ObjectMapper + */ +public class XmlConverter extends SentinelConverter { + + public XmlConverter(XmlMapper xmlMapper, Class ruleClass) { + super(xmlMapper, ruleClass); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ApolloDataSourceFactoryBean.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ApolloDataSourceFactoryBean.java new file mode 100644 index 00000000..47843e5c --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ApolloDataSourceFactoryBean.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.factorybean; + +import org.springframework.beans.factory.FactoryBean; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.apollo.ApolloDataSource; + +/** + * A {@link FactoryBean} for creating {@link ApolloDataSource} instance. + * + * @author Jim + * @see ApolloDataSource + */ +public class ApolloDataSourceFactoryBean implements FactoryBean { + + private String namespaceName; + private String flowRulesKey; + private String defaultFlowRuleValue; + private Converter converter; + + @Override + public ApolloDataSource getObject() throws Exception { + return new ApolloDataSource(namespaceName, flowRulesKey, defaultFlowRuleValue, + converter); + } + + @Override + public Class getObjectType() { + return ApolloDataSource.class; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getFlowRulesKey() { + return flowRulesKey; + } + + public void setFlowRulesKey(String flowRulesKey) { + this.flowRulesKey = flowRulesKey; + } + + public String getDefaultFlowRuleValue() { + return defaultFlowRuleValue; + } + + public void setDefaultFlowRuleValue(String defaultFlowRuleValue) { + this.defaultFlowRuleValue = defaultFlowRuleValue; + } + + public Converter getConverter() { + return converter; + } + + public void setConverter(Converter converter) { + this.converter = converter; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/FileRefreshableDataSourceFactoryBean.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/FileRefreshableDataSourceFactoryBean.java new file mode 100644 index 00000000..0042134b --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/FileRefreshableDataSourceFactoryBean.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.factorybean; + +import java.io.File; +import java.nio.charset.Charset; + +import org.springframework.beans.factory.FactoryBean; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; + +/** + * A {@link FactoryBean} for creating {@link FileRefreshableDataSource} instance. + * + * @author Jim + * @see FileRefreshableDataSource + */ +public class FileRefreshableDataSourceFactoryBean + implements FactoryBean { + + private String file; + private String charset; + private long recommendRefreshMs; + private int bufSize; + private Converter converter; + + @Override + public FileRefreshableDataSource getObject() throws Exception { + return new FileRefreshableDataSource(new File(file), converter, + recommendRefreshMs, bufSize, Charset.forName(charset)); + } + + @Override + public Class getObjectType() { + return FileRefreshableDataSource.class; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public long getRecommendRefreshMs() { + return recommendRefreshMs; + } + + public void setRecommendRefreshMs(long recommendRefreshMs) { + this.recommendRefreshMs = recommendRefreshMs; + } + + public int getBufSize() { + return bufSize; + } + + public void setBufSize(int bufSize) { + this.bufSize = bufSize; + } + + public Converter getConverter() { + return converter; + } + + public void setConverter(Converter converter) { + this.converter = converter; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/NacosDataSourceFactoryBean.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/NacosDataSourceFactoryBean.java new file mode 100644 index 00000000..da640f8e --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/NacosDataSourceFactoryBean.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.factorybean; + +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.StringUtils; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource; +import com.alibaba.nacos.api.PropertyKeyConst; + +/** + * A {@link FactoryBean} for creating {@link NacosDataSource} instance. + * + * @author Jim + * @see NacosDataSource + */ +public class NacosDataSourceFactoryBean implements FactoryBean { + + private String serverAddr; + private String groupId; + private String dataId; + private Converter converter; + + private String endpoint; + private String namespace; + private String accessKey; + private String secretKey; + + @Override + public NacosDataSource getObject() throws Exception { + Properties properties = new Properties(); + if (!StringUtils.isEmpty(this.serverAddr)) { + properties.setProperty(PropertyKeyConst.SERVER_ADDR, this.serverAddr); + } + else { + properties.setProperty(PropertyKeyConst.ACCESS_KEY, this.accessKey); + properties.setProperty(PropertyKeyConst.SECRET_KEY, this.secretKey); + properties.setProperty(PropertyKeyConst.ENDPOINT, this.endpoint); + } + if (!StringUtils.isEmpty(this.namespace)) { + properties.setProperty(PropertyKeyConst.NAMESPACE, this.namespace); + } + return new NacosDataSource(properties, groupId, dataId, converter); + } + + @Override + public Class getObjectType() { + return NacosDataSource.class; + } + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public Converter getConverter() { + return converter; + } + + public void setConverter(Converter converter) { + this.converter = converter; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ZookeeperDataSourceFactoryBean.java b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ZookeeperDataSourceFactoryBean.java new file mode 100644 index 00000000..5731d8f1 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/java/com/alibaba/cloud/sentinel/datasource/factorybean/ZookeeperDataSourceFactoryBean.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource.factorybean; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.FactoryBean; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.zookeeper.ZookeeperDataSource; + +/** + * A {@link FactoryBean} for creating {@link ZookeeperDataSource} instance. + * + * @author Jim + * @see ZookeeperDataSource + */ +public class ZookeeperDataSourceFactoryBean implements FactoryBean { + + private String serverAddr; + + private String path; + + private String groupId; + private String dataId; + + private Converter converter; + + @Override + public ZookeeperDataSource getObject() throws Exception { + if (StringUtils.isNotEmpty(groupId) && StringUtils.isNotEmpty(dataId)) { + // the path will be /{groupId}/{dataId} + return new ZookeeperDataSource(serverAddr, groupId, dataId, converter); + } + else { + // using path directly + return new ZookeeperDataSource(serverAddr, path, converter); + } + } + + @Override + public Class getObjectType() { + return ZookeeperDataSource.class; + } + + public String getServerAddr() { + return serverAddr; + } + + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public Converter getConverter() { + return converter; + } + + public void setConverter(Converter converter) { + this.converter = converter; + } +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/main/resources/META-INF/sentinel-datasource.properties b/spring-cloud-alibaba-sentinel-datasource/src/main/resources/META-INF/sentinel-datasource.properties new file mode 100644 index 00000000..8326eff6 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/main/resources/META-INF/sentinel-datasource.properties @@ -0,0 +1,4 @@ +nacos = com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource +file =com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource +apollo = com.alibaba.csp.sentinel.datasource.apollo.ApolloDataSource +zk = com.alibaba.csp.sentinel.datasource.zookeeper.ZookeeperDataSource \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ApolloDataSourceFactoryBeanTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ApolloDataSourceFactoryBeanTests.java new file mode 100644 index 00000000..730d3855 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ApolloDataSourceFactoryBeanTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter; +import com.alibaba.cloud.sentinel.datasource.factorybean.ApolloDataSourceFactoryBean; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.apollo.ApolloDataSource; + +/** + * @author Jim + */ +public class ApolloDataSourceFactoryBeanTests { + + private String flowRuleKey = "sentinel"; + private String namespace = "namespace"; + private String defaultFlowValue = "{}"; + + @Test + public void testApolloFactoryBean() throws Exception { + ApolloDataSourceFactoryBean factoryBean = spy(new ApolloDataSourceFactoryBean()); + + Converter converter = mock(JsonConverter.class); + + factoryBean.setDefaultFlowRuleValue(defaultFlowValue); + factoryBean.setFlowRulesKey(flowRuleKey); + factoryBean.setNamespaceName(namespace); + factoryBean.setConverter(converter); + + ApolloDataSource apolloDataSource = mock(ApolloDataSource.class); + + when(apolloDataSource.readSource()).thenReturn("{}"); + doReturn(apolloDataSource).when(factoryBean).getObject(); + + assertEquals("ApolloDataSourceFactoryBean getObject error", apolloDataSource, + factoryBean.getObject()); + assertEquals("ApolloDataSource read source value was wrong", "{}", + factoryBean.getObject().readSource()); + assertEquals("ApolloDataSource converter was wrong", converter, + factoryBean.getConverter()); + assertEquals("ApolloDataSourceFactoryBean flowRuleKey was wrong", flowRuleKey, + factoryBean.getFlowRulesKey()); + assertEquals("ApolloDataSourceFactoryBean namespace was wrong", namespace, + factoryBean.getNamespaceName()); + assertEquals("ApolloDataSourceFactoryBean defaultFlowValue was wrong", + defaultFlowValue, factoryBean.getDefaultFlowRuleValue()); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesConfigurationTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesConfigurationTests.java new file mode 100644 index 00000000..903bb377 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesConfigurationTests.java @@ -0,0 +1,250 @@ +/// * +// * Copyright (C) 2018 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 +// * +// * http://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 com.alibaba.cloud.sentinel.datasource; +// +// import static org.junit.Assert.assertEquals; +// import static org.junit.Assert.assertNotNull; +// import static org.junit.Assert.assertNull; +// +// import org.junit.Test; +// import ApolloDataSourceProperties; +// import DataSourcePropertiesConfiguration; +// import FileDataSourceProperties; +// import NacosDataSourceProperties; +// import ZookeeperDataSourceProperties; +// +/// ** +// * @author Jim +// */ +// public class DataSourcePropertiesConfigurationTests { +// +// @Test +// public void testFileAttr() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration(); +// assertEquals("DataSourcePropertiesConfiguration valid field size was wrong", 0, +// dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull("DataSourcePropertiesConfiguration valid properties was not null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// +// FileDataSourceProperties fileDataSourceProperties = buildFileProperties(); +// +// dataSourcePropertiesConfiguration.setFile(fileDataSourceProperties); +// +// assertEquals( +// "DataSourcePropertiesConfiguration valid field size was wrong after set file +/// attribute", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration file properties was null after set file attribute", +// dataSourcePropertiesConfiguration.getFile()); +// assertNotNull( +// "DataSourcePropertiesConfiguration valid properties was null after set file attribute", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testNacosAttr() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration(); +// assertEquals("DataSourcePropertiesConfiguration valid field size was wrong", 0, +// dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull("DataSourcePropertiesConfiguration valid properties was not null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// +// NacosDataSourceProperties nacosDataSourceProperties = buildNacosProperties(); +// +// dataSourcePropertiesConfiguration.setNacos(nacosDataSourceProperties); +// +// assertEquals( +// "DataSourcePropertiesConfiguration valid field size was wrong after set nacos +/// attribute", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration nacos properties was null after set nacos +/// attribute", +// dataSourcePropertiesConfiguration.getNacos()); +// assertNotNull( +// "DataSourcePropertiesConfiguration valid properties was null after set nacos +/// attribute", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testZKAttr() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration(); +// assertEquals("DataSourcePropertiesConfiguration valid field size was wrong", 0, +// dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull("DataSourcePropertiesConfiguration valid properties was not null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// +// ZookeeperDataSourceProperties zookeeperDataSourceProperties = buildZKProperties(); +// +// dataSourcePropertiesConfiguration.setZk(zookeeperDataSourceProperties); +// +// assertEquals( +// "DataSourcePropertiesConfiguration valid field size was wrong after set zk attribute", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration zk properties was null after set zk attribute", +// dataSourcePropertiesConfiguration.getZk()); +// assertNotNull( +// "DataSourcePropertiesConfiguration valid properties was null after set zk attribute", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testApolloAttr() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration(); +// assertEquals("DataSourcePropertiesConfiguration valid field size was wrong", 0, +// dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull("DataSourcePropertiesConfiguration valid properties was not null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// +// ApolloDataSourceProperties apolloDataSourceProperties = buildApolloProperties(); +// +// dataSourcePropertiesConfiguration.setApollo(apolloDataSourceProperties); +// +// assertEquals( +// "DataSourcePropertiesConfiguration valid field size was wrong after set apollo +/// attribute", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration apollo properties was null after set apollo +/// attribute", +// dataSourcePropertiesConfiguration.getApollo()); +// assertNotNull( +// "DataSourcePropertiesConfiguration valid properties was null after set apollo +/// attribute", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testMultiAttr() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration(); +// assertEquals("DataSourcePropertiesConfiguration valid field size was wrong", 0, +// dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull("DataSourcePropertiesConfiguration valid properties was not null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// +// FileDataSourceProperties fileDataSourceProperties = buildFileProperties(); +// NacosDataSourceProperties nacosDataSourceProperties = buildNacosProperties(); +// +// dataSourcePropertiesConfiguration.setFile(fileDataSourceProperties); +// dataSourcePropertiesConfiguration.setNacos(nacosDataSourceProperties); +// +// assertEquals( +// "DataSourcePropertiesConfiguration valid field size was wrong after set file and nacos +/// attribute", +// 2, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNull( +// "DataSourcePropertiesConfiguration valid properties was not null after set file and +/// nacos attribute", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testFileConstructor() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration( +// buildFileProperties()); +// assertEquals( +// "DataSourcePropertiesConfiguration file constructor valid field size was wrong", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration file constructor valid properties was null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testNacosConstructor() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration( +// buildNacosProperties()); +// assertEquals( +// "DataSourcePropertiesConfiguration nacos constructor valid field size was wrong", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration nacos constructor valid properties was null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testApolloConstructor() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration( +// buildApolloProperties()); +// assertEquals( +// "DataSourcePropertiesConfiguration apollo constructor valid field size was wrong", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration apollo constructor valid properties was null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// @Test +// public void testZKConstructor() { +// DataSourcePropertiesConfiguration dataSourcePropertiesConfiguration = new +/// DataSourcePropertiesConfiguration( +// buildZKProperties()); +// assertEquals( +// "DataSourcePropertiesConfiguration zk constructor valid field size was wrong", +// 1, dataSourcePropertiesConfiguration.getValidField().size()); +// assertNotNull( +// "DataSourcePropertiesConfiguration zk constructor valid properties was null", +// dataSourcePropertiesConfiguration.getValidDataSourceProperties()); +// } +// +// private FileDataSourceProperties buildFileProperties() { +// FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties(); +// +// fileDataSourceProperties.setFile("/tmp/test.json"); +// fileDataSourceProperties.setBufSize(1024); +// fileDataSourceProperties.setRecommendRefreshMs(2000); +// return fileDataSourceProperties; +// } +// +// private NacosDataSourceProperties buildNacosProperties() { +// NacosDataSourceProperties nacosDataSourceProperties = new NacosDataSourceProperties(); +// nacosDataSourceProperties.setServerAddr("127.0.0.1:8848"); +// nacosDataSourceProperties.setDataId("sentinel"); +// nacosDataSourceProperties.setGroupId("custom-group"); +// return nacosDataSourceProperties; +// } +// +// private ApolloDataSourceProperties buildApolloProperties() { +// ApolloDataSourceProperties apolloDataSourceProperties = new +/// ApolloDataSourceProperties(); +// apolloDataSourceProperties.setFlowRulesKey("test-key"); +// apolloDataSourceProperties.setDefaultFlowRuleValue("dft-val"); +// apolloDataSourceProperties.setNamespaceName("namespace"); +// return apolloDataSourceProperties; +// } +// +// private ZookeeperDataSourceProperties buildZKProperties() { +// ZookeeperDataSourceProperties zookeeperDataSourceProperties = new +/// ZookeeperDataSourceProperties(); +// +// zookeeperDataSourceProperties.setServerAddr("localhost:2181"); +// zookeeperDataSourceProperties.setPath("/path"); +// return zookeeperDataSourceProperties; +// } +// +// } diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesTests.java new file mode 100644 index 00000000..86384e6a --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/DataSourcePropertiesTests.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.config.ApolloDataSourceProperties; +import com.alibaba.cloud.sentinel.datasource.config.FileDataSourceProperties; +import com.alibaba.cloud.sentinel.datasource.config.ZookeeperDataSourceProperties; +import com.alibaba.cloud.sentinel.datasource.factorybean.ApolloDataSourceFactoryBean; +import com.alibaba.cloud.sentinel.datasource.factorybean.FileRefreshableDataSourceFactoryBean; +import com.alibaba.cloud.sentinel.datasource.factorybean.ZookeeperDataSourceFactoryBean; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jim + */ +public class DataSourcePropertiesTests { + + @Test + public void testApollo() { + ApolloDataSourceProperties apolloDataSourceProperties = new ApolloDataSourceProperties(); + apolloDataSourceProperties.setFlowRulesKey("test-key"); + apolloDataSourceProperties.setDefaultFlowRuleValue("dft-val"); + apolloDataSourceProperties.setNamespaceName("namespace"); + apolloDataSourceProperties.setRuleType(RuleType.DEGRADE); + assertEquals("Apollo flow rule key was wrong", "test-key", + apolloDataSourceProperties.getFlowRulesKey()); + assertEquals("Apollo namespace was wrong", "namespace", + apolloDataSourceProperties.getNamespaceName()); + assertEquals("Apollo default data type was wrong", "json", + apolloDataSourceProperties.getDataType()); + Assert.assertEquals("Apollo rule type was wrong", RuleType.DEGRADE, + apolloDataSourceProperties.getRuleType()); + assertEquals("Apollo default flow value was wrong", "dft-val", + apolloDataSourceProperties.getDefaultFlowRuleValue()); + assertEquals("Apollo factory bean was wrong", + ApolloDataSourceFactoryBean.class.getName(), + apolloDataSourceProperties.getFactoryBeanName()); + assertNull("Apollo converterClass was not null", + apolloDataSourceProperties.getConverterClass()); + } + + @Test + public void testZK() { + ZookeeperDataSourceProperties zookeeperDataSourceProperties = new ZookeeperDataSourceProperties(); + + zookeeperDataSourceProperties.setServerAddr("localhost:2181"); + zookeeperDataSourceProperties.setGroupId("groupId"); + zookeeperDataSourceProperties.setDataId("dataId"); + zookeeperDataSourceProperties.setPath("/path"); + zookeeperDataSourceProperties.setConverterClass("test.ConverterClass"); + zookeeperDataSourceProperties.setRuleType(RuleType.AUTHORITY); + + assertEquals("ZK serverAddr was wrong", "localhost:2181", + zookeeperDataSourceProperties.getServerAddr()); + assertEquals("ZK groupId was wrong", "groupId", + zookeeperDataSourceProperties.getGroupId()); + assertEquals("ZK dataId was wrong", "dataId", + zookeeperDataSourceProperties.getDataId()); + assertEquals("ZK path was wrong", "/path", + zookeeperDataSourceProperties.getPath()); + assertEquals("ZK factory bean was wrong", + ZookeeperDataSourceFactoryBean.class.getName(), + zookeeperDataSourceProperties.getFactoryBeanName()); + assertEquals("ZK custom converter class was wrong", "test.ConverterClass", + zookeeperDataSourceProperties.getConverterClass()); + Assert.assertEquals("ZK rule type was wrong", RuleType.AUTHORITY, + zookeeperDataSourceProperties.getRuleType()); + } + + @Test + public void testFileDefaultValue() { + FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties(); + + fileDataSourceProperties.setFile("/tmp/test.json"); + fileDataSourceProperties.setRuleType(RuleType.PARAM_FLOW); + + assertEquals("File path was wrong", "/tmp/test.json", + fileDataSourceProperties.getFile()); + assertEquals("File charset was wrong", "utf-8", + fileDataSourceProperties.getCharset()); + assertEquals("File refresh time was wrong", 3000L, + fileDataSourceProperties.getRecommendRefreshMs()); + assertEquals("File buf size was wrong", 1024 * 1024, + fileDataSourceProperties.getBufSize()); + assertEquals("File factory bean was wrong", + FileRefreshableDataSourceFactoryBean.class.getName(), + fileDataSourceProperties.getFactoryBeanName()); + Assert.assertEquals("File rule type was wrong", RuleType.PARAM_FLOW, + fileDataSourceProperties.getRuleType()); + } + + @Test + public void testFileCustomValue() { + FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties(); + + fileDataSourceProperties.setFile("/tmp/test.json"); + fileDataSourceProperties.setBufSize(1024); + fileDataSourceProperties.setRecommendRefreshMs(2000); + fileDataSourceProperties.setCharset("ISO8859-1"); + + assertEquals("File path was wrong", "/tmp/test.json", + fileDataSourceProperties.getFile()); + assertEquals("File charset was wrong", "ISO8859-1", + fileDataSourceProperties.getCharset()); + assertEquals("File refresh time was wrong", 2000L, + fileDataSourceProperties.getRecommendRefreshMs()); + assertEquals("File buf size was wrong", 1024, + fileDataSourceProperties.getBufSize()); + } + + @Test(expected = RuntimeException.class) + public void testFileException() { + FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties(); + fileDataSourceProperties.setFile("classpath: 1.json"); + fileDataSourceProperties.preCheck("test-ds"); + } + + @Test + public void testPostRegister() throws Exception { + FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties(); + + fileDataSourceProperties.setFile("classpath: flowrule.json"); + fileDataSourceProperties.setRuleType(RuleType.FLOW); + + FileRefreshableDataSource fileRefreshableDataSource = new FileRefreshableDataSource( + ResourceUtils + .getFile(StringUtils + .trimAllWhitespace(fileDataSourceProperties.getFile())) + .getAbsolutePath(), + new Converter>() { + ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public List convert(String source) { + try { + return objectMapper.readValue(source, + new TypeReference>() { + }); + } + catch (IOException e) { + // ignore + } + return null; + } + }); + fileDataSourceProperties.postRegister(fileRefreshableDataSource); + assertEquals("DataSourceProperties postRegister error", + fileRefreshableDataSource.loadConfig(), FlowRuleManager.getRules()); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/FileRefreshableDataSourceFactoryBeanTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/FileRefreshableDataSourceFactoryBeanTests.java new file mode 100644 index 00000000..5e49678e --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/FileRefreshableDataSourceFactoryBeanTests.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ResourceUtils; + +import com.alibaba.cloud.sentinel.datasource.factorybean.FileRefreshableDataSourceFactoryBean; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jim + */ +public class FileRefreshableDataSourceFactoryBeanTests { + + @Test + public void testFile() throws Exception { + AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext( + TestConfig.class); + assertNotNull("FileRefreshableDataSourceFactoryBean was not created", + annotationConfigApplicationContext.getBean("fileBean")); + FileRefreshableDataSource fileRefreshableDataSource = annotationConfigApplicationContext + .getBean("fileBean", FileRefreshableDataSource.class); + assertEquals("FileRefreshableDataSourceFactoryBean flow rule size was wrong", 1, + ((List) fileRefreshableDataSource.loadConfig()).size()); + FileRefreshableDataSourceFactoryBean factoryBean = annotationConfigApplicationContext + .getBean("&fileBean", FileRefreshableDataSourceFactoryBean.class); + assertEquals("FileRefreshableDataSourceFactoryBean buf size was wrong", 1024, + factoryBean.getBufSize()); + assertEquals("FileRefreshableDataSourceFactoryBean charset was wrong", "utf-8", + factoryBean.getCharset()); + assertEquals("FileRefreshableDataSourceFactoryBean recommendRefreshMs was wrong", + 2000, factoryBean.getRecommendRefreshMs()); + assertNotNull("FileRefreshableDataSourceFactoryBean file was null", + factoryBean.getFile()); + assertNotNull("FileRefreshableDataSourceFactoryBean converter was null", + factoryBean.getConverter()); + } + + @Configuration + public static class TestConfig { + + @Bean + public FileRefreshableDataSourceFactoryBean fileBean() { + FileRefreshableDataSourceFactoryBean factoryBean = new FileRefreshableDataSourceFactoryBean(); + factoryBean.setBufSize(1024); + factoryBean.setCharset("utf-8"); + factoryBean.setRecommendRefreshMs(2000); + try { + factoryBean.setFile(ResourceUtils.getFile("classpath:flowrule.json") + .getAbsolutePath()); + } + catch (FileNotFoundException e) { + // ignore + } + factoryBean.setConverter(buildConverter()); + return factoryBean; + } + + private Converter buildConverter() { + return new Converter>() { + ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public List convert(String source) { + try { + return objectMapper.readValue(source, + new TypeReference>() { + }); + } + catch (IOException e) { + // ignore + } + return null; + } + }; + } + + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourceFactoryBeanTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourceFactoryBeanTests.java new file mode 100644 index 00000000..3fbf1060 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourceFactoryBeanTests.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import com.alibaba.cloud.sentinel.datasource.converter.SentinelConverter; +import com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource; + +/** + * @author Jim + */ +public class NacosDataSourceFactoryBeanTests { + + private String dataId = "sentinel"; + private String groupId = "DEFAULT_GROUP"; + private String serverAddr = "localhost:8848"; + private String accessKey = "ak"; + private String secretKey = "sk"; + private String endpoint = "endpoint"; + private String namespace = "namespace"; + + @Test + public void testNacosFactoryBeanServerAddr() throws Exception { + NacosDataSourceFactoryBean factoryBean = spy(new NacosDataSourceFactoryBean()); + + Converter converter = mock(SentinelConverter.class); + + factoryBean.setDataId(dataId); + factoryBean.setGroupId(groupId); + factoryBean.setServerAddr(serverAddr); + factoryBean.setConverter(converter); + + NacosDataSource nacosDataSource = mock(NacosDataSource.class); + + doReturn(nacosDataSource).when(factoryBean).getObject(); + when(nacosDataSource.readSource()).thenReturn("{}"); + + assertEquals("NacosDataSourceFactoryBean getObject was wrong", nacosDataSource, + factoryBean.getObject()); + assertEquals("NacosDataSource read source value was wrong", "{}", + factoryBean.getObject().readSource()); + assertEquals("NacosDataSource converter was wrong", converter, + factoryBean.getConverter()); + assertEquals("NacosDataSourceFactoryBean dataId was wrong", dataId, + factoryBean.getDataId()); + assertEquals("NacosDataSourceFactoryBean groupId was wrong", groupId, + factoryBean.getGroupId()); + assertEquals("NacosDataSourceFactoryBean serverAddr was wrong", serverAddr, + factoryBean.getServerAddr()); + } + + @Test + public void testNacosFactoryBeanProperties() throws Exception { + NacosDataSourceFactoryBean factoryBean = spy(new NacosDataSourceFactoryBean()); + + Converter converter = mock(SentinelConverter.class); + + factoryBean.setDataId(dataId); + factoryBean.setGroupId(groupId); + factoryBean.setAccessKey(accessKey); + factoryBean.setSecretKey(secretKey); + factoryBean.setEndpoint(endpoint); + factoryBean.setNamespace(namespace); + factoryBean.setConverter(converter); + + NacosDataSource nacosDataSource = mock(NacosDataSource.class); + + doReturn(nacosDataSource).when(factoryBean).getObject(); + when(nacosDataSource.readSource()).thenReturn("{}"); + + assertEquals("NacosDataSourceFactoryBean getObject was wrong", nacosDataSource, + factoryBean.getObject()); + assertEquals("NacosDataSource read source value was wrong", "{}", + factoryBean.getObject().readSource()); + assertEquals("NacosDataSource converter was wrong", converter, + factoryBean.getConverter()); + assertEquals("NacosDataSourceFactoryBean dataId was wrong", dataId, + factoryBean.getDataId()); + assertEquals("NacosDataSourceFactoryBean groupId was wrong", groupId, + factoryBean.getGroupId()); + assertEquals("NacosDataSourceFactoryBean namespace was wrong", namespace, + factoryBean.getNamespace()); + assertEquals("NacosDataSourceFactoryBean endpoint was wrong", endpoint, + factoryBean.getEndpoint()); + assertEquals("NacosDataSourceFactoryBean ak was wrong", accessKey, + factoryBean.getAccessKey()); + assertEquals("NacosDataSourceFactoryBean sk was wrong", secretKey, + factoryBean.getSecretKey()); + + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourcePropertiesTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourcePropertiesTests.java new file mode 100644 index 00000000..6b3471f1 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/NacosDataSourcePropertiesTests.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; + +import org.junit.Assert; +import org.junit.Test; + +import com.alibaba.cloud.sentinel.datasource.config.NacosDataSourceProperties; +import com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean; + +/** + * @author Jim + */ +public class NacosDataSourcePropertiesTests { + + @Test + public void testNacosWithAddr() { + NacosDataSourceProperties nacosDataSourceProperties = new NacosDataSourceProperties(); + nacosDataSourceProperties.setServerAddr("127.0.0.1:8848"); + nacosDataSourceProperties.setRuleType(RuleType.FLOW); + nacosDataSourceProperties.setDataId("sentinel"); + nacosDataSourceProperties.setGroupId("custom-group"); + nacosDataSourceProperties.setDataType("xml"); + + assertEquals("Nacos groupId was wrong", "custom-group", + nacosDataSourceProperties.getGroupId()); + assertEquals("Nacos dataId was wrong", "sentinel", + nacosDataSourceProperties.getDataId()); + assertEquals("Nacos default data type was wrong", "xml", + nacosDataSourceProperties.getDataType()); + Assert.assertEquals("Nacos rule type was wrong", RuleType.FLOW, + nacosDataSourceProperties.getRuleType()); + assertEquals("Nacos default factory bean was wrong", + NacosDataSourceFactoryBean.class.getName(), + nacosDataSourceProperties.getFactoryBeanName()); + } + + @Test + public void testNacosWithProperties() { + NacosDataSourceProperties nacosDataSourceProperties = new NacosDataSourceProperties(); + nacosDataSourceProperties.setAccessKey("ak"); + nacosDataSourceProperties.setSecretKey("sk"); + nacosDataSourceProperties.setEndpoint("endpoint"); + nacosDataSourceProperties.setNamespace("namespace"); + nacosDataSourceProperties.setRuleType(RuleType.SYSTEM); + + assertEquals("Nacos ak was wrong", "ak", + nacosDataSourceProperties.getAccessKey()); + assertEquals("Nacos sk was wrong", "sk", + nacosDataSourceProperties.getSecretKey()); + assertEquals("Nacos endpoint was wrong", "endpoint", + nacosDataSourceProperties.getEndpoint()); + assertEquals("Nacos namespace was wrong", "namespace", + nacosDataSourceProperties.getNamespace()); + Assert.assertEquals("Nacos rule type was wrong", RuleType.SYSTEM, + nacosDataSourceProperties.getRuleType()); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/RuleTypeTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/RuleTypeTests.java new file mode 100644 index 00000000..4eea620a --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/RuleTypeTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +/** + * @author Jim + */ +public class RuleTypeTests { + + @Test + public void testGetByName() { + assertFalse("empty str rule name was not null", + RuleType.getByName("").isPresent()); + assertFalse("test rule name was not null", + RuleType.getByName("test").isPresent()); + assertFalse("param_flow rule name was not null", + RuleType.getByName("param_flow").isPresent()); + assertFalse("param rule name was not null", + RuleType.getByName("param").isPresent()); + assertFalse("FLOW rule name was not null", + RuleType.getByName("FLOW").isPresent()); + assertTrue("flow rule name was null", RuleType.getByName("flow").isPresent()); + assertTrue("degrade rule name was null", + RuleType.getByName("degrade").isPresent()); + assertTrue("param-flow rule name was null", + RuleType.getByName("param-flow").isPresent()); + assertTrue("system rule name was null", RuleType.getByName("system").isPresent()); + assertTrue("authority rule name was null", + RuleType.getByName("authority").isPresent()); + assertEquals("flow rule name was not equals RuleType.FLOW", RuleType.FLOW, + RuleType.getByName("flow").get()); + assertEquals("flow rule name was not equals RuleType.DEGRADE", RuleType.DEGRADE, + RuleType.getByName("degrade").get()); + assertEquals("flow rule name was not equals RuleType.PARAM_FLOW", + RuleType.PARAM_FLOW, RuleType.getByName("param-flow").get()); + assertEquals("flow rule name was not equals RuleType.SYSTEM", RuleType.SYSTEM, + RuleType.getByName("system").get()); + assertEquals("flow rule name was not equals RuleType.AUTHORITY", + RuleType.AUTHORITY, RuleType.getByName("authority").get()); + } + + @Test + public void testGetByClass() { + assertFalse("Object.class type type was not null", + RuleType.getByClass(Object.class).isPresent()); + assertFalse("AbstractRule.class rule type was not null", + RuleType.getByClass(AbstractRule.class).isPresent()); + assertTrue("FlowRule.class rule type was null", + RuleType.getByClass(FlowRule.class).isPresent()); + assertTrue("DegradeRule.class rule type was null", + RuleType.getByClass(DegradeRule.class).isPresent()); + assertTrue("ParamFlowRule.class rule type was null", + RuleType.getByClass(ParamFlowRule.class).isPresent()); + assertTrue("SystemRule.class rule type was null", + RuleType.getByClass(SystemRule.class).isPresent()); + assertTrue("AuthorityRule.class rule type was null", + RuleType.getByClass(AuthorityRule.class).isPresent()); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/SentinelConverterTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/SentinelConverterTests.java new file mode 100644 index 00000000..e909b751 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/SentinelConverterTests.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter; +import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * @author Jim + */ +public class SentinelConverterTests { + + private ObjectMapper objectMapper = new ObjectMapper(); + + private XmlMapper xmlMapper = new XmlMapper(); + + @Test + public void testJsonConverter() { + JsonConverter jsonConverter = new JsonConverter(objectMapper, FlowRule.class); + List flowRules = (List) jsonConverter + .convert(readFileContent("classpath: flowrule.json")); + assertEquals("json converter flow rule size was wrong", 1, flowRules.size()); + assertEquals("json converter flow rule resource name was wrong", "resource", + flowRules.get(0).getResource()); + assertEquals("json converter flow rule limit app was wrong", "default", + flowRules.get(0).getLimitApp()); + assertEquals("json converter flow rule count was wrong", "1.0", + String.valueOf(flowRules.get(0).getCount())); + assertEquals("json converter flow rule control behavior was wrong", + RuleConstant.CONTROL_BEHAVIOR_DEFAULT, + flowRules.get(0).getControlBehavior()); + assertEquals("json converter flow rule strategy was wrong", + RuleConstant.STRATEGY_DIRECT, flowRules.get(0).getStrategy()); + assertEquals("json converter flow rule grade was wrong", + RuleConstant.FLOW_GRADE_QPS, flowRules.get(0).getGrade()); + } + + @Test + public void testConverterEmptyContent() { + JsonConverter jsonConverter = new JsonConverter(objectMapper, FlowRule.class); + List flowRules = (List) jsonConverter.convert(""); + assertEquals("json converter flow rule size was not empty", 0, flowRules.size()); + } + + @Test(expected = RuntimeException.class) + public void testConverterErrorFormat() { + JsonConverter jsonConverter = new JsonConverter(objectMapper, FlowRule.class); + jsonConverter.convert(readFileContent("classpath: flowrule-errorformat.json")); + } + + @Test(expected = RuntimeException.class) + public void testConverterErrorContent() { + JsonConverter jsonConverter = new JsonConverter(objectMapper, FlowRule.class); + jsonConverter.convert(readFileContent("classpath: flowrule-errorcontent.json")); + } + + @Test + public void testXmlConverter() { + XmlConverter jsonConverter = new XmlConverter(xmlMapper, FlowRule.class); + List flowRules = (List) jsonConverter + .convert(readFileContent("classpath: flowrule.xml")); + assertEquals("xml converter flow rule size was wrong", 2, flowRules.size()); + assertEquals("xml converter flow rule1 resource name was wrong", "resource", + flowRules.get(0).getResource()); + assertEquals("xml converter flow rule2 limit app was wrong", "default", + flowRules.get(0).getLimitApp()); + assertEquals("xml converter flow rule1 count was wrong", "1.0", + String.valueOf(flowRules.get(0).getCount())); + assertEquals("xml converter flow rule1 control behavior was wrong", + RuleConstant.CONTROL_BEHAVIOR_DEFAULT, + flowRules.get(0).getControlBehavior()); + assertEquals("xml converter flow rule1 strategy was wrong", + RuleConstant.STRATEGY_DIRECT, flowRules.get(0).getStrategy()); + assertEquals("xml converter flow rule1 grade was wrong", + RuleConstant.FLOW_GRADE_QPS, flowRules.get(0).getGrade()); + + assertEquals("xml converter flow rule2 resource name was wrong", "test", + flowRules.get(1).getResource()); + assertEquals("xml converter flow rule2 limit app was wrong", "default", + flowRules.get(1).getLimitApp()); + assertEquals("xml converter flow rule2 count was wrong", "1.0", + String.valueOf(flowRules.get(1).getCount())); + assertEquals("xml converter flow rule2 control behavior was wrong", + RuleConstant.CONTROL_BEHAVIOR_DEFAULT, + flowRules.get(1).getControlBehavior()); + assertEquals("xml converter flow rule2 strategy was wrong", + RuleConstant.STRATEGY_DIRECT, flowRules.get(1).getStrategy()); + assertEquals("xml converter flow rule2 grade was wrong", + RuleConstant.FLOW_GRADE_QPS, flowRules.get(1).getGrade()); + } + + private String readFileContent(String file) { + try { + return FileUtils.readFileToString( + ResourceUtils.getFile(StringUtils.trimAllWhitespace(file))); + } + catch (IOException e) { + return ""; + } + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ZookeeperDataSourceFactoryBeanTests.java b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ZookeeperDataSourceFactoryBeanTests.java new file mode 100644 index 00000000..813b4053 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/java/com/alibaba/cloud/sentinel/datasource/ZookeeperDataSourceFactoryBeanTests.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.datasource; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter; +import com.alibaba.cloud.sentinel.datasource.factorybean.ZookeeperDataSourceFactoryBean; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.zookeeper.ZookeeperDataSource; + +/** + * @author Jim + */ +public class ZookeeperDataSourceFactoryBeanTests { + + private String dataId = "dataId"; + private String groupId = "groupId"; + private String serverAddr = "localhost:2181"; + + private String path = "/sentinel"; + + @Test + public void testZKWithoutPathFactoryBean() throws Exception { + ZookeeperDataSourceFactoryBean factoryBean = spy( + ZookeeperDataSourceFactoryBean.class); + + Converter converter = mock(XmlConverter.class); + + ZookeeperDataSource zookeeperDataSource = mock(ZookeeperDataSource.class); + + factoryBean.setConverter(converter); + factoryBean.setDataId(dataId); + factoryBean.setGroupId(groupId); + factoryBean.setServerAddr(serverAddr); + + when(zookeeperDataSource.readSource()).thenReturn("{}"); + doReturn(zookeeperDataSource).when(factoryBean).getObject(); + + assertEquals("ZookeeperDataSource getObject was wrong", zookeeperDataSource, + factoryBean.getObject()); + assertEquals("ZookeeperDataSource read source value was wrong", "{}", + factoryBean.getObject().readSource()); + assertEquals("ZookeeperDataSourceFactoryBean dataId was wrong", dataId, + factoryBean.getDataId()); + assertEquals("ZookeeperDataSourceFactoryBean converter was wrong", converter, + factoryBean.getConverter()); + assertEquals("ZookeeperDataSourceFactoryBean groupId was wrong", groupId, + factoryBean.getGroupId()); + assertEquals("ZookeeperDataSourceFactoryBean serverAddr was wrong", serverAddr, + factoryBean.getServerAddr()); + } + + @Test + public void testZKWithPathFactoryBean() throws Exception { + ZookeeperDataSourceFactoryBean factoryBean = spy( + ZookeeperDataSourceFactoryBean.class); + + Converter converter = mock(XmlConverter.class); + + ZookeeperDataSource zookeeperDataSource = mock(ZookeeperDataSource.class); + + factoryBean.setConverter(converter); + factoryBean.setPath(path); + factoryBean.setServerAddr(serverAddr); + + when(zookeeperDataSource.readSource()).thenReturn("{}"); + doReturn(zookeeperDataSource).when(factoryBean).getObject(); + + assertEquals("ZookeeperDataSource value was wrong", zookeeperDataSource, + factoryBean.getObject()); + assertEquals("ZookeeperDataSource read source value was wrong", "{}", + factoryBean.getObject().readSource()); + assertEquals("ZookeeperDataSourceFactoryBean converter was wrong", converter, + factoryBean.getConverter()); + assertEquals("ZookeeperDataSourceFactoryBean path was wrong", path, + factoryBean.getPath()); + assertEquals("ZookeeperDataSourceFactoryBean serverAddr was wrong", serverAddr, + factoryBean.getServerAddr()); + } + +} diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorcontent.json b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorcontent.json new file mode 100644 index 00000000..7bf60e61 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorcontent.json @@ -0,0 +1,11 @@ +[ + { + "test": 1, + "controlBehavior": 0, + "count": 1, + "grade": 1, + "limitApp": "default", + "strategy": 0 + } +] + diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorformat.json b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorformat.json new file mode 100644 index 00000000..48c738b6 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule-errorformat.json @@ -0,0 +1,10 @@ +[ + { + "resource": "resource", + "controlBehavior": 0, + "count": 1, + "grade": 1, + "limitApp": "default", + "strategy": 0 + }== +] diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.json b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.json new file mode 100644 index 00000000..0f8fbe2b --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.json @@ -0,0 +1,10 @@ +[ + { + "resource": "resource", + "controlBehavior": 0, + "count": 1, + "grade": 1, + "limitApp": "default", + "strategy": 0 + } +] diff --git a/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.xml b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.xml new file mode 100644 index 00000000..7f1926d3 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-datasource/src/test/resources/flowrule.xml @@ -0,0 +1,21 @@ + + + + resource + 0 + 1 + 1 + default + 0 + + + test + 0 + 1 + 1 + default + 0 + + + + diff --git a/spring-cloud-alibaba-sentinel-gateway/README.md b/spring-cloud-alibaba-sentinel-gateway/README.md new file mode 100755 index 00000000..d2dc9063 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/README.md @@ -0,0 +1,141 @@ +# Sentinel Spring Cloud Zuul Adapter + +Zuul does not provide rateLimit function, If use default `SentinelRibbonFilter` route filter. it wrapped by Hystrix Command. so only provide Service level +circuit protect. + +Sentinel can provide `ServiceId` level and `API Path` level flow control for spring cloud zuul gateway service. + +*Note*: this project is for zuul 1. + +## How to use + +1. Add maven dependency + +```xml + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-gateway + x.y.z + + +``` + +2. Set application.property + +``` +// default value is false +spring.cloud.sentinel.zuul.enabled=true +``` + +## How it works + +As Zuul run as per thread per connection block model, we add filters around `route Filter` to trace sentinel statistics. + +- `SentinelPreFilter`: Get an entry of resource,the first order is **ServiceId**, then **API Path**. +- `SentinelPostFilter`: When success response,exit entry. +- `SentinelErrorFilter`: When get an `Exception`, trace the exception and exit context. + + +the order of Filter can be changed by configuration: + +``` +spring.cloud.sentinel.zuul.order.post=0 +spring.cloud.sentinel.zuul.order.pre=10000 +spring.cloud.sentinel.zuul.order.error=-1 +``` + + +Filters create structure like: + + +```bash + +EntranceNode: machine-root(t:3 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +-EntranceNode: coke(t:2 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +--coke(t:2 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +---/coke/uri(t:0 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +-EntranceNode: sentinel_default_context(t:0 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +-EntranceNode: book(t:1 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +--book(t:1 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) +---/book/uri(t:0 pq:0 bq:0 tq:0 rt:0 prq:0 1mp:0 1mb:0 1mt:0) + +``` + +`book` and `coke` are serviceId. + +`---/book/uri` is api path, the real uri is `/uri`. + + +## Integration with Sentinel DashBord + +Start [Sentinel DashBord](https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0). + +## Rule config with dataSource + +Sentinel has full rule config features. see [Dynamic-Rule-Configuration](https://github.com/alibaba/Sentinel/wiki/Dynamic-Rule-Configuration) + + +## Custom Fallbacks + +Implements `SentinelFallbackProvider` to define your own Fallback Provider when Sentinel Block Exception throwing for different rout. the default +Fallback Provider is `DefaultBlockFallbackProvider`. + +By default Fallback route is `ServiveId + URI PATH`, example `/book/coke`, first `book` is serviceId, `/uri` is URI PATH, so both +can be needed. + +Here is an example: + +```java + +// custom provider +public class MyCokeServiceBlockFallbackProvider implements SentinelFallbackProvider { + + private Logger logger = LoggerFactory.getLogger(DefaultBlockFallbackProvider.class); + + // you can define root as service level + @Override + public String getRoute() { + return "/coke/uri"; + } + + @Override + public ClientHttpResponse fallbackResponse(String route, Throwable cause) { + if (cause instanceof BlockException) { + logger.info("get in fallback block exception:{}", cause); + return response(HttpStatus.TOO_MANY_REQUESTS, route); + } else { + return response(HttpStatus.INTERNAL_SERVER_ERROR, route); + } + } + } + +``` + +## Custom Request Origin Parser +By default this adapter use `DefaultRequestOriginParser` to parse sentinel origin. + +```java + +public class CustomRequestOriginParser implements RequestOriginParser { + @Override + public String parseOrigin(HttpServletRequest request) { + // do custom logic. + return ""; + } +} + +``` + +## Custom UrlCleaner +By default this adapter use `DefaultUrlCleaner` to define uri resource. + +```java +public class CustomUrlCleaner implements UrlCleaner { + + @Override + public String clean(String originUrl) { + // do custom logic. + return originUrl; + } +} +``` \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel-gateway/pom.xml b/spring-cloud-alibaba-sentinel-gateway/pom.xml new file mode 100644 index 00000000..fbdb5d71 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/pom.xml @@ -0,0 +1,69 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-sentinel-gateway + Spring Cloud Alibaba Sentinel Gateway + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + true + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + + + com.alibaba.csp + sentinel-parameter-flow-control + + + com.alibaba.csp + sentinel-zuul-adapter + + + com.alibaba.csp + sentinel-spring-cloud-gateway-adapter + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-datasource + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/ConfigConstants.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/ConfigConstants.java new file mode 100644 index 00000000..edbb6df4 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/ConfigConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway; + +/** + * @author Jim + */ +public interface ConfigConstants { + + String APP_TYPE_ZUUL_GATEWAY = "12"; + + String ZUUl_PREFIX = "spring.cloud.sentinel.zuul"; + + String GATEWAY_PREFIX = "spring.cloud.sentinel.scg"; + + String FALLBACK_MSG_RESPONSE = "response"; + String FALLBACK_REDIRECT = "redirect"; + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/FallbackProperties.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/FallbackProperties.java new file mode 100644 index 00000000..9d910a0a --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/FallbackProperties.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +/** + * @author Jim + */ +public class FallbackProperties { + + /** + * The fallback mode for sentinel spring-cloud-gateway. choose `redirect` or + * `response`. + */ + private String mode; + /** + * Redirect Url for `redirect` mode. + */ + private String redirect; + /** + * Response Body for `response` mode. + */ + private String responseBody; + /** + * Response Status for `response` mode. + */ + private Integer responseStatus = HttpStatus.TOO_MANY_REQUESTS.value(); + /** + * Content-Type for `response` mode. + */ + private String contentType = MediaType.APPLICATION_JSON_UTF8.toString(); + + public String getMode() { + return mode; + } + + public FallbackProperties setMode(String mode) { + this.mode = mode; + return this; + } + + public String getRedirect() { + return redirect; + } + + public FallbackProperties setRedirect(String redirect) { + this.redirect = redirect; + return this; + } + + public String getResponseBody() { + return responseBody; + } + + public FallbackProperties setResponseBody(String responseBody) { + this.responseBody = responseBody; + return this; + } + + public Integer getResponseStatus() { + return responseStatus; + } + + public FallbackProperties setResponseStatus(Integer responseStatus) { + this.responseStatus = responseStatus; + return this; + } + + public String getContentType() { + return contentType; + } + + public FallbackProperties setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/SentinelGatewayAutoConfiguration.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/SentinelGatewayAutoConfiguration.java new file mode 100644 index 00000000..3efd42ab --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/SentinelGatewayAutoConfiguration.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter; +import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateGroupItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * @author Jim + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true) +public class SentinelGatewayAutoConfiguration { + + @ConditionalOnClass(ObjectMapper.class) + @Configuration + protected static class SentinelConverterConfiguration { + + static class ApiPredicateItemDeserializer + extends StdDeserializer { + private Map> registry = new HashMap>(); + + ApiPredicateItemDeserializer() { + super(ApiPredicateItem.class); + } + + void registerApiPredicateItem(String uniqueAttribute, + Class apiPredicateItemClass) { + registry.put(uniqueAttribute, apiPredicateItemClass); + } + + @Override + public ApiPredicateItem deserialize(JsonParser jp, + DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + ObjectNode root = mapper.readTree(jp); + Class apiPredicateItemClass = null; + Iterator> elementsIterator = root.fields(); + while (elementsIterator.hasNext()) { + Entry element = elementsIterator.next(); + String name = element.getKey(); + if (registry.containsKey(name)) { + apiPredicateItemClass = registry.get(name); + break; + } + } + if (apiPredicateItemClass == null) { + return null; + } + return mapper.readValue(root.toString(), apiPredicateItemClass); + } + } + + @Configuration + protected static class SentinelJsonConfiguration { + + private ObjectMapper objectMapper = new ObjectMapper(); + + public SentinelJsonConfiguration() { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false); + + ApiPredicateItemDeserializer deserializer = new ApiPredicateItemDeserializer(); + deserializer.registerApiPredicateItem("pattern", + ApiPathPredicateItem.class); + deserializer.registerApiPredicateItem("items", + ApiPredicateGroupItem.class); + SimpleModule module = new SimpleModule( + "PolymorphicApiPredicateItemDeserializerModule", + new Version(1, 0, 0, null)); + module.addDeserializer(ApiPredicateItem.class, deserializer); + objectMapper.registerModule(module); + } + + @Bean("sentinel-json-gw-flow-converter") + public JsonConverter jsonGatewayFlowConverter() { + return new JsonConverter(objectMapper, GatewayFlowRule.class); + } + + @Bean("sentinel-json-gw-api-group-converter") + public JsonConverter jsonApiConverter() { + return new JsonConverter(objectMapper, ApiDefinition.class); + } + } + + @ConditionalOnClass(XmlMapper.class) + @Configuration + protected static class SentinelXmlConfiguration { + + private XmlMapper xmlMapper = new XmlMapper(); + + public SentinelXmlConfiguration() { + xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false); + ApiPredicateItemDeserializer deserializer = new ApiPredicateItemDeserializer(); + deserializer.registerApiPredicateItem("pattern", + ApiPathPredicateItem.class); + deserializer.registerApiPredicateItem("items", + ApiPredicateGroupItem.class); + SimpleModule module = new SimpleModule( + "PolymorphicGatewayDeserializerModule", + new Version(1, 0, 0, null)); + module.addDeserializer(ApiPredicateItem.class, deserializer); + xmlMapper.registerModule(module); + } + + @Bean("sentinel-xml-gw-flow-converter") + public XmlConverter xmlGatewayFlowConverter() { + return new XmlConverter(xmlMapper, GatewayFlowRule.class); + } + + @Bean("sentinel-xml-gw-api-group-converter") + public XmlConverter xmlApiConverter() { + return new XmlConverter(xmlMapper, ApiDefinition.class); + } + + } + } + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelGatewayProperties.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelGatewayProperties.java new file mode 100644 index 00000000..35ead382 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelGatewayProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway.scg; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import com.alibaba.cloud.sentinel.gateway.ConfigConstants; +import com.alibaba.cloud.sentinel.gateway.FallbackProperties; + +/** + * @author Jim + */ +@ConfigurationProperties(prefix = ConfigConstants.GATEWAY_PREFIX) +public class SentinelGatewayProperties { + + @NestedConfigurationProperty + private FallbackProperties fallback; + + public FallbackProperties getFallback() { + return fallback; + } + + public SentinelGatewayProperties setFallback(FallbackProperties fallback) { + this.fallback = fallback; + return this; + } + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelSCGAutoConfiguration.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelSCGAutoConfiguration.java new file mode 100644 index 00000000..1a8e6835 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/scg/SentinelSCGAutoConfiguration.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway.scg; + +import static org.springframework.web.reactive.function.BodyInserters.fromObject; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + +import com.alibaba.cloud.sentinel.gateway.ConfigConstants; +import com.alibaba.cloud.sentinel.gateway.FallbackProperties; +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.RedirectBlockRequestHandler; +import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +import reactor.core.publisher.Mono; + +/** + * @author Jim + */ +@Configuration +@ConditionalOnClass(GlobalFilter.class) +@ConditionalOnProperty(prefix = ConfigConstants.GATEWAY_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(SentinelGatewayProperties.class) +public class SentinelSCGAutoConfiguration { + + private static final Logger logger = LoggerFactory + .getLogger(SentinelSCGAutoConfiguration.class); + + private final List viewResolvers; + private final ServerCodecConfigurer serverCodecConfigurer; + + @Autowired + private Optional blockRequestHandlerOptional; + + @Autowired + private SentinelGatewayProperties gatewayProperties; + + @PostConstruct + private void init() { + // blockRequestHandlerOptional has low priority + blockRequestHandlerOptional.ifPresent(GatewayCallbackManager::setBlockHandler); + initAppType(); + initFallback(); + } + + public SentinelSCGAutoConfiguration( + ObjectProvider> viewResolversProvider, + ServerCodecConfigurer serverCodecConfigurer) { + this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); + this.serverCodecConfigurer = serverCodecConfigurer; + } + + private void initAppType() { + System.setProperty(SentinelConfig.APP_TYPE, + String.valueOf(SentinelGatewayConstants.APP_TYPE_GATEWAY)); + } + + private void initFallback() { + FallbackProperties fallbackProperties = gatewayProperties.getFallback(); + if (fallbackProperties == null + || StringUtil.isBlank(fallbackProperties.getMode())) { + return; + } + if (ConfigConstants.FALLBACK_MSG_RESPONSE.equals(fallbackProperties.getMode())) { + if (StringUtil.isNotBlank(fallbackProperties.getResponseBody())) { + GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() { + @Override + public Mono handleRequest(ServerWebExchange exchange, + Throwable t) { + return ServerResponse + .status(fallbackProperties.getResponseStatus()) + .contentType(MediaType + .valueOf(fallbackProperties.getContentType())) + .body(fromObject(fallbackProperties.getResponseBody())); + } + }); + logger.info( + "[Sentinel SpringCloudGateway] using AnonymousBlockRequestHandler, responseStatus: " + + fallbackProperties.getResponseStatus() + + ", responseBody: " + + fallbackProperties.getResponseStatus()); + } + } + String redirectUrl = fallbackProperties.getRedirect(); + if (ConfigConstants.FALLBACK_REDIRECT.equals(fallbackProperties.getMode()) + && StringUtil.isNotBlank(redirectUrl)) { + GatewayCallbackManager + .setBlockHandler(new RedirectBlockRequestHandler(redirectUrl)); + logger.info( + "[Sentinel SpringCloudGateway] using RedirectBlockRequestHandler, redirectUrl: " + + redirectUrl); + } + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + @ConditionalOnMissingBean + public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { + // Register the block exception handler for Spring Cloud Gateway. + logger.info( + "[Sentinel SpringCloudGateway] register SentinelGatewayBlockExceptionHandler"); + return new SentinelGatewayBlockExceptionHandler(viewResolvers, + serverCodecConfigurer); + } + + @Bean + @Order(-1) + @ConditionalOnMissingBean + public SentinelGatewayFilter sentinelGatewayFilter() { + logger.info("[Sentinel SpringCloudGateway] register SentinelGatewayFilter"); + return new SentinelGatewayFilter(); + } + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/FallBackProviderHandler.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/FallBackProviderHandler.java new file mode 100644 index 00000000..9f9074e6 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/FallBackProviderHandler.java @@ -0,0 +1,45 @@ +package com.alibaba.cloud.sentinel.gateway.zuul; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.util.CollectionUtils; + +import com.alibaba.csp.sentinel.adapter.gateway.zuul.fallback.DefaultBlockFallbackProvider; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.fallback.ZuulBlockFallbackManager; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.fallback.ZuulBlockFallbackProvider; + +/** + * @author tiger + */ +public class FallBackProviderHandler implements SmartInitializingSingleton { + + private static final Logger logger = LoggerFactory + .getLogger(FallBackProviderHandler.class); + + private final DefaultListableBeanFactory beanFactory; + + public FallBackProviderHandler(DefaultListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + Map providerMap = beanFactory + .getBeansOfType(ZuulBlockFallbackProvider.class); + if (!CollectionUtils.isEmpty(providerMap)) { + providerMap.forEach((k, v) -> { + logger.info("[Sentinel Zuul] Register provider name:{}, instance: {}", k, + v); + ZuulBlockFallbackManager.registerProvider(v); + }); + } + else { + logger.info("[Sentinel Zuul] Register default fallback provider. "); + ZuulBlockFallbackManager.registerProvider(new DefaultBlockFallbackProvider()); + } + } +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulAutoConfiguration.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulAutoConfiguration.java new file mode 100644 index 00000000..12a866b4 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulAutoConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 + * + * http://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 com.alibaba.cloud.sentinel.gateway.zuul; + +import java.util.Optional; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.sentinel.gateway.ConfigConstants; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.callback.ZuulGatewayCallbackManager; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulErrorFilter; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPostFilter; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPreFilter; +import com.alibaba.csp.sentinel.config.SentinelConfig; + +import com.netflix.zuul.http.ZuulServlet; + +/** + * Sentinel Spring Cloud Zuul AutoConfiguration + * + * @author tiger + */ +@Configuration +@ConditionalOnClass(ZuulServlet.class) +@ConditionalOnProperty(prefix = ConfigConstants.ZUUl_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(SentinelZuulProperties.class) +public class SentinelZuulAutoConfiguration { + + private static final Logger logger = LoggerFactory + .getLogger(SentinelZuulAutoConfiguration.class); + + @Autowired + private Optional requestOriginParserOptional; + + @Autowired + private SentinelZuulProperties zuulProperties; + + @PostConstruct + private void init() { + requestOriginParserOptional + .ifPresent(ZuulGatewayCallbackManager::setOriginParser); + System.setProperty(SentinelConfig.APP_TYPE, + String.valueOf(ConfigConstants.APP_TYPE_ZUUL_GATEWAY)); + } + + @Bean + @ConditionalOnMissingBean + public SentinelZuulPreFilter sentinelZuulPreFilter() { + logger.info("[Sentinel Zuul] register SentinelZuulPreFilter {}", + zuulProperties.getOrder().getPre()); + return new SentinelZuulPreFilter(zuulProperties.getOrder().getPre()); + } + + @Bean + @ConditionalOnMissingBean + public SentinelZuulPostFilter sentinelZuulPostFilter() { + logger.info("[Sentinel Zuul] register SentinelZuulPostFilter {}", + zuulProperties.getOrder().getPost()); + return new SentinelZuulPostFilter(zuulProperties.getOrder().getPost()); + } + + @Bean + @ConditionalOnMissingBean + public SentinelZuulErrorFilter sentinelZuulErrorFilter() { + logger.info("[Sentinel Zuul] register SentinelZuulErrorFilter {}", + zuulProperties.getOrder().getError()); + return new SentinelZuulErrorFilter(zuulProperties.getOrder().getError()); + } + + @Bean + public FallBackProviderHandler fallBackProviderHandler( + DefaultListableBeanFactory beanFactory) { + return new FallBackProviderHandler(beanFactory); + } + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulProperties.java b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulProperties.java new file mode 100644 index 00000000..d690c252 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/java/com/alibaba/cloud/sentinel/gateway/zuul/SentinelZuulProperties.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.sentinel.gateway.zuul; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import com.alibaba.cloud.sentinel.gateway.ConfigConstants; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.constants.ZuulConstant; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulErrorFilter; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPostFilter; +import com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPreFilter; + +/** + * @author Jim + */ +@ConfigurationProperties(prefix = ConfigConstants.ZUUl_PREFIX) +public class SentinelZuulProperties { + + @NestedConfigurationProperty + private SentinelZuulProperties.Order order; + + public Order getOrder() { + return order; + } + + public SentinelZuulProperties setOrder(Order order) { + this.order = order; + return this; + } + + public static class Order { + + /** + * The order of {@link SentinelZuulPreFilter}. + */ + private int pre = 10000; + + /** + * The order of {@link SentinelZuulPostFilter}. + */ + private int post = ZuulConstant.SEND_RESPONSE_FILTER_ORDER; + + /** + * The order of {@link SentinelZuulErrorFilter}. + */ + private int error = -1; + + public int getPre() { + return pre; + } + + public void setPre(int pre) { + this.pre = pre; + } + + public int getPost() { + return post; + } + + public void setPost(int post) { + this.post = post; + } + + public int getError() { + return error; + } + + public void setError(int error) { + this.error = error; + } + } + +} diff --git a/spring-cloud-alibaba-sentinel-gateway/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-sentinel-gateway/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..d4c02887 --- /dev/null +++ b/spring-cloud-alibaba-sentinel-gateway/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.sentinel.gateway.zuul.SentinelZuulAutoConfiguration,\ +com.alibaba.cloud.sentinel.gateway.scg.SentinelSCGAutoConfiguration,\ +com.alibaba.cloud.sentinel.gateway.SentinelGatewayAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel/pom.xml b/spring-cloud-alibaba-sentinel/pom.xml new file mode 100644 index 00000000..5f2a2ada --- /dev/null +++ b/spring-cloud-alibaba-sentinel/pom.xml @@ -0,0 +1,187 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alibaba-sentinel + Spring Cloud Alibaba Sentinel + + + + + com.alibaba.csp + sentinel-transport-simple-http + + + + com.alibaba.csp + sentinel-annotation-aspectj + + + + com.alibaba.csp + sentinel-dubbo-adapter + true + + + + com.alibaba.csp + sentinel-apache-dubbo-adapter + true + + + + com.alibaba.csp + sentinel-web-servlet + + + + org.springframework.boot + spring-boot-starter-web + true + + + + com.alibaba.csp + sentinel-spring-webflux-adapter + + + + org.springframework.boot + spring-boot-starter-webflux + true + + + + org.springframework.cloud + spring-cloud-starter-openfeign + provided + true + + + + org.springframework.cloud + spring-cloud-commons + true + + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + provided + true + + + + com.alibaba.csp + sentinel-parameter-flow-control + + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + true + + + + com.alibaba.csp + sentinel-cluster-server-default + + + + com.alibaba.csp + sentinel-cluster-client-default + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-datasource + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-actuator + provided + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelConstants.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelConstants.java new file mode 100644 index 00000000..921458e3 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +/** + * @author fangjian + */ +public interface SentinelConstants { + + String PROPERTY_PREFIX = "spring.cloud.sentinel"; + + String BLOCK_TYPE = "block"; + String FALLBACK_TYPE = "fallback"; + + // commercialization + + String FLOW_DATASOURCE_NAME = "edas-flow"; + String DEGRADE_DATASOURCE_NAME = "edas-degrade"; + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelProperties.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelProperties.java new file mode 100644 index 00000000..8fab12bf --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelProperties.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.Ordered; +import org.springframework.validation.annotation.Validated; + +import com.alibaba.cloud.sentinel.datasource.config.DataSourcePropertiesConfiguration; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; + +/** + * {@link ConfigurationProperties} for Sentinel. + * + * @author xiaojing + * @author hengyunabc + * @author jiashuai.xie + * @author Jim + */ +@ConfigurationProperties(prefix = SentinelConstants.PROPERTY_PREFIX) +@Validated +public class SentinelProperties { + + /** + * Earlier initialize heart-beat when the spring container starts when the transport + * dependency is on classpath, the configuration is effective. + */ + private boolean eager = false; + + /** + * Enable sentinel auto configure, the default value is true. + */ + private boolean enabled = true; + + /** + * Configurations about datasource, like 'nacos', 'apollo', 'file', 'zookeeper'. + */ + private Map datasource = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + + /** + * Transport configuration about dashboard and client. + */ + private Transport transport = new Transport(); + + /** + * Metric configuration about resource. + */ + private Metric metric = new Metric(); + + /** + * Web servlet configuration when the application is web, the configuration is + * effective. + */ + private Servlet servlet = new Servlet(); + + /** + * Sentinel filter when the application is web, the configuration is effective. + */ + private Filter filter = new Filter(); + + /** + * Sentinel Flow configuration. + */ + private Flow flow = new Flow(); + + /** + * Sentinel log configuration {@link LogBase}. + */ + private Log log = new Log(); + + public boolean isEager() { + return eager; + } + + public void setEager(boolean eager) { + this.eager = eager; + } + + public Flow getFlow() { + return flow; + } + + public void setFlow(Flow flow) { + this.flow = flow; + } + + public Transport getTransport() { + return transport; + } + + public void setTransport(Transport transport) { + this.transport = transport; + } + + public Metric getMetric() { + return metric; + } + + public void setMetric(Metric metric) { + this.metric = metric; + } + + public Servlet getServlet() { + return servlet; + } + + public void setServlet(Servlet servlet) { + this.servlet = servlet; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Filter getFilter() { + return filter; + } + + public void setFilter(Filter filter) { + this.filter = filter; + } + + public Map getDatasource() { + return datasource; + } + + public void setDatasource(Map datasource) { + this.datasource = datasource; + } + + public Log getLog() { + return log; + } + + public void setLog(Log log) { + this.log = log; + } + + public static class Flow { + + /** + * The cold factor {@link SentinelConfig#COLD_FACTOR}. + */ + private String coldFactor = "3"; + + public String getColdFactor() { + return coldFactor; + } + + public void setColdFactor(String coldFactor) { + this.coldFactor = coldFactor; + } + + } + + public static class Servlet { + + /** + * The process page when the flow control is triggered. + */ + private String blockPage; + + public String getBlockPage() { + return blockPage; + } + + public void setBlockPage(String blockPage) { + this.blockPage = blockPage; + } + } + + public static class Metric { + + /** + * The metric file size {@link SentinelConfig#SINGLE_METRIC_FILE_SIZE}. + */ + private String fileSingleSize; + + /** + * The total metric file count {@link SentinelConfig#TOTAL_METRIC_FILE_COUNT}. + */ + private String fileTotalCount; + + /** + * Charset when sentinel write or search metric file. + * {@link SentinelConfig#CHARSET} + */ + private String charset = "UTF-8"; + + public String getFileSingleSize() { + return fileSingleSize; + } + + public void setFileSingleSize(String fileSingleSize) { + this.fileSingleSize = fileSingleSize; + } + + public String getFileTotalCount() { + return fileTotalCount; + } + + public void setFileTotalCount(String fileTotalCount) { + this.fileTotalCount = fileTotalCount; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + } + + public static class Transport { + + /** + * Sentinel api port, default value is 8719 {@link TransportConfig#SERVER_PORT}. + */ + private String port = "8719"; + + /** + * Sentinel dashboard address, won't try to connect dashboard when address is + * empty {@link TransportConfig#CONSOLE_SERVER}. + */ + private String dashboard = ""; + + /** + * Send heartbeat interval millisecond + * {@link TransportConfig#HEARTBEAT_INTERVAL_MS}. + */ + private String heartbeatIntervalMs; + + /** + * Get heartbeat client local ip. If the client ip not configured, it will be the + * address of local host. + */ + private String clientIp; + + public String getHeartbeatIntervalMs() { + return heartbeatIntervalMs; + } + + public void setHeartbeatIntervalMs(String heartbeatIntervalMs) { + this.heartbeatIntervalMs = heartbeatIntervalMs; + } + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + + public String getDashboard() { + return dashboard; + } + + public void setDashboard(String dashboard) { + this.dashboard = dashboard; + } + + public String getClientIp() { + return clientIp; + } + + public void setClientIp(String clientIp) { + this.clientIp = clientIp; + } + } + + public static class Filter { + + /** + * Sentinel filter chain order. + */ + private int order = Ordered.HIGHEST_PRECEDENCE; + + /** + * URL pattern for sentinel filter, default is /* + */ + private List urlPatterns; + + /** + * Enable to instance + * {@link com.alibaba.csp.sentinel.adapter.servlet.CommonFilter}. + */ + private boolean enabled = true; + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + public List getUrlPatterns() { + return urlPatterns; + } + + public void setUrlPatterns(List urlPatterns) { + this.urlPatterns = urlPatterns; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + + public static class Log { + + /** + * Sentinel log base dir. + */ + private String dir; + + /** + * Distinguish the log file by pid number. + */ + private boolean switchPid = false; + + public String getDir() { + return dir; + } + + public void setDir(String dir) { + this.dir = dir; + } + + public boolean isSwitchPid() { + return switchPid; + } + + public void setSwitchPid(boolean switchPid) { + this.switchPid = switchPid; + } + + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebAutoConfiguration.java new file mode 100644 index 00000000..dadb235f --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebAutoConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.annotation.PostConstruct; +import javax.servlet.Filter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; + +/** + * @author xiaojing + */ +@Configuration +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(CommonFilter.class) +@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true) +@EnableConfigurationProperties(SentinelProperties.class) +public class SentinelWebAutoConfiguration { + + private static final Logger log = LoggerFactory + .getLogger(SentinelWebAutoConfiguration.class); + + @Autowired + private SentinelProperties properties; + + @Autowired + private Optional urlCleanerOptional; + + @Autowired + private Optional urlBlockHandlerOptional; + + @Autowired + private Optional requestOriginParserOptional; + + @PostConstruct + public void init() { + urlBlockHandlerOptional.ifPresent(WebCallbackManager::setUrlBlockHandler); + urlCleanerOptional.ifPresent(WebCallbackManager::setUrlCleaner); + requestOriginParserOptional.ifPresent(WebCallbackManager::setRequestOriginParser); + } + + @Bean + @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true) + public FilterRegistrationBean sentinelFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + + SentinelProperties.Filter filterConfig = properties.getFilter(); + + if (filterConfig.getUrlPatterns() == null + || filterConfig.getUrlPatterns().isEmpty()) { + List defaultPatterns = new ArrayList<>(); + defaultPatterns.add("/*"); + filterConfig.setUrlPatterns(defaultPatterns); + } + + registration.addUrlPatterns(filterConfig.getUrlPatterns().toArray(new String[0])); + Filter filter = new CommonFilter(); + registration.setFilter(filter); + registration.setOrder(filterConfig.getOrder()); + log.info( + "[Sentinel Starter] register Sentinel CommonFilter with urlPatterns: {}.", + filterConfig.getUrlPatterns()); + return registration; + + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebFluxAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebFluxAutoConfiguration.java new file mode 100644 index 00000000..86a6bcbc --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/SentinelWebFluxAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.result.view.ViewResolver; + +import com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorTransformer; +import com.alibaba.csp.sentinel.adapter.spring.webflux.SentinelWebFluxFilter; +import com.alibaba.csp.sentinel.adapter.spring.webflux.callback.BlockRequestHandler; +import com.alibaba.csp.sentinel.adapter.spring.webflux.callback.WebFluxCallbackManager; +import com.alibaba.csp.sentinel.adapter.spring.webflux.exception.SentinelBlockExceptionHandler; + +/** + * @author Jim + */ +@Configuration +@ConditionalOnWebApplication(type = Type.REACTIVE) +@ConditionalOnClass(SentinelReactorTransformer.class) +@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true) +@EnableConfigurationProperties(SentinelProperties.class) +public class SentinelWebFluxAutoConfiguration { + + private static final Logger log = LoggerFactory + .getLogger(SentinelWebFluxAutoConfiguration.class); + + private final List viewResolvers; + private final ServerCodecConfigurer serverCodecConfigurer; + + @Autowired + private Optional blockRequestHandler; + + public SentinelWebFluxAutoConfiguration( + ObjectProvider> viewResolvers, + ServerCodecConfigurer serverCodecConfigurer) { + this.viewResolvers = viewResolvers.getIfAvailable(Collections::emptyList); + this.serverCodecConfigurer = serverCodecConfigurer; + } + + @PostConstruct + public void init() { + blockRequestHandler.ifPresent(WebFluxCallbackManager::setBlockHandler); + } + + @Bean + @Order(-2) + @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true) + public SentinelBlockExceptionHandler sentinelBlockExceptionHandler() { + return new SentinelBlockExceptionHandler(viewResolvers, serverCodecConfigurer); + } + + @Bean + @Order(-1) + @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true) + public SentinelWebFluxFilter sentinelWebFluxFilter() { + log.info("[Sentinel Starter] register Sentinel SentinelWebFluxFilter"); + return new SentinelWebFluxFilter(); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/annotation/SentinelRestTemplate.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/annotation/SentinelRestTemplate.java new file mode 100644 index 00000000..2638ee33 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/annotation/SentinelRestTemplate.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.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; + +/** + * @author fangjian + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SentinelRestTemplate { + + String blockHandler() default ""; + + Class blockHandlerClass() default void.class; + + String fallback() default ""; + + Class fallbackClass() default void.class; + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/BlockClassRegistry.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/BlockClassRegistry.java new file mode 100644 index 00000000..1e5d6c8b --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/BlockClassRegistry.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.custom; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author fangjian + */ +final class BlockClassRegistry { + + private static final Map FALLBACK_MAP = new ConcurrentHashMap<>(); + private static final Map BLOCK_HANDLER_MAP = new ConcurrentHashMap<>(); + + static Method lookupFallback(Class clazz, String name) { + return FALLBACK_MAP.get(getKey(clazz, name)); + } + + static Method lookupBlockHandler(Class clazz, String name) { + return BLOCK_HANDLER_MAP.get(getKey(clazz, name)); + } + + static void updateFallbackFor(Class clazz, String name, Method method) { + if (clazz == null || StringUtil.isBlank(name)) { + throw new IllegalArgumentException("Bad argument"); + } + FALLBACK_MAP.put(getKey(clazz, name), method); + } + + static void updateBlockHandlerFor(Class clazz, String name, Method method) { + if (clazz == null || StringUtil.isBlank(name)) { + throw new IllegalArgumentException("Bad argument"); + } + BLOCK_HANDLER_MAP.put(getKey(clazz, name), method); + } + + private static String getKey(Class clazz, String name) { + return String.format("%s:%s", clazz.getCanonicalName(), name); + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java new file mode 100644 index 00000000..a3b435bd --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelAutoConfiguration.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.custom; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter; +import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.util.AppNameUtil; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * @author xiaojing + * @author jiashuai.xie + * @author Jim + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true) +@EnableConfigurationProperties(SentinelProperties.class) +public class SentinelAutoConfiguration { + + @Value("${project.name:${spring.application.name:}}") + private String projectName; + + @Autowired + private SentinelProperties properties; + + @PostConstruct + private void init() { + if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_DIR)) + && StringUtils.hasText(properties.getLog().getDir())) { + System.setProperty(LogBase.LOG_DIR, properties.getLog().getDir()); + } + if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_NAME_USE_PID)) + && properties.getLog().isSwitchPid()) { + System.setProperty(LogBase.LOG_NAME_USE_PID, + String.valueOf(properties.getLog().isSwitchPid())); + } + if (StringUtils.isEmpty(System.getProperty(AppNameUtil.APP_NAME)) + && StringUtils.hasText(projectName)) { + System.setProperty(AppNameUtil.APP_NAME, projectName); + } + if (StringUtils.isEmpty(System.getProperty(TransportConfig.SERVER_PORT)) + && StringUtils.hasText(properties.getTransport().getPort())) { + System.setProperty(TransportConfig.SERVER_PORT, + properties.getTransport().getPort()); + } + if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER)) + && StringUtils.hasText(properties.getTransport().getDashboard())) { + System.setProperty(TransportConfig.CONSOLE_SERVER, + properties.getTransport().getDashboard()); + } + if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_INTERVAL_MS)) + && StringUtils + .hasText(properties.getTransport().getHeartbeatIntervalMs())) { + System.setProperty(TransportConfig.HEARTBEAT_INTERVAL_MS, + properties.getTransport().getHeartbeatIntervalMs()); + } + if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_CLIENT_IP)) + && StringUtils.hasText(properties.getTransport().getClientIp())) { + System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, + properties.getTransport().getClientIp()); + } + if (StringUtils.isEmpty(System.getProperty(SentinelConfig.CHARSET)) + && StringUtils.hasText(properties.getMetric().getCharset())) { + System.setProperty(SentinelConfig.CHARSET, + properties.getMetric().getCharset()); + } + if (StringUtils + .isEmpty(System.getProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE)) + && StringUtils.hasText(properties.getMetric().getFileSingleSize())) { + System.setProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE, + properties.getMetric().getFileSingleSize()); + } + if (StringUtils + .isEmpty(System.getProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT)) + && StringUtils.hasText(properties.getMetric().getFileTotalCount())) { + System.setProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT, + properties.getMetric().getFileTotalCount()); + } + if (StringUtils.isEmpty(System.getProperty(SentinelConfig.COLD_FACTOR)) + && StringUtils.hasText(properties.getFlow().getColdFactor())) { + System.setProperty(SentinelConfig.COLD_FACTOR, + properties.getFlow().getColdFactor()); + } + if (StringUtils.hasText(properties.getServlet().getBlockPage())) { + WebServletConfig.setBlockPage(properties.getServlet().getBlockPage()); + } + + // earlier initialize + if (properties.isEager()) { + InitExecutor.doInit(); + } + + } + + @Bean + @ConditionalOnMissingBean + public SentinelResourceAspect sentinelResourceAspect() { + return new SentinelResourceAspect(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") + @ConditionalOnProperty(name = "resttemplate.sentinel.enabled", havingValue = "true", matchIfMissing = true) + public SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @ConditionalOnMissingBean + public SentinelDataSourceHandler sentinelDataSourceHandler( + DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties, + Environment env) { + return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env); + } + + @ConditionalOnClass(ObjectMapper.class) + @Configuration + protected static class SentinelConverterConfiguration { + + @Configuration + protected static class SentinelJsonConfiguration { + + private ObjectMapper objectMapper = new ObjectMapper(); + + public SentinelJsonConfiguration() { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false); + } + + @Bean("sentinel-json-flow-converter") + public JsonConverter jsonFlowConverter() { + return new JsonConverter(objectMapper, FlowRule.class); + } + + @Bean("sentinel-json-degrade-converter") + public JsonConverter jsonDegradeConverter() { + return new JsonConverter(objectMapper, DegradeRule.class); + } + + @Bean("sentinel-json-system-converter") + public JsonConverter jsonSystemConverter() { + return new JsonConverter(objectMapper, SystemRule.class); + } + + @Bean("sentinel-json-authority-converter") + public JsonConverter jsonAuthorityConverter() { + return new JsonConverter(objectMapper, AuthorityRule.class); + } + + @Bean("sentinel-json-param-flow-converter") + public JsonConverter jsonParamFlowConverter() { + return new JsonConverter(objectMapper, ParamFlowRule.class); + } + + } + + @ConditionalOnClass(XmlMapper.class) + @Configuration + protected static class SentinelXmlConfiguration { + + private XmlMapper xmlMapper = new XmlMapper(); + + public SentinelXmlConfiguration() { + xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false); + } + + @Bean("sentinel-xml-flow-converter") + public XmlConverter xmlFlowConverter() { + return new XmlConverter(xmlMapper, FlowRule.class); + } + + @Bean("sentinel-xml-degrade-converter") + public XmlConverter xmlDegradeConverter() { + return new XmlConverter(xmlMapper, DegradeRule.class); + } + + @Bean("sentinel-xml-system-converter") + public XmlConverter xmlSystemConverter() { + return new XmlConverter(xmlMapper, SystemRule.class); + } + + @Bean("sentinel-xml-authority-converter") + public XmlConverter xmlAuthorityConverter() { + return new XmlConverter(xmlMapper, AuthorityRule.class); + } + + @Bean("sentinel-xml-param-flow-converter") + public XmlConverter xmlParamFlowConverter() { + return new XmlConverter(xmlMapper, ParamFlowRule.class); + } + + } + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelBeanPostProcessor.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelBeanPostProcessor.java new file mode 100644 index 00000000..178060bf --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelBeanPostProcessor.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.custom; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.core.type.StandardMethodMetadata; +import org.springframework.core.type.classreading.MethodMetadataReadingVisitor; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.sentinel.SentinelConstants; +import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * PostProcessor handle @SentinelRestTemplate Annotation, add interceptor for RestTemplate + * + * @author Jim + * @see SentinelRestTemplate + * @see SentinelProtectInterceptor + */ +public class SentinelBeanPostProcessor implements MergedBeanDefinitionPostProcessor { + + private static final Logger log = LoggerFactory + .getLogger(SentinelBeanPostProcessor.class); + + private final ApplicationContext applicationContext; + + public SentinelBeanPostProcessor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + private ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, + Class beanType, String beanName) { + if (checkSentinelProtect(beanDefinition, beanType)) { + SentinelRestTemplate sentinelRestTemplate; + if (beanDefinition.getSource() instanceof StandardMethodMetadata) { + sentinelRestTemplate = ((StandardMethodMetadata) beanDefinition + .getSource()).getIntrospectedMethod() + .getAnnotation(SentinelRestTemplate.class); + } + else { + sentinelRestTemplate = beanDefinition.getResolvedFactoryMethod() + .getAnnotation(SentinelRestTemplate.class); + } + // check class and method validation + checkSentinelRestTemplate(sentinelRestTemplate, beanName); + cache.put(beanName, sentinelRestTemplate); + } + } + + private void checkSentinelRestTemplate(SentinelRestTemplate sentinelRestTemplate, + String beanName) { + checkBlock4RestTemplate(sentinelRestTemplate.blockHandlerClass(), + sentinelRestTemplate.blockHandler(), beanName, + SentinelConstants.BLOCK_TYPE); + checkBlock4RestTemplate(sentinelRestTemplate.fallbackClass(), + sentinelRestTemplate.fallback(), beanName, + SentinelConstants.FALLBACK_TYPE); + } + + private void checkBlock4RestTemplate(Class blockClass, String blockMethod, + String beanName, String type) { + if (blockClass == void.class && StringUtils.isEmpty(blockMethod)) { + return; + } + if (blockClass != void.class && StringUtils.isEmpty(blockMethod)) { + log.error( + "{} class attribute exists but {} method attribute is not exists in bean[{}]", + type, type, beanName); + throw new IllegalArgumentException(type + " class attribute exists but " + + type + " method attribute is not exists in bean[" + beanName + "]"); + } + else if (blockClass == void.class && !StringUtils.isEmpty(blockMethod)) { + log.error( + "{} method attribute exists but {} class attribute is not exists in bean[{}]", + type, type, beanName); + throw new IllegalArgumentException(type + " method attribute exists but " + + type + " class attribute is not exists in bean[" + beanName + "]"); + } + Class[] args = new Class[] { HttpRequest.class, byte[].class, + ClientHttpRequestExecution.class, BlockException.class }; + String argsStr = Arrays.toString( + Arrays.stream(args).map(clazz -> clazz.getSimpleName()).toArray()); + Method foundMethod = ClassUtils.getStaticMethod(blockClass, blockMethod, args); + if (foundMethod == null) { + log.error( + "{} static method can not be found in bean[{}]. The right method signature is {}#{}{}, please check your class name, method name and arguments", + type, beanName, blockClass.getName(), blockMethod, argsStr); + throw new IllegalArgumentException(type + + " static method can not be found in bean[" + beanName + + "]. The right method signature is " + blockClass.getName() + "#" + + blockMethod + argsStr + + ", please check your class name, method name and arguments"); + } + + if (!ClientHttpResponse.class.isAssignableFrom(foundMethod.getReturnType())) { + log.error( + "{} method return value in bean[{}] is not ClientHttpResponse: {}#{}{}", + type, beanName, blockClass.getName(), blockMethod, argsStr); + throw new IllegalArgumentException(type + " method return value in bean[" + + beanName + "] is not ClientHttpResponse: " + blockClass.getName() + + "#" + blockMethod + argsStr); + } + if (type.equals(SentinelConstants.BLOCK_TYPE)) { + BlockClassRegistry.updateBlockHandlerFor(blockClass, blockMethod, + foundMethod); + } + else { + BlockClassRegistry.updateFallbackFor(blockClass, blockMethod, foundMethod); + } + } + + private boolean checkSentinelProtect(RootBeanDefinition beanDefinition, + Class beanType) { + return beanType == RestTemplate.class + && (checkStandardMethodMetadata(beanDefinition) + || checkMethodMetadataReadingVisitor(beanDefinition)); + } + + private boolean checkStandardMethodMetadata(RootBeanDefinition beanDefinition) { + return beanDefinition.getSource() instanceof StandardMethodMetadata + && ((StandardMethodMetadata) beanDefinition.getSource()) + .isAnnotated(SentinelRestTemplate.class.getName()); + } + + private boolean checkMethodMetadataReadingVisitor(RootBeanDefinition beanDefinition) { + return beanDefinition.getSource() instanceof MethodMetadataReadingVisitor + && ((MethodMetadataReadingVisitor) beanDefinition.getSource()) + .isAnnotated(SentinelRestTemplate.class.getName()); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + if (cache.containsKey(beanName)) { + // add interceptor for each RestTemplate with @SentinelRestTemplate annotation + StringBuilder interceptorBeanNamePrefix = new StringBuilder(); + SentinelRestTemplate sentinelRestTemplate = cache.get(beanName); + interceptorBeanNamePrefix + .append(StringUtils.uncapitalize( + SentinelProtectInterceptor.class.getSimpleName())) + .append("_") + .append(sentinelRestTemplate.blockHandlerClass().getSimpleName()) + .append(sentinelRestTemplate.blockHandler()).append("_") + .append(sentinelRestTemplate.fallbackClass().getSimpleName()) + .append(sentinelRestTemplate.fallback()); + RestTemplate restTemplate = (RestTemplate) bean; + String interceptorBeanName = interceptorBeanNamePrefix + "@" + + bean.toString(); + registerBean(interceptorBeanName, sentinelRestTemplate, (RestTemplate) bean); + SentinelProtectInterceptor sentinelProtectInterceptor = applicationContext + .getBean(interceptorBeanName, SentinelProtectInterceptor.class); + restTemplate.getInterceptors().add(0, sentinelProtectInterceptor); + } + return bean; + } + + private void registerBean(String interceptorBeanName, + SentinelRestTemplate sentinelRestTemplate, RestTemplate restTemplate) { + // register SentinelProtectInterceptor bean + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext + .getAutowireCapableBeanFactory(); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder + .genericBeanDefinition(SentinelProtectInterceptor.class); + beanDefinitionBuilder.addConstructorArgValue(sentinelRestTemplate); + beanDefinitionBuilder.addConstructorArgValue(restTemplate); + BeanDefinition interceptorBeanDefinition = beanDefinitionBuilder + .getRawBeanDefinition(); + beanFactory.registerBeanDefinition(interceptorBeanName, + interceptorBeanDefinition); + } + +} \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelCircuitBreakerConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelCircuitBreakerConfiguration.java new file mode 100644 index 00000000..526719bc --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelCircuitBreakerConfiguration.java @@ -0,0 +1,12 @@ +package com.alibaba.cloud.sentinel.custom; + +import org.springframework.context.annotation.Configuration; + +/** + * @author lengleng + *

+ * support @EnableCircuitBreaker ,Do nothing + */ +@Configuration +public class SentinelCircuitBreakerConfiguration { +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelDataSourceHandler.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelDataSourceHandler.java new file mode 100644 index 00000000..6c7d3f1d --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelDataSourceHandler.java @@ -0,0 +1,191 @@ +package com.alibaba.cloud.sentinel.custom; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.cloud.sentinel.datasource.config.AbstractDataSourceProperties; +import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter; +import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter; +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; + +/** + * Sentinel {@link ReadableDataSource} Handler Handle the configurations of + * 'spring.cloud.sentinel.datasource' + * + * @author Jim + * @see SentinelProperties#datasource + * @see JsonConverter + * @see XmlConverter + */ +public class SentinelDataSourceHandler implements SmartInitializingSingleton { + + private static final Logger log = LoggerFactory + .getLogger(SentinelDataSourceHandler.class); + + private List dataTypeList = Arrays.asList("json", "xml"); + + private final String DATA_TYPE_FIELD = "dataType"; + private final String CUSTOM_DATA_TYPE = "custom"; + private final String CONVERTER_CLASS_FIELD = "converterClass"; + + private final DefaultListableBeanFactory beanFactory; + + private final SentinelProperties sentinelProperties; + + private final Environment env; + + public SentinelDataSourceHandler(DefaultListableBeanFactory beanFactory, + SentinelProperties sentinelProperties, Environment env) { + this.beanFactory = beanFactory; + this.sentinelProperties = sentinelProperties; + this.env = env; + } + + @Override + public void afterSingletonsInstantiated() { + sentinelProperties.getDatasource() + .forEach((dataSourceName, dataSourceProperties) -> { + try { + List validFields = dataSourceProperties.getValidField(); + if (validFields.size() != 1) { + log.error("[Sentinel Starter] DataSource " + dataSourceName + + " multi datasource active and won't loaded: " + + dataSourceProperties.getValidField()); + return; + } + AbstractDataSourceProperties abstractDataSourceProperties = dataSourceProperties + .getValidDataSourceProperties(); + abstractDataSourceProperties.setEnv(env); + abstractDataSourceProperties.preCheck(dataSourceName); + registerBean(abstractDataSourceProperties, dataSourceName + + "-sentinel-" + validFields.get(0) + "-datasource"); + } + catch (Exception e) { + log.error("[Sentinel Starter] DataSource " + dataSourceName + + " build error: " + e.getMessage(), e); + } + }); + } + + private void registerBean(final AbstractDataSourceProperties dataSourceProperties, + String dataSourceName) { + + Map propertyMap = Arrays + .stream(dataSourceProperties.getClass().getDeclaredFields()) + .collect(HashMap::new, (m, v) -> { + try { + v.setAccessible(true); + m.put(v.getName(), v.get(dataSourceProperties)); + } + catch (IllegalAccessException e) { + log.error("[Sentinel Starter] DataSource " + dataSourceName + + " field: " + v.getName() + " invoke error"); + throw new RuntimeException( + "[Sentinel Starter] DataSource " + dataSourceName + + " field: " + v.getName() + " invoke error", + e); + } + }, HashMap::putAll); + propertyMap.put(CONVERTER_CLASS_FIELD, dataSourceProperties.getConverterClass()); + propertyMap.put(DATA_TYPE_FIELD, dataSourceProperties.getDataType()); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(dataSourceProperties.getFactoryBeanName()); + + propertyMap.forEach((propertyName, propertyValue) -> { + Field field = ReflectionUtils.findField(dataSourceProperties.getClass(), + propertyName); + if (null == field) { + return; + } + if (DATA_TYPE_FIELD.equals(propertyName)) { + String dataType = StringUtils.trimAllWhitespace(propertyValue.toString()); + if (CUSTOM_DATA_TYPE.equals(dataType)) { + try { + if (StringUtils + .isEmpty(dataSourceProperties.getConverterClass())) { + throw new RuntimeException("[Sentinel Starter] DataSource " + + dataSourceName + + "dataType is custom, please set converter-class " + + "property"); + } + // construct custom Converter with 'converterClass' + // configuration and register + String customConvertBeanName = "sentinel-" + + dataSourceProperties.getConverterClass(); + if (!this.beanFactory.containsBean(customConvertBeanName)) { + this.beanFactory.registerBeanDefinition(customConvertBeanName, + BeanDefinitionBuilder + .genericBeanDefinition( + Class.forName(dataSourceProperties + .getConverterClass())) + .getBeanDefinition()); + } + builder.addPropertyReference("converter", customConvertBeanName); + } + catch (ClassNotFoundException e) { + log.error("[Sentinel Starter] DataSource " + dataSourceName + + " handle " + + dataSourceProperties.getClass().getSimpleName() + + " error, class name: " + + dataSourceProperties.getConverterClass()); + throw new RuntimeException("[Sentinel Starter] DataSource " + + dataSourceName + " handle " + + dataSourceProperties.getClass().getSimpleName() + + " error, class name: " + + dataSourceProperties.getConverterClass(), e); + } + } + else { + if (!dataTypeList.contains( + StringUtils.trimAllWhitespace(propertyValue.toString()))) { + throw new RuntimeException("[Sentinel Starter] DataSource " + + dataSourceName + " dataType: " + propertyValue + + " is not support now. please using these types: " + + dataTypeList.toString()); + } + // converter type now support xml or json. + // The bean name of these converters wrapped by + // 'sentinel-{converterType}-{ruleType}-converter' + builder.addPropertyReference("converter", + "sentinel-" + propertyValue.toString() + "-" + + dataSourceProperties.getRuleType().getName() + + "-converter"); + } + } + else if (CONVERTER_CLASS_FIELD.equals(propertyName)) { + return; + } + else { + // wired properties + Optional.ofNullable(propertyValue) + .ifPresent(v -> builder.addPropertyValue(propertyName, v)); + } + }); + + this.beanFactory.registerBeanDefinition(dataSourceName, + builder.getBeanDefinition()); + // init in Spring + AbstractDataSource newDataSource = (AbstractDataSource) this.beanFactory + .getBean(dataSourceName); + + // register property in RuleManager + dataSourceProperties.postRegister(newDataSource); + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelProtectInterceptor.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelProtectInterceptor.java new file mode 100644 index 00000000..4594caef --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/custom/SentinelProtectInterceptor.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.custom; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate; +import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; + +/** + * Interceptor using by SentinelRestTemplate + * + * @author Jim + */ +public class SentinelProtectInterceptor implements ClientHttpRequestInterceptor { + + private final SentinelRestTemplate sentinelRestTemplate; + + private final RestTemplate restTemplate; + + public SentinelProtectInterceptor(SentinelRestTemplate sentinelRestTemplate, + RestTemplate restTemplate) { + this.sentinelRestTemplate = sentinelRestTemplate; + this.restTemplate = restTemplate; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + URI uri = request.getURI(); + String hostResource = request.getMethod().toString() + ":" + uri.getScheme() + + "://" + uri.getHost() + + (uri.getPort() == -1 ? "" : ":" + uri.getPort()); + String hostWithPathResource = hostResource + uri.getPath(); + boolean entryWithPath = true; + if (hostResource.equals(hostWithPathResource)) { + entryWithPath = false; + } + Entry hostEntry = null, hostWithPathEntry = null; + ClientHttpResponse response = null; + try { + hostEntry = SphU.entry(hostResource, EntryType.OUT); + if (entryWithPath) { + hostWithPathEntry = SphU.entry(hostWithPathResource, EntryType.OUT); + } + response = execution.execute(request, body); + if (this.restTemplate.getErrorHandler().hasError(response)) { + Tracer.trace( + new IllegalStateException("RestTemplate ErrorHandler has error")); + } + } + catch (Throwable e) { + if (!BlockException.isBlockException(e)) { + Tracer.trace(e); + } + else { + return handleBlockException(request, body, execution, (BlockException) e); + } + } + finally { + if (hostWithPathEntry != null) { + hostWithPathEntry.exit(); + } + if (hostEntry != null) { + hostEntry.exit(); + } + } + return response; + } + + private ClientHttpResponse handleBlockException(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex) { + Object[] args = new Object[] { request, body, execution, ex }; + // handle degrade + if (isDegradeFailure(ex)) { + Method fallbackMethod = extractFallbackMethod(sentinelRestTemplate.fallback(), + sentinelRestTemplate.fallbackClass()); + if (fallbackMethod != null) { + return methodInvoke(fallbackMethod, args); + } + else { + return new SentinelClientHttpResponse(); + } + } + // handle flow + Method blockHandler = extractBlockHandlerMethod( + sentinelRestTemplate.blockHandler(), + sentinelRestTemplate.blockHandlerClass()); + if (blockHandler != null) { + return methodInvoke(blockHandler, args); + } + else { + return new SentinelClientHttpResponse(); + } + } + + private ClientHttpResponse methodInvoke(Method method, Object... args) { + try { + return (ClientHttpResponse) method.invoke(null, args); + } + catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private Method extractFallbackMethod(String fallback, Class fallbackClass) { + return BlockClassRegistry.lookupFallback(fallbackClass, fallback); + } + + private Method extractBlockHandlerMethod(String block, Class blockClass) { + return BlockClassRegistry.lookupBlockHandler(blockClass, block); + } + + private boolean isDegradeFailure(BlockException ex) { + return ex instanceof DegradeException; + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpoint.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpoint.java new file mode 100644 index 00000000..3d523ebb --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpoint.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.endpoint; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.util.AppNameUtil; + +/** + * Endpoint for Sentinel, contains ans properties and rules + * @author xiaojing + */ +@Endpoint(id = "sentinel") +public class SentinelEndpoint { + + private final SentinelProperties sentinelProperties; + + public SentinelEndpoint(SentinelProperties sentinelProperties) { + this.sentinelProperties = sentinelProperties; + } + + @ReadOperation + public Map invoke() { + final Map result = new HashMap<>(); + if (sentinelProperties.isEnabled()) { + + result.put("appName", AppNameUtil.getAppName()); + result.put("logDir", LogBase.getLogBaseDir()); + result.put("logUsePid", LogBase.isLogNameUsePid()); + result.put("blockPage", WebServletConfig.getBlockPage()); + result.put("metricsFileSize", SentinelConfig.singleMetricFileSize()); + result.put("metricsFileCharset", SentinelConfig.charset()); + result.put("totalMetricsFileCount", SentinelConfig.totalMetricFileCount()); + result.put("consoleServer", TransportConfig.getConsoleServer()); + result.put("clientIp", TransportConfig.getHeartbeatClientIp()); + result.put("heartbeatIntervalMs", TransportConfig.getHeartbeatIntervalMs()); + result.put("clientPort", TransportConfig.getPort()); + result.put("coldFactor", sentinelProperties.getFlow().getColdFactor()); + result.put("filter", sentinelProperties.getFilter()); + result.put("datasource", sentinelProperties.getDatasource()); + + final Map rules = new HashMap<>(); + result.put("rules", rules); + rules.put("flowRules", FlowRuleManager.getRules()); + rules.put("degradeRules", DegradeRuleManager.getRules()); + rules.put("systemRules", SystemRuleManager.getRules()); + rules.put("authorityRule", AuthorityRuleManager.getRules()); + rules.put("paramFlowRule", ParamFlowRuleManager.getRules()); + } + return result; + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpointAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpointAutoConfiguration.java new file mode 100644 index 00000000..ded2f47c --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelEndpointAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.endpoint; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import com.alibaba.cloud.sentinel.SentinelProperties; + +/** + * @author hengyunabc + */ +@ConditionalOnClass(Endpoint.class) +@EnableConfigurationProperties({ SentinelProperties.class }) +public class SentinelEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public SentinelEndpoint sentinelEndPoint(SentinelProperties sentinelProperties) { + return new SentinelEndpoint(sentinelProperties); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledHealthIndicator("sentinel") + public SentinelHealthIndicator sentinelHealthIndicator( + DefaultListableBeanFactory beanFactory, + SentinelProperties sentinelProperties) { + return new SentinelHealthIndicator(beanFactory, sentinelProperties); + } +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicator.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicator.java new file mode 100644 index 00000000..08864b8b --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicator.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.endpoint; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.heartbeat.HeartbeatSenderProvider; +import com.alibaba.csp.sentinel.transport.HeartbeatSender; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; + +/** + * A {@link HealthIndicator} for Sentinel, which checks the status of Sentinel Dashboard + * and DataSource. + * + *

+ * Check the status of Sentinel Dashboard by sending a heartbeat message to it. If return + * true, it's OK. + * + * Check the status of Sentinel DataSource by calling loadConfig method of + * {@link AbstractDataSource}. If no Exception thrown, it's OK. + * + * If Dashboard and DataSource are both OK, the health status is UP. + *

+ * + *

+ * Note: If Sentinel isn't enabled, the health status is up. If Sentinel Dashboard isn't + * configured, it's OK and mark the status of Dashboard with UNKNOWN. More informations + * are provided in details. + *

+ * + * @author cdfive + */ +public class SentinelHealthIndicator extends AbstractHealthIndicator { + + private DefaultListableBeanFactory beanFactory; + + private SentinelProperties sentinelProperties; + + public SentinelHealthIndicator(DefaultListableBeanFactory beanFactory, + SentinelProperties sentinelProperties) { + this.beanFactory = beanFactory; + this.sentinelProperties = sentinelProperties; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + Map detailMap = new HashMap<>(); + + // If sentinel isn't enabled, set the status up and set the enabled to false in + // detail + if (!sentinelProperties.isEnabled()) { + detailMap.put("enabled", false); + builder.up().withDetails(detailMap); + return; + } + + detailMap.put("enabled", true); + + // Check health of Dashboard + boolean dashboardUp = true; + String consoleServer = TransportConfig.getConsoleServer(); + if (StringUtils.isEmpty(consoleServer)) { + // If Dashboard isn't configured, it's OK and mark the status of Dashboard + // with UNKNOWN. + detailMap.put("dashboard", + new Status(Status.UNKNOWN.getCode(), "dashboard isn't configured")); + } + else { + // If Dashboard is configured, send a heartbeat message to it and check the + // result + HeartbeatSender heartbeatSender = HeartbeatSenderProvider + .getHeartbeatSender(); + boolean result = heartbeatSender.sendHeartbeat(); + if (result) { + detailMap.put("dashboard", Status.UP); + } + else { + // If failed to send heartbeat message, means that the Dashboard is DOWN + dashboardUp = false; + detailMap.put("dashboard", new Status(Status.DOWN.getCode(), + consoleServer + " can't be connected")); + } + } + + // Check health of DataSource + boolean dataSourceUp = true; + Map dataSourceDetailMap = new HashMap<>(); + detailMap.put("dataSource", dataSourceDetailMap); + + // Get all DataSources and each call loadConfig to check if it's OK + // If no Exception thrown, it's OK + // Note: + // Even if the dynamic config center is down, the loadConfig() might return + // successfully + // e.g. for Nacos client, it might retrieve from the local cache) + // But in most circumstances it's okay + Map dataSourceMap = beanFactory + .getBeansOfType(AbstractDataSource.class); + for (Map.Entry dataSourceMapEntry : dataSourceMap + .entrySet()) { + String dataSourceBeanName = dataSourceMapEntry.getKey(); + AbstractDataSource dataSource = dataSourceMapEntry.getValue(); + try { + dataSource.loadConfig(); + dataSourceDetailMap.put(dataSourceBeanName, Status.UP); + } + catch (Exception e) { + // If one DataSource failed to loadConfig, means that the DataSource is + // DOWN + dataSourceUp = false; + dataSourceDetailMap.put(dataSourceBeanName, + new Status(Status.DOWN.getCode(), e.getMessage())); + } + } + + // If Dashboard and DataSource are both OK, the health status is UP + if (dashboardUp && dataSourceUp) { + builder.up().withDetails(detailMap); + } + else { + builder.down().withDetails(detailMap); + } + } +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelContractHolder.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelContractHolder.java new file mode 100644 index 00000000..b83fbfd9 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelContractHolder.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.feign; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import feign.Contract; +import feign.MethodMetadata; + +/** + * + * Using static field {@link SentinelContractHolder#METADATA_MAP} to hold + * {@link MethodMetadata} data + * + * @author Jim + */ +public class SentinelContractHolder implements Contract { + + private final Contract delegate; + + /** + * map key is constructed by ClassFullName + configKey. configKey is constructed by + * {@link feign.Feign#configKey} + */ + public final static Map METADATA_MAP = new HashMap<>(); + + public SentinelContractHolder(Contract delegate) { + this.delegate = delegate; + } + + @Override + public List parseAndValidatateMetadata(Class targetType) { + List metadatas = delegate.parseAndValidatateMetadata(targetType); + metadatas.forEach(metadata -> METADATA_MAP + .put(targetType.getName() + metadata.configKey(), metadata)); + return metadatas; + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeign.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeign.java new file mode 100644 index 00000000..b869f844 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeign.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.feign; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.ReflectionUtils; + +import feign.Contract; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.Target; +import feign.hystrix.FallbackFactory; +import feign.hystrix.HystrixFeign; + +/** + * {@link Feign.Builder} like {@link HystrixFeign.Builder} + * + * @author Jim + */ +public class SentinelFeign { + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends Feign.Builder + implements ApplicationContextAware { + + private Contract contract = new Contract.Default(); + + private ApplicationContext applicationContext; + + private FeignContext feignContext; + + @Override + public Feign.Builder invocationHandlerFactory( + InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + @Override + public Feign build() { + super.invocationHandlerFactory(new InvocationHandlerFactory() { + @Override + public InvocationHandler create(Target target, + Map dispatch) { + // using reflect get fallback and fallbackFactory properties from + // FeignClientFactoryBean because FeignClientFactoryBean is a package + // level class, we can not use it in our package + Object feignClientFactoryBean = Builder.this.applicationContext + .getBean("&" + target.type().getName()); + + Class fallback = (Class) getFieldValue(feignClientFactoryBean, + "fallback"); + Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean, + "fallbackFactory"); + String name = (String) getFieldValue(feignClientFactoryBean, "name"); + + Object fallbackInstance; + FallbackFactory fallbackFactoryInstance; + // check fallback and fallbackFactory properties + if (void.class != fallback) { + fallbackInstance = getFromContext(name, "fallback", fallback, + target.type()); + return new SentinelInvocationHandler(target, dispatch, + new FallbackFactory.Default(fallbackInstance)); + } + if (void.class != fallbackFactory) { + fallbackFactoryInstance = (FallbackFactory) getFromContext(name, + "fallbackFactory", fallbackFactory, + FallbackFactory.class); + return new SentinelInvocationHandler(target, dispatch, + fallbackFactoryInstance); + } + return new SentinelInvocationHandler(target, dispatch); + } + + private Object getFromContext(String name, String type, + Class fallbackType, Class targetType) { + Object fallbackInstance = feignContext.getInstance(name, + fallbackType); + if (fallbackInstance == null) { + throw new IllegalStateException(String.format( + "No %s instance of type %s found for feign client %s", + type, fallbackType, name)); + } + + if (!targetType.isAssignableFrom(fallbackType)) { + throw new IllegalStateException(String.format( + "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + type, fallbackType, targetType, name)); + } + return fallbackInstance; + } + }); + + super.contract(new SentinelContractHolder(contract)); + return super.build(); + } + + private Object getFieldValue(Object instance, String fieldName) { + Field field = ReflectionUtils.findField(instance.getClass(), fieldName); + field.setAccessible(true); + try { + return field.get(instance); + } + catch (IllegalAccessException e) { + // ignore + } + return null; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + feignContext = this.applicationContext.getBean(FeignContext.class); + } + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeignAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeignAutoConfiguration.java new file mode 100644 index 00000000..9989c996 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelFeignAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.feign; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import com.alibaba.csp.sentinel.SphU; + +import feign.Feign; + +/** + * @author Jim + */ +@Configuration +@ConditionalOnClass({ SphU.class, Feign.class }) +public class SentinelFeignAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "feign.sentinel.enabled") + public Feign.Builder feignSentinelBuilder() { + return SentinelFeign.builder(); + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelInvocationHandler.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelInvocationHandler.java new file mode 100644 index 00000000..df68afb2 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/feign/SentinelInvocationHandler.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.feign; + +import static feign.Util.checkNotNull; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import feign.Feign; +import feign.InvocationHandlerFactory.MethodHandler; +import feign.MethodMetadata; +import feign.Target; +import feign.hystrix.FallbackFactory; + +/** + * {@link InvocationHandler} handle invocation that protected by Sentinel + * + * @author Jim + */ +public class SentinelInvocationHandler implements InvocationHandler { + + private final Target target; + private final Map dispatch; + + private FallbackFactory fallbackFactory; + private Map fallbackMethodMap; + + SentinelInvocationHandler(Target target, Map dispatch, + FallbackFactory fallbackFactory) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + this.fallbackFactory = fallbackFactory; + this.fallbackMethodMap = toFallbackMethod(dispatch); + } + + SentinelInvocationHandler(Target target, Map dispatch) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + if ("equals".equals(method.getName())) { + try { + Object otherHandler = args.length > 0 && args[0] != null + ? Proxy.getInvocationHandler(args[0]) + : null; + return equals(otherHandler); + } + catch (IllegalArgumentException e) { + return false; + } + } + else if ("hashCode".equals(method.getName())) { + return hashCode(); + } + else if ("toString".equals(method.getName())) { + return toString(); + } + + Object result; + MethodHandler methodHandler = this.dispatch.get(method); + // only handle by HardCodedTarget + if (target instanceof Target.HardCodedTarget) { + Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target; + MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP + .get(hardCodedTarget.type().getName() + + Feign.configKey(hardCodedTarget.type(), method)); + // resource default is HttpMethod:protocol://url + String resourceName = methodMetadata.template().method().toUpperCase() + ":" + + hardCodedTarget.url() + methodMetadata.template().path(); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.OUT, 1, args); + result = methodHandler.invoke(args); + } + catch (Throwable ex) { + // fallback handle + if (!BlockException.isBlockException(ex)) { + Tracer.trace(ex); + } + if (fallbackFactory != null) { + try { + Object fallbackResult = fallbackMethodMap.get(method) + .invoke(fallbackFactory.create(ex), args); + return fallbackResult; + } + catch (IllegalAccessException e) { + // shouldn't happen as method is public due to being an interface + throw new AssertionError(e); + } + catch (InvocationTargetException e) { + throw new AssertionError(e.getCause()); + } + } + else { + // throw exception if fallbackFactory is null + throw ex; + } + } + finally { + if (entry != null) { + entry.exit(1, args); + } + ContextUtil.exit(); + } + } + else { + // other target type using default strategy + result = methodHandler.invoke(args); + } + + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SentinelInvocationHandler) { + SentinelInvocationHandler other = (SentinelInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + static Map toFallbackMethod(Map dispatch) { + Map result = new LinkedHashMap<>(); + for (Method method : dispatch.keySet()) { + method.setAccessible(true); + result.put(method, method); + } + return result; + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/rest/SentinelClientHttpResponse.java b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/rest/SentinelClientHttpResponse.java new file mode 100644 index 00000000..a38084e3 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/java/com/alibaba/cloud/sentinel/rest/SentinelClientHttpResponse.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.rest; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.AbstractClientHttpResponse; + +import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate; +import com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor; + +/** + * Using by {@link SentinelRestTemplate} and {@link SentinelProtectInterceptor} + * + * @author Jim + */ +public class SentinelClientHttpResponse extends AbstractClientHttpResponse { + + private String blockResponse = "RestTemplate request block by sentinel"; + + public SentinelClientHttpResponse() { + } + + public SentinelClientHttpResponse(String blockResponse) { + this.blockResponse = blockResponse; + } + + @Override + public int getRawStatusCode() throws IOException { + return HttpStatus.OK.value(); + } + + @Override + public String getStatusText() throws IOException { + return blockResponse; + } + + @Override + public void close() { + // nothing do + } + + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(blockResponse.getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + Map> headers = new HashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, + Arrays.asList(MediaType.APPLICATION_JSON_UTF8_VALUE)); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(headers); + return httpHeaders; + } +} diff --git a/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..ec4c63d7 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,105 @@ +{ + "properties": [ + { + "name": "spring.cloud.sentinel.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "enable or disable sentinel auto configure." + }, + { + "name": "resttemplate.sentinel.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "enable or disable @SentinelRestTemplate." + }, + { + "name": "spring.cloud.sentinel.eager", + "type": "java.lang.Boolean", + "defaultValue": false, + "description": "earlier initialize heart-beat when the spring container starts when the transport dependency is on classpath, the configuration is effective." + }, + { + "name": "spring.cloud.sentinel.transport.port", + "type": "java.lang.String", + "defaultValue": "8719", + "description": "sentinel api port." + }, + { + "name": "spring.cloud.sentinel.transport.clientIp", + "type": "java.lang.String", + "description": "sentinel client ip connect to dashboard." + }, + { + "name": "spring.cloud.sentinel.transport.dashboard", + "type": "java.lang.String", + "description": "sentinel dashboard address, won't try to connect dashboard when address is empty." + }, + { + "name": "spring.cloud.sentinel.transport.heartbeatIntervalMs", + "type": "java.lang.String", + "description": "send heartbeat interval millisecond." + }, + { + "name": "spring.cloud.sentinel.filter.order", + "type": "java.lang.Integer", + "defaultValue": "Integer.MIN_VALUE", + "description": "sentinel filter chain order, will be set to FilterRegistrationBean." + }, + { + "name": "spring.cloud.sentinel.filter.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "Enable to instance com.alibaba.csp.sentinel.adapter.servlet.CommonFilter." + }, + { + "name": "spring.cloud.sentinel.metric.charset", + "type": "java.lang.String", + "defaultValue": "UTF-8", + "description": "charset when sentinel write or search metric file." + }, + { + "name": "spring.cloud.sentinel.metric.fileSingleSize", + "type": "java.lang.String", + "description": "the metric file size." + }, + { + "name": "spring.cloud.sentinel.metric.fileTotalCount", + "type": "java.lang.String", + "description": "the total metric file count." + }, + { + "name": "spring.cloud.sentinel.log.dir", + "type": "java.lang.String", + "description": "log base directory." + }, + { + "name": "spring.cloud.sentinel.log.switch-pid", + "type": "java.lang.Boolean", + "defaultValue": false, + "description": "log file should with pid." + }, + { + "name": "spring.cloud.sentinel.servlet.blockPage", + "type": "java.lang.String", + "description": "the process page when the flow control is triggered." + }, + { + "name": "spring.cloud.sentinel.flow.coldFactor", + "type": "java.lang.String", + "defaultValue": "3", + "description": "sentinel the cold factor." + }, + { + "name": "management.health.sentinel.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable sentinel health check.", + "defaultValue": true + }, + { + "defaultValue": "false", + "name": "feign.sentinel.enabled", + "description": "If true, an OpenFeign client will be wrapped with a Sentinel circuit breaker.", + "type": "java.lang.Boolean" + } + ] +} diff --git a/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..9ce988d8 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories @@ -0,0 +1,9 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\ +com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\ +com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\ +com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\ +com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration + +org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\ +com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelAutoConfigurationTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelAutoConfigurationTests.java new file mode 100644 index 00000000..bc60fa98 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelAutoConfigurationTests.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.util.Arrays; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate; +import com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration; +import com.alibaba.cloud.sentinel.custom.SentinelBeanPostProcessor; +import com.alibaba.cloud.sentinel.endpoint.SentinelEndpoint; +import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; + +/** + * @author Jim + * @author jiashuai.xie + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { + SentinelAutoConfigurationTests.TestConfig.class }, properties = { + "spring.cloud.sentinel.filter.order=123", + "spring.cloud.sentinel.filter.urlPatterns=/*,/test", + "spring.cloud.sentinel.metric.fileSingleSize=9999", + "spring.cloud.sentinel.metric.fileTotalCount=100", + "spring.cloud.sentinel.servlet.blockPage=/error", + "spring.cloud.sentinel.flow.coldFactor=3", + "spring.cloud.sentinel.eager=true", + "spring.cloud.sentinel.log.switchPid=true", + "spring.cloud.sentinel.transport.dashboard=http://localhost:8080", + "spring.cloud.sentinel.transport.port=9999", + "spring.cloud.sentinel.transport.clientIp=1.1.1.1", + "spring.cloud.sentinel.transport.heartbeatIntervalMs=20000" }, webEnvironment = RANDOM_PORT) +public class SentinelAutoConfigurationTests { + + @Autowired + private SentinelProperties sentinelProperties; + + @Autowired + private FilterRegistrationBean filterRegistrationBean; + + @Autowired + private SentinelBeanPostProcessor sentinelBeanPostProcessor; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private RestTemplate restTemplateWithBlockClass; + + @Autowired + private RestTemplate restTemplateWithoutBlockClass; + + @Autowired + private RestTemplate restTemplateWithFallbackClass; + + @LocalServerPort + private int port; + + private String flowUrl = "http://localhost:" + port + "/flow"; + + private String degradeUrl = "http://localhost:" + port + "/degrade"; + + @Before + public void setUp() { + FlowRule rule = new FlowRule(); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setResource("GET:" + flowUrl); + rule.setLimitApp("default"); + rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + rule.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRuleManager.loadRules(Arrays.asList(rule)); + + DegradeRule degradeRule = new DegradeRule(); + degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); + degradeRule.setResource("GET:" + degradeUrl); + degradeRule.setCount(0); + degradeRule.setTimeWindow(60); + DegradeRuleManager.loadRules(Arrays.asList(degradeRule)); + } + + @Test + public void contextLoads() throws Exception { + assertNotNull("FilterRegistrationBean was not created", filterRegistrationBean); + assertNotNull("SentinelProperties was not created", sentinelProperties); + assertNotNull("SentinelBeanPostProcessor was not created", + sentinelBeanPostProcessor); + + checkSentinelLog(); + checkSentinelEager(); + checkSentinelTransport(); + checkSentinelColdFactor(); + checkSentinelMetric(); + checkSentinelFilter(); + checkEndpoint(); + } + + private void checkEndpoint() { + SentinelEndpoint sentinelEndpoint = new SentinelEndpoint(sentinelProperties); + Map map = sentinelEndpoint.invoke(); + assertEquals("Endpoint Sentinel log pid was wrong", true, map.get("logUsePid")); + assertEquals("Endpoint Sentinel transport console server was wrong", + "http://localhost:8080", map.get("consoleServer")); + assertEquals("Endpoint Sentinel transport port was wrong", "9999", + map.get("clientPort")); + assertEquals("Endpoint Sentinel transport heartbeatIntervalMs was wrong", 20000l, + map.get("heartbeatIntervalMs")); + assertEquals("Endpoint Sentinel transport clientIp was wrong", "1.1.1.1", + map.get("clientIp")); + assertEquals("Endpoint Sentinel metric file size was wrong", 9999l, + map.get("metricsFileSize")); + assertEquals("Endpoint Sentinel metric file count was wrong", 100, + map.get("totalMetricsFileCount")); + assertEquals("Endpoint Sentinel metric file charset was wrong", "UTF-8", + map.get("metricsFileCharset")); + assertEquals("Endpoint Sentinel block page was wrong", "/error", + map.get("blockPage")); + } + + private void checkSentinelFilter() { + assertEquals("SentinelProperties filter order was wrong", 123, + sentinelProperties.getFilter().getOrder()); + assertEquals("SentinelProperties filter url pattern size was wrong", 2, + sentinelProperties.getFilter().getUrlPatterns().size()); + assertEquals("SentinelProperties filter url pattern item was wrong", "/*", + sentinelProperties.getFilter().getUrlPatterns().get(0)); + assertEquals("SentinelProperties filter url pattern item was wrong", "/test", + sentinelProperties.getFilter().getUrlPatterns().get(1)); + } + + private void checkSentinelMetric() { + assertEquals("SentinelProperties metric charset was wrong", "UTF-8", + sentinelProperties.getMetric().getCharset()); + assertEquals("SentinelProperties metric file single size was wrong", "9999", + sentinelProperties.getMetric().getFileSingleSize()); + assertEquals("SentinelProperties metric file total count was wrong", "100", + sentinelProperties.getMetric().getFileTotalCount()); + } + + private void checkSentinelColdFactor() { + assertEquals("SentinelProperties coldFactor was wrong", "3", + sentinelProperties.getFlow().getColdFactor()); + } + + private void checkSentinelTransport() { + assertEquals("SentinelProperties transport port was wrong", "9999", + sentinelProperties.getTransport().getPort()); + assertEquals("SentinelProperties transport dashboard was wrong", + "http://localhost:8080", + sentinelProperties.getTransport().getDashboard()); + assertEquals("SentinelProperties transport clientIp was wrong", "1.1.1.1", + sentinelProperties.getTransport().getClientIp()); + assertEquals("SentinelProperties transport heartbeatIntervalMs was wrong", + "20000", sentinelProperties.getTransport().getHeartbeatIntervalMs()); + } + + private void checkSentinelEager() { + assertEquals("SentinelProperties eager was wrong", true, + sentinelProperties.isEager()); + } + + private void checkSentinelLog() { + assertEquals("SentinelProperties log file pid was wrong", true, + sentinelProperties.getLog().isSwitchPid()); + } + + @Test + public void testFilter() { + assertEquals("Sentinel Filter order was wrong", filterRegistrationBean.getOrder(), + 123); + assertEquals("Sentinel Filter url-pattern was wrong", + filterRegistrationBean.getUrlPatterns().size(), 2); + } + + @Test + public void testSentinelSystemProperties() { + assertEquals("Sentinel log pid was wrong", true, LogBase.isLogNameUsePid()); + assertEquals("Sentinel transport console server was wrong", + "http://localhost:8080", TransportConfig.getConsoleServer()); + assertEquals("Sentinel transport port was wrong", "9999", + TransportConfig.getPort()); + assertEquals("Sentinel transport heartbeatIntervalMs was wrong", 20000l, + TransportConfig.getHeartbeatIntervalMs().longValue()); + assertEquals("Sentinel transport clientIp was wrong", "1.1.1.1", + TransportConfig.getHeartbeatClientIp()); + assertEquals("Sentinel metric file size was wrong", 9999, + SentinelConfig.singleMetricFileSize()); + assertEquals("Sentinel metric file count was wrong", 100, + SentinelConfig.totalMetricFileCount()); + assertEquals("Sentinel metric file charset was wrong", "UTF-8", + SentinelConfig.charset()); + assertEquals("Sentinel block page was wrong", "/error", + WebServletConfig.getBlockPage()); + } + + @Test + public void testFlowRestTemplate() { + assertEquals("RestTemplate interceptors size was wrong", 2, + restTemplate.getInterceptors().size()); + assertEquals("RestTemplateWithBlockClass interceptors size was wrong", 1, + restTemplateWithBlockClass.getInterceptors().size()); + ResponseEntity responseEntityBlock = restTemplateWithBlockClass + .getForEntity(flowUrl, String.class); + assertEquals("RestTemplateWithBlockClass Sentinel Block Message was wrong", + "Oops", responseEntityBlock.getBody()); + assertEquals( + "RestTemplateWithBlockClass Sentinel Block Http Status Code was wrong", + HttpStatus.OK, responseEntityBlock.getStatusCode()); + ResponseEntity responseEntityRaw = restTemplate.getForEntity(flowUrl, + String.class); + assertEquals("RestTemplate Sentinel Block Message was wrong", + "RestTemplate request block by sentinel", responseEntityRaw.getBody()); + assertEquals("RestTemplate Sentinel Block Http Status Code was wrong", + HttpStatus.OK, responseEntityRaw.getStatusCode()); + } + + @Test + public void testNormalRestTemplate() { + assertEquals("RestTemplateWithoutBlockClass interceptors size was wrong", 0, + restTemplateWithoutBlockClass.getInterceptors().size()); + assertThatExceptionOfType(RestClientException.class).isThrownBy(() -> { + restTemplateWithoutBlockClass.getForEntity(flowUrl, String.class); + }); + } + + @Test + public void testFallbackRestTemplate() { + ResponseEntity responseEntity = restTemplateWithFallbackClass + .getForEntity(degradeUrl, String.class); + assertEquals("RestTemplateWithFallbackClass Sentinel Message was wrong", + "Oops fallback", responseEntity.getBody()); + assertEquals("RestTemplateWithFallbackClass Sentinel Http Status Code was wrong", + HttpStatus.OK, responseEntity.getStatusCode()); + } + + @Configuration + static class SentinelTestConfiguration { + + @Bean + @SentinelRestTemplate + RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(mock(ClientHttpRequestInterceptor.class)); + return restTemplate; + } + + @Bean + @SentinelRestTemplate(blockHandlerClass = ExceptionUtil.class, blockHandler = "handleException") + RestTemplate restTemplateWithBlockClass() { + return new RestTemplate(); + } + + @Bean + @SentinelRestTemplate(fallbackClass = ExceptionUtil.class, fallback = "fallbackException") + RestTemplate restTemplateWithFallbackClass() { + return new RestTemplate(); + } + + @Bean + RestTemplate restTemplateWithoutBlockClass() { + return new RestTemplate(); + } + + } + + public static class ExceptionUtil { + public static SentinelClientHttpResponse handleException(HttpRequest request, + byte[] body, ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + return new SentinelClientHttpResponse("Oops"); + } + + public static SentinelClientHttpResponse fallbackException(HttpRequest request, + byte[] body, ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + return new SentinelClientHttpResponse("Oops fallback"); + } + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ SentinelAutoConfiguration.class, + SentinelWebAutoConfiguration.class, SentinelTestConfiguration.class }) + public static class TestConfig { + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelBeanAutowiredTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelBeanAutowiredTests.java new file mode 100644 index 00000000..ef8c67e3 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelBeanAutowiredTests.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration; +import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * @author Jim + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { SentinelBeanAutowiredTests.TestConfig.class }, properties = { + "spring.cloud.sentinel.filter.order=111" }) +public class SentinelBeanAutowiredTests { + + @Autowired + private UrlCleaner urlCleaner; + + @Autowired + private UrlBlockHandler urlBlockHandler; + + @Autowired + private RequestOriginParser requestOriginParser; + + @Autowired + private SentinelProperties sentinelProperties; + + @Test + public void contextLoads() throws Exception { + assertNotNull("UrlCleaner was not created", urlCleaner); + assertNotNull("UrlBlockHandler was not created", urlBlockHandler); + assertNotNull("RequestOriginParser was not created", requestOriginParser); + assertNotNull("SentinelProperties was not created", sentinelProperties); + + checkUrlPattern(); + } + + private void checkUrlPattern() { + assertEquals("SentinelProperties filter order was wrong", 111, + sentinelProperties.getFilter().getOrder()); + assertEquals("SentinelProperties filter url pattern size was wrong", 1, + sentinelProperties.getFilter().getUrlPatterns().size()); + assertEquals("SentinelProperties filter url pattern was wrong", "/*", + sentinelProperties.getFilter().getUrlPatterns().get(0)); + } + + @Test + public void testBeanAutowired() { + assertEquals("UrlCleaner was not autowired", urlCleaner, + WebCallbackManager.getUrlCleaner()); + assertEquals("UrlBlockHandler was not autowired", urlBlockHandler, + WebCallbackManager.getUrlBlockHandler()); + assertEquals("RequestOriginParser was not autowired", requestOriginParser, + WebCallbackManager.getRequestOriginParser()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ SentinelAutoConfiguration.class, + SentinelWebAutoConfiguration.class }) + public static class TestConfig { + + @Bean + public UrlCleaner urlCleaner() { + return new UrlCleaner() { + @Override + public String clean(String s) { + return s; + } + }; + } + + @Bean + public RequestOriginParser requestOriginParser() { + return new RequestOriginParser() { + @Override + public String parseOrigin(HttpServletRequest httpServletRequest) { + return httpServletRequest.getRemoteAddr(); + } + }; + } + + @Bean + public UrlBlockHandler urlBlockHandler() { + return new UrlBlockHandler() { + @Override + public void blocked(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, BlockException e) + throws IOException { + FilterUtil.blockRequest(httpServletRequest, httpServletResponse); + } + }; + } + + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelDataSourceTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelDataSourceTests.java new file mode 100644 index 00000000..40446485 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelDataSourceTests.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration; +import com.alibaba.cloud.sentinel.datasource.RuleType; + +/** + * @author Jim + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { SentinelDataSourceTests.TestConfig.class }, properties = { + "spring.cloud.sentinel.datasource.ds1.file.file=classpath: flowrule.json", + "spring.cloud.sentinel.datasource.ds1.file.data-type=json", + "spring.cloud.sentinel.datasource.ds1.file.rule-type=flow", + + "spring.cloud.sentinel.datasource.ds2.file.file=classpath: degraderule.json", + "spring.cloud.sentinel.datasource.ds2.file.data-type=json", + "spring.cloud.sentinel.datasource.ds2.file.rule-type=degrade", + + "spring.cloud.sentinel.datasource.ds3.file.file=classpath: authority.json", + "spring.cloud.sentinel.datasource.ds3.file.rule-type=authority", + + "spring.cloud.sentinel.datasource.ds4.file.file=classpath: system.json", + "spring.cloud.sentinel.datasource.ds4.file.rule-type=system", + + "spring.cloud.sentinel.datasource.ds5.file.file=classpath: param-flow.json", + "spring.cloud.sentinel.datasource.ds5.file.data-type=custom", + "spring.cloud.sentinel.datasource.ds5.file.converter-class=TestConverter", + "spring.cloud.sentinel.datasource.ds5.file.rule-type=param-flow" }) +public class SentinelDataSourceTests { + + @Autowired + private SentinelProperties sentinelProperties; + + @Test + public void contextLoads() throws Exception { + assertNotNull("SentinelProperties was not created", sentinelProperties); + + checkUrlPattern(); + } + + private void checkUrlPattern() { + assertEquals("SentinelProperties filter order was wrong", Integer.MIN_VALUE, + sentinelProperties.getFilter().getOrder()); + assertEquals("SentinelProperties filter url pattern size was wrong", 1, + sentinelProperties.getFilter().getUrlPatterns().size()); + assertEquals("SentinelProperties filter url pattern was wrong", "/*", + sentinelProperties.getFilter().getUrlPatterns().get(0)); + } + + @Test + public void testDataSource() { + assertEquals("DataSource size was wrong", 5, + sentinelProperties.getDatasource().size()); + assertNull("DataSource ds1 apollo is not null", + sentinelProperties.getDatasource().get("ds1").getApollo()); + assertNull("DataSource ds1 nacos is not null", + sentinelProperties.getDatasource().get("ds1").getNacos()); + assertNull("DataSource ds1 zk is not null", + sentinelProperties.getDatasource().get("ds1").getZk()); + assertNotNull("DataSource ds1 file is null", + sentinelProperties.getDatasource().get("ds1").getFile()); + + assertEquals("DataSource ds1 file dataType was wrong", "json", + sentinelProperties.getDatasource().get("ds1").getFile().getDataType()); + assertEquals("DataSource ds1 file ruleType was wrong", RuleType.FLOW, + sentinelProperties.getDatasource().get("ds1").getFile().getRuleType()); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ SentinelAutoConfiguration.class, + SentinelWebAutoConfiguration.class }) + public static class TestConfig { + + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelFeignTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelFeignTests.java new file mode 100644 index 00000000..92e17bea --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelFeignTests.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +/** + * @author Jim + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { SentinelFeignTests.TestConfig.class }, properties = { + "feign.sentinel.enabled=true" }) +public class SentinelFeignTests { + + @Autowired + private EchoService echoService; + + @Autowired + private FooService fooService; + + @Autowired + private BarService barService; + + @Autowired + private BazService bazService; + + @Before + public void setUp() { + FlowRule rule1 = new FlowRule(); + rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule1.setCount(0); + rule1.setResource("GET:http://test-service/echo/{str}"); + rule1.setLimitApp("default"); + rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + rule1.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRule rule2 = new FlowRule(); + rule2.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule2.setCount(0); + rule2.setResource("GET:http://foo-service/echo/{str}"); + rule2.setLimitApp("default"); + rule2.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + rule2.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRule rule3 = new FlowRule(); + rule3.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule3.setCount(0); + rule3.setResource("GET:http://bar-service/bar"); + rule3.setLimitApp("default"); + rule3.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + rule3.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRule rule4 = new FlowRule(); + rule4.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule4.setCount(0); + rule4.setResource("GET:http://baz-service/baz"); + rule4.setLimitApp("default"); + rule4.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + rule4.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRuleManager.loadRules(Arrays.asList(rule1, rule2, rule3, rule4)); + } + + @Test + public void contextLoads() throws Exception { + assertNotNull("EchoService was not created", echoService); + assertNotNull("FooService was not created", fooService); + } + + @Test + public void testFeignClient() { + assertEquals("Sentinel Feign Client fallback success", "echo fallback", + echoService.echo("test")); + assertEquals("Sentinel Feign Client fallbackFactory success", "foo fallback", + fooService.echo("test")); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + barService.bar(); + }); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + bazService.baz(); + }); + + assertNotEquals("ToString method invoke was not in SentinelInvocationHandler", + echoService.toString(), fooService.toString()); + assertNotEquals("HashCode method invoke was not in SentinelInvocationHandler", + echoService.hashCode(), fooService.hashCode()); + assertFalse("Equals method invoke was not in SentinelInvocationHandler", + echoService.equals(fooService)); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ SentinelFeignAutoConfiguration.class }) + @EnableFeignClients + public static class TestConfig { + + @Bean + public EchoServiceFallback echoServiceFallback() { + return new EchoServiceFallback(); + } + + @Bean + public CustomFallbackFactory customFallbackFactory() { + return new CustomFallbackFactory(); + } + + } + + @FeignClient(value = "test-service", fallback = EchoServiceFallback.class) + public interface EchoService { + @RequestMapping(path = "echo/{str}") + String echo(@RequestParam("str") String param); + } + + @FeignClient(value = "foo-service", fallbackFactory = CustomFallbackFactory.class) + public interface FooService { + @RequestMapping(path = "echo/{str}") + String echo(@RequestParam("str") String param); + } + + @FeignClient(value = "bar-service") + public interface BarService { + @RequestMapping(path = "bar") + String bar(); + } + + public interface BazService { + @RequestMapping(path = "baz") + String baz(); + } + + @FeignClient(value = "baz-service") + public interface BazClient extends BazService { + } + + public static class EchoServiceFallback implements EchoService { + + @Override + public String echo(@RequestParam("str") String param) { + return "echo fallback"; + } + + } + + public static class FooServiceFallback implements FooService { + + @Override + public String echo(@RequestParam("str") String param) { + return "foo fallback"; + } + } + + public static class CustomFallbackFactory + implements feign.hystrix.FallbackFactory { + + private FooService fooService = new FooServiceFallback(); + + @Override + public FooService create(Throwable throwable) { + return fooService; + } + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelRestTemplateTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelRestTemplateTests.java new file mode 100644 index 00000000..d05624fb --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/SentinelRestTemplateTests.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate; +import com.alibaba.cloud.sentinel.custom.SentinelBeanPostProcessor; +import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * @author Jim + */ +public class SentinelRestTemplateTests { + + @Test(expected = BeanCreationException.class) + public void testFbkMethod() { + new AnnotationConfigApplicationContext(TestConfig1.class); + } + + @Test(expected = BeanCreationException.class) + public void testFbkClass() { + new AnnotationConfigApplicationContext(TestConfig2.class); + } + + @Test(expected = BeanCreationException.class) + public void testblkMethod() { + new AnnotationConfigApplicationContext(TestConfig3.class); + } + + @Test(expected = BeanCreationException.class) + public void testblkClass() { + new AnnotationConfigApplicationContext(TestConfig4.class); + } + + @Test + public void testNormal() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfig5.class); + assertEquals("RestTemplate size was wrong", 1, + context.getBeansOfType(RestTemplate.class).size()); + } + + @Test(expected = BeanCreationException.class) + public void testBlkMethodExists() { + new AnnotationConfigApplicationContext(TestConfig6.class); + } + + @Test(expected = BeanCreationException.class) + public void testFbkMethodExists() { + new AnnotationConfigApplicationContext(TestConfig7.class); + } + + @Test(expected = BeanCreationException.class) + public void testBlkReturnValue() { + new AnnotationConfigApplicationContext(TestConfig8.class); + } + + @Test(expected = BeanCreationException.class) + public void testFbkReturnValue() { + new AnnotationConfigApplicationContext(TestConfig9.class); + } + + @Test + public void testNormalWithoutParam() { + new AnnotationConfigApplicationContext(TestConfig10.class); + } + + @Configuration + public static class TestConfig1 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(fallback = "fbk") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig2 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(fallbackClass = ExceptionUtil.class) + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig3 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(blockHandler = "blk") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig4 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(blockHandlerClass = ExceptionUtil.class) + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig5 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(blockHandlerClass = SentinelRestTemplateTests.ExceptionUtil.class, blockHandler = "handleException", fallbackClass = SentinelRestTemplateTests.ExceptionUtil.class, fallback = "fallbackException") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig6 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(blockHandlerClass = SentinelRestTemplateTests.ExceptionUtil.class, blockHandler = "handleException1") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig7 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(fallbackClass = SentinelRestTemplateTests.ExceptionUtil.class, fallback = "fallbackException1") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig8 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(blockHandlerClass = SentinelRestTemplateTests.ExceptionUtil.class, blockHandler = "handleException2") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig9 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate(fallbackClass = SentinelRestTemplateTests.ExceptionUtil.class, fallback = "fallbackException2") + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + + @Configuration + public static class TestConfig10 { + @Bean + SentinelBeanPostProcessor sentinelBeanPostProcessor( + ApplicationContext applicationContext) { + return new SentinelBeanPostProcessor(applicationContext); + } + + @Bean + @SentinelRestTemplate + RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + @SentinelRestTemplate + RestTemplate restTemplate2() { + return new RestTemplate(); + } + } + + public static class ExceptionUtil { + public static SentinelClientHttpResponse handleException(HttpRequest request, + byte[] body, ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + return new SentinelClientHttpResponse("Oops"); + } + + public static void handleException2(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + } + + public static SentinelClientHttpResponse fallbackException(HttpRequest request, + byte[] body, ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + return new SentinelClientHttpResponse("Oops fallback"); + } + + public static void fallbackException2(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex) { + System.out.println("Oops: " + ex.getClass().getCanonicalName()); + } + } + +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/TestConverter.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/TestConverter.java new file mode 100644 index 00000000..b17f45ae --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/TestConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel; + +import java.io.IOException; +import java.util.List; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jim + */ +public class TestConverter implements Converter> { + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public List convert(String s) { + try { + return objectMapper.readValue(s, new TypeReference>() { + }); + } + catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicatorTests.java b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicatorTests.java new file mode 100644 index 00000000..b5586ce0 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/java/com/alibaba/cloud/sentinel/endpoint/SentinelHealthIndicatorTests.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.sentinel.endpoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.ReflectionUtils; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.heartbeat.HeartbeatSenderProvider; +import com.alibaba.csp.sentinel.transport.HeartbeatSender; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; + +/** + * Test cases for {@link SentinelHealthIndicator}. + * + * @author cdfive + */ +public class SentinelHealthIndicatorTests { + + private SentinelHealthIndicator sentinelHealthIndicator; + + private DefaultListableBeanFactory beanFactory; + + private SentinelProperties sentinelProperties; + + private HeartbeatSender heartbeatSender; + + @Before + public void setUp() { + beanFactory = mock(DefaultListableBeanFactory.class); + sentinelProperties = mock(SentinelProperties.class); + sentinelHealthIndicator = new SentinelHealthIndicator(beanFactory, + sentinelProperties); + + SentinelConfig.setConfig(TransportConfig.CONSOLE_SERVER, ""); + + heartbeatSender = mock(HeartbeatSender.class); + Field heartbeatSenderField = ReflectionUtils + .findField(HeartbeatSenderProvider.class, "heartbeatSender"); + heartbeatSenderField.setAccessible(true); + ReflectionUtils.setField(heartbeatSenderField, null, heartbeatSender); + } + + @Test + public void testSentinelNotEnabled() { + when(sentinelProperties.isEnabled()).thenReturn(false); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("enabled")).isEqualTo(false); + } + + @Test + public void testSentinelDashboardNotConfigured() { + when(sentinelProperties.isEnabled()).thenReturn(true); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("dashboard")).isEqualTo(Status.UNKNOWN); + } + + @Test + public void testSentinelDashboardConfiguredSuccess() throws Exception { + when(sentinelProperties.isEnabled()).thenReturn(true); + SentinelConfig.setConfig(TransportConfig.CONSOLE_SERVER, "localhost:8080"); + when(heartbeatSender.sendHeartbeat()).thenReturn(true); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + public void testSentinelDashboardConfiguredFailed() throws Exception { + when(sentinelProperties.isEnabled()).thenReturn(true); + SentinelConfig.setConfig(TransportConfig.CONSOLE_SERVER, "localhost:8080"); + when(heartbeatSender.sendHeartbeat()).thenReturn(false); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("dashboard")).isEqualTo( + new Status(Status.DOWN.getCode(), "localhost:8080 can't be connected")); + } + + @Test + public void testSentinelDataSourceSuccess() throws Exception { + when(sentinelProperties.isEnabled()).thenReturn(true); + SentinelConfig.setConfig(TransportConfig.CONSOLE_SERVER, "localhost:8080"); + when(heartbeatSender.sendHeartbeat()).thenReturn(true); + + Map dataSourceMap = new HashMap<>(); + + FileRefreshableDataSource fileDataSource1 = mock(FileRefreshableDataSource.class); + dataSourceMap.put("ds1-sentinel-file-datasource", fileDataSource1); + + FileRefreshableDataSource fileDataSource2 = mock(FileRefreshableDataSource.class); + dataSourceMap.put("ds2-sentinel-file-datasource", fileDataSource2); + + when(beanFactory.getBeansOfType(AbstractDataSource.class)) + .thenReturn(dataSourceMap); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.UP); + Map dataSourceDetailMap = (Map) health + .getDetails().get("dataSource"); + assertThat(dataSourceDetailMap.get("ds1-sentinel-file-datasource")) + .isEqualTo(Status.UP); + assertThat(dataSourceDetailMap.get("ds2-sentinel-file-datasource")) + .isEqualTo(Status.UP); + } + + @Test + public void testSentinelDataSourceFailed() throws Exception { + when(sentinelProperties.isEnabled()).thenReturn(true); + SentinelConfig.setConfig(TransportConfig.CONSOLE_SERVER, "localhost:8080"); + when(heartbeatSender.sendHeartbeat()).thenReturn(true); + + Map dataSourceMap = new HashMap<>(); + + FileRefreshableDataSource fileDataSource1 = mock(FileRefreshableDataSource.class); + dataSourceMap.put("ds1-sentinel-file-datasource", fileDataSource1); + + FileRefreshableDataSource fileDataSource2 = mock(FileRefreshableDataSource.class); + when(fileDataSource2.loadConfig()) + .thenThrow(new RuntimeException("fileDataSource2 error")); + dataSourceMap.put("ds2-sentinel-file-datasource", fileDataSource2); + + when(beanFactory.getBeansOfType(AbstractDataSource.class)) + .thenReturn(dataSourceMap); + + Health health = sentinelHealthIndicator.health(); + + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + Map dataSourceDetailMap = (Map) health + .getDetails().get("dataSource"); + assertThat(dataSourceDetailMap.get("ds1-sentinel-file-datasource")) + .isEqualTo(Status.UP); + assertThat(dataSourceDetailMap.get("ds2-sentinel-file-datasource")) + .isEqualTo(new Status(Status.DOWN.getCode(), "fileDataSource2 error")); + } +} diff --git a/spring-cloud-alibaba-sentinel/src/test/resources/authority.json b/spring-cloud-alibaba-sentinel/src/test/resources/authority.json new file mode 100644 index 00000000..3fb4b249 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/resources/authority.json @@ -0,0 +1,17 @@ +[ + { + "resource": "good", + "limitApp": "abc", + "strategy": 0 + }, + { + "resource": "bad", + "limitApp": "bcd", + "strategy": 1 + }, + { + "resource": "terrible", + "limitApp": "aaa", + "strategy": 1 + } +] diff --git a/spring-cloud-alibaba-sentinel/src/test/resources/degraderule.json b/spring-cloud-alibaba-sentinel/src/test/resources/degraderule.json new file mode 100644 index 00000000..5977c5fc --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/resources/degraderule.json @@ -0,0 +1,16 @@ +[ + { + "resource": "abc0", + "count": 20.0, + "grade": 0, + "passCount": 0, + "timeWindow": 10 + }, + { + "resource": "abc1", + "count": 15.0, + "grade": 0, + "passCount": 0, + "timeWindow": 10 + } +] \ No newline at end of file diff --git a/spring-cloud-alibaba-sentinel/src/test/resources/flowrule.json b/spring-cloud-alibaba-sentinel/src/test/resources/flowrule.json new file mode 100644 index 00000000..d798f805 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/resources/flowrule.json @@ -0,0 +1,26 @@ +[ + { + "resource": "resource", + "controlBehavior": 0, + "count": 1, + "grade": 1, + "limitApp": "default", + "strategy": 0 + }, + { + "resource": "p", + "controlBehavior": 0, + "count": 1, + "grade": 1, + "limitApp": "default", + "strategy": 0 + }, + { + "resource": "http://www.taobao.com", + "controlBehavior": 0, + "count": 0, + "grade": 1, + "limitApp": "default", + "strategy": 0 + } +] diff --git a/spring-cloud-alibaba-sentinel/src/test/resources/param-flow.json b/spring-cloud-alibaba-sentinel/src/test/resources/param-flow.json new file mode 100644 index 00000000..72e1c2dc --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/resources/param-flow.json @@ -0,0 +1,16 @@ +[ + { + "resource": "hotResource", + "count": 0, + "grade": 1, + "limitApp": "default", + "paramIdx": 0, + "paramFlowItemList": [ + { + "object": "2", + "classType": "int", + "count": 1 + } + ] + } +] diff --git a/spring-cloud-alibaba-sentinel/src/test/resources/system.json b/spring-cloud-alibaba-sentinel/src/test/resources/system.json new file mode 100644 index 00000000..7aa623a7 --- /dev/null +++ b/spring-cloud-alibaba-sentinel/src/test/resources/system.json @@ -0,0 +1,8 @@ +[ + { + "highestSystemLoad": -1, + "qps": 100, + "avgRt": -1, + "maxThread": 10 + } +] diff --git a/spring-cloud-alicloud-acm/pom.xml b/spring-cloud-alicloud-acm/pom.xml new file mode 100644 index 00000000..b9bbba73 --- /dev/null +++ b/spring-cloud-alicloud-acm/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + + spring-cloud-alicloud-acm + Spring Cloud Alibaba Cloud ACM + + + + + com.alibaba.cloud + spring-cloud-alicloud-context + + + + com.aliyun + aliyun-java-sdk-core + + + + com.aliyun + aliyun-java-sdk-edas + + + + com.alibaba.edas.acm + acm-sdk + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-starter-actuator + provided + true + + + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.powermock + powermock-module-junit4 + 2.0.0 + test + + + org.powermock + powermock-api-mockito2 + 2.0.0 + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + + diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java new file mode 100644 index 00000000..4bb29694 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm; + +import org.springframework.beans.BeansException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.acm.refresh.AcmContextRefresher; +import com.alibaba.alicloud.acm.refresh.AcmRefreshHistory; +import com.alibaba.alicloud.context.acm.AcmIntegrationProperties; + +import com.taobao.diamond.client.Diamond; + +/** + * Created on 01/10/2017. + * + * @author juven.xuxb + */ +@Configuration +@ConditionalOnClass({ Diamond.class }) +public class AcmAutoConfiguration implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Bean + public AcmPropertySourceRepository acmPropertySourceRepository() { + return new AcmPropertySourceRepository(applicationContext); + } + + @Bean + public AcmRefreshHistory acmRefreshHistory() { + return new AcmRefreshHistory(); + } + + @Bean + public AcmContextRefresher acmContextRefresher( + AcmIntegrationProperties acmIntegrationProperties, + ContextRefresher contextRefresher, AcmRefreshHistory refreshHistory, + AcmPropertySourceRepository propertySourceRepository) { + return new AcmContextRefresher(contextRefresher, acmIntegrationProperties, + refreshHistory, propertySourceRepository); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmPropertySourceRepository.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmPropertySourceRepository.java new file mode 100644 index 00000000..e16d686c --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/AcmPropertySourceRepository.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; + +import com.alibaba.alicloud.acm.bootstrap.AcmPropertySource; + +/** + * @author juven.xuxb, 5/17/16. + */ +public class AcmPropertySourceRepository { + + private final ApplicationContext applicationContext; + + public AcmPropertySourceRepository(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * get all acm properties from application context + * @return + */ + public List getAll() { + List result = new ArrayList<>(); + ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) applicationContext; + for (PropertySource p : ctx.getEnvironment().getPropertySources()) { + if (p instanceof AcmPropertySource) { + result.add((AcmPropertySource) p); + } + else if (p instanceof CompositePropertySource) { + collectAcmPropertySources((CompositePropertySource) p, result); + } + } + return result; + } + + private void collectAcmPropertySources(CompositePropertySource composite, + List result) { + for (PropertySource p : composite.getPropertySources()) { + if (p instanceof AcmPropertySource) { + result.add((AcmPropertySource) p); + } + else if (p instanceof CompositePropertySource) { + collectAcmPropertySources((CompositePropertySource) p, result); + } + } + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySource.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySource.java new file mode 100644 index 00000000..caf7e62a --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySource.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.bootstrap; + +import java.util.Date; +import java.util.Map; + +import org.springframework.core.env.MapPropertySource; + +/** + * @author juven.xuxb + * @author xiaolongzuo + */ +public class AcmPropertySource extends MapPropertySource { + + private final String dataId; + + private final Date timestamp; + + private final boolean groupLevel; + + AcmPropertySource(String dataId, Map source, Date timestamp, + boolean groupLevel) { + super(dataId, source); + this.dataId = dataId; + this.timestamp = timestamp; + this.groupLevel = groupLevel; + } + + public String getDataId() { + return dataId; + } + + public Date getTimestamp() { + return timestamp; + } + + public boolean isGroupLevel() { + return groupLevel; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceBuilder.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceBuilder.java new file mode 100644 index 00000000..5c45cfeb --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceBuilder.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.bootstrap; + +import java.io.StringReader; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.util.StringUtils; + +import com.alibaba.edas.acm.ConfigService; +import com.alibaba.edas.acm.exception.ConfigException; + +/** + * @author juven.xuxb + * @author xiaolongzuo + */ +class AcmPropertySourceBuilder { + + private Logger log = LoggerFactory.getLogger(AcmPropertySourceBuilder.class); + + /** + * 传入 ACM 的 DataId 和 groupID,获取到解析后的 AcmProperty 对象 + * + * @param dataId + * @param diamondGroup + * @param groupLevel + * @return + */ + AcmPropertySource build(String dataId, String diamondGroup, boolean groupLevel) { + Properties properties = loadDiamondData(dataId, diamondGroup); + if (properties == null) { + return null; + } + return new AcmPropertySource(dataId, toMap(properties), new Date(), groupLevel); + } + + private Properties loadDiamondData(String dataId, String diamondGroup) { + try { + String data = ConfigService.getConfig(dataId, diamondGroup, 3000L); + if (StringUtils.isEmpty(data)) { + return null; + } + if (dataId.endsWith(".properties")) { + Properties properties = new Properties(); + log.info(String.format("Loading acm data, dataId: '%s', group: '%s'", + dataId, diamondGroup)); + properties.load(new StringReader(data)); + return properties; + } + else if (dataId.endsWith(".yaml") || dataId.endsWith(".yml")) { + YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean(); + yamlFactory.setResources(new ByteArrayResource(data.getBytes())); + return yamlFactory.getObject(); + } + } + catch (Exception e) { + if (e instanceof ConfigException) { + log.error("DIAMOND-100500:" + dataId + ", " + e.toString(), e); + } + else { + log.error("DIAMOND-100500:" + dataId, e); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private Map toMap(Properties properties) { + Map result = new HashMap<>(); + Enumeration keys = (Enumeration) properties.propertyNames(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + Object value = properties.getProperty(key); + if (value != null) { + result.put(key, ((String) value).trim()); + } + else { + result.put(key, null); + } + } + return result; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceLocator.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceLocator.java new file mode 100644 index 00000000..6f59f333 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/bootstrap/AcmPropertySourceLocator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.bootstrap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; + +import com.alibaba.alicloud.context.acm.AcmIntegrationProperties; + +/** + * @author juven.xuxb + * @author xiaolongzuo + */ +public class AcmPropertySourceLocator implements PropertySourceLocator { + + private static final String DIAMOND_PROPERTY_SOURCE_NAME = "diamond"; + + private AcmPropertySourceBuilder acmPropertySourceBuilder = new AcmPropertySourceBuilder(); + + @Autowired + private AcmIntegrationProperties acmIntegrationProperties; + + @Override + public PropertySource locate(Environment environment) { + + CompositePropertySource compositePropertySource = new CompositePropertySource( + DIAMOND_PROPERTY_SOURCE_NAME); + + acmIntegrationProperties.setActiveProfiles(environment.getActiveProfiles()); + + for (String dataId : acmIntegrationProperties.getGroupConfigurationDataIds()) { + loadDiamondDataIfPresent(compositePropertySource, dataId, + acmIntegrationProperties.getAcmProperties().getGroup(), true); + } + + for (String dataId : acmIntegrationProperties + .getApplicationConfigurationDataIds()) { + loadDiamondDataIfPresent(compositePropertySource, dataId, + acmIntegrationProperties.getAcmProperties().getGroup(), false); + } + + return compositePropertySource; + } + + private void loadDiamondDataIfPresent(final CompositePropertySource composite, + final String dataId, final String diamondGroup, final boolean groupLevel) { + AcmPropertySource ps = acmPropertySourceBuilder.build(dataId, diamondGroup, + groupLevel); + if (ps != null) { + composite.addFirstPropertySource(ps); + } + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpoint.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpoint.java new file mode 100644 index 00000000..172b058a --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpoint.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.endpoint; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.alicloud.acm.AcmPropertySourceRepository; +import com.alibaba.alicloud.acm.bootstrap.AcmPropertySource; +import com.alibaba.alicloud.acm.refresh.AcmRefreshHistory; +import com.alibaba.alicloud.context.acm.AcmProperties; + +/** + * Created on 01/10/2017. + * + * @author juven.xuxb + */ +@Endpoint(id = "acm") +public class AcmEndpoint { + + private final AcmProperties properties; + + private final AcmRefreshHistory refreshHistory; + + private final AcmPropertySourceRepository propertySourceRepository; + + private ThreadLocal dateFormat = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public AcmEndpoint(AcmProperties properties, AcmRefreshHistory refreshHistory, + AcmPropertySourceRepository propertySourceRepository) { + this.properties = properties; + this.refreshHistory = refreshHistory; + this.propertySourceRepository = propertySourceRepository; + } + + @ReadOperation + public Map invoke() { + Map result = new HashMap<>(); + result.put("config", properties); + + Map runtime = new HashMap<>(); + List all = propertySourceRepository.getAll(); + + List> sources = new ArrayList<>(); + for (AcmPropertySource ps : all) { + Map source = new HashMap<>(); + source.put("dataId", ps.getDataId()); + source.put("lastSynced", dateFormat.get().format(ps.getTimestamp())); + sources.add(source); + } + runtime.put("sources", sources); + runtime.put("refreshHistory", refreshHistory.getRecords()); + + result.put("runtime", runtime); + return result; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointAutoConfiguration.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointAutoConfiguration.java new file mode 100644 index 00000000..e997b1ac --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.endpoint; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; + +import com.alibaba.alicloud.acm.AcmPropertySourceRepository; +import com.alibaba.alicloud.acm.refresh.AcmRefreshHistory; +import com.alibaba.alicloud.context.acm.AcmProperties; + +/** + * @author xiaojing + */ +@ConditionalOnWebApplication +@ConditionalOnClass(name = "org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration") +public class AcmEndpointAutoConfiguration { + + @Autowired + private AcmProperties acmProperties; + + @Autowired + private AcmRefreshHistory acmRefreshHistory; + + @Autowired + private AcmPropertySourceRepository acmPropertySourceRepository; + + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + @Bean + public AcmEndpoint acmEndpoint() { + return new AcmEndpoint(acmProperties, acmRefreshHistory, + acmPropertySourceRepository); + } + + @Bean + @ConditionalOnMissingBean + public AcmHealthIndicator acmHealthIndicator(AcmProperties acmProperties, + AcmPropertySourceRepository acmPropertySourceRepository) { + return new AcmHealthIndicator(acmProperties, acmPropertySourceRepository); + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmHealthIndicator.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmHealthIndicator.java new file mode 100644 index 00000000..d172baa4 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/endpoint/AcmHealthIndicator.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.endpoint; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.util.StringUtils; + +import com.alibaba.alicloud.acm.AcmPropertySourceRepository; +import com.alibaba.alicloud.acm.bootstrap.AcmPropertySource; +import com.alibaba.alicloud.context.acm.AcmProperties; +import com.alibaba.edas.acm.ConfigService; + +/** + * @author leijuan + * @author juven + */ +public class AcmHealthIndicator extends AbstractHealthIndicator { + + private final AcmProperties acmProperties; + + private final AcmPropertySourceRepository acmPropertySourceRepository; + + private final List dataIds; + + public AcmHealthIndicator(AcmProperties acmProperties, + AcmPropertySourceRepository acmPropertySourceRepository) { + this.acmProperties = acmProperties; + this.acmPropertySourceRepository = acmPropertySourceRepository; + + this.dataIds = new ArrayList<>(); + for (AcmPropertySource acmPropertySource : this.acmPropertySourceRepository + .getAll()) { + this.dataIds.add(acmPropertySource.getDataId()); + } + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + for (String dataId : dataIds) { + try { + String config = ConfigService.getConfig(dataId, acmProperties.getGroup(), + acmProperties.getTimeout()); + if (StringUtils.isEmpty(config)) { + builder.down().withDetail(String.format("dataId: '%s', group: '%s'", + dataId, acmProperties.getGroup()), "config is empty"); + } + } + catch (Exception e) { + builder.down().withDetail(String.format("dataId: '%s', group: '%s'", + dataId, acmProperties.getGroup()), e.getMessage()); + } + } + builder.up().withDetail("dataIds", dataIds); + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmContextRefresher.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmContextRefresher.java new file mode 100644 index 00000000..245d578e --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmContextRefresher.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.refresh; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.cloud.endpoint.event.RefreshEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.util.StringUtils; + +import com.alibaba.alicloud.acm.AcmPropertySourceRepository; +import com.alibaba.alicloud.context.acm.AcmIntegrationProperties; +import com.alibaba.edas.acm.ConfigService; +import com.alibaba.edas.acm.listener.ConfigChangeListener; + +/** + * On application start up, AcmContextRefresher add diamond listeners to all application + * level dataIds, when there is a change in the data, listeners will refresh + * configurations. + * + * @author juven.xuxb, 5/13/16. + */ +public class AcmContextRefresher + implements ApplicationListener, ApplicationContextAware { + + private Logger log = LoggerFactory.getLogger(AcmContextRefresher.class); + + private final ContextRefresher contextRefresher; + + private final AcmIntegrationProperties acmIntegrationProperties; + + private final AcmRefreshHistory refreshHistory; + + private ApplicationContext applicationContext; + + private final AcmPropertySourceRepository acmPropertySourceRepository; + + private Map listenerMap = new ConcurrentHashMap<>(16); + + public AcmContextRefresher(ContextRefresher contextRefresher, + AcmIntegrationProperties acmIntegrationProperties, + AcmRefreshHistory refreshHistory, + AcmPropertySourceRepository acmPropertySourceRepository) { + this.contextRefresher = contextRefresher; + this.acmIntegrationProperties = acmIntegrationProperties; + this.refreshHistory = refreshHistory; + this.acmPropertySourceRepository = acmPropertySourceRepository; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + this.registerDiamondListenersForApplications(); + } + + private void registerDiamondListenersForApplications() { + if (acmIntegrationProperties.getAcmProperties().isRefreshEnabled()) { + for (String dataId : acmIntegrationProperties + .getApplicationConfigurationDataIds()) { + registerDiamondListener(dataId); + } + } + } + + private void registerDiamondListener(final String dataId) { + + ConfigChangeListener listener = listenerMap.computeIfAbsent(dataId, + i -> new ConfigChangeListener() { + @Override + public void receiveConfigInfo(String configInfo) { + String md5 = ""; + if (!StringUtils.isEmpty(configInfo)) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md5 = new BigInteger(1, + md.digest(configInfo.getBytes("UTF-8"))) + .toString(16); + } + catch (NoSuchAlgorithmException + | UnsupportedEncodingException e) { + log.warn("unable to get md5 for dataId: " + dataId, e); + } + } + refreshHistory.add(dataId, md5); + applicationContext.publishEvent(new RefreshEvent(this, md5, + "ACM Refresh, dataId=" + dataId)); + } + }); + ConfigService.addListener(dataId, + acmIntegrationProperties.getAcmProperties().getGroup(), listener); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmRefreshHistory.java b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmRefreshHistory.java new file mode 100644 index 00000000..f974dd65 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/java/com/alibaba/alicloud/acm/refresh/AcmRefreshHistory.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.refresh; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; + +/** + * @author juven.xuxb, 5/16/16. + */ +public class AcmRefreshHistory { + + private static final int MAX_SIZE = 20; + + private LinkedList records = new LinkedList<>(); + + private ThreadLocal dateFormat = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public void add(String dataId, String md5) { + records.addFirst(new Record(dateFormat.get().format(new Date()), dataId, md5)); + if (records.size() > MAX_SIZE) { + records.removeLast(); + } + } + + public LinkedList getRecords() { + return records; + } +} + +class Record { + + private final String timestamp; + + private final String dataId; + + private final String md5; + + public Record(String timestamp, String dataId, String md5) { + this.timestamp = timestamp; + this.dataId = dataId; + this.md5 = md5; + } + + public String getTimestamp() { + return timestamp; + } + + public String getDataId() { + return dataId; + } + + public String getMd5() { + return md5; + } +} diff --git a/spring-cloud-alicloud-acm/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-alicloud-acm/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..29f53c76 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "properties": [ + { + "name": "spring.application.group", + "type": "java.lang.String", + "description": "spring application group." + } + ] +} \ No newline at end of file diff --git a/spring-cloud-alicloud-acm/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-acm/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..0ed3fa01 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +com.alibaba.alicloud.acm.bootstrap.AcmPropertySourceLocator + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.alicloud.acm.AcmAutoConfiguration,\ +com.alibaba.alicloud.acm.endpoint.AcmEndpointAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmConfigurationTests.java b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmConfigurationTests.java new file mode 100644 index 00000000..038983d9 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmConfigurationTests.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.acm.bootstrap.AcmPropertySourceLocator; +import com.alibaba.alicloud.acm.endpoint.AcmEndpointAutoConfiguration; +import com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration; +import com.alibaba.alicloud.context.acm.AcmIntegrationProperties; +import com.alibaba.alicloud.context.acm.AcmProperties; +import com.alibaba.edas.acm.ConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ ConfigService.class }) +@SpringBootTest(classes = AcmConfigurationTests.TestConfig.class, properties = { + "spring.application.name=test-name", "spring.profiles.active=dev,test", + "spring.cloud.alicloud.acm.server-list=127.0.0.1", + "spring.cloud.alicloud.acm.server-port=8848", + "spring.cloud.alicloud.acm.endpoint=test-endpoint", + "spring.cloud.alicloud.acm.namespace=test-namespace", + "spring.cloud.alicloud.acm.timeout=1000", + "spring.cloud.alicloud.acm.group=test-group", + "spring.cloud.alicloud.acm.refresh-enabled=false", + "spring.cloud.alicloud.acm.file-extension=properties" }, webEnvironment = NONE) +public class AcmConfigurationTests { + + static { + + try { + + Method method = PowerMockito.method(ConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if ("test-name.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "user.name=hello\nuser.age=12"; + } + + if ("test-name-dev.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "user.name=dev"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Autowired + private AcmPropertySourceLocator locator; + + @Autowired + private AcmIntegrationProperties integrationProperties; + + @Autowired + private AcmProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AcmPropertySourceLocator was not created", locator); + assertNotNull("AcmProperties was not created", properties); + assertNotNull("AcmIntegrationProperties was not created", integrationProperties); + + checkoutAcmServerAddr(); + checkoutAcmServerPort(); + checkoutAcmEndpoint(); + checkoutAcmNamespace(); + checkoutAcmGroup(); + checkoutAcmFileExtension(); + checkoutAcmTimeout(); + checkoutAcmProfiles(); + checkoutAcmRefreshEnabled(); + checkoutDataLoad(); + checkoutProfileDataLoad(); + } + + private void checkoutAcmServerAddr() { + assertEquals("AcmProperties server address is wrong", "127.0.0.1", + properties.getServerList()); + + } + + private void checkoutAcmServerPort() { + assertEquals("AcmProperties server port is wrong", "8848", + properties.getServerPort()); + + } + + private void checkoutAcmEndpoint() { + assertEquals("AcmProperties endpoint is wrong", "test-endpoint", + properties.getEndpoint()); + + } + + private void checkoutAcmNamespace() { + assertEquals("AcmProperties namespace is wrong", "test-namespace", + properties.getNamespace()); + + } + + private void checkoutAcmGroup() { + assertEquals("AcmProperties' group is wrong", "test-group", + properties.getGroup()); + } + + private void checkoutAcmFileExtension() { + assertEquals("AcmProperties' file extension is wrong", "properties", + properties.getFileExtension()); + } + + private void checkoutAcmTimeout() { + assertEquals("AcmProperties' timeout is wrong", 1000, properties.getTimeout()); + } + + private void checkoutAcmRefreshEnabled() { + assertEquals("AcmProperties' refresh enabled is wrong", false, + properties.isRefreshEnabled()); + } + + private void checkoutAcmProfiles() { + assertArrayEquals("AcmProperties' profiles is wrong", + new String[] { "dev", "test" }, + integrationProperties.getActiveProfiles()); + } + + private void checkoutDataLoad() { + Assert.assertEquals(environment.getProperty("user.age"), "12"); + } + + private void checkoutProfileDataLoad() { + Assert.assertEquals(environment.getProperty("user.name"), "dev"); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AcmEndpointAutoConfiguration.class, + AcmAutoConfiguration.class, AcmContextBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmFileExtensionTest.java b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmFileExtensionTest.java new file mode 100644 index 00000000..52daad49 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmFileExtensionTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.acm; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.acm.endpoint.AcmEndpointAutoConfiguration; +import com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration; +import com.alibaba.edas.acm.ConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ ConfigService.class }) +@SpringBootTest(classes = AcmFileExtensionTest.TestConfig.class, properties = { + "spring.application.name=test-name", + "spring.cloud.alicloud.acm.server-list=127.0.0.1", + "spring.cloud.alicloud.acm.server-port=8080", + "spring.cloud.alicloud.acm.file-extension=yaml" }, webEnvironment = NONE) +public class AcmFileExtensionTest { + + static { + + try { + Method method = PowerMockito.method(ConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if ("test-name.yaml".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user:\n name: hello\n age: 12"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Test + public void contextLoads() throws Exception { + + Assert.assertEquals(environment.getProperty("user.name"), "hello"); + Assert.assertEquals(environment.getProperty("user.age"), "12"); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AcmEndpointAutoConfiguration.class, + AcmAutoConfiguration.class, AcmContextBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmGroupConfigurationTest.java b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmGroupConfigurationTest.java new file mode 100644 index 00000000..ba25f8db --- /dev/null +++ b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/AcmGroupConfigurationTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.acm; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.acm.endpoint.AcmEndpointAutoConfiguration; +import com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration; +import com.alibaba.edas.acm.ConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ ConfigService.class }) +@SpringBootTest(classes = AcmGroupConfigurationTest.TestConfig.class, properties = { + "spring.application.name=test-name", "spring.application.group=com.test.hello", + "spring.cloud.alicloud.acm.server-list=127.0.0.1", + "spring.cloud.alicloud.acm.server-port=8080", + "spring.cloud.alicloud.acm.timeout=1000", + "spring.cloud.alicloud.acm.group=test-group" }, webEnvironment = NONE) +public class AcmGroupConfigurationTest { + + static { + + try { + Method method = PowerMockito.method(ConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if ("com.test:application.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "com.test.value=com.test\ntest.priority=1"; + } + if ("com.test.hello:application.properties".equals(args[0]) + && "test-group".equals(args[1])) { + return "com.test.hello.value=com.test.hello\ntest.priority=2"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private Environment environment; + + @Test + public void contextLoads() throws Exception { + + Assert.assertEquals(environment.getProperty("com.test.value"), "com.test"); + Assert.assertEquals(environment.getProperty("test.priority"), "2"); + Assert.assertEquals(environment.getProperty("com.test.hello.value"), + "com.test.hello"); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AcmEndpointAutoConfiguration.class, + AcmAutoConfiguration.class, AcmContextBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointTests.java b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointTests.java new file mode 100644 index 00000000..36376ae9 --- /dev/null +++ b/spring-cloud-alicloud-acm/src/test/java/com/alibaba/alicloud/acm/endpoint/AcmEndpointTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm.endpoint; + +import static org.junit.Assert.assertEquals; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.MethodProxy; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.acm.AcmAutoConfiguration; +import com.alibaba.alicloud.acm.AcmPropertySourceRepository; +import com.alibaba.alicloud.acm.refresh.AcmRefreshHistory; +import com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration; +import com.alibaba.alicloud.context.acm.AcmProperties; +import com.alibaba.edas.acm.ConfigService; + +/** + * @author xiaojing + */ + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({ ConfigService.class }) +@SpringBootTest(classes = AcmEndpointTests.TestConfig.class, properties = { + "spring.application.name=test-name", + "spring.cloud.alicloud.acm.server-list=127.0.0.1", + "spring.cloud.alicloud.acm.server-port=8848", + "spring.cloud.alicloud.acm.file-extension=properties" }, webEnvironment = NONE) +public class AcmEndpointTests { + + static { + + try { + + Method method = PowerMockito.method(ConfigService.class, "getConfig", + String.class, String.class, long.class); + MethodProxy.proxy(method, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if ("test-name.properties".equals(args[0]) + && "DEFAULT_GROUP".equals(args[1])) { + return "user.name=hello\nuser.age=12"; + } + return ""; + } + }); + + } + catch (Exception ignore) { + ignore.printStackTrace(); + + } + } + + @Autowired + private AcmProperties properties; + + @Autowired + private AcmRefreshHistory refreshHistory; + + @Autowired + private AcmPropertySourceRepository propertySourceRepository; + + @Autowired + private AcmPropertySourceRepository acmPropertySourceRepository; + + @Test + public void contextLoads() throws Exception { + + checkoutEndpoint(); + checkoutAcmHealthIndicator(); + + } + + private void checkoutAcmHealthIndicator() { + try { + Builder builder = new Builder(); + + AcmHealthIndicator healthIndicator = new AcmHealthIndicator(properties, + acmPropertySourceRepository); + healthIndicator.doHealthCheck(builder); + + Builder builder1 = new Builder(); + List dataIds = new ArrayList<>(); + dataIds.add("test-name.properties"); + builder1.up().withDetail("dataIds", dataIds); + + Assert.assertTrue(builder.build().equals(builder1.build())); + + } + catch (Exception ignoreE) { + + } + + } + + private void checkoutEndpoint() throws Exception { + AcmEndpoint acmEndpoint = new AcmEndpoint(properties, refreshHistory, + propertySourceRepository); + Map map = acmEndpoint.invoke(); + assertEquals(map.get("config"), properties); + assertEquals(((Map) map.get("runtime")).get("refreshHistory"), + refreshHistory.getRecords()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AcmEndpointAutoConfiguration.class, + AcmAutoConfiguration.class, AcmContextBootstrapConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/pom.xml b/spring-cloud-alicloud-ans/pom.xml new file mode 100644 index 00000000..e6bf9917 --- /dev/null +++ b/spring-cloud-alicloud-ans/pom.xml @@ -0,0 +1,144 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alicloud-ans + Spring Cloud Alibaba Cloud ANS + + + + + com.alibaba.ans + ans-sdk + + + + com.aliyun + aliyun-java-sdk-core + + + + com.aliyun + aliyun-java-sdk-edas + + + + com.alibaba.cloud + spring-cloud-alicloud-context + + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-commons + + + + org.slf4j + slf4j-api + + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + + org.springframework.boot + spring-boot-starter + + + + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-actuator + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + org.powermock + powermock-module-junit4 + 2.0.0 + test + + + org.powermock + powermock-api-mockito2 + 2.0.0 + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + \ No newline at end of file diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java new file mode 100644 index 00000000..c86049e9 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.ans.registry.AnsAutoServiceRegistration; +import com.alibaba.alicloud.ans.registry.AnsRegistration; +import com.alibaba.alicloud.ans.registry.AnsServiceRegistry; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnClass(name = "org.springframework.boot.web.context.WebServerInitializedEvent") +@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) +@ConditionalOnAnsEnabled +@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class, + AutoServiceRegistrationAutoConfiguration.class }) +public class AnsAutoConfiguration { + + @Bean + public AnsServiceRegistry ansServiceRegistry() { + return new AnsServiceRegistry(); + } + + @Bean + @ConditionalOnBean(AutoServiceRegistrationProperties.class) + @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) + public AnsRegistration ansRegistration(AnsProperties ansProperties, + ApplicationContext applicationContext) { + return new AnsRegistration(ansProperties, applicationContext); + } + + @Bean + @ConditionalOnBean(AutoServiceRegistrationProperties.class) + @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) + public AnsAutoServiceRegistration ansAutoServiceRegistration( + AnsServiceRegistry registry, + AutoServiceRegistrationProperties autoServiceRegistrationProperties, + AnsRegistration registration) { + return new AnsAutoServiceRegistration(registry, autoServiceRegistrationProperties, + registration); + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClient.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClient.java new file mode 100644 index 00000000..7f7d34ab --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClient.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +/** + * @author xiaolongzuo + * @author pbting + */ +public class AnsDiscoveryClient implements DiscoveryClient { + + public static final String DESCRIPTION = "Spring Cloud ANS Discovery Client"; + + @Override + public String description() { + return DESCRIPTION; + } + + @Override + public List getInstances(String serviceId) { + try { + List hosts = NamingService.getHosts(serviceId); + return hostToServiceInstanceList(hosts, serviceId); + } + catch (Exception e) { + throw new RuntimeException( + "Can not get hosts from ans server. serviceId: " + serviceId, e); + } + } + + private static ServiceInstance hostToServiceInstance(Host host, String serviceId) { + AnsServiceInstance ansServiceInstance = new AnsServiceInstance(); + ansServiceInstance.setHost(host.getIp()); + ansServiceInstance.setPort(host.getPort()); + ansServiceInstance.setServiceId(serviceId); + Map metadata = new HashMap(5); + metadata.put("appUseType", host.getAppUseType()); + metadata.put("site", host.getSite()); + metadata.put("unit", host.getUnit()); + metadata.put("doubleWeight", "" + host.getDoubleWeight()); + metadata.put("weight", "" + host.getWeight()); + ansServiceInstance.setMetadata(metadata); + + return ansServiceInstance; + } + + private static List hostToServiceInstanceList(List hosts, + String serviceId) { + List result = new ArrayList(hosts.size()); + for (Host host : hosts) { + result.add(hostToServiceInstance(host, serviceId)); + } + return result; + } + + @Override + public List getServices() { + Set publishers = NamingService.getPublishes(); + Set doms = NamingService.getDomsSubscribed(); + doms.addAll(publishers); + List result = new LinkedList<>(); + for (String service : doms) { + result.add(service); + } + return result; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClientAutoConfiguration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..6cb09207 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsDiscoveryClientAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author xiaolongzuo + * @author pbting + */ +@Configuration +@ConditionalOnMissingBean(DiscoveryClient.class) +@EnableConfigurationProperties +@AutoConfigureBefore(SimpleDiscoveryClientAutoConfiguration.class) +public class AnsDiscoveryClientAutoConfiguration { + + @Bean + public DiscoveryClient ansDiscoveryClient() { + return new AnsDiscoveryClient(); + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsServiceInstance.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsServiceInstance.java new file mode 100644 index 00000000..2c7bc37c --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/AnsServiceInstance.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +import java.net.URI; +import java.util.Map; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; + +/** + * @author xiaolongzuo + */ +public class AnsServiceInstance implements ServiceInstance { + + private String serviceId; + + private String host; + + private int port; + + private boolean secure; + + private Map metadata; + + @Override + public String getServiceId() { + return serviceId; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public URI getUri() { + return DefaultServiceInstance.getUri(this); + } + + @Override + public Map getMetadata() { + return metadata; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ConditionalOnAnsEnabled.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ConditionalOnAnsEnabled.java new file mode 100644 index 00000000..98926d59 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ConditionalOnAnsEnabled.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +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; + +/** + * @author xiaolongzuo + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnProperty(value = "spring.cloud.ans.enabled", matchIfMissing = true) +public @interface ConditionalOnAnsEnabled { +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpoint.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpoint.java new file mode 100644 index 00000000..d119791d --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpoint.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.endpoint; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.alicloud.context.ans.AnsProperties; +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +/** + * @author xiaolongzuo + * @author pbting + */ +@Endpoint(id = "ans") +public class AnsEndpoint { + + private static final Logger log = LoggerFactory.getLogger(AnsEndpoint.class); + + private AnsProperties ansProperties; + + public AnsEndpoint(AnsProperties ansProperties) { + this.ansProperties = ansProperties; + } + + /** + * @return ans endpoint + */ + @ReadOperation + public Map invoke() { + Map ansEndpoint = new HashMap<>(); + log.info("ANS endpoint invoke, ansProperties is " + ansProperties); + ansEndpoint.put("ansProperties", ansProperties); + + Map subscribes = new HashMap<>(); + Set subscribeServices = NamingService.getDomsSubscribed(); + for (String service : subscribeServices) { + try { + List hosts = NamingService.getHosts(service); + subscribes.put(service, hosts); + } + catch (Exception ignoreException) { + + } + } + ansEndpoint.put("subscribes", subscribes); + log.info("ANS endpoint invoke, subscribes is " + subscribes); + return ansEndpoint; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpointAutoConfiguration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpointAutoConfiguration.java new file mode 100644 index 00000000..befe6733 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/endpoint/AnsEndpointAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.endpoint; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; + +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaolongzuo + */ +@ConditionalOnWebApplication +@ConditionalOnClass(Endpoint.class) +public class AnsEndpointAutoConfiguration { + + @Bean + public AnsEndpoint ansEndpoint(AnsProperties ansProperties) { + return new AnsEndpoint(ansProperties); + } +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistration.java new file mode 100644 index 00000000..c7612698 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistration.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author xiaolongzuo + * @author pbting + */ +public class AnsAutoServiceRegistration + extends AbstractAutoServiceRegistration { + private static final Logger log = LoggerFactory + .getLogger(AnsAutoServiceRegistration.class); + + private AnsRegistration registration; + + public AnsAutoServiceRegistration(ServiceRegistry serviceRegistry, + AutoServiceRegistrationProperties autoServiceRegistrationProperties, + AnsRegistration registration) { + super(serviceRegistry, autoServiceRegistrationProperties); + this.registration = registration; + } + + @Deprecated + public void setPort(int port) { + getPort().set(port); + } + + @Override + protected AnsRegistration getRegistration() { + if (this.registration.getPort() < 0 && this.getPort().get() > 0) { + this.registration.setPort(this.getPort().get()); + } + Assert.isTrue(this.registration.getPort() > 0, "service.port has not been set"); + return this.registration; + } + + @Override + protected AnsRegistration getManagementRegistration() { + return null; + } + + @Override + protected void register() { + if (!this.registration.getAnsProperties().isRegisterEnabled()) { + log.debug("Registration disabled."); + return; + } + if (this.registration.getPort() < 0) { + this.registration.setPort(getPort().get()); + } + super.register(); + } + + @Override + protected void registerManagement() { + if (!this.registration.getAnsProperties().isRegisterEnabled()) { + return; + } + super.registerManagement(); + + } + + @Override + protected Object getConfiguration() { + return this.registration.getAnsProperties(); + } + + @Override + protected boolean isEnabled() { + return this.registration.getAnsProperties().isRegisterEnabled(); + } + + @Override + @SuppressWarnings("deprecation") + protected String getAppName() { + String appName = registration.getServiceId(); + return StringUtils.isEmpty(appName) ? super.getAppName() : appName; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsRegistration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsRegistration.java new file mode 100644 index 00000000..8f738260 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsRegistration.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import java.net.URI; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ManagementServerPortUtils; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaolongzuo + */ +public class AnsRegistration implements Registration, ServiceInstance { + + static final String MANAGEMENT_PORT = "management.port"; + static final String MANAGEMENT_CONTEXT_PATH = "management.context-path"; + static final String MANAGEMENT_ADDRESS = "management.address"; + static final String MANAGEMENT_ENDPOINT_BASE_PATH = "management.endpoints.web.base-path"; + + private AnsProperties ansProperties; + private ApplicationContext context; + + public AnsRegistration(AnsProperties ansProperties, ApplicationContext context) { + this.ansProperties = ansProperties; + this.context = context; + } + + @PostConstruct + public void init() { + + Map metadata = ansProperties.getClientMetadata(); + Environment env = context.getEnvironment(); + + String endpointBasePath = env.getProperty(MANAGEMENT_ENDPOINT_BASE_PATH); + if (!StringUtils.isEmpty(endpointBasePath)) { + metadata.put(MANAGEMENT_ENDPOINT_BASE_PATH, endpointBasePath); + } + + Integer managementPort = ManagementServerPortUtils.getPort(context); + if (null != managementPort) { + metadata.put(MANAGEMENT_PORT, managementPort.toString()); + String contextPath = env + .getProperty("management.server.servlet.context-path"); + String address = env.getProperty("management.server.address"); + if (!StringUtils.isEmpty(contextPath)) { + metadata.put(MANAGEMENT_CONTEXT_PATH, contextPath); + } + if (!StringUtils.isEmpty(address)) { + metadata.put(MANAGEMENT_ADDRESS, address); + } + } + } + + @Override + public String getServiceId() { + return ansProperties.getClientDomains(); + } + + @Override + public String getHost() { + return ansProperties.getClientIp(); + } + + @Override + public int getPort() { + return ansProperties.getClientPort(); + } + + public void setPort(int port) { + // if spring.cloud.ans.port is not set,use the port detected from context + if (ansProperties.getClientPort() < 0) { + this.ansProperties.setClientPort(port); + } + } + + @Override + public boolean isSecure() { + return ansProperties.isSecure(); + } + + @Override + public URI getUri() { + return DefaultServiceInstance.getUri(this); + } + + @Override + public Map getMetadata() { + return ansProperties.getClientMetadata(); + } + + public boolean isRegisterEnabled() { + return ansProperties.isRegisterEnabled(); + } + + public String getCluster() { + return ansProperties.getClientCluster(); + } + + public float getRegisterWeight(String dom) { + if (null != ansProperties.getClientWeights().get(dom) + && ansProperties.getClientWeights().get(dom) > 0) { + return ansProperties.getClientWeights().get(dom); + } + return ansProperties.getClientWeight(); + } + + public AnsProperties getAnsProperties() { + return ansProperties; + } + + @Override + public String toString() { + return "AnsRegistration{" + "ansProperties=" + ansProperties + '}'; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsServiceRegistry.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsServiceRegistry.java new file mode 100644 index 00000000..fda95738 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/registry/AnsServiceRegistry.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; + +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.ipms.NodeReactor; + +/** + * @author xiaolongzuo + */ +public class AnsServiceRegistry implements ServiceRegistry { + + private static final Logger log = LoggerFactory.getLogger(AnsServiceRegistry.class); + + private static final String SEPARATOR = ","; + + @Override + public void register(AnsRegistration registration) { + + if (!registration.isRegisterEnabled()) { + log.warn("Registration is disabled..."); + return; + } + if (StringUtils.isEmpty(registration.getServiceId())) { + log.warn("No service to register for client..."); + return; + } + + List tags = new ArrayList<>(); + for (Map.Entry entry : registration.getAnsProperties().getTags() + .entrySet()) { + NodeReactor.Tag tag = new NodeReactor.Tag(); + tag.setName(entry.getKey()); + tag.setValue(entry.getValue()); + tags.add(tag); + } + + for (String dom : registration.getServiceId().split(SEPARATOR)) { + try { + NamingService.regDom(dom, registration.getHost(), registration.getPort(), + registration.getRegisterWeight(dom), registration.getCluster(), + tags); + log.info("INFO_ANS_REGISTER, {} {}:{} register finished", dom, + registration.getAnsProperties().getClientIp(), + registration.getAnsProperties().getClientPort()); + } + catch (Exception e) { + log.error("ERR_ANS_REGISTER, {} register failed...{},", dom, + registration.toString(), e); + } + } + } + + @Override + public void deregister(AnsRegistration registration) { + + log.info("De-registering from ANSServer now..."); + + if (StringUtils.isEmpty(registration.getServiceId())) { + log.warn("No dom to de-register for client..."); + return; + } + + try { + NamingService.deRegDom(registration.getServiceId(), registration.getHost(), + registration.getPort(), registration.getCluster()); + } + catch (Exception e) { + log.error("ERR_ANS_DEREGISTER, de-register failed...{},", + registration.toString(), e); + } + + log.info("De-registration finished."); + } + + @Override + public void close() { + + } + + @Override + public void setStatus(AnsRegistration registration, String status) { + + } + + @Override + public T getStatus(AnsRegistration registration) { + return null; + } + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfiguration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfiguration.java new file mode 100644 index 00000000..59ba3fa6 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ServerList; + +/** + * @author xiaolongzuo + * @author pbting + */ +@Configuration +public class AnsRibbonClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public ServerList ansRibbonServerList(IClientConfig config) { + AnsServerList serverList = new AnsServerList(config.getClientName()); + return serverList; + } + +} \ No newline at end of file diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServer.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServer.java new file mode 100644 index 00000000..5d36f51c --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServer.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import java.util.Collections; +import java.util.Map; + +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +import com.netflix.loadbalancer.Server; + +/** + * @author xiaolongzuo + */ +public class AnsServer extends Server { + + private final MetaInfo metaInfo; + private final Host host; + private final Map metadata; + + public AnsServer(final Host host, final String dom) { + super(host.getIp(), host.getPort()); + this.host = host; + this.metadata = Collections.emptyMap(); + metaInfo = new MetaInfo() { + @Override + public String getAppName() { + return dom; + } + + @Override + public String getServerGroup() { + return getMetadata().get("group"); + } + + @Override + public String getServiceIdForDiscovery() { + return dom; + } + + @Override + public String getInstanceId() { + return AnsServer.this.host.getIp() + ":" + dom + ":" + + AnsServer.this.host.getPort(); + } + }; + } + + @Override + public MetaInfo getMetaInfo() { + return metaInfo; + } + + public Host getHealthService() { + return this.host; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public String toString() { + return "AnsServer{" + "metaInfo=" + metaInfo + ", host=" + host + ", metadata=" + + metadata + '}'; + } +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServerList.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServerList.java new file mode 100644 index 00000000..b3f749f4 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/AnsServerList.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AbstractServerList; + +/** + * @author xiaolongzuo + */ +public class AnsServerList extends AbstractServerList { + + private String dom; + + public AnsServerList(String dom) { + this.dom = dom; + } + + @Override + public List getInitialListOfServers() { + try { + List hosts = NamingService.getHosts(getDom()); + return hostsToServerList(hosts); + } + catch (Exception e) { + throw new IllegalStateException("Can not get ans hosts, dom=" + getDom(), e); + } + } + + @Override + public List getUpdatedListOfServers() { + return getInitialListOfServers(); + } + + private AnsServer hostToServer(Host host) { + AnsServer server = new AnsServer(host, getDom()); + return server; + } + + private List hostsToServerList(List hosts) { + List result = new ArrayList(hosts.size()); + for (Host host : hosts) { + if (host.isValid()) { + result.add(hostToServer(host)); + } + } + + return result; + } + + public String getDom() { + return dom; + } + + @Override + public void initWithNiwsConfig(IClientConfig iClientConfig) { + this.dom = iClientConfig.getClientName(); + } +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/ConditionalOnRibbonAns.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/ConditionalOnRibbonAns.java new file mode 100644 index 00000000..7094d43b --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/ConditionalOnRibbonAns.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +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; + +/** + * @author xiaolongzuo + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnProperty(value = "ribbon.ans.enabled", matchIfMissing = true) +public @interface ConditionalOnRibbonAns { + +} diff --git a/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/RibbonAnsAutoConfiguration.java b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/RibbonAnsAutoConfiguration.java new file mode 100644 index 00000000..5c4ddb88 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/java/com/alibaba/alicloud/ans/ribbon/RibbonAnsAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonClients; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.ans.ConditionalOnAnsEnabled; + +/** + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnAnsEnabled +@ConditionalOnBean(SpringClientFactory.class) +@ConditionalOnRibbonAns +@AutoConfigureAfter(RibbonAutoConfiguration.class) +@RibbonClients(defaultConfiguration = AnsRibbonClientConfiguration.class) +public class RibbonAnsAutoConfiguration { +} diff --git a/spring-cloud-alicloud-ans/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-ans/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c297fef7 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.alicloud.ans.endpoint.AnsEndpointAutoConfiguration,\ + com.alibaba.alicloud.ans.ribbon.RibbonAnsAutoConfiguration,\ + com.alibaba.alicloud.ans.AnsAutoConfiguration,\ + com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/AnsDiscoveryClientTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/AnsDiscoveryClientTests.java new file mode 100644 index 00000000..009db660 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/AnsDiscoveryClientTests.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.cloud.client.ServiceInstance; + +import com.alibaba.alicloud.ans.test.AnsMockTest; +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +/** + * @author xiaojing + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest(NamingService.class) +public class AnsDiscoveryClientTests { + + private String host = "123.123.123.123"; + private int port = 8888; + private String serviceName = "test-service"; + + @Test + public void testGetServers() throws Exception { + + ArrayList hosts = new ArrayList<>(); + + HashMap map = new HashMap<>(); + map.put("test-key", "test-value"); + map.put("secure", "true"); + + hosts.add(AnsMockTest.hostInstance(serviceName, false, host, port, map)); + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getHosts(eq(serviceName))).thenReturn(hosts); + + AnsDiscoveryClient discoveryClient = new AnsDiscoveryClient(); + + List serviceInstances = discoveryClient + .getInstances(serviceName); + + assertThat(serviceInstances.size()).isEqualTo(1); + + ServiceInstance serviceInstance = serviceInstances.get(0); + + assertThat(serviceInstance.getServiceId()).isEqualTo(serviceName); + assertThat(serviceInstance.getHost()).isEqualTo(host); + assertThat(serviceInstance.getPort()).isEqualTo(port); + // assertThat(serviceInstance.isSecure()).isEqualTo(true); + // ans doesn't support metadata + assertThat(serviceInstance.getUri().toString()) + .isEqualTo(getUri(serviceInstance)); + // assertThat(serviceInstance.getMetadata().get("test-key")).isEqualTo("test-value"); + // ans doesn't support metadata + + } + + @Test + public void testGetAllService() throws Exception { + + Set subscribedServices = new HashSet<>(); + + subscribedServices.add(serviceName + "1"); + subscribedServices.add(serviceName + "2"); + subscribedServices.add(serviceName + "3"); + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getDomsSubscribed()).thenReturn(subscribedServices); + + AnsDiscoveryClient discoveryClient = new AnsDiscoveryClient(); + List services = discoveryClient.getServices(); + + assertThat(services.size()).isEqualTo(3); + assertThat(services.contains(serviceName + "1")); + assertThat(services.contains(serviceName + "2")); + assertThat(services.contains(serviceName + "3")); + + } + + private String getUri(ServiceInstance instance) { + + if (instance.isSecure()) { + return "https://" + host + ":" + port; + } + + return "http://" + host + ":" + port; + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationEnabledTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationEnabledTests.java new file mode 100644 index 00000000..21c78b6b --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationEnabledTests.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationEnabledTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080", + "spring.cloud.alicloud.ans.register-enabled=false" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationEnabledTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @Autowired + private AnsProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkEnabled(); + + } + + private void checkEnabled() { + assertFalse("Ans Auto Registration should not start", + ansAutoServiceRegistration.isEnabled()); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpNetworkInterfaceTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpNetworkInterfaceTests.java new file mode 100644 index 00000000..ebfbf090 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpNetworkInterfaceTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationIpNetworkInterfaceTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationIpNetworkInterfaceTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @Autowired + private AnsProperties properties; + + @Autowired + private InetUtils inetUtils; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkoutAnsDiscoveryServiceIP(); + + } + + private void checkoutAnsDiscoveryServiceIP() { + assertEquals("AnsProperties service IP was wrong", + getIPFromNetworkInterface(TestConfig.netWorkInterfaceName), + registration.getHost()); + + } + + private String getIPFromNetworkInterface(String networkInterface) { + + if (!TestConfig.hasValidNetworkInterface) { + return inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + } + + try { + NetworkInterface netInterface = NetworkInterface.getByName(networkInterface); + + Enumeration inetAddress = netInterface.getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + return currentAddress.getHostAddress(); + } + } + return networkInterface; + } + catch (Exception e) { + return networkInterface; + } + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + + static boolean hasValidNetworkInterface = false; + static String netWorkInterfaceName; + + static { + + try { + Enumeration enumeration = NetworkInterface + .getNetworkInterfaces(); + while (enumeration.hasMoreElements() && !hasValidNetworkInterface) { + NetworkInterface networkInterface = enumeration.nextElement(); + Enumeration inetAddress = networkInterface + .getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + hasValidNetworkInterface = true; + netWorkInterfaceName = networkInterface.getName(); + System.setProperty( + "spring.cloud.alicloud.ans.client-interface-name", + networkInterface.getName()); + break; + } + } + } + + } + catch (Exception e) { + + } + } + } + +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpTests.java new file mode 100644 index 00000000..ed185a98 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationIpTests.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationIpTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.client-domains=myTestService2", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.client-weight=2", + "spring.cloud.alicloud.ans.server-port=8080", + "spring.cloud.alicloud.ans.client-ip=123.123.123.123" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationIpTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @Autowired + private AnsProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkoutAnsDiscoveryServiceIP(); + checkoutAnsDiscoveryServiceName(); + checkoutAnsDiscoveryWeight(); + } + + private void checkoutAnsDiscoveryServiceIP() { + assertEquals("AnsProperties service IP was wrong", "123.123.123.123", + registration.getHost()); + } + + private void checkoutAnsDiscoveryServiceName() { + assertEquals("AnsDiscoveryProperties service name was wrong", "myTestService2", + properties.getClientDomains()); + } + + private void checkoutAnsDiscoveryWeight() { + assertEquals(2L, properties.getClientWeight(), 0); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationManagementPortTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationManagementPortTests.java new file mode 100644 index 00000000..0b2b2968 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationManagementPortTests.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationManagementPortTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", "management.server.port=8888", + "management.server.servlet.context-path=/test-context-path", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationManagementPortTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @Autowired + private AnsProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkoutNacosDiscoveryManagementData(); + + } + + private void checkoutNacosDiscoveryManagementData() { + assertEquals("AnsProperties management port was wrong", "8888", + properties.getClientMetadata().get(AnsRegistration.MANAGEMENT_PORT)); + + assertEquals("AnsProperties management context path was wrong", + "/test-context-path", properties.getClientMetadata() + .get(AnsRegistration.MANAGEMENT_CONTEXT_PATH)); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationPortTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationPortTests.java new file mode 100644 index 00000000..46ed81b2 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationPortTests.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationPortTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080", + "spring.cloud.alicloud.ans.client-port=8888" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationPortTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @Autowired + private AnsProperties properties; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsDiscoveryProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkoutAnsDiscoveryServicePort(); + + } + + private void checkoutAnsDiscoveryServicePort() { + assertEquals("AnsDiscoveryProperties service Port was wrong", 8888, + registration.getPort()); + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationTests.java new file mode 100644 index 00000000..e44467fc --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/registry/AnsAutoServiceRegistrationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; +import com.alibaba.alicloud.ans.endpoint.AnsEndpoint; +import com.alibaba.alicloud.context.ans.AnsProperties; +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +/** + * @author xiaojing + */ + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsAutoServiceRegistrationTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080", + "spring.cloud.alicloud.ans.secure=true", + "spring.cloud.alicloud.ans.endpoint=test-endpoint" }, webEnvironment = RANDOM_PORT) +public class AnsAutoServiceRegistrationTests { + + @Autowired + private AnsRegistration registration; + + @Autowired + private AnsAutoServiceRegistration ansAutoServiceRegistration; + + @LocalServerPort + private int port; + + @Autowired + private AnsProperties properties; + + @Autowired + private InetUtils inetUtils; + + @Test + public void contextLoads() throws Exception { + + assertNotNull("AnsRegistration was not created", registration); + assertNotNull("AnsProperties was not created", properties); + assertNotNull("AnsAutoServiceRegistration was not created", + ansAutoServiceRegistration); + + checkoutAnsDiscoveryServerList(); + checkoutAnsDiscoveryServerPort(); + + checkoutAnsDiscoveryServiceName(); + checkoutAnsDiscoveryServiceIP(); + checkoutAnsDiscoveryServicePort(); + checkoutAnsDiscoverySecure(); + + checkAutoRegister(); + + checkoutEndpoint(); + + } + + private void checkAutoRegister() { + assertTrue("Ans Auto Registration was not start", + ansAutoServiceRegistration.isRunning()); + } + + private void checkoutAnsDiscoveryServerList() { + assertEquals("AnsDiscoveryProperties server list was wrong", "127.0.0.1", + properties.getServerList()); + } + + private void checkoutAnsDiscoveryServerPort() { + assertEquals("AnsDiscoveryProperties server port was wrong", "8080", + properties.getServerPort()); + } + + private void checkoutAnsDiscoveryServiceName() { + assertEquals("AnsDiscoveryProperties service name was wrong", "myTestService1", + properties.getClientDomains()); + } + + private void checkoutAnsDiscoveryServiceIP() { + assertEquals("AnsDiscoveryProperties service IP was wrong", + inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(), + registration.getHost()); + } + + private void checkoutAnsDiscoveryServicePort() { + assertEquals("AnsDiscoveryProperties service Port was wrong", port, + registration.getPort()); + } + + private void checkoutAnsDiscoverySecure() { + assertTrue("AnsDiscoveryProperties secure should be true", properties.isSecure()); + + } + + private void checkoutEndpoint() throws Exception { + AnsEndpoint ansEndpoint = new AnsEndpoint(properties); + Map map = ansEndpoint.invoke(); + assertEquals(map.get("ansProperties"), properties); + + Map subscribes = new HashMap<>(); + Set subscribeServices = NamingService.getDomsSubscribed(); + for (String service : subscribeServices) { + try { + List hosts = NamingService.getHosts(service); + subscribes.put(service, hosts); + } + catch (Exception ignoreException) { + + } + } + + assertEquals(map.get("subscribes"), subscribes); + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class }) + public static class TestConfig { + } +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfigurationTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfigurationTests.java new file mode 100644 index 00000000..f5d1587e --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsRibbonClientConfigurationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +import com.alibaba.alicloud.ans.AnsAutoConfiguration; +import com.alibaba.alicloud.ans.AnsDiscoveryClientAutoConfiguration; + +import com.netflix.client.config.DefaultClientConfigImpl; +import com.netflix.client.config.IClientConfig; + +/** + * @author xiaojing + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AnsRibbonClientConfigurationTests.TestConfig.class, properties = { + "spring.application.name=myTestService1", + "spring.cloud.alicloud.ans.server-list=127.0.0.1", + "spring.cloud.alicloud.ans.server-port=8080", + "spring.cloud.alicloud.ans.endpoint=test-endpoint" }, webEnvironment = RANDOM_PORT) +public class AnsRibbonClientConfigurationTests { + + @Autowired + private AnsServerList serverList; + + @Test + public void contextLoads() throws Exception { + assertThat(serverList.getDom()).isEqualTo("myapp"); + } + + @Configuration + public static class AnsRibbonTestConfiguration { + + @Bean + IClientConfig iClientConfig() { + DefaultClientConfigImpl config = new DefaultClientConfigImpl(); + config.setClientName("myapp"); + return config; + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplate(); + } + + } + + @Configuration + @EnableAutoConfiguration + @ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, + AnsDiscoveryClientAutoConfiguration.class, AnsAutoConfiguration.class, + AnsRibbonTestConfiguration.class, RibbonAnsAutoConfiguration.class, + AnsRibbonClientConfiguration.class }) + public static class TestConfig { + } + +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServerListTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServerListTests.java new file mode 100644 index 00000000..9d62610d --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServerListTests.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.alibaba.alicloud.ans.test.AnsMockTest; +import com.alibaba.ans.core.NamingService; +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +import com.netflix.client.config.IClientConfig; + +/** + * @author xiaojing + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ NamingService.class, AnsServer.class }) +public class AnsServerListTests { + + @Test + @SuppressWarnings("unchecked") + public void testEmptyInstancesReturnsEmptyList() throws Exception { + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getHosts(anyString())).thenReturn(Collections.EMPTY_LIST); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + AnsServerList serverList = new AnsServerList("test-service"); + serverList.initWithNiwsConfig(clientConfig); + List servers = serverList.getInitialListOfServers(); + Assertions.assertThat(servers).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetServers() throws Exception { + + ArrayList hosts = new ArrayList<>(); + hosts.add(AnsMockTest.hostInstance("test-service", true, Collections.emptyMap())); + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getHosts(anyString())).thenReturn(hosts); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + AnsServerList serverList = new AnsServerList("test-service"); + serverList.initWithNiwsConfig(clientConfig); + List servers = serverList.getInitialListOfServers(); + Assertions.assertThat(servers).hasSize(1); + + servers = serverList.getUpdatedListOfServers(); + Assertions.assertThat(servers).hasSize(1); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetServersWithInstanceStatus() throws Exception { + ArrayList hosts = new ArrayList<>(); + + HashMap map1 = new HashMap<>(); + map1.put("instanceNum", "1"); + HashMap map2 = new HashMap<>(); + map2.put("instanceNum", "2"); + hosts.add(AnsMockTest.hostInstance("test-service", false, map1)); + hosts.add(AnsMockTest.hostInstance("test-service", true, map2)); + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getHosts(eq("test-service"))).thenReturn( + hosts.stream().filter(Host::isValid).collect(Collectors.toList())); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + AnsServerList serverList = new AnsServerList("test-service"); + serverList.initWithNiwsConfig(clientConfig); + List servers = serverList.getInitialListOfServers(); + Assertions.assertThat(servers).hasSize(1); + + AnsServer ansServer = servers.get(0); + Host host = ansServer.getHealthService(); + + assertThat(ansServer.getMetaInfo().getInstanceId()).isEqualTo( + host.getIp() + ":" + host.getHostname() + ":" + host.getPort()); + assertThat(ansServer.getHealthService().isValid()).isEqualTo(true); + assertThat(ansServer.getHealthService().getHostname()).isEqualTo("test-service"); + + } + + @Test + public void testUpdateServers() throws Exception { + ArrayList hosts = new ArrayList<>(); + + HashMap map = new HashMap<>(); + map.put("instanceNum", "1"); + hosts.add(AnsMockTest.hostInstance("test-service", true, map)); + + PowerMockito.mockStatic(NamingService.class); + when(NamingService.getHosts(eq("test-service"))).thenReturn( + hosts.stream().filter(Host::isValid).collect(Collectors.toList())); + + IClientConfig clientConfig = mock(IClientConfig.class); + when(clientConfig.getClientName()).thenReturn("test-service"); + AnsServerList serverList = new AnsServerList("test-service"); + serverList.initWithNiwsConfig(clientConfig); + + List servers = serverList.getUpdatedListOfServers(); + Assertions.assertThat(servers).hasSize(1); + + assertThat(servers.get(0).getHealthService().isValid()).isEqualTo(true); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServiceListTests.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServiceListTests.java new file mode 100644 index 00000000..bef39ad7 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/ribbon/AnsServiceListTests.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.ribbon; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +import com.netflix.loadbalancer.Server; + +/** + * @author xiaolongzuo + */ +public class AnsServiceListTests { + + static final String IP_ADDR = "10.0.0.2"; + + static final int PORT = 8080; + + @Test + public void testAnsServer() { + AnsServerList serverList = getAnsServerList(); + List servers = serverList.getInitialListOfServers(); + assertNotNull("servers was null", servers); + assertEquals("servers was not size 1", 1, servers.size()); + Server des = assertAnsServer(servers); + assertEquals("hostPort was wrong", IP_ADDR + ":" + PORT, des.getHostPort()); + } + + protected Server assertAnsServer(List servers) { + Server actualServer = servers.get(0); + assertTrue("server was not a DomainExtractingServer", + actualServer instanceof AnsServer); + AnsServer des = AnsServer.class.cast(actualServer); + assertNotNull("host is null", des.getHealthService()); + assertEquals("unit was wrong", "DEFAULT", des.getHealthService().getUnit()); + return des; + } + + protected AnsServerList getAnsServerList() { + Host host = mock(Host.class); + given(host.getIp()).willReturn(IP_ADDR); + given(host.getDoubleWeight()).willReturn(1.0); + given(host.getPort()).willReturn(PORT); + given(host.getWeight()).willReturn(1); + given(host.getUnit()).willReturn("DEFAULT"); + + AnsServer server = new AnsServer(host, "testDom"); + @SuppressWarnings("unchecked") + AnsServerList originalServerList = mock(AnsServerList.class); + given(originalServerList.getInitialListOfServers()) + .willReturn(Arrays.asList(server)); + return originalServerList; + } + +} diff --git a/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/test/AnsMockTest.java b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/test/AnsMockTest.java new file mode 100644 index 00000000..8eaeb0e7 --- /dev/null +++ b/spring-cloud-alicloud-ans/src/test/java/com/alibaba/alicloud/ans/test/AnsMockTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans.test; + +import java.util.Map; + +import com.alibaba.ans.shaded.com.taobao.vipserver.client.core.Host; + +/** + * @author xiaojing + */ +public class AnsMockTest { + + public static Host hostInstance(String serviceName, boolean valid, + Map metadata) { + Host host = new Host(); + host.setHostname(serviceName); + host.setValid(valid); + return host; + } + + public static Host hostInstance(String serviceName, boolean valid, String ip, + int port, Map metadata) { + Host host = new Host(); + host.setIp(ip); + host.setPort(port); + host.setValid(valid); + host.setHostname(serviceName); + return host; + } +} diff --git a/spring-cloud-alicloud-context/pom.xml b/spring-cloud-alicloud-context/pom.xml new file mode 100644 index 00000000..59cbbc63 --- /dev/null +++ b/spring-cloud-alicloud-context/pom.xml @@ -0,0 +1,148 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alicloud-context + Spring Cloud AliCloud Context + + + + + com.aliyun + aliyun-java-sdk-edas + provided + + + com.aliyun + aliyun-java-sdk-core + + + + + + com.alibaba.cloud + alicloud-context + + + + com.aliyun + aliyun-java-sdk-core + provided + + + + com.alibaba.edas + schedulerX-client + provided + + + + com.alibaba.ans + ans-sdk + provided + + + + com.aliyun.oss + aliyun-sdk-oss + provided + + + + com.alibaba.edas.acm + acm-sdk + provided + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.boot + spring-boot-starter-logging + provided + true + + + + org.springframework.boot + spring-boot + provided + true + + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + test + + + + org.powermock + powermock-module-junit4 + 2.0.0 + test + + + + org.powermock + powermock-api-mockito2 + 2.0.0 + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudContextAutoConfiguration.java new file mode 100644 index 00000000..32c93b78 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudContextAutoConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.InetUtilsProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties({ AliCloudProperties.class, InetUtilsProperties.class }) +public class AliCloudContextAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public InetUtils inetUtils(InetUtilsProperties inetUtilsProperties) { + return new InetUtils(inetUtilsProperties); + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudProperties.java new file mode 100644 index 00000000..a43c695d --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/AliCloudProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.context.AliCloudConfiguration; + +/** + * @author xiaolongzuo + */ +@ConfigurationProperties("spring.cloud.alicloud") +public class AliCloudProperties implements AliCloudConfiguration { + + /** + * alibaba cloud access key. + */ + private String accessKey; + + /** + * alibaba cloud secret key. + */ + private String secretKey; + + @Override + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + @Override + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/Constants.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/Constants.java new file mode 100644 index 00000000..8e3fe893 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/Constants.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context; + +/** + * @author Jim + */ +public interface Constants { + + interface Sentinel { + String PROPERTY_PREFIX = "spring.cloud.sentinel"; + String NACOS_DATASOURCE_AK = PROPERTY_PREFIX + ".nacos.config.access-key"; + String NACOS_DATASOURCE_SK = PROPERTY_PREFIX + ".nacos.config.secret-key"; + String NACOS_DATASOURCE_NAMESPACE = PROPERTY_PREFIX + ".nacos.config.namespace"; + String NACOS_DATASOURCE_ENDPOINT = PROPERTY_PREFIX + ".nacos.config.endpoint"; + String PROJECT_NAME = PROPERTY_PREFIX + ".nacos.config.project-name"; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmContextBootstrapConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmContextBootstrapConfiguration.java new file mode 100644 index 00000000..0be77f62 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmContextBootstrapConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.acm; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.cloud.context.acm.AliCloudAcmInitializer; + +/** + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties(AcmProperties.class) +@ConditionalOnClass(name = "com.alibaba.alicloud.acm.AcmAutoConfiguration") +@ImportAutoConfiguration(EdasContextAutoConfiguration.class) +public class AcmContextBootstrapConfiguration { + + @Autowired + private AcmProperties acmProperties; + + @Autowired + private EdasProperties edasProperties; + + @Autowired + private AliCloudProperties aliCloudProperties; + + @Autowired + private Environment environment; + + @PostConstruct + public void initAcmProperties() { + AliCloudAcmInitializer.initialize(aliCloudProperties, edasProperties, + acmProperties); + } + + @Bean + public AcmIntegrationProperties acmIntegrationProperties() { + AcmIntegrationProperties acmIntegrationProperties = new AcmIntegrationProperties(); + String applicationName = environment.getProperty("spring.application.name"); + String applicationGroup = environment.getProperty("spring.application.group"); + Assert.isTrue(!StringUtils.isEmpty(applicationName), + "'spring.application.name' must be configured in bootstrap.properties or bootstrap.yml/yaml..."); + acmIntegrationProperties.setApplicationName(applicationName); + acmIntegrationProperties.setApplicationGroup(applicationGroup); + acmIntegrationProperties.setActiveProfiles(environment.getActiveProfiles()); + acmIntegrationProperties.setAcmProperties(acmProperties); + return acmIntegrationProperties; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmIntegrationProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmIntegrationProperties.java new file mode 100644 index 00000000..da5abac5 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmIntegrationProperties.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.acm; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * @author xiaolongzuo + */ +public class AcmIntegrationProperties { + + private String applicationName; + + private String applicationGroup; + + private String[] activeProfiles = new String[0]; + + private AcmProperties acmProperties; + + public String getApplicationConfigurationDataIdWithoutGroup() { + return applicationName + "." + acmProperties.getFileExtension(); + } + + public List getGroupConfigurationDataIds() { + List groupConfigurationDataIds = new ArrayList<>(); + if (StringUtils.isEmpty(applicationGroup)) { + return groupConfigurationDataIds; + } + String[] parts = applicationGroup.split("\\."); + for (int i = 1; i < parts.length; i++) { + StringBuilder subGroup = new StringBuilder(parts[0]); + for (int j = 1; j <= i; j++) { + subGroup.append(".").append(parts[j]); + } + groupConfigurationDataIds + .add(subGroup + ":application." + acmProperties.getFileExtension()); + } + return groupConfigurationDataIds; + } + + public List getApplicationConfigurationDataIds() { + List applicationConfigurationDataIds = new ArrayList<>(); + if (!StringUtils.isEmpty(applicationGroup)) { + applicationConfigurationDataIds.add(applicationGroup + ":" + applicationName + + "." + acmProperties.getFileExtension()); + for (String profile : activeProfiles) { + applicationConfigurationDataIds + .add(applicationGroup + ":" + applicationName + "-" + profile + + "." + acmProperties.getFileExtension()); + } + + } + applicationConfigurationDataIds + .add(applicationName + "." + acmProperties.getFileExtension()); + for (String profile : activeProfiles) { + applicationConfigurationDataIds.add(applicationName + "-" + profile + "." + + acmProperties.getFileExtension()); + } + return applicationConfigurationDataIds; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public void setApplicationGroup(String applicationGroup) { + this.applicationGroup = applicationGroup; + } + + public void setActiveProfiles(String[] activeProfiles) { + this.activeProfiles = activeProfiles; + } + + public String[] getActiveProfiles() { + return activeProfiles; + } + + public void setAcmProperties(AcmProperties acmProperties) { + this.acmProperties = acmProperties; + } + + public AcmProperties getAcmProperties() { + return acmProperties; + } +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmProperties.java new file mode 100644 index 00000000..f66ba230 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/acm/AcmProperties.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.acm; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.context.AliCloudServerMode; +import com.alibaba.cloud.context.acm.AcmConfiguration; + +/** + * acm properties + * + * @author leijuan + * @author xiaolongzuo + */ +@ConfigurationProperties(prefix = "spring.cloud.alicloud.acm") +public class AcmProperties implements AcmConfiguration { + + private AliCloudServerMode serverMode = AliCloudServerMode.LOCAL; + + private String serverList = "127.0.0.1"; + + private String serverPort = "8080"; + + /** + * diamond group + */ + private String group = "DEFAULT_GROUP"; + + /** + * timeout to get configuration + */ + private int timeout = 3000; + + /** + * the AliYun endpoint for ACM + */ + private String endpoint; + + /** + * ACM namespace + */ + private String namespace; + + /** + * name of ram role granted to ECS + */ + private String ramRoleName; + + private String fileExtension = "properties"; + + private boolean refreshEnabled = true; + + public String getFileExtension() { + return fileExtension; + } + + public void setFileExtension(String fileExtension) { + this.fileExtension = fileExtension; + } + + @Override + public String getServerList() { + return serverList; + } + + public void setServerList(String serverList) { + this.serverList = serverList; + } + + @Override + public String getServerPort() { + return serverPort; + } + + public void setServerPort(String serverPort) { + this.serverPort = serverPort; + } + + @Override + public boolean isRefreshEnabled() { + return refreshEnabled; + } + + public void setRefreshEnabled(boolean refreshEnabled) { + this.refreshEnabled = refreshEnabled; + } + + @Override + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @Override + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + @Override + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + @Override + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @Override + public String getRamRoleName() { + return ramRoleName; + } + + public void setRamRoleName(String ramRoleName) { + this.ramRoleName = ramRoleName; + } + + @Override + public AliCloudServerMode getServerMode() { + return serverMode; + } + + public void setServerMode(AliCloudServerMode serverMode) { + this.serverMode = serverMode; + } +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListener.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListener.java new file mode 100644 index 00000000..6d028331 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListener.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.ans; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; + +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.alicloud.context.listener.AbstractOnceApplicationListener; +import com.alibaba.cloud.context.ans.AliCloudAnsInitializer; +import com.alibaba.cloud.context.edas.AliCloudEdasSdk; + +/** + * Init {@link com.alibaba.ans.core.NamingService} properties. + * + * @author xiaolongzuo + */ +public class AnsContextApplicationListener + extends AbstractOnceApplicationListener { + + @Override + protected String conditionalOnClass() { + return "com.alibaba.alicloud.ans.AnsAutoConfiguration"; + } + + @Override + public void handleEvent(ContextRefreshedEvent event) { + ApplicationContext applicationContext = event.getApplicationContext(); + AliCloudProperties aliCloudProperties = applicationContext + .getBean(AliCloudProperties.class); + EdasProperties edasProperties = applicationContext.getBean(EdasProperties.class); + AnsProperties ansProperties = applicationContext.getBean(AnsProperties.class); + AliCloudEdasSdk aliCloudEdasSdk = applicationContext + .getBean(AliCloudEdasSdk.class); + AliCloudAnsInitializer.initialize(aliCloudProperties, edasProperties, + ansProperties, aliCloudEdasSdk); + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextAutoConfiguration.java new file mode 100644 index 00000000..56f8ebc9 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsContextAutoConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.ans; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; + +/** + * @author xiaolongzuo + */ +@Configuration +@ConditionalOnClass(name = "com.alibaba.alicloud.ans.AnsAutoConfiguration") +@EnableConfigurationProperties(AnsProperties.class) +@ImportAutoConfiguration(EdasContextAutoConfiguration.class) +public class AnsContextAutoConfiguration { + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsProperties.java new file mode 100644 index 00000000..99aa4a69 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/ans/AnsProperties.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.ans; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.context.AliCloudServerMode; +import com.alibaba.cloud.context.ans.AnsConfiguration; + +/** + * @author xiaolongzuo + */ +@ConfigurationProperties("spring.cloud.alicloud.ans") +public class AnsProperties implements AnsConfiguration { + + /** + * Server side mode,the default is LOCAL. + */ + private AliCloudServerMode serverMode = AliCloudServerMode.LOCAL; + + /** + * Server list. + */ + private String serverList = "127.0.0.1"; + + /** + * Server port. + */ + private String serverPort = "8080"; + + /** + * Service names,default value is ${spring.cloud.alicloud.ans.doms}. When not + * configured, use ${spring.application.name}. + */ + @Value("${spring.cloud.alicloud.ans.client-domains:${spring.application.name:}}") + private String clientDomains; + + /** + * The weight of the registration service, obtained from the configuration + * ${spring.cloud.alicloud.ans.weight}, the default is 1. + */ + private float clientWeight = 1; + + /** + * When there are multiple doms and need to correspond to different weights, configure + * them by spring.cloud.alicloud.ans.weight.dom1=weight1. + */ + private Map clientWeights = new HashMap(); + + /** + * The token of the registration service, obtained from + * ${spring.cloud.alicloud.ans.token}. + */ + private String clientToken; + + /** + * When there are multiple doms and need to correspond to different tokens, configure + * them by spring.cloud.alicloud.ans.tokens.dom1=token1. + */ + private Map clientTokens = new HashMap(); + + /** + * Configure which cluster to register with, obtained from + * ${spring.cloud.alicloud.ans.cluster}, defaults to DEFAULT. + */ + private String clientCluster = "DEFAULT"; + + /** + * Temporarily not supported, reserved fields. + */ + private Map clientMetadata = new HashMap<>(); + + /** + * Registration is turned on by default, and registration can be turned off by the + * configuration of spring.cloud.alicloud.ans.register-enabled=false. + */ + private boolean registerEnabled = true; + + /** + * The ip of the service you want to publish, obtained from + * ${spring.cloud.alicloud.ans.client-ip}. + */ + private String clientIp; + + /** + * Configure which NIC the ip of the service you want to publish is obtained from. + */ + private String clientInterfaceName; + + /** + * The port of the service you want to publish. + */ + private int clientPort = -1; + + /** + * The environment isolation configuration under the tenant, the services in the same + * environment of the same tenant can discover each other. + */ + @Value("${spring.cloud.alicloud.ans.env:${env.id:DEFAULT}}") + private String env; + + /** + * Whether to register as https, configured by ${spring.cloud.alicloud.ans.secure}, + * default is false. + */ + private boolean secure = false; + + @Autowired + private InetUtils inetUtils; + + private Map tags = new HashMap<>(); + + @PostConstruct + public void init() throws SocketException { + + // Marked as spring cloud application + tags.put("ANS_SERVICE_TYPE", "SPRING_CLOUD"); + + if (StringUtils.isEmpty(clientIp)) { + if (StringUtils.isEmpty(clientInterfaceName)) { + clientIp = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + } + else { + NetworkInterface networkInterface = NetworkInterface + .getByName(clientInterfaceName); + if (null == networkInterface) { + throw new RuntimeException( + "no such network interface " + clientInterfaceName); + } + + Enumeration inetAddress = networkInterface + .getInetAddresses(); + while (inetAddress.hasMoreElements()) { + InetAddress currentAddress = inetAddress.nextElement(); + if (currentAddress instanceof Inet4Address + && !currentAddress.isLoopbackAddress()) { + clientIp = currentAddress.getHostAddress(); + break; + } + } + + if (StringUtils.isEmpty(clientIp)) { + throw new RuntimeException( + "cannot find available ip from network interface " + + clientInterfaceName); + } + + } + } + } + + @Override + public String getServerPort() { + return serverPort; + } + + public void setServerPort(String serverPort) { + this.serverPort = serverPort; + } + + @Override + public String getServerList() { + return serverList; + } + + public void setServerList(String serverList) { + this.serverList = serverList; + } + + @Override + public boolean isRegisterEnabled() { + return registerEnabled; + } + + public void setRegisterEnabled(boolean registerEnabled) { + this.registerEnabled = registerEnabled; + } + + @Override + public boolean isSecure() { + return secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + @Override + public String getEnv() { + return env; + } + + public void setEnv(String env) { + this.env = env; + } + + @Override + public Map getTags() { + return tags; + } + + public void setTags(Map tags) { + this.tags = tags; + } + + @Override + public AliCloudServerMode getServerMode() { + return serverMode; + } + + public void setServerMode(AliCloudServerMode serverMode) { + this.serverMode = serverMode; + } + + @Override + public String getClientDomains() { + return clientDomains; + } + + public void setClientDomains(String clientDomains) { + this.clientDomains = clientDomains; + } + + @Override + public float getClientWeight() { + return clientWeight; + } + + public void setClientWeight(float clientWeight) { + this.clientWeight = clientWeight; + } + + @Override + public Map getClientWeights() { + return clientWeights; + } + + public void setClientWeights(Map clientWeights) { + this.clientWeights = clientWeights; + } + + @Override + public String getClientToken() { + return clientToken; + } + + public void setClientToken(String clientToken) { + this.clientToken = clientToken; + } + + @Override + public Map getClientTokens() { + return clientTokens; + } + + public void setClientTokens(Map clientTokens) { + this.clientTokens = clientTokens; + } + + @Override + public String getClientCluster() { + return clientCluster; + } + + public void setClientCluster(String clientCluster) { + this.clientCluster = clientCluster; + } + + @Override + public Map getClientMetadata() { + return clientMetadata; + } + + public void setClientMetadata(Map clientMetadata) { + this.clientMetadata = clientMetadata; + } + + @Override + public String getClientIp() { + return clientIp; + } + + public void setClientIp(String clientIp) { + this.clientIp = clientIp; + } + + @Override + public String getClientInterfaceName() { + return clientInterfaceName; + } + + public void setClientInterfaceName(String clientInterfaceName) { + this.clientInterfaceName = clientInterfaceName; + } + + @Override + public int getClientPort() { + return clientPort; + } + + public void setClientPort(int clientPort) { + this.clientPort = clientPort; + } + + @Override + public String toString() { + return "AnsProperties{" + "doms='" + clientDomains + '\'' + ", weight=" + + clientWeight + ", weights=" + clientWeights + ", token='" + clientToken + + '\'' + ", tokens=" + clientTokens + ", cluster='" + clientCluster + '\'' + + ", metadata=" + clientMetadata + ", registerEnabled=" + registerEnabled + + ", ip='" + clientIp + '\'' + ", interfaceName='" + clientInterfaceName + + '\'' + ", port=" + clientPort + ", env='" + env + '\'' + ", secure=" + + secure + ", tags=" + tags + '}'; + } +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasContextAutoConfiguration.java new file mode 100644 index 00000000..61a8e1da --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasContextAutoConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.edas; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.cloud.context.edas.AliCloudEdasSdk; +import com.alibaba.cloud.context.edas.AliCloudEdasSdkFactory; + +/** + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties(EdasProperties.class) +@ImportAutoConfiguration(AliCloudContextAutoConfiguration.class) +public class EdasContextAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "com.aliyuncs.edas.model.v20170801.GetSecureTokenRequest") + public AliCloudEdasSdk aliCloudEdasSdk(AliCloudProperties aliCloudProperties, + EdasProperties edasProperties) { + return AliCloudEdasSdkFactory.getDefaultAliCloudEdasSdk(aliCloudProperties, + edasProperties.getRegionId()); + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasProperties.java new file mode 100644 index 00000000..c34979c9 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/edas/EdasProperties.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.edas; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.context.edas.EdasConfiguration; + +/** + * @author xiaolongzuo + */ +@ConfigurationProperties("spring.cloud.alicloud.edas") +public class EdasProperties implements EdasConfiguration { + + private static final String DEFAULT_APPLICATION_NAME = ""; + + /** + * edas application name. + */ + @Value("${spring.application.name:${spring.cloud.alicloud.edas.application.name:}}") + private String applicationName; + + /** + * edas namespace + */ + private String namespace; + + /** + * whether or not connect edas. + */ + private boolean enabled; + + @Override + public String getRegionId() { + if (namespace == null) { + return null; + } + return namespace.contains(":") ? namespace.split(":")[0] : namespace; + } + + @Override + public boolean isApplicationNameValid() { + return !DEFAULT_APPLICATION_NAME.equals(applicationName); + } + + @Override + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + @Override + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/listener/AbstractOnceApplicationListener.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/listener/AbstractOnceApplicationListener.java new file mode 100644 index 00000000..ffc09c7b --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/listener/AbstractOnceApplicationListener.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.listener; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ApplicationContextEvent; + +/** + * @author xiaolongzuo + */ +public abstract class AbstractOnceApplicationListener + implements ApplicationListener { + + private static final String BOOTSTRAP_CONFIG_NAME_VALUE = "bootstrap"; + + private static final String BOOTSTRAP_CONFIG_NAME_KEY = "spring.config.name"; + + private static ConcurrentHashMap, AtomicBoolean> lockMap = new ConcurrentHashMap<>(); + + @Override + public void onApplicationEvent(T event) { + if (event instanceof ApplicationContextEvent) { + ApplicationContext applicationContext = ((ApplicationContextEvent) event) + .getApplicationContext(); + // skip bootstrap context or super parent context. + if (BOOTSTRAP_CONFIG_NAME_VALUE.equals(applicationContext.getEnvironment() + .getProperty(BOOTSTRAP_CONFIG_NAME_KEY))) { + return; + } + } + Class clazz = getClass(); + lockMap.putIfAbsent(clazz, new AtomicBoolean(false)); + AtomicBoolean handled = lockMap.get(clazz); + // only execute once. + if (!handled.compareAndSet(false, true)) { + return; + } + if (conditionalOnClass() != null) { + try { + Class.forName(conditionalOnClass()); + } + catch (ClassNotFoundException e) { + // ignored + return; + } + } + handleEvent(event); + } + + /** + * handle event. + * + * @param event + */ + protected abstract void handleEvent(T event); + + /** + * condition on class. + * + * @return + */ + protected String conditionalOnClass() { + return null; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListener.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListener.java new file mode 100644 index 00000000..e413edcf --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListener.java @@ -0,0 +1,55 @@ +package com.alibaba.alicloud.context.nacos; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; + +import com.alibaba.alicloud.context.listener.AbstractOnceApplicationListener; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfiguration; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author pbting + */ +public class NacosConfigParameterInitListener + extends AbstractOnceApplicationListener { + private static final Logger log = LoggerFactory + .getLogger(NacosConfigParameterInitListener.class); + + @Override + protected String conditionalOnClass() { + return "com.alibaba.cloud.nacos.NacosConfigAutoConfiguration"; + } + + @Override + protected void handleEvent(ApplicationEnvironmentPreparedEvent event) { + preparedNacosConfiguration(); + } + + private void preparedNacosConfiguration() { + EdasChangeOrderConfiguration edasChangeOrderConfiguration = EdasChangeOrderConfigurationFactory + .getEdasChangeOrderConfiguration(); + + if (log.isDebugEnabled()) { + log.debug("Initialize Nacos Config Parameter ,is managed {}.", + edasChangeOrderConfiguration.isEdasManaged()); + } + + if (!edasChangeOrderConfiguration.isEdasManaged()) { + return; + } + + System.getProperties().setProperty("spring.cloud.nacos.config.server-mode", + "EDAS"); + // initialize nacos configuration + System.getProperties().setProperty("spring.cloud.nacos.config.server-addr", ""); + System.getProperties().setProperty("spring.cloud.nacos.config.endpoint", + edasChangeOrderConfiguration.getAddressServerDomain()); + System.getProperties().setProperty("spring.cloud.nacos.config.namespace", + edasChangeOrderConfiguration.getTenantId()); + System.getProperties().setProperty("spring.cloud.nacos.config.access-key", + edasChangeOrderConfiguration.getDauthAccessKey()); + System.getProperties().setProperty("spring.cloud.nacos.config.secret-key", + edasChangeOrderConfiguration.getDauthSecretKey()); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListener.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListener.java new file mode 100644 index 00000000..2abba6ee --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListener.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.context.nacos; + +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; + +import com.alibaba.alicloud.context.listener.AbstractOnceApplicationListener; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfiguration; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author pbting + * @date 2019-02-14 11:12 AM + */ +public class NacosDiscoveryParameterInitListener + extends AbstractOnceApplicationListener { + private static final Logger log = LoggerFactory + .getLogger(NacosDiscoveryParameterInitListener.class); + + @Override + protected String conditionalOnClass() { + return "com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration"; + } + + @Override + protected void handleEvent(ApplicationEnvironmentPreparedEvent event) { + EdasChangeOrderConfiguration edasChangeOrderConfiguration = EdasChangeOrderConfigurationFactory + .getEdasChangeOrderConfiguration(); + + if (log.isDebugEnabled()) { + log.debug("Initialize Nacos Discovery Parameter ,is managed {}.", + edasChangeOrderConfiguration.isEdasManaged()); + } + + if (!edasChangeOrderConfiguration.isEdasManaged()) { + return; + } + // initialize nacos configuration + Properties properties = System.getProperties(); + properties.setProperty("spring.cloud.nacos.discovery.server-mode", "EDAS"); + // step 1: set some properties for spring cloud alibaba nacos discovery + properties.setProperty("spring.cloud.nacos.discovery.server-addr", ""); + properties.setProperty("spring.cloud.nacos.discovery.endpoint", + edasChangeOrderConfiguration.getAddressServerDomain()); + properties.setProperty("spring.cloud.nacos.discovery.namespace", + edasChangeOrderConfiguration.getTenantId()); + properties.setProperty("spring.cloud.nacos.discovery.access-key", + edasChangeOrderConfiguration.getDauthAccessKey()); + properties.setProperty("spring.cloud.nacos.discovery.secret-key", + edasChangeOrderConfiguration.getDauthSecretKey()); + + // step 2: set these properties for nacos client + properties.setProperty("nacos.naming.web.context", "/vipserver"); + properties.setProperty("nacos.naming.exposed.port", "80"); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssContextAutoConfiguration.java new file mode 100644 index 00000000..840783f8 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssContextAutoConfiguration.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.oss; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.cloud.context.AliCloudAuthorizationMode; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; + +/** + * OSS Auto {@link Configuration} + * + * @author Jim + * @author xiaolongzuo + */ +@Configuration +@ConditionalOnClass(name = "com.alibaba.alicloud.oss.OssAutoConfiguration") +@ConditionalOnProperty(name = "spring.cloud.alicloud.oss.enabled", matchIfMissing = true) +@EnableConfigurationProperties(OssProperties.class) +@ImportAutoConfiguration(AliCloudContextAutoConfiguration.class) +public class OssContextAutoConfiguration { + + @ConditionalOnMissingBean + @Bean + public OSS ossClient(AliCloudProperties aliCloudProperties, + OssProperties ossProperties) { + if (ossProperties.getAuthorizationMode() == AliCloudAuthorizationMode.AK_SK) { + Assert.isTrue(!StringUtils.isEmpty(ossProperties.getEndpoint()), + "Oss endpoint can't be empty."); + Assert.isTrue(!StringUtils.isEmpty(aliCloudProperties.getAccessKey()), + "${spring.cloud.alicloud.access-key} can't be empty."); + Assert.isTrue(!StringUtils.isEmpty(aliCloudProperties.getSecretKey()), + "${spring.cloud.alicloud.secret-key} can't be empty."); + return new OSSClientBuilder().build(ossProperties.getEndpoint(), + aliCloudProperties.getAccessKey(), aliCloudProperties.getSecretKey(), + ossProperties.getConfig()); + } + else if (ossProperties.getAuthorizationMode() == AliCloudAuthorizationMode.STS) { + Assert.isTrue(!StringUtils.isEmpty(ossProperties.getEndpoint()), + "Oss endpoint can't be empty."); + Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getAccessKey()), + "Access key can't be empty."); + Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getSecretKey()), + "Secret key can't be empty."); + Assert.isTrue(!StringUtils.isEmpty(ossProperties.getSts().getSecurityToken()), + "Security Token can't be empty."); + return new OSSClientBuilder().build(ossProperties.getEndpoint(), + ossProperties.getSts().getAccessKey(), + ossProperties.getSts().getSecretKey(), + ossProperties.getSts().getSecurityToken(), ossProperties.getConfig()); + } + else { + throw new IllegalArgumentException("Unknown auth mode."); + } + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssProperties.java new file mode 100644 index 00000000..a261e6ad --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/oss/OssProperties.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.oss; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.context.AliCloudAuthorizationMode; + +import com.aliyun.oss.ClientBuilderConfiguration; + +/** + * {@link ConfigurationProperties} for configuring OSS. + * + * @author Jim + * @author xiaolongzuo + */ +@ConfigurationProperties("spring.cloud.alicloud.oss") +public class OssProperties { + + /** + * Authorization Mode, please see oss + * docs. + */ + @Value("${spring.cloud.alicloud.oss.authorization-mode:AK_SK}") + private AliCloudAuthorizationMode authorizationMode; + + /** + * Endpoint, please see oss + * docs. + */ + private String endpoint; + + /** + * Sts token, please see oss + * docs. + */ + private StsToken sts; + + /** + * Client Configuration, please see oss + * docs. + */ + private ClientBuilderConfiguration config; + + public AliCloudAuthorizationMode getAuthorizationMode() { + return authorizationMode; + } + + public void setAuthorizationMode(AliCloudAuthorizationMode authorizationMode) { + this.authorizationMode = authorizationMode; + } + + public ClientBuilderConfiguration getConfig() { + return config; + } + + public void setConfig(ClientBuilderConfiguration config) { + this.config = config; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public StsToken getSts() { + return sts; + } + + public void setSts(StsToken sts) { + this.sts = sts; + } + + public static class StsToken { + + private String accessKey; + + private String secretKey; + + private String securityToken; + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getSecurityToken() { + return securityToken; + } + + public void setSecurityToken(String securityToken) { + this.securityToken = securityToken; + } + + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxContextAutoConfiguration.java new file mode 100644 index 00000000..69504bb9 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxContextAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.scx; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.cloud.context.edas.AliCloudEdasSdk; +import com.alibaba.cloud.context.scx.AliCloudScxInitializer; +import com.alibaba.edas.schedulerx.SchedulerXClient; + +/** + * @author xiaolongzuo + */ +@Configuration +@ConditionalOnClass(name = "com.alibaba.alicloud.scx.ScxAutoConfiguration") +@ConditionalOnProperty(name = "spring.cloud.alicloud.scx.enabled", matchIfMissing = true) +@EnableConfigurationProperties(ScxProperties.class) +@ImportAutoConfiguration(EdasContextAutoConfiguration.class) +public class ScxContextAutoConfiguration { + + @Bean(initMethod = "init") + @ConditionalOnMissingBean + public SchedulerXClient schedulerXClient(AliCloudProperties aliCloudProperties, + EdasProperties edasProperties, ScxProperties scxProperties, + AliCloudEdasSdk aliCloudEdasSdk) { + return AliCloudScxInitializer.initialize(aliCloudProperties, edasProperties, + scxProperties, aliCloudEdasSdk); + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxProperties.java new file mode 100644 index 00000000..e0671705 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/scx/ScxProperties.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.scx; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.context.scx.ScxConfiguration; + +/** + * @author xiaolongzuo + */ +@ConfigurationProperties("spring.cloud.alicloud.scx") +public class ScxProperties implements ScxConfiguration { + + /** + * Group id, please see scx + * docs. + */ + private String groupId; + + /** + * Domain name, please see scx + * docs. + */ + private String domainName; + + @Override + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @Override + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListener.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListener.java new file mode 100644 index 00000000..2a0c6b50 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListener.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.sentinel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; + +import com.alibaba.alicloud.context.Constants; +import com.alibaba.alicloud.context.listener.AbstractOnceApplicationListener; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfiguration; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author Jim + */ +public class SentinelAliCloudListener + extends AbstractOnceApplicationListener { + + private static final Logger logger = LoggerFactory + .getLogger(SentinelAliCloudListener.class); + + @Override + protected void handleEvent(ApplicationEnvironmentPreparedEvent event) { + EdasChangeOrderConfiguration edasChangeOrderConfiguration = EdasChangeOrderConfigurationFactory + .getEdasChangeOrderConfiguration(); + logger.info("Sentinel Nacos datasource will" + + (edasChangeOrderConfiguration.isEdasManaged() ? " be " : " not be ") + + "changed by edas change order."); + if (!edasChangeOrderConfiguration.isEdasManaged()) { + return; + } + System.getProperties().setProperty(Constants.Sentinel.NACOS_DATASOURCE_ENDPOINT, + edasChangeOrderConfiguration.getAddressServerDomain()); + System.getProperties().setProperty(Constants.Sentinel.NACOS_DATASOURCE_NAMESPACE, + edasChangeOrderConfiguration.getTenantId()); + System.getProperties().setProperty(Constants.Sentinel.NACOS_DATASOURCE_AK, + edasChangeOrderConfiguration.getDauthAccessKey()); + System.getProperties().setProperty(Constants.Sentinel.NACOS_DATASOURCE_SK, + edasChangeOrderConfiguration.getDauthSecretKey()); + System.getProperties().setProperty(Constants.Sentinel.PROJECT_NAME, + edasChangeOrderConfiguration.getProjectName()); + } + + @Override + protected String conditionalOnClass() { + return "com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource"; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsContextAutoConfiguration.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsContextAutoConfiguration.java new file mode 100644 index 00000000..4b23c3fc --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsContextAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.alibaba.alicloud.context.sms; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author pbting + * @author xiaolongzuo + */ +@Configuration +@EnableConfigurationProperties(SmsProperties.class) +@ConditionalOnClass(name = "com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest") +@ConditionalOnProperty(value = "spring.cloud.alibaba.deshao.enable.sms", matchIfMissing = true) +public class SmsContextAutoConfiguration { + +} \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsProperties.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsProperties.java new file mode 100644 index 00000000..8ada0a80 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/sms/SmsProperties.java @@ -0,0 +1,74 @@ +package com.alibaba.alicloud.context.sms; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author pbting + * @author xiaolongzuo + */ +@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms") +public class SmsProperties { + + /** + * Product name. + */ + public static final String SMS_PRODUCT = "Dysmsapi"; + + /** + * Product domain. + */ + public static final String SMS_DOMAIN = "dysmsapi.aliyuncs.com"; + + /** + * Report queue name. + */ + private String reportQueueName; + + /** + * Up queue name. + */ + private String upQueueName; + + /** + * Connect timeout. + */ + private String connectTimeout = "10000"; + + /** + * Read timeout. + */ + private String readTimeout = "10000"; + + public String getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(String connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public String getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(String readTimeout) { + this.readTimeout = readTimeout; + } + + public String getReportQueueName() { + return reportQueueName; + } + + public void setReportQueueName(String reportQueueName) { + this.reportQueueName = reportQueueName; + } + + public String getUpQueueName() { + return upQueueName; + } + + public void setUpQueueName(String upQueueName) { + this.upQueueName = upQueueName; + } + +} \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/statistics/StatisticsTaskStarter.java b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/statistics/StatisticsTaskStarter.java new file mode 100644 index 00000000..c3e64a66 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/java/com/alibaba/alicloud/context/statistics/StatisticsTaskStarter.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.statistics; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration; +import com.alibaba.alicloud.context.acm.AcmProperties; +import com.alibaba.alicloud.context.ans.AnsContextAutoConfiguration; +import com.alibaba.alicloud.context.ans.AnsProperties; +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.alicloud.context.oss.OssContextAutoConfiguration; +import com.alibaba.alicloud.context.oss.OssProperties; +import com.alibaba.alicloud.context.scx.ScxContextAutoConfiguration; +import com.alibaba.alicloud.context.scx.ScxProperties; +import com.alibaba.cloud.context.AliCloudServerMode; +import com.alibaba.cloud.context.edas.AliCloudEdasSdk; +import com.alibaba.cloud.context.statistics.StatisticsTask; + +/** + * @author xiaolongzuo + */ +@Configuration +@AutoConfigureAfter({ ScxContextAutoConfiguration.class, + OssContextAutoConfiguration.class, AnsContextAutoConfiguration.class, + AcmContextBootstrapConfiguration.class }) +public class StatisticsTaskStarter implements InitializingBean { + + private static final String NACOS_CONFIG_SERVER_MODE_KEY = "spring.cloud.nacos.config.server-mode"; + + private static final String NACOS_DISCOVERY_SERVER_MODE_KEY = "spring.cloud.nacos.discovery.server-mode"; + + private static final String NACOS_SERVER_MODE_VALUE = "EDAS"; + + @Autowired(required = false) + private AliCloudEdasSdk aliCloudEdasSdk; + + @Autowired(required = false) + private EdasProperties edasProperties; + + @Autowired(required = false) + private ScxProperties scxProperties; + + @Autowired(required = false) + private OssProperties ossProperties; + + @Autowired(required = false) + private AnsProperties ansProperties; + + @Autowired(required = false) + private AcmProperties acmProperties; + + @Autowired(required = false) + private ScxContextAutoConfiguration scxContextAutoConfiguration; + + @Autowired(required = false) + private OssContextAutoConfiguration ossContextAutoConfiguration; + + @Autowired(required = false) + private AnsContextAutoConfiguration ansContextAutoConfiguration; + + @Autowired(required = false) + private AcmContextBootstrapConfiguration acmContextBootstrapConfiguration; + + @Override + public void afterPropertiesSet() { + StatisticsTask statisticsTask = new StatisticsTask(aliCloudEdasSdk, + edasProperties, getComponents()); + statisticsTask.start(); + } + + private List getComponents() { + List components = new ArrayList<>(); + if (scxContextAutoConfiguration != null && scxProperties != null) { + components.add("SC-SCX"); + } + if (ossContextAutoConfiguration != null && ossProperties != null) { + components.add("SC-OSS"); + } + boolean edasEnabled = edasProperties != null && edasProperties.isEnabled(); + boolean ansEnableEdas = edasEnabled || (ansProperties != null + && ansProperties.getServerMode() == AliCloudServerMode.EDAS); + if (ansContextAutoConfiguration != null && ansEnableEdas) { + components.add("SC-ANS"); + } + boolean acmEnableEdas = edasEnabled || (acmProperties != null + && acmProperties.getServerMode() == AliCloudServerMode.EDAS); + if (acmContextBootstrapConfiguration != null && acmEnableEdas) { + components.add("SC-ACM"); + } + if (NACOS_SERVER_MODE_VALUE + .equals(System.getProperty(NACOS_CONFIG_SERVER_MODE_KEY))) { + components.add("SC-NACOS-CONFIG"); + } + if (NACOS_SERVER_MODE_VALUE + .equals(System.getProperty(NACOS_DISCOVERY_SERVER_MODE_KEY))) { + components.add("SC-NACOS-DISCOVERY"); + } + return components; + } + +} diff --git a/spring-cloud-alicloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-alicloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..2b81aad2 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,16 @@ +{ + "properties": [ + { + "name": "spring.cloud.alicloud.ans.client-domains", + "type": "java.lang.String", + "defaultValue": "", + "description": "Service name list, default value is ${spring.application.name}." + }, + { + "name": "spring.cloud.alicloud.ans.env", + "type": "java.lang.String", + "defaultValue": "DEFAULT", + "description": "The env for ans, default value is DEFAULT." + } + ] +} \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-context/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..8c6c9957 --- /dev/null +++ b/spring-cloud-alicloud-context/src/main/resources/META-INF/spring.factories @@ -0,0 +1,15 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + com.alibaba.alicloud.context.acm.AcmContextBootstrapConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.alicloud.context.AliCloudContextAutoConfiguration,\ + com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration,\ + com.alibaba.alicloud.context.ans.AnsContextAutoConfiguration,\ + com.alibaba.alicloud.context.oss.OssContextAutoConfiguration,\ + com.alibaba.alicloud.context.scx.ScxContextAutoConfiguration,\ + com.alibaba.alicloud.context.statistics.StatisticsTaskStarter,\ + com.alibaba.alicloud.context.sms.SmsContextAutoConfiguration +org.springframework.context.ApplicationListener=\ + com.alibaba.alicloud.context.ans.AnsContextApplicationListener,\ + com.alibaba.alicloud.context.nacos.NacosConfigParameterInitListener,\ + com.alibaba.alicloud.context.nacos.NacosDiscoveryParameterInitListener,\ + com.alibaba.alicloud.context.sentinel.SentinelAliCloudListener \ No newline at end of file diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java new file mode 100644 index 00000000..9fbb2ed8 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/acm/AcmAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.acm; + +/** + * @author xiaolongzuo + */ +public class AcmAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java new file mode 100644 index 00000000..94946f00 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/ans/AnsAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.ans; + +/** + * @author xiaolongzuo + */ +public class AnsAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/AliCloudPropertiesTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/AliCloudPropertiesTests.java new file mode 100644 index 00000000..34afd223 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/AliCloudPropertiesTests.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * @author xiaolongzuo + */ +public class AliCloudPropertiesTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(AliCloudContextAutoConfiguration.class)); + + @Test + public void testConfigurationValueDefaultsAreAsExpected() { + this.contextRunner.run(context -> { + AliCloudProperties aliCloudProperties = context + .getBean(AliCloudProperties.class); + assertThat(aliCloudProperties.getAccessKey()).isNull(); + assertThat(aliCloudProperties.getSecretKey()).isNull(); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded() { + this.contextRunner.withPropertyValues("spring.cloud.alicloud.access-key=123", + "spring.cloud.alicloud.secret-key=123456").run(context -> { + AliCloudProperties aliCloudProperties = context + .getBean(AliCloudProperties.class); + assertThat(aliCloudProperties.getAccessKey()).isEqualTo("123"); + assertThat(aliCloudProperties.getSecretKey()).isEqualTo("123456"); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/BaseAliCloudSpringApplication.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/BaseAliCloudSpringApplication.java new file mode 100644 index 00000000..72fe9d28 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/BaseAliCloudSpringApplication.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context; + +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author xiaolongzuo + */ +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PowerMockIgnore("javax.management.*") +@SpringBootTest(classes = BaseAliCloudSpringApplication.AliCloudApplication.class, properties = { + "spring.application.name=myapp", + "spring.cloud.alicloud.edas.application.name=myapp", + "spring.cloud.alicloud.access-key=ak", "spring.cloud.alicloud.secret-key=sk", + "spring.cloud.alicloud.oss.endpoint=test", + "spring.cloud.alicloud.scx.group-id=1-2-3-4", + "spring.cloud.alicloud.edas.namespace=cn-test", + "spring.cloud.alicloud.ans.server-list=192.168.1.100", + "spring.cloud.alicloud.ans.server-port=8888", + "spring.cloud.alicloud.oss.enabled=false", + "spring.cloud.alicloud.scx.enabled=false" }) +public abstract class BaseAliCloudSpringApplication { + + @SpringBootApplication + public static class AliCloudApplication { + + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/acm/AcmPropertiesTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/acm/AcmPropertiesTests.java new file mode 100644 index 00000000..cbd4ff90 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/acm/AcmPropertiesTests.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.acm; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; +import com.alibaba.cloud.context.AliCloudServerMode; + +/** + * @author xiaolongzuo + */ +public class AcmPropertiesTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(AcmContextBootstrapConfiguration.class, + EdasContextAutoConfiguration.class, + AliCloudContextAutoConfiguration.class)); + + @Test + public void testConfigurationValueDefaultsAreAsExpected() { + this.contextRunner.withPropertyValues("spring.application.name=myapp") + .run(context -> { + AcmProperties config = context.getBean(AcmProperties.class); + assertThat(config.getServerMode()) + .isEqualTo(AliCloudServerMode.LOCAL); + assertThat(config.getServerList()).isEqualTo("127.0.0.1"); + assertThat(config.getServerPort()).isEqualTo("8080"); + assertThat(config.getEndpoint()).isNull(); + assertThat(config.getFileExtension()).isEqualTo("properties"); + assertThat(config.getGroup()).isEqualTo("DEFAULT_GROUP"); + assertThat(config.getNamespace()).isNull(); + assertThat(config.getRamRoleName()).isNull(); + assertThat(config.getTimeout()).isEqualTo(3000); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded() { + this.contextRunner.withPropertyValues("spring.application.name=myapp", + "spring.cloud.alicloud.access-key=ak", + "spring.cloud.alicloud.secret-key=sk", + "spring.cloud.alicloud.acm.server-mode=EDAS", + "spring.cloud.alicloud.acm.server-port=11111", + "spring.cloud.alicloud.acm.server-list=10.10.10.10", + "spring.cloud.alicloud.acm.namespace=testNamespace", + "spring.cloud.alicloud.acm.endpoint=testDomain", + "spring.cloud.alicloud.acm.group=testGroup", + "spring.cloud.alicloud.acm.file-extension=yaml").run(context -> { + AcmProperties acmProperties = context.getBean(AcmProperties.class); + assertThat(acmProperties.getServerMode()) + .isEqualTo(AliCloudServerMode.EDAS); + assertThat(acmProperties.getServerList()).isEqualTo("10.10.10.10"); + assertThat(acmProperties.getServerPort()).isEqualTo("11111"); + assertThat(acmProperties.getEndpoint()).isEqualTo("testDomain"); + assertThat(acmProperties.getGroup()).isEqualTo("testGroup"); + assertThat(acmProperties.getFileExtension()).isEqualTo("yaml"); + assertThat(acmProperties.getNamespace()).isEqualTo("testNamespace"); + }); + } + + @Test + public void testAcmIntegrationConfigurationValuesAreCorrectlyLoaded() { + this.contextRunner.withPropertyValues("spring.application.name=myapp", + "spring.application.group=com.alicloud.test", + "spring.cloud.alicloud.access-key=ak", + "spring.cloud.alicloud.secret-key=sk", + "spring.cloud.alicloud.acm.server-mode=EDAS", + "spring.cloud.alicloud.acm.server-port=11111", + "spring.cloud.alicloud.acm.server-list=10.10.10.10", + "spring.cloud.alicloud.acm.namespace=testNamespace", + "spring.cloud.alicloud.acm.endpoint=testDomain", + "spring.cloud.alicloud.acm.group=testGroup", + "spring.cloud.alicloud.acm.file-extension=yaml").run(context -> { + AcmIntegrationProperties acmIntegrationProperties = context + .getBean(AcmIntegrationProperties.class); + assertThat(acmIntegrationProperties.getGroupConfigurationDataIds() + .size()).isEqualTo(2); + assertThat(acmIntegrationProperties + .getApplicationConfigurationDataIds().size()).isEqualTo(2); + }); + } + + @Test + public void testAcmIntegrationConfigurationValuesAreCorrectlyLoaded2() { + this.contextRunner.withPropertyValues("spring.application.name=myapp", + "spring.application.group=com.alicloud.test", + "spring.profiles.active=profile1,profile2", + "spring.cloud.alicloud.access-key=ak", + "spring.cloud.alicloud.secret-key=sk", + "spring.cloud.alicloud.acm.server-mode=EDAS", + "spring.cloud.alicloud.acm.server-port=11111", + "spring.cloud.alicloud.acm.server-list=10.10.10.10", + "spring.cloud.alicloud.acm.namespace=testNamespace", + "spring.cloud.alicloud.acm.endpoint=testDomain", + "spring.cloud.alicloud.acm.group=testGroup", + "spring.cloud.alicloud.acm.file-extension=yaml").run(context -> { + AcmIntegrationProperties acmIntegrationProperties = context + .getBean(AcmIntegrationProperties.class); + assertThat(acmIntegrationProperties.getGroupConfigurationDataIds() + .size()).isEqualTo(2); + assertThat(acmIntegrationProperties + .getApplicationConfigurationDataIds().size()).isEqualTo(6); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListenerTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListenerTests.java new file mode 100644 index 00000000..a60923be --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsContextApplicationListenerTests.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.ans; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.Test; + +import com.alibaba.alicloud.context.BaseAliCloudSpringApplication; + +/** + * @author xiaolongzuo + */ +public class AnsContextApplicationListenerTests extends BaseAliCloudSpringApplication { + + @Test + public void testAnsContextApplicationListenerDefault() { + assertThat(System + .getProperty("com.alibaba.ans.shaded.com.taobao.vipserver.serverlist")) + .isEqualTo("192.168.1.100"); + assertThat(System.getProperty("vipserver.server.port")).isEqualTo("8888"); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsPropertiesTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsPropertiesTests.java new file mode 100644 index 00000000..225acc7b --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/ans/AnsPropertiesTests.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.ans; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Vector; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; +import com.alibaba.cloud.context.AliCloudServerMode; + +/** + * @author xiaolongzuo + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ NetworkInterface.class, AnsProperties.class }) +public class AnsPropertiesTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AnsContextAutoConfiguration.class, + EdasContextAutoConfiguration.class, + AliCloudContextAutoConfiguration.class)); + + @Test + public void testConfigurationValueDefaultsAreAsExpected() { + this.contextRunner.withPropertyValues().run(context -> { + AnsProperties ansProperties = context.getBean(AnsProperties.class); + assertThat(ansProperties.getServerMode()).isEqualTo(AliCloudServerMode.LOCAL); + assertThat(ansProperties.getServerList()).isEqualTo("127.0.0.1"); + assertThat(ansProperties.getServerPort()).isEqualTo("8080"); + assertThat(ansProperties.getClientDomains()).isEqualTo(""); + assertThat(ansProperties.getClientWeight()).isEqualTo(1.0F); + assertThat(ansProperties.getClientWeights().size()).isEqualTo(0); + assertThat(ansProperties.getClientTokens().size()).isEqualTo(0); + assertThat(ansProperties.getClientMetadata().size()).isEqualTo(0); + assertThat(ansProperties.getClientToken()).isNull(); + assertThat(ansProperties.getClientCluster()).isEqualTo("DEFAULT"); + assertThat(ansProperties.isRegisterEnabled()).isTrue(); + assertThat(ansProperties.getClientInterfaceName()).isNull(); + assertThat(ansProperties.getClientPort()).isEqualTo(-1); + assertThat(ansProperties.getEnv()).isEqualTo("DEFAULT"); + assertThat(ansProperties.isSecure()).isFalse(); + assertThat(ansProperties.getTags().size()).isEqualTo(1); + assertThat(ansProperties.getTags().keySet().iterator().next()) + .isEqualTo("ANS_SERVICE_TYPE"); + assertThat(ansProperties.getTags().get("ANS_SERVICE_TYPE")) + .isEqualTo("SPRING_CLOUD"); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded1() { + this.contextRunner + .withPropertyValues("spring.cloud.alicloud.ans.server-mode=EDAS", + "spring.cloud.alicloud.ans.server-port=11111", + "spring.cloud.alicloud.ans.server-list=10.10.10.10", + "spring.cloud.alicloud.ans.client-domains=testDomain", + "spring.cloud.alicloud.ans.client-weight=0.9", + "spring.cloud.alicloud.ans.client-weights.testDomain=0.9") + .run(context -> { + AnsProperties ansProperties = context.getBean(AnsProperties.class); + assertThat(ansProperties.getServerMode()) + .isEqualTo(AliCloudServerMode.EDAS); + assertThat(ansProperties.getServerList()).isEqualTo("10.10.10.10"); + assertThat(ansProperties.getServerPort()).isEqualTo("11111"); + assertThat(ansProperties.getClientDomains()).isEqualTo("testDomain"); + assertThat(ansProperties.getClientWeight()).isEqualTo(0.9F); + assertThat(ansProperties.getClientWeights().size()).isEqualTo(1); + assertThat(ansProperties.getClientTokens().size()).isEqualTo(0); + assertThat(ansProperties.getClientMetadata().size()).isEqualTo(0); + assertThat(ansProperties.getClientToken()).isNull(); + assertThat(ansProperties.getClientCluster()).isEqualTo("DEFAULT"); + assertThat(ansProperties.isRegisterEnabled()).isTrue(); + assertThat(ansProperties.getClientInterfaceName()).isNull(); + assertThat(ansProperties.getClientPort()).isEqualTo(-1); + assertThat(ansProperties.getEnv()).isEqualTo("DEFAULT"); + assertThat(ansProperties.isSecure()).isFalse(); + assertThat(ansProperties.getTags().size()).isEqualTo(1); + assertThat(ansProperties.getTags().keySet().iterator().next()) + .isEqualTo("ANS_SERVICE_TYPE"); + assertThat(ansProperties.getTags().get("ANS_SERVICE_TYPE")) + .isEqualTo("SPRING_CLOUD"); + }); + } + + @Test(expected = RuntimeException.class) + public void testConfigurationValuesAreCorrectlyLoaded2() { + this.contextRunner.withPropertyValues( + "spring.cloud.alicloud.ans.client-interface-name=noneinterfacename") + .run(context -> { + AnsProperties ansProperties = context.getBean(AnsProperties.class); + assertThat(ansProperties.getClientInterfaceName()) + .isEqualTo("noneinterfacename"); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded3() throws SocketException { + NetworkInterface networkInterface = PowerMockito.mock(NetworkInterface.class); + Vector inetAddressList = new Vector<>(); + Inet4Address inetAddress = PowerMockito.mock(Inet4Address.class); + PowerMockito.when(inetAddress.getHostAddress()).thenReturn("192.168.1.100"); + inetAddressList.add(inetAddress); + PowerMockito.when(networkInterface.getInetAddresses()) + .thenReturn(inetAddressList.elements()); + PowerMockito.mockStatic(NetworkInterface.class); + PowerMockito.when(NetworkInterface.getByName("eth0")) + .thenReturn(networkInterface); + this.contextRunner + .withPropertyValues( + "spring.cloud.alicloud.ans.client-interface-name=eth0") + .run(context -> { + AnsProperties ansProperties = context.getBean(AnsProperties.class); + assertThat(ansProperties.getClientInterfaceName()).isEqualTo("eth0"); + assertThat(ansProperties.getClientIp()).isEqualTo("192.168.1.100"); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/edas/EdasPropertiesTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/edas/EdasPropertiesTests.java new file mode 100644 index 00000000..f16d9f6c --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/edas/EdasPropertiesTests.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.edas; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; + +/** + * @author xiaolongzuo + */ +public class EdasPropertiesTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EdasContextAutoConfiguration.class, + AliCloudContextAutoConfiguration.class)); + + @Test + public void testConfigurationValueDefaultsAreAsExpected() { + this.contextRunner.withPropertyValues().run(context -> { + EdasProperties edasProperties = context.getBean(EdasProperties.class); + assertThat(edasProperties.getNamespace()).isNull(); + assertThat(edasProperties.isApplicationNameValid()).isFalse(); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded1() { + this.contextRunner + .withPropertyValues("spring.cloud.alicloud.edas.namespace=testns", + "spring.application.name=myapps") + .run(context -> { + EdasProperties edasProperties = context.getBean(EdasProperties.class); + assertThat(edasProperties.getNamespace()).isEqualTo("testns"); + assertThat(edasProperties.getApplicationName()).isEqualTo("myapps"); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded2() { + this.contextRunner + .withPropertyValues("spring.cloud.alicloud.edas.namespace=testns", + "spring.cloud.alicloud.edas.application.name=myapps") + .run(context -> { + EdasProperties edasProperties = context.getBean(EdasProperties.class); + assertThat(edasProperties.getNamespace()).isEqualTo("testns"); + assertThat(edasProperties.getApplicationName()).isEqualTo("myapps"); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListenerTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListenerTests.java new file mode 100644 index 00000000..6b028f09 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosConfigParameterInitListenerTests.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.nacos; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import com.alibaba.alicloud.context.BaseAliCloudSpringApplication; +import com.alibaba.alicloud.utils.ChangeOrderUtils; +import com.alibaba.cloud.context.ans.AliCloudAnsInitializer; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author xiaolongzuo + */ +@PrepareForTest({ EdasChangeOrderConfigurationFactory.class, + NacosConfigParameterInitListener.class, AliCloudAnsInitializer.class }) +public class NacosConfigParameterInitListenerTests extends BaseAliCloudSpringApplication { + + @BeforeClass + public static void setUp() { + ChangeOrderUtils.mockChangeOrder(); + } + + @Test + public void testNacosParameterInitListener() { + assertThat(System.getProperty("spring.cloud.nacos.config.server-mode")) + .isEqualTo("EDAS"); + assertThat(System.getProperty("spring.cloud.nacos.config.server-addr")) + .isEqualTo(""); + assertThat(System.getProperty("spring.cloud.nacos.config.endpoint")) + .isEqualTo("testDomain"); + assertThat(System.getProperty("spring.cloud.nacos.config.namespace")) + .isEqualTo("testTenantId"); + assertThat(System.getProperty("spring.cloud.nacos.config.access-key")) + .isEqualTo("testAK"); + assertThat(System.getProperty("spring.cloud.nacos.config.secret-key")) + .isEqualTo("testSK"); + + } +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListenerTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListenerTests.java new file mode 100644 index 00000000..58ceebd6 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/nacos/NacosDiscoveryParameterInitListenerTests.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.nacos; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import com.alibaba.alicloud.context.BaseAliCloudSpringApplication; +import com.alibaba.alicloud.utils.ChangeOrderUtils; +import com.alibaba.cloud.context.ans.AliCloudAnsInitializer; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author xiaolongzuo + */ +@PrepareForTest({ EdasChangeOrderConfigurationFactory.class, + NacosDiscoveryParameterInitListener.class, AliCloudAnsInitializer.class }) +public class NacosDiscoveryParameterInitListenerTests + extends BaseAliCloudSpringApplication { + + @BeforeClass + public static void setUp() { + ChangeOrderUtils.mockChangeOrder(); + } + + @Test + public void testNacosParameterInitListener() { + assertThat(System.getProperty("spring.cloud.nacos.discovery.server-mode")) + .isEqualTo("EDAS"); + assertThat(System.getProperty("spring.cloud.nacos.discovery.server-addr")) + .isEqualTo(""); + assertThat(System.getProperty("spring.cloud.nacos.discovery.endpoint")) + .isEqualTo("testDomain"); + assertThat(System.getProperty("spring.cloud.nacos.discovery.namespace")) + .isEqualTo("testTenantId"); + assertThat(System.getProperty("spring.cloud.nacos.discovery.access-key")) + .isEqualTo("testAK"); + assertThat(System.getProperty("spring.cloud.nacos.discovery.secret-key")) + .isEqualTo("testSK"); + assertThat(System.getProperties().getProperty("nacos.naming.web.context")) + .isEqualTo("/vipserver"); + assertThat(System.getProperties().getProperty("nacos.naming.exposed.port")) + .isEqualTo("80"); + } +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/oss/OssAutoConfigurationTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/oss/OssAutoConfigurationTests.java new file mode 100644 index 00000000..b3317a29 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/oss/OssAutoConfigurationTests.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.oss; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.AliCloudProperties; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClient; + +/** + * {@link OSS} {@link OssProperties} Test + * + * @author Jim + */ +public class OssAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OssContextAutoConfiguration.class)) + .withPropertyValues("spring.cloud.alicloud.accessKey=your-ak", + "spring.cloud.alicloud.secretKey=your-sk", + "spring.cloud.alicloud.oss.endpoint=http://oss-cn-beijing.aliyuncs.com", + "spring.cloud.alicloud.oss.config.userAgent=alibaba", + "spring.cloud.alicloud.oss.sts.access-key=your-sts-ak", + "spring.cloud.alicloud.oss.sts.secret-key=your-sts-sk", + "spring.cloud.alicloud.oss.sts.security-token=your-sts-token"); + + @Test + public void testOSSProperties() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(OssProperties.class).size() == 1).isTrue(); + AliCloudProperties aliCloudProperties = context + .getBean(AliCloudProperties.class); + OssProperties ossProperties = context.getBean(OssProperties.class); + assertThat(aliCloudProperties.getAccessKey()).isEqualTo("your-ak"); + assertThat(aliCloudProperties.getSecretKey()).isEqualTo("your-sk"); + assertThat(ossProperties.getEndpoint()) + .isEqualTo("http://oss-cn-beijing.aliyuncs.com"); + assertThat(ossProperties.getConfig().getUserAgent()).isEqualTo("alibaba"); + assertThat(ossProperties.getSts().getAccessKey()).isEqualTo("your-sts-ak"); + assertThat(ossProperties.getSts().getSecretKey()).isEqualTo("your-sts-sk"); + assertThat(ossProperties.getSts().getSecurityToken()) + .isEqualTo("your-sts-token"); + }); + } + + @Test + public void testOSSClient1() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(OSS.class).size() == 1).isTrue(); + assertThat(context.getBeanNamesForType(OSS.class)[0]).isEqualTo("ossClient"); + OSSClient ossClient = (OSSClient) context.getBean(OSS.class); + assertThat(ossClient.getEndpoint().toString()) + .isEqualTo("http://oss-cn-beijing.aliyuncs.com"); + assertThat(ossClient.getClientConfiguration().getUserAgent()) + .isEqualTo("alibaba"); + assertThat( + ossClient.getCredentialsProvider().getCredentials().getAccessKeyId()) + .isEqualTo("your-ak"); + assertThat(ossClient.getCredentialsProvider().getCredentials() + .getSecretAccessKey()).isEqualTo("your-sk"); + }); + } + + @Test + public void testOSSClient2() { + this.contextRunner + .withPropertyValues("spring.cloud.alicloud.oss.authorization-mode=STS") + .run(context -> { + assertThat(context.getBeansOfType(OSS.class).size() == 1).isTrue(); + assertThat(context.getBeanNamesForType(OSS.class)[0]) + .isEqualTo("ossClient"); + OSSClient ossClient = (OSSClient) context.getBean(OSS.class); + assertThat(ossClient.getEndpoint().toString()) + .isEqualTo("http://oss-cn-beijing.aliyuncs.com"); + assertThat(ossClient.getClientConfiguration().getUserAgent()) + .isEqualTo("alibaba"); + assertThat(ossClient.getCredentialsProvider().getCredentials() + .getAccessKeyId()).isEqualTo("your-sts-ak"); + assertThat(ossClient.getCredentialsProvider().getCredentials() + .getSecretAccessKey()).isEqualTo("your-sts-sk"); + assertThat(ossClient.getCredentialsProvider().getCredentials() + .getSecurityToken()).isEqualTo("your-sts-token"); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/scx/ScxAutoConfigurationTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/scx/ScxAutoConfigurationTests.java new file mode 100644 index 00000000..c9803f81 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/scx/ScxAutoConfigurationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.scx; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.edas.EdasProperties; + +/** + * @author xiaolongzuo + */ +public class ScxAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ScxContextAutoConfiguration.class)) + .withPropertyValues("spring.cloud.alicloud.scx.group-id=1-2-3-4") + .withPropertyValues("spring.cloud.alicloud.edas.namespace=cn-test"); + + @Test + public void testSxcProperties() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(ScxProperties.class).size() == 1).isTrue(); + EdasProperties edasProperties = context.getBean(EdasProperties.class); + ScxProperties scxProperties = context.getBean(ScxProperties.class); + assertThat(scxProperties.getGroupId()).isEqualTo("1-2-3-4"); + assertThat(edasProperties.getNamespace()).isEqualTo("cn-test"); + assertThat(scxProperties.getDomainName()).isNull(); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListenerTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListenerTests.java new file mode 100644 index 00000000..abc88e33 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sentinel/SentinelAliCloudListenerTests.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.sentinel; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import com.alibaba.alicloud.context.BaseAliCloudSpringApplication; +import com.alibaba.alicloud.context.Constants; +import com.alibaba.alicloud.utils.ChangeOrderUtils; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author xiaolongzuo + */ +@PrepareForTest({ EdasChangeOrderConfigurationFactory.class, + SentinelAliCloudListener.class }) +public class SentinelAliCloudListenerTests extends BaseAliCloudSpringApplication { + + @BeforeClass + public static void setUp() { + ChangeOrderUtils.mockChangeOrder(); + } + + @Test + public void testNacosParameterInitListener() { + assertThat(System.getProperty(Constants.Sentinel.NACOS_DATASOURCE_ENDPOINT)) + .isEqualTo("testDomain"); + assertThat(System.getProperty(Constants.Sentinel.PROJECT_NAME)) + .isEqualTo("testProjectName"); + assertThat(System.getProperty(Constants.Sentinel.NACOS_DATASOURCE_NAMESPACE)) + .isEqualTo("testTenantId"); + assertThat(System.getProperty(Constants.Sentinel.NACOS_DATASOURCE_AK)) + .isEqualTo("testAK"); + assertThat(System.getProperty(Constants.Sentinel.NACOS_DATASOURCE_SK)) + .isEqualTo("testSK"); + + } +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sms/SmsPropertiesTests.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sms/SmsPropertiesTests.java new file mode 100644 index 00000000..acc8201a --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/context/sms/SmsPropertiesTests.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.context.sms; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.alicloud.context.AliCloudContextAutoConfiguration; +import com.alibaba.alicloud.context.edas.EdasContextAutoConfiguration; + +/** + * @author xiaolongzuo + */ +public class SmsPropertiesTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SmsContextAutoConfiguration.class, + EdasContextAutoConfiguration.class, + AliCloudContextAutoConfiguration.class)); + + @Test + public void testConfigurationValueDefaultsAreAsExpected() { + this.contextRunner.run(context -> { + SmsProperties config = context.getBean(SmsProperties.class); + assertThat(config.getReportQueueName()).isNull(); + assertThat(config.getUpQueueName()).isNull(); + assertThat(config.getConnectTimeout()).isEqualTo("10000"); + assertThat(config.getReadTimeout()).isEqualTo("10000"); + }); + } + + @Test + public void testConfigurationValuesAreCorrectlyLoaded() { + this.contextRunner + .withPropertyValues("spring.cloud.alicloud.sms.reportQueueName=q1", + "spring.cloud.alicloud.sms.upQueueName=q2", + "spring.cloud.alicloud.sms.connect-timeout=20", + "spring.cloud.alicloud.sms.read-timeout=30") + .run(context -> { + SmsProperties config = context.getBean(SmsProperties.class); + assertThat(config.getReportQueueName()).isEqualTo("q1"); + assertThat(config.getUpQueueName()).isEqualTo("q2"); + assertThat(config.getConnectTimeout()).isEqualTo("20"); + assertThat(config.getReadTimeout()).isEqualTo("30"); + }); + } + +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java new file mode 100644 index 00000000..159d28ee --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss; + +/** + * @author xiaolongzuo + */ +public class OssAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java new file mode 100644 index 00000000..cae4aade --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.scx; + +/** + * @author xiaolongzuo + */ +public class ScxAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/utils/ChangeOrderUtils.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/utils/ChangeOrderUtils.java new file mode 100644 index 00000000..316f1758 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/alicloud/utils/ChangeOrderUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.utils; + +import org.powermock.api.mockito.PowerMockito; + +import com.alibaba.cloud.context.edas.EdasChangeOrderConfiguration; +import com.alibaba.cloud.context.edas.EdasChangeOrderConfigurationFactory; + +/** + * @author xiaolongzuo + */ +public class ChangeOrderUtils { + + private ChangeOrderUtils() { + } + + public static void mockChangeOrder() { + EdasChangeOrderConfiguration edasChangeOrderConfiguration = PowerMockito + .mock(EdasChangeOrderConfiguration.class); + PowerMockito.when(edasChangeOrderConfiguration.isEdasManaged()).thenReturn(true); + PowerMockito.when(edasChangeOrderConfiguration.getAddressServerDomain()) + .thenReturn("testDomain"); + PowerMockito.when(edasChangeOrderConfiguration.getTenantId()) + .thenReturn("testTenantId"); + PowerMockito.when(edasChangeOrderConfiguration.getDauthAccessKey()) + .thenReturn("testAK"); + PowerMockito.when(edasChangeOrderConfiguration.getDauthSecretKey()) + .thenReturn("testSK"); + PowerMockito.when(edasChangeOrderConfiguration.getProjectName()) + .thenReturn("testProjectName"); + PowerMockito.when(edasChangeOrderConfiguration.getAddressServerPort()) + .thenReturn("8080"); + PowerMockito.mockStatic(EdasChangeOrderConfigurationFactory.class); + PowerMockito + .when(EdasChangeOrderConfigurationFactory + .getEdasChangeOrderConfiguration()) + .thenReturn(edasChangeOrderConfiguration); + } +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java new file mode 100644 index 00000000..bef13171 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +/** + * @author xiaolongzuo + */ +public class NacosConfigAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java new file mode 100644 index 00000000..d9f47e11 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/cloud/nacos/NacosDiscoveryAutoConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.nacos; + +/** + * @author xiaolongzuo + */ +public class NacosDiscoveryAutoConfiguration { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/alibaba/csp/sentinel/datasource/nacos/NacosDataSource.java b/spring-cloud-alicloud-context/src/test/java/com/alibaba/csp/sentinel/datasource/nacos/NacosDataSource.java new file mode 100644 index 00000000..7159a2f4 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/alibaba/csp/sentinel/datasource/nacos/NacosDataSource.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.csp.sentinel.datasource.nacos; + +/** + * @author xiaolongzuo + */ +public class NacosDataSource { +} diff --git a/spring-cloud-alicloud-context/src/test/java/com/aliyuncs/dysmsapi/model/v20170525/SendSmsRequest.java b/spring-cloud-alicloud-context/src/test/java/com/aliyuncs/dysmsapi/model/v20170525/SendSmsRequest.java new file mode 100644 index 00000000..ef660e84 --- /dev/null +++ b/spring-cloud-alicloud-context/src/test/java/com/aliyuncs/dysmsapi/model/v20170525/SendSmsRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.aliyuncs.dysmsapi.model.v20170525; + +/** + * @author xiaolongzuo + */ +public class SendSmsRequest { +} diff --git a/spring-cloud-alicloud-oss/pom.xml b/spring-cloud-alicloud-oss/pom.xml new file mode 100644 index 00000000..68d30b85 --- /dev/null +++ b/spring-cloud-alicloud-oss/pom.xml @@ -0,0 +1,87 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alicloud-oss + Spring Cloud Alibaba Cloud OSS + + + + + com.alibaba.cloud + spring-cloud-alicloud-context + + + + com.aliyun.oss + aliyun-sdk-oss + + + + org.springframework.boot + spring-boot-actuator + provided + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.slf4j + slf4j-api + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssApplicationListener.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssApplicationListener.java new file mode 100644 index 00000000..7024cfc2 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssApplicationListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; + +import com.aliyun.oss.OSS; + +/** + * Shutdown All OSS Clients when {@code ApplicationContext} gets closed + * {@link ApplicationListener} + * + * @author Jim + */ +public class OssApplicationListener implements ApplicationListener { + + private static final Logger log = LoggerFactory + .getLogger(OssApplicationListener.class); + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + Map ossClientMap = event.getApplicationContext() + .getBeansOfType(OSS.class); + log.info("{} OSSClients will be shutdown soon", ossClientMap.size()); + ossClientMap.keySet().forEach(beanName -> { + log.info("shutdown ossClient: {}", beanName); + ossClientMap.get(beanName).shutdown(); + }); + } +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java new file mode 100644 index 00000000..0cbf049e --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.oss.resource.OssStorageProtocolResolver; + +import com.aliyun.oss.OSS; + +/** + * OSS Auto {@link Configuration} + * + * @author Jim + */ +@Configuration +@ConditionalOnClass(OSS.class) +@ConditionalOnProperty(name = OssConstants.ENABLED, havingValue = "true", matchIfMissing = true) +public class OssAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OssStorageProtocolResolver ossStorageProtocolResolver() { + return new OssStorageProtocolResolver(); + } + +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssConstants.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssConstants.java new file mode 100644 index 00000000..d018b892 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/OssConstants.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss; + +/** + * OSS constants + * + * @author Jim + */ +public interface OssConstants { + + String PREFIX = "spring.cloud.alibaba.oss"; + String ENABLED = PREFIX + ".enabled"; + +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpoint.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpoint.java new file mode 100644 index 00000000..e7ef504d --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpoint.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss.endpoint; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.context.ApplicationContext; + +import com.aliyun.oss.OSSClient; + +/** + * Actuator {@link Endpoint} to expose OSS Meta Data + * + * @author Jim + */ +@Endpoint(id = "oss") +public class OssEndpoint { + + @Autowired + private ApplicationContext applicationContext; + + @ReadOperation + public Map invoke() { + Map result = new HashMap<>(); + + Map ossClientMap = applicationContext + .getBeansOfType(OSSClient.class); + + int size = ossClientMap.size(); + + List ossClientList = new ArrayList<>(); + + ossClientMap.keySet().forEach(beanName -> { + Map ossProperties = new HashMap<>(); + OSSClient client = ossClientMap.get(beanName); + ossProperties.put("beanName", beanName); + ossProperties.put("endpoint", client.getEndpoint().toString()); + ossProperties.put("clientConfiguration", client.getClientConfiguration()); + ossProperties.put("credentials", + client.getCredentialsProvider().getCredentials()); + ossProperties.put("bucketList", client.listBuckets().stream() + .map(bucket -> bucket.getName()).toArray()); + ossClientList.add(ossProperties); + }); + + result.put("size", size); + result.put("info", ossClientList); + + return result; + } + +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpointAutoConfiguration.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpointAutoConfiguration.java new file mode 100644 index 00000000..e4e49f34 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/endpoint/OssEndpointAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss.endpoint; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * OSS {@link Endpoint} Auto-{@link Configuration} + * + * @author Jim + */ +@ConditionalOnClass(Endpoint.class) +public class OssEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public OssEndpoint ossEndpoint() { + return new OssEndpoint(); + } + +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageProtocolResolver.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageProtocolResolver.java new file mode 100644 index 00000000..2d1f2d50 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageProtocolResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss.resource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import com.aliyun.oss.OSS; + +/** + * A {@link ProtocolResolver} implementation for the {@code oss://} protocol. + * + * @author Jim + */ +public class OssStorageProtocolResolver + implements ProtocolResolver, BeanFactoryPostProcessor, ResourceLoaderAware { + + public static final String PROTOCOL = "oss://"; + + private static final Logger log = LoggerFactory + .getLogger(OssStorageProtocolResolver.class); + + private ConfigurableListableBeanFactory beanFactory; + + private OSS oss; + + private OSS getOSS() { + if (this.oss == null) { + if (this.beanFactory.getBeansOfType(OSS.class).size() > 1) { + log.warn( + "There are multiple OSS instances, consider marking one of them as @Primary to resolve oss " + + "protocol."); + } + this.oss = this.beanFactory.getBean(OSS.class); + } + return this.oss; + } + + @Override + public Resource resolve(String location, ResourceLoader resourceLoader) { + if (!location.startsWith(PROTOCOL)) { + return null; + } + return new OssStorageResource(getOSS(), location); + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + if (DefaultResourceLoader.class.isAssignableFrom(resourceLoader.getClass())) { + ((DefaultResourceLoader) resourceLoader).addProtocolResolver(this); + } + else { + log.warn("The provided delegate resource loader is not an implementation " + + "of DefaultResourceLoader. Custom Protocol using oss:// prefix will not be enabled."); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + this.beanFactory = beanFactory; + } +} diff --git a/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageResource.java b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageResource.java new file mode 100644 index 00000000..6137b2d5 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/java/com/alibaba/alicloud/oss/resource/OssStorageResource.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.oss.resource; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +import com.aliyun.oss.ClientException; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.model.Bucket; +import com.aliyun.oss.model.OSSObject; + +/** + * Implements {@link Resource} for reading and writing objects in Aliyun Object Storage + * Service (OSS). An instance of this class represents a handle to a bucket or an + * OSSObject. + * + * @author Jim + * @see OSS + * @see Bucket + * @see OSSObject + */ +public class OssStorageResource implements Resource { + + private final OSS oss; + private final String bucketName; + private final String objectKey; + private final URI location; + + public OssStorageResource(OSS oss, String location) { + Assert.notNull(oss, "Object Storage Service can not be null"); + Assert.isTrue(location.startsWith(OssStorageProtocolResolver.PROTOCOL), + "Location must start with " + OssStorageProtocolResolver.PROTOCOL); + this.oss = oss; + try { + URI locationUri = new URI(location); + this.bucketName = locationUri.getAuthority(); + + if (locationUri.getPath() != null && locationUri.getPath().length() > 1) { + this.objectKey = locationUri.getPath().substring(1); + } + else { + this.objectKey = null; + } + this.location = locationUri; + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid location: " + location, e); + } + } + + @Override + public boolean exists() { + try { + return isBucket() ? getBucket() != null : getOSSObject() != null; + } + catch (Exception e) { + return false; + } + } + + /** + * Since the oss: protocol will normally not have a URL stream handler registered, + * this method will always throw a {@link java.net.MalformedURLException}. + * @return The URL for the OSS resource, if a URL stream handler is registered for the + * oss protocol. + */ + @Override + public URL getURL() throws IOException { + return this.location.toURL(); + } + + @Override + public URI getURI() throws IOException { + return this.location; + } + + @Override + public File getFile() throws IOException { + throw new UnsupportedOperationException( + getDescription() + " cannot be resolved to absolute file path"); + } + + @Override + public long contentLength() throws IOException { + assertExisted(); + if (isBucket()) { + throw new FileNotFoundException("OSSObject not existed."); + } + return getOSSObject().getObjectMetadata().getContentLength(); + } + + @Override + public long lastModified() throws IOException { + assertExisted(); + if (isBucket()) { + throw new FileNotFoundException("OSSObject not existed."); + } + return getOSSObject().getObjectMetadata().getLastModified().getTime(); + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + return new OssStorageResource(this.oss, + this.location.resolve(relativePath).toString()); + } + + @Override + public String getFilename() { + return isBucket() ? this.bucketName : this.objectKey; + } + + @Override + public String getDescription() { + return this.location.toString(); + } + + @Override + public InputStream getInputStream() throws IOException { + assertExisted(); + if (isBucket()) { + throw new IllegalStateException( + "Cannot open an input stream to a bucket: '" + this.location + "'"); + } + else { + return getOSSObject().getObjectContent(); + } + } + + /** + * Returns the {@link Bucket} associated with the resource. + * @return the bucket if it exists, or null otherwise + */ + public Bucket getBucket() { + return this.oss.listBuckets().stream() + .filter(bucket -> bucket.getName().equals(this.bucketName)).findFirst() + .get(); + } + + /** + * Checks for the existence of the {@link Bucket} associated with the resource. + * @return true if the bucket exists + */ + public boolean bucketExists() { + return getBucket() != null; + } + + /** + * Gets the underlying resource object in Aliyun Object Storage Service. + * @return The resource object, will be null if it does not exist in Aliyun Object + * Storage Service. + * @throws OSSException it is thrown upon error when accessing OSS + * @throws ClientException it is the one thrown by the client side when accessing OSS + */ + public OSSObject getOSSObject() { + return this.oss.getObject(this.bucketName, this.objectKey); + } + + /** + * Check if this resource references a bucket and not a blob. + * @return if the resource is bucket + */ + public boolean isBucket() { + return this.objectKey == null; + } + + private void assertExisted() throws FileNotFoundException { + if (!exists()) { + throw new FileNotFoundException("Bucket or OSSObject not existed."); + } + } + +} diff --git a/spring-cloud-alicloud-oss/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-oss/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..23156e69 --- /dev/null +++ b/spring-cloud-alicloud-oss/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.alicloud.oss.OssAutoConfiguration,\ +com.alibaba.alicloud.oss.endpoint.OssEndpointAutoConfiguration +org.springframework.context.ApplicationListener=\ +com.alibaba.alicloud.oss.OssApplicationListener \ No newline at end of file diff --git a/spring-cloud-alicloud-schedulerx/pom.xml b/spring-cloud-alicloud-schedulerx/pom.xml new file mode 100644 index 00000000..df28db4a --- /dev/null +++ b/spring-cloud-alicloud-schedulerx/pom.xml @@ -0,0 +1,77 @@ + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + spring-cloud-alicloud-schedulerx + Spring Cloud Alibaba Cloud SchedulerX + + + + com.alibaba.cloud + spring-cloud-alicloud-context + + + org.slf4j + slf4j-api + + + com.alibaba.edas + schedulerX-client + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-edas + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + org.springframework.boot + spring-boot-actuator + true + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + \ No newline at end of file diff --git a/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java new file mode 100644 index 00000000..47beae5c --- /dev/null +++ b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/ScxAutoConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.scx; + +import org.springframework.context.annotation.Configuration; + +/** + * placeholder configuration + * + * @author xiaolongzuo + */ +@Configuration +public class ScxAutoConfiguration { + +} diff --git a/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpoint.java b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpoint.java new file mode 100644 index 00000000..2f173666 --- /dev/null +++ b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpoint.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.scx.endpoint; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.alicloud.context.scx.ScxProperties; + +/** + * @author xiaolongzuo + */ +@Endpoint(id = "scx") +public class ScxEndpoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScxEndpoint.class); + + private ScxProperties scxProperties; + + private EdasProperties edasProperties; + + public ScxEndpoint(EdasProperties edasProperties, ScxProperties scxProperties) { + this.edasProperties = edasProperties; + this.scxProperties = scxProperties; + } + + /** + * @return scx endpoint + */ + @ReadOperation + public Map invoke() { + Map scxEndpoint = new HashMap<>(); + LOGGER.info("SCX endpoint invoke, scxProperties is {}", scxProperties); + scxEndpoint.put("namespace", + edasProperties == null ? "" : edasProperties.getNamespace()); + scxEndpoint.put("scxProperties", scxProperties); + return scxEndpoint; + } + +} diff --git a/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpointAutoConfiguration.java b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpointAutoConfiguration.java new file mode 100644 index 00000000..98563e3a --- /dev/null +++ b/spring-cloud-alicloud-schedulerx/src/main/java/com/alibaba/alicloud/scx/endpoint/ScxEndpointAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.alicloud.scx.endpoint; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; + +import com.alibaba.alicloud.context.edas.EdasProperties; +import com.alibaba.alicloud.context.scx.ScxProperties; + +/** + * @author xiaolongzuo + */ +@ConditionalOnWebApplication +@ConditionalOnClass(Endpoint.class) +public class ScxEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public ScxEndpoint scxEndpoint(EdasProperties edasProperties, + ScxProperties scxProperties) { + return new ScxEndpoint(edasProperties, scxProperties); + } + +} diff --git a/spring-cloud-alicloud-schedulerx/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-schedulerx/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..3032200c --- /dev/null +++ b/spring-cloud-alicloud-schedulerx/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.alicloud.scx.endpoint.ScxEndpointAutoConfiguration,\ + com.alibaba.alicloud.scx.ScxAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/pom.xml b/spring-cloud-alicloud-sms/pom.xml new file mode 100644 index 00000000..14f53905 --- /dev/null +++ b/spring-cloud-alicloud-sms/pom.xml @@ -0,0 +1,102 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-alicloud-sms + Spring Cloud Alibaba Cloud SMS + + + + + com.alibaba.cloud + spring-cloud-alicloud-context + + + com.aliyun + aliyun-java-sdk-core + + + + + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + com.aliyun.mns + aliyun-sdk-mns + + + + org.springframework.boot + spring-boot-actuator + provided + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + + org.slf4j + slf4j-api + provided + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + + diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/AbstractSmsService.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/AbstractSmsService.java new file mode 100644 index 00000000..b27c5dab --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/AbstractSmsService.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +import java.util.concurrent.ConcurrentHashMap; + +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.profile.DefaultProfile; + +/** + * + * @author pbting + */ +public abstract class AbstractSmsService implements ISmsService { + + private ConcurrentHashMap acsClientConcurrentHashMap = new ConcurrentHashMap<>(); + + @Override + public IAcsClient getHangZhouRegionClientProfile(String accessKeyId, + String accessKeySecret) { + + return acsClientConcurrentHashMap.computeIfAbsent( + getKey("cn-hangzhou", accessKeyId, accessKeySecret), + (iacsClient) -> new DefaultAcsClient(DefaultProfile + .getProfile("cn-hangzhou", accessKeyId, accessKeySecret))); + } + + private String getKey(String regionId, String accessKeyId, String accessKeySecret) { + + return regionId + ":" + accessKeyId + ":" + accessKeySecret; + } + +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/ISmsService.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/ISmsService.java new file mode 100644 index 00000000..45f81b44 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/ISmsService.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.exceptions.ServerException; + +/** + * @author pbting + */ +public interface ISmsService { + + /** + * + * @param accessKeyId + * @param secret + * @return IAcsClient + */ + IAcsClient getHangZhouRegionClientProfile(String accessKeyId, String secret); + + /** + * + * @param sendSmsRequest + * @throws ServerException + * @throws ClientException + * @return SendSmsResponse + */ + SendSmsResponse sendSmsRequest(SendSmsRequest sendSmsRequest) + throws ServerException, ClientException; + + /** + * + * @param sendBatchSmsRequest + * @throws ServerException + * @throws ClientException + * @return SendBatchSmsResponse + */ + SendBatchSmsResponse sendSmsBatchRequest(SendBatchSmsRequest sendBatchSmsRequest) + throws ServerException, ClientException; + + /** + * 因为阿里云支持多个 + * accessKeyId/accessKeySecret,当不想使用默认的配置accessKeyId/accessKeySecret时,可以使用这个方法来支持额外 + * 的accessKeyId/accessKeySecret 发送 + * @param sendSmsRequest + * @param accessKeyId + * @param accessKeySecret + * @return + * @throws ServerException + * @throws ClientException + */ + SendSmsResponse sendSmsRequest(SendSmsRequest sendSmsRequest, String accessKeyId, + String accessKeySecret) throws ServerException, ClientException; + + /** + * + * @param sendSmsRequest + * @param accessKeyId + * @param accessKeySecret + * @throws ServerException + * @throws ClientException + * @return SendBatchSmsResponse + */ + SendBatchSmsResponse sendSmsBatchRequest(SendBatchSmsRequest sendSmsRequest, + String accessKeyId, String accessKeySecret) + throws ServerException, ClientException; + + /** + * + * @param smsReportMessageListener + * @return boolean + */ + boolean startSmsReportMessageListener( + SmsReportMessageListener smsReportMessageListener); + + /** + * + * @param smsUpMessageListener + * @return boolean + */ + boolean startSmsUpMessageListener(SmsUpMessageListener smsUpMessageListener); + + /** + * + * @param request + * @param accessKeyId + * @param accessKeySecret + * @return QuerySendDetailsResponse + */ + QuerySendDetailsResponse querySendDetails(QuerySendDetailsRequest request, + String accessKeyId, String accessKeySecret) throws ClientException; + + /** + * + * @param request + * @return QuerySendDetailsResponse + */ + QuerySendDetailsResponse querySendDetails(QuerySendDetailsRequest request) + throws ClientException; +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsInitializerEventListener.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsInitializerEventListener.java new file mode 100644 index 00000000..5724785c --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsInitializerEventListener.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import com.alibaba.alicloud.context.sms.SmsProperties; +import com.alibaba.alicloud.sms.base.MessageListener; + +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; + +/** + * @author pbting + */ +@Component +public class SmsInitializerEventListener + implements ApplicationListener { + + private final AtomicBoolean isCalled = new AtomicBoolean(false); + + private SmsProperties msConfigProperties; + + private ISmsService smsService; + + public SmsInitializerEventListener(SmsProperties msConfigProperties, + ISmsService smsService) { + this.msConfigProperties = msConfigProperties; + this.smsService = smsService; + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + if (!isCalled.compareAndSet(false, true)) { + return; + } + + // 整个application context refreshed then do + // 可自助调整超时时间 + System.setProperty("sun.net.client.defaultConnectTimeout", + msConfigProperties.getConnectTimeout()); + System.setProperty("sun.net.client.defaultReadTimeout", + msConfigProperties.getReadTimeout()); + // 初始化acsClient,暂不支持region化 + try { + DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", + SmsProperties.SMS_PRODUCT, SmsProperties.SMS_DOMAIN); + Collection messageListeners = event.getApplicationContext() + .getBeansOfType(MessageListener.class).values(); + if (messageListeners.isEmpty()) { + return; + } + + for (MessageListener messageListener : messageListeners) { + if (SmsReportMessageListener.class.isInstance(messageListener)) { + if (msConfigProperties.getReportQueueName() != null + && msConfigProperties.getReportQueueName().trim() + .length() > 0) { + smsService.startSmsReportMessageListener( + (SmsReportMessageListener) messageListener); + continue; + } + + throw new IllegalArgumentException("the SmsReport queue name for " + + messageListener.getClass().getCanonicalName() + + " must be set."); + } + + if (SmsUpMessageListener.class.isInstance(messageListener)) { + + if (msConfigProperties.getUpQueueName() != null + && msConfigProperties.getUpQueueName().trim().length() > 0) { + smsService.startSmsUpMessageListener( + (SmsUpMessageListener) messageListener); + continue; + } + + throw new IllegalArgumentException("the SmsUp queue name for " + + messageListener.getClass().getCanonicalName() + + " must be set."); + } + } + } + catch (ClientException e) { + throw new RuntimeException( + "initialize sms profile end point cause an exception"); + } + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsMessageListener.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsMessageListener.java new file mode 100644 index 00000000..5904f053 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsMessageListener.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +import com.alibaba.alicloud.sms.base.MessageListener; + +/** + * @author pbting + */ +public interface SmsMessageListener extends MessageListener { +} diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsReportMessageListener.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsReportMessageListener.java new file mode 100644 index 00000000..39deafa6 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsReportMessageListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +/** + * @author pbting + */ +public interface SmsReportMessageListener extends SmsMessageListener { +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsServiceImpl.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsServiceImpl.java new file mode 100644 index 00000000..b7e6ea3b --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsServiceImpl.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +import java.text.ParseException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.alicloud.context.sms.SmsProperties; +import com.alibaba.alicloud.sms.base.DefaultAlicomMessagePuller; +import com.alibaba.alicloud.sms.endpoint.EndpointManager; +import com.alibaba.alicloud.sms.endpoint.ReceiveMessageEntity; + +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.exceptions.ServerException; + +/** + * @author pbting + */ +public final class SmsServiceImpl extends AbstractSmsService { + + private static final Logger log = LoggerFactory.getLogger(SmsServiceImpl.class); + /** + * will expose user to call this method send sms message + * @param sendSmsRequest + * @return + */ + private SmsProperties smsProperties; + + private AliCloudProperties aliCloudProperties; + + public SmsServiceImpl(AliCloudProperties aliCloudProperties, + SmsProperties smsProperties) { + this.aliCloudProperties = aliCloudProperties; + this.smsProperties = smsProperties; + } + + @Override + public SendSmsResponse sendSmsRequest(SendSmsRequest sendSmsRequest) + throws ClientException { + + return sendSmsRequest(sendSmsRequest, aliCloudProperties.getAccessKey(), + aliCloudProperties.getSecretKey()); + } + + /** + * 因为阿里云支持多个 + * accessKeyId/accessKeySecret,当不想使用默认的配置accessKeyId/accessKeySecret时,可以使用这个方法来支持额外 + * 的accessKeyId/accessKeySecret 发送 + * @param sendSmsRequest + * @param accessKeyId + * @param accessKeySecret + * @throws ServerException + * @throws ClientException + * @return SendSmsResponse + */ + @Override + public SendSmsResponse sendSmsRequest(SendSmsRequest sendSmsRequest, + String accessKeyId, String accessKeySecret) + throws ServerException, ClientException { + EndpointManager.addSendSmsRequest(sendSmsRequest); + // hint 此处可能会抛出异常,注意catch + return getHangZhouRegionClientProfile(accessKeyId, accessKeySecret) + .getAcsResponse(sendSmsRequest); + } + + /** + * + * @param smsReportMessageListener + * @return boolean + */ + @Override + public boolean startSmsReportMessageListener( + SmsReportMessageListener smsReportMessageListener) { + // 短信回执:SmsReport,短信上行:SmsUp + String messageType = "SmsReport"; + String queueName = smsProperties.getReportQueueName(); + return startReceiveMsg(messageType, queueName, smsReportMessageListener); + } + + /** + * + * @param smsUpMessageListener + * @return boolean + */ + @Override + public boolean startSmsUpMessageListener(SmsUpMessageListener smsUpMessageListener) { + // 短信回执:SmsReport,短信上行:SmsUp + String messageType = "SmsUp"; + String queueName = smsProperties.getUpQueueName(); + return startReceiveMsg(messageType, queueName, smsUpMessageListener); + } + + /** + * + * @param messageType + * @param queueName + * @param messageListener + * @return boolean + */ + private boolean startReceiveMsg(String messageType, String queueName, + SmsMessageListener messageListener) { + String accessKeyId = aliCloudProperties.getAccessKey(); + String accessKeySecret = aliCloudProperties.getSecretKey(); + boolean result = true; + try { + new DefaultAlicomMessagePuller().startReceiveMsg(accessKeyId, accessKeySecret, + messageType, queueName, messageListener); + EndpointManager.addReceiveMessageEntity( + new ReceiveMessageEntity(messageType, queueName, messageListener)); + } + catch (ClientException e) { + log.error("start sms report message listener cause an exception", e); + result = false; + } + catch (ParseException e) { + log.error("start sms report message listener cause an exception", e); + result = false; + } + return result; + } + + /** + * + * @param sendBatchSmsRequest + * @throws ServerException + * @throws ClientException + * @return SendBatchSmsResponse + */ + @Override + public SendBatchSmsResponse sendSmsBatchRequest( + SendBatchSmsRequest sendBatchSmsRequest) + throws ServerException, ClientException { + + return sendSmsBatchRequest(sendBatchSmsRequest, aliCloudProperties.getAccessKey(), + aliCloudProperties.getSecretKey()); + } + + /** + * + * @param sendBatchSmsRequest + * @param accessKeyId + * @param accessKeySecret + * @throws ClientException + * @return SendBatchSmsResponse + */ + @Override + public SendBatchSmsResponse sendSmsBatchRequest( + SendBatchSmsRequest sendBatchSmsRequest, String accessKeyId, + String accessKeySecret) throws ClientException { + EndpointManager.addSendBatchSmsRequest(sendBatchSmsRequest); + return getHangZhouRegionClientProfile(accessKeyId, accessKeySecret) + .getAcsResponse(sendBatchSmsRequest); + } + + /** + * + * @param request + * @param accessKeyId + * @param accessKeySecret + * @throws ClientException + * @return QuerySendDetailsResponse + */ + @Override + public QuerySendDetailsResponse querySendDetails(QuerySendDetailsRequest request, + String accessKeyId, String accessKeySecret) throws ClientException { + return getHangZhouRegionClientProfile(accessKeyId, accessKeySecret) + .getAcsResponse(request); + } + + /** + * + * @param request + * @throws ClientException + * @return QuerySendDetailsResponse + */ + @Override + public QuerySendDetailsResponse querySendDetails(QuerySendDetailsRequest request) + throws ClientException { + return querySendDetails(request, aliCloudProperties.getAccessKey(), + aliCloudProperties.getSecretKey()); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsUpMessageListener.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsUpMessageListener.java new file mode 100644 index 00000000..9e4e8124 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/SmsUpMessageListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms; + +/** + * @author pbting + */ +public interface SmsUpMessageListener extends SmsMessageListener { +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/DefaultAlicomMessagePuller.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/DefaultAlicomMessagePuller.java new file mode 100755 index 00000000..4cf87d7b --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/DefaultAlicomMessagePuller.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.aliyun.mns.client.CloudQueue; +import com.aliyun.mns.common.ClientException; +import com.aliyun.mns.common.ServiceException; +import com.aliyun.mns.model.Message; + +/** + * 阿里通信官方消息默认拉取工具类 + */ +public class DefaultAlicomMessagePuller { + + private static final Logger log = LoggerFactory + .getLogger(DefaultAlicomMessagePuller.class); + + private String mnsAccountEndpoint = "https://1943695596114318.mns.cn-hangzhou.aliyuncs.com/";// 阿里通信消息的endpoint,固定。 + private String endpointNameForPop = "cn-hangzhou"; + private String regionIdForPop = "cn-hangzhou"; + private String domainForPop = "dybaseapi.aliyuncs.com"; + private TokenGetterForAlicom tokenGetter; + private MessageListener messageListener; + private boolean isRunning = false; + private Integer pullMsgThreadSize = 1; + private boolean debugLogOpen = false; + private Integer sleepSecondWhenNoData = 30; + + public void openDebugLog(boolean debugLogOpen) { + this.debugLogOpen = debugLogOpen; + } + + public Integer getSleepSecondWhenNoData() { + return sleepSecondWhenNoData; + } + + public void setSleepSecondWhenNoData(Integer sleepSecondWhenNoData) { + this.sleepSecondWhenNoData = sleepSecondWhenNoData; + } + + public Integer getPullMsgThreadSize() { + return pullMsgThreadSize; + } + + public void setPullMsgThreadSize(Integer pullMsgThreadSize) { + if (pullMsgThreadSize != null && pullMsgThreadSize > 1) { + this.pullMsgThreadSize = pullMsgThreadSize; + } + } + + private ExecutorService executorService; + + public ExecutorService getExecutorService() { + return executorService; + } + + public void setExecutorService(ExecutorService executorService) { + this.executorService = executorService; + } + + protected static final Map S_LOCK_OBJ_MAP = new HashMap<>(); + protected static Map sPollingMap = new ConcurrentHashMap<>(); + protected Object lockObj; + + public boolean setPolling(String queueName) { + synchronized (lockObj) { + Boolean ret = sPollingMap.get(queueName); + if (ret == null || !ret) { + sPollingMap.put(queueName, true); + return true; + } + return false; + } + } + + public void clearPolling(String queueName) { + synchronized (lockObj) { + sPollingMap.put(queueName, false); + lockObj.notifyAll(); + if (debugLogOpen) { + log.info("PullMessageTask_WakeUp:Everyone WakeUp and Work!"); + } + } + } + + public boolean isRunning() { + return isRunning; + } + + public void setRunning(boolean running) { + isRunning = running; + } + + private class PullMessageTask implements Runnable { + private String messageType; + private String queueName; + + @Override + public void run() { + + boolean polling = false; + while (isRunning) { + try { + synchronized (lockObj) { + Boolean p = sPollingMap.get(queueName); + if (p != null && p) { + try { + if (debugLogOpen) { + log.info("PullMessageTask_sleep:" + + Thread.currentThread().getName() + + " Have a nice sleep!"); + } + polling = false; + lockObj.wait(); + } + catch (InterruptedException e) { + if (debugLogOpen) { + log.info("PullMessageTask_Interrupted!" + + Thread.currentThread().getName() + + " QueueName is " + queueName); + } + continue; + } + } + } + + TokenForAlicom tokenObject = tokenGetter.getTokenByMessageType( + messageType, queueName, mnsAccountEndpoint); + CloudQueue queue = tokenObject.getQueue(); + Message popMsg = null; + if (!polling) { + popMsg = queue.popMessage(); + if (debugLogOpen) { + SimpleDateFormat format = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + log.info("PullMessageTask_popMessage:" + + Thread.currentThread().getName() + "-popDone at " + + "," + format.format(new Date()) + " msgSize=" + + (popMsg == null ? 0 : popMsg.getMessageId())); + } + if (popMsg == null) { + polling = true; + continue; + } + } + else { + if (setPolling(queueName)) { + if (debugLogOpen) { + log.info("PullMessageTask_setPolling:" + + Thread.currentThread().getName() + " Polling!"); + } + } + else { + continue; + } + do { + if (debugLogOpen) { + log.info("PullMessageTask_Keep_Polling" + + Thread.currentThread().getName() + + "KEEP Polling!"); + } + try { + popMsg = queue.popMessage(sleepSecondWhenNoData); + } + catch (ClientException e) { + if (debugLogOpen) { + log.info( + "PullMessageTask_Pop_Message:ClientException Refresh accessKey" + + e); + } + tokenObject = tokenGetter.getTokenByMessageType( + messageType, queueName, mnsAccountEndpoint); + queue = tokenObject.getQueue(); + + } + catch (ServiceException e) { + if (debugLogOpen) { + log.info( + "PullMessageTask_Pop_Message:ServiceException Refresh accessKey" + + e); + } + tokenObject = tokenGetter.getTokenByMessageType( + messageType, queueName, mnsAccountEndpoint); + queue = tokenObject.getQueue(); + + } + catch (Exception e) { + if (debugLogOpen) { + log.info( + "PullMessageTask_Pop_Message:Exception Happened when polling popMessage: " + + e); + } + } + } + while (popMsg == null && isRunning); + clearPolling(queueName); + } + boolean dealResult = messageListener.dealMessage(popMsg); + if (dealResult) { + // remember to delete message when consume message successfully. + if (debugLogOpen) { + log.info("PullMessageTask_Deal_Message:" + + Thread.currentThread().getName() + "deleteMessage " + + popMsg.getMessageId()); + } + queue.deleteMessage(popMsg.getReceiptHandle()); + } + } + catch (ClientException e) { + log.error("PullMessageTask_execute_error,messageType:" + messageType + + ",queueName:" + queueName, e); + break; + + } + catch (ServiceException e) { + if (e.getErrorCode().equals("AccessDenied")) { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName + + ",please check messageType and queueName", e); + } + else { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName, e); + } + break; + + } + catch (com.aliyuncs.exceptions.ClientException e) { + if (e.getErrCode().equals("InvalidAccessKeyId.NotFound")) { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName + + ",please check AccessKeyId", e); + } + if (e.getErrCode().equals("SignatureDoesNotMatch")) { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName + + ",please check AccessKeySecret", e); + } + else { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName, e); + } + break; + + } + catch (Exception e) { + log.error("PullMessageTask_execute_error,messageType:" + messageType + + ",queueName:" + queueName, e); + try { + Thread.sleep(sleepSecondWhenNoData); + } + catch (InterruptedException e1) { + log.error("PullMessageTask_execute_error,messageType:" + + messageType + ",queueName:" + queueName, e); + } + } + } + + } + + } + + /** + * @param accessKeyId accessKeyId + * @param accessKeySecret accessKeySecret + * @param messageType 消息类型 + * @param queueName 队列名称 + * @param messageListener 回调的listener,用户自己实现 + * @throws com.aliyuncs.exceptions.ClientException + * @throws ParseException + */ + public void startReceiveMsg(String accessKeyId, String accessKeySecret, + String messageType, String queueName, MessageListener messageListener) + throws com.aliyuncs.exceptions.ClientException, ParseException { + + tokenGetter = new TokenGetterForAlicom(accessKeyId, accessKeySecret, + endpointNameForPop, regionIdForPop, domainForPop, null); + + this.messageListener = messageListener; + isRunning = true; + PullMessageTask task = new PullMessageTask(); + task.messageType = messageType; + task.queueName = queueName; + + synchronized (S_LOCK_OBJ_MAP) { + lockObj = S_LOCK_OBJ_MAP.get(queueName); + if (lockObj == null) { + lockObj = new Object(); + S_LOCK_OBJ_MAP.put(queueName, lockObj); + } + } + + if (executorService == null) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor( + pullMsgThreadSize, + new BasicThreadFactory.Builder() + .namingPattern( + "PullMessageTask-" + messageType + "-thread-pool-%d") + .daemon(true).build()); + executorService = scheduledExecutorService; + } + for (int i = 0; i < pullMsgThreadSize; i++) { + executorService.execute(task); + } + } + + /** + * @param accessKeyId accessKeyId + * @param accessKeySecret accessKeySecret + * @param messageType 消息类型 + * @param queueName 队列名称 + * @param messageListener 回调的listener,用户自己实现 + * @throws com.aliyuncs.exceptions.ClientException + * @throws ParseException + */ + public void startReceiveMsgForVPC(String accessKeyId, String accessKeySecret, + String messageType, String queueName, String regionIdForPop, + String endpointNameForPop, String domainForPop, String mnsAccountEndpoint, + MessageListener messageListener) + throws com.aliyuncs.exceptions.ClientException, ParseException { + this.mnsAccountEndpoint = mnsAccountEndpoint; + tokenGetter = new TokenGetterForAlicom(accessKeyId, accessKeySecret, + endpointNameForPop, regionIdForPop, domainForPop, null); + + this.messageListener = messageListener; + isRunning = true; + PullMessageTask task = new PullMessageTask(); + task.messageType = messageType; + task.queueName = queueName; + + synchronized (S_LOCK_OBJ_MAP) { + lockObj = S_LOCK_OBJ_MAP.get(queueName); + if (lockObj == null) { + lockObj = new Object(); + S_LOCK_OBJ_MAP.put(queueName, lockObj); + } + } + + if (executorService == null) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor( + pullMsgThreadSize, + new BasicThreadFactory.Builder() + .namingPattern( + "PullMessageTask-" + messageType + "-thread-pool-%d") + .daemon(true).build()); + executorService = scheduledExecutorService; + } + for (int i = 0; i < pullMsgThreadSize; i++) { + executorService.execute(task); + } + } + + /** + * 虚商用户定制接收消息方法 + * @param accessKeyId accessKeyId + * @param accessKeySecret accessKeySecret + * @param ownerId 实际的ownerId + * @param messageType 消息类型 + * @param queueName 队列名称 + * @param messageListener 回调listener + * @throws com.aliyuncs.exceptions.ClientException + * @throws ParseException + */ + public void startReceiveMsgForPartnerUser(String accessKeyId, String accessKeySecret, + Long ownerId, String messageType, String queueName, + MessageListener messageListener) + throws com.aliyuncs.exceptions.ClientException, ParseException { + + tokenGetter = new TokenGetterForAlicom(accessKeyId, accessKeySecret, + endpointNameForPop, regionIdForPop, domainForPop, ownerId); + + this.messageListener = messageListener; + isRunning = true; + PullMessageTask task = new PullMessageTask(); + task.messageType = messageType; + task.queueName = queueName; + + synchronized (S_LOCK_OBJ_MAP) { + lockObj = S_LOCK_OBJ_MAP.get(queueName); + if (lockObj == null) { + lockObj = new Object(); + S_LOCK_OBJ_MAP.put(queueName, lockObj); + } + } + + if (executorService == null) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor( + pullMsgThreadSize, + new BasicThreadFactory.Builder() + .namingPattern( + "PullMessageTask-" + messageType + "-thread-pool-%d") + .daemon(true).build()); + executorService = scheduledExecutorService; + } + for (int i = 0; i < pullMsgThreadSize; i++) { + executorService.execute(task); + } + } + + public void stop() { + isRunning = false; + } + +} diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/MessageListener.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/MessageListener.java new file mode 100755 index 00000000..9e857f2a --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/MessageListener.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import com.aliyun.mns.model.Message; + +public interface MessageListener { + + boolean dealMessage(Message message); + +} diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueRequest.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueRequest.java new file mode 100644 index 00000000..e8214812 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueRequest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import com.aliyuncs.RpcAcsRequest; + +public class QueryTokenForMnsQueueRequest + extends RpcAcsRequest { + private String resourceOwnerAccount; + private String messageType; + private Long resourceOwnerId; + private Long ownerId; + + public QueryTokenForMnsQueueRequest() { + super("Dybaseapi", "2017-05-25", "QueryTokenForMnsQueue"); + } + + public String getResourceOwnerAccount() { + return this.resourceOwnerAccount; + } + + public void setResourceOwnerAccount(String resourceOwnerAccount) { + this.resourceOwnerAccount = resourceOwnerAccount; + if (resourceOwnerAccount != null) { + this.putQueryParameter("ResourceOwnerAccount", resourceOwnerAccount); + } + + } + + public String getMessageType() { + return this.messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + if (messageType != null) { + this.putQueryParameter("MessageType", messageType); + } + + } + + public Long getResourceOwnerId() { + return this.resourceOwnerId; + } + + public void setResourceOwnerId(Long resourceOwnerId) { + this.resourceOwnerId = resourceOwnerId; + if (resourceOwnerId != null) { + this.putQueryParameter("ResourceOwnerId", resourceOwnerId.toString()); + } + + } + + public Long getOwnerId() { + return this.ownerId; + } + + public void setOwnerId(Long ownerId) { + this.ownerId = ownerId; + if (ownerId != null) { + this.putQueryParameter("OwnerId", ownerId.toString()); + } + + } + + @Override + public Class getResponseClass() { + return QueryTokenForMnsQueueResponse.class; + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponse.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponse.java new file mode 100644 index 00000000..724804e8 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponse.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import com.aliyuncs.AcsResponse; +import com.aliyuncs.transform.UnmarshallerContext; + +public class QueryTokenForMnsQueueResponse extends AcsResponse { + private String requestId; + private String code; + private String message; + private QueryTokenForMnsQueueResponse.MessageTokenDTO messageTokenDTO; + + public QueryTokenForMnsQueueResponse() { + } + + public String getRequestId() { + return this.requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getCode() { + return this.code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + + public QueryTokenForMnsQueueResponse.MessageTokenDTO getMessageTokenDTO() { + return this.messageTokenDTO; + } + + public void setMessageTokenDTO( + QueryTokenForMnsQueueResponse.MessageTokenDTO messageTokenDTO) { + this.messageTokenDTO = messageTokenDTO; + } + + @Override + public QueryTokenForMnsQueueResponse getInstance(UnmarshallerContext context) { + return QueryTokenForMnsQueueResponseUnmarshaller.unmarshall(this, context); + } + + public static class MessageTokenDTO { + private String accessKeyId; + private String accessKeySecret; + private String securityToken; + private String createTime; + private String expireTime; + + public MessageTokenDTO() { + } + + public String getAccessKeyId() { + return this.accessKeyId; + } + + public void setAccessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + } + + public String getAccessKeySecret() { + return this.accessKeySecret; + } + + public void setAccessKeySecret(String accessKeySecret) { + this.accessKeySecret = accessKeySecret; + } + + public String getSecurityToken() { + return this.securityToken; + } + + public void setSecurityToken(String securityToken) { + this.securityToken = securityToken; + } + + public String getCreateTime() { + return this.createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getExpireTime() { + return this.expireTime; + } + + public void setExpireTime(String expireTime) { + this.expireTime = expireTime; + } + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponseUnmarshaller.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponseUnmarshaller.java new file mode 100644 index 00000000..87c58c28 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/QueryTokenForMnsQueueResponseUnmarshaller.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import com.aliyuncs.transform.UnmarshallerContext; + +public class QueryTokenForMnsQueueResponseUnmarshaller { + + public QueryTokenForMnsQueueResponseUnmarshaller() { + } + + public static QueryTokenForMnsQueueResponse unmarshall( + QueryTokenForMnsQueueResponse queryTokenForMnsQueueResponse, + UnmarshallerContext context) { + queryTokenForMnsQueueResponse.setRequestId( + context.stringValue("QueryTokenForMnsQueueResponse.RequestId")); + queryTokenForMnsQueueResponse + .setCode(context.stringValue("QueryTokenForMnsQueueResponse.Code")); + queryTokenForMnsQueueResponse + .setMessage(context.stringValue("QueryTokenForMnsQueueResponse.Message")); + QueryTokenForMnsQueueResponse.MessageTokenDTO messageTokenDTO = new QueryTokenForMnsQueueResponse.MessageTokenDTO(); + messageTokenDTO.setAccessKeyId(context.stringValue( + "QueryTokenForMnsQueueResponse.MessageTokenDTO.AccessKeyId")); + messageTokenDTO.setAccessKeySecret(context.stringValue( + "QueryTokenForMnsQueueResponse.MessageTokenDTO.AccessKeySecret")); + messageTokenDTO.setSecurityToken(context.stringValue( + "QueryTokenForMnsQueueResponse.MessageTokenDTO.SecurityToken")); + messageTokenDTO.setCreateTime(context + .stringValue("QueryTokenForMnsQueueResponse.MessageTokenDTO.CreateTime")); + messageTokenDTO.setExpireTime(context + .stringValue("QueryTokenForMnsQueueResponse.MessageTokenDTO.ExpireTime")); + queryTokenForMnsQueueResponse.setMessageTokenDTO(messageTokenDTO); + return queryTokenForMnsQueueResponse; + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenForAlicom.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenForAlicom.java new file mode 100755 index 00000000..443e8517 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenForAlicom.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import com.aliyun.mns.client.CloudQueue; +import com.aliyun.mns.client.MNSClient; + +/** + * 用于接收云通信消息的临时token + * + */ +public class TokenForAlicom { + private String messageType; + private String token; + private Long expireTime; + private String tempAccessKeyId; + private String tempAccessKeySecret; + private MNSClient client; + private CloudQueue queue; + + public String getMessageType() { + return messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Long getExpireTime() { + return expireTime; + } + + public void setExpireTime(Long expireTime) { + this.expireTime = expireTime; + } + + public String getTempAccessKeyId() { + return tempAccessKeyId; + } + + public void setTempAccessKeyId(String tempAccessKeyId) { + this.tempAccessKeyId = tempAccessKeyId; + } + + public String getTempAccessKeySecret() { + return tempAccessKeySecret; + } + + public void setTempAccessKeySecret(String tempAccessKeySecret) { + this.tempAccessKeySecret = tempAccessKeySecret; + } + + public MNSClient getClient() { + return client; + } + + public void setClient(MNSClient client) { + this.client = client; + } + + public CloudQueue getQueue() { + return queue; + } + + public void setQueue(CloudQueue queue) { + this.queue = queue; + } + + public void closeClient() { + if (client != null) { + this.client.close(); + } + } + +} diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenGetterForAlicom.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenGetterForAlicom.java new file mode 100755 index 00000000..70def740 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/base/TokenGetterForAlicom.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.base; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.aliyun.mns.client.CloudAccount; +import com.aliyun.mns.client.CloudQueue; +import com.aliyun.mns.client.MNSClient; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.exceptions.ServerException; +import com.aliyuncs.http.FormatType; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.http.ProtocolType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; + +/** + * 获取接收云通信消息的临时token + * + */ +public class TokenGetterForAlicom { + private static final Logger log = LoggerFactory.getLogger(TokenGetterForAlicom.class); + private String accessKeyId; + private String accessKeySecret; + private String endpointNameForPop; + private String regionIdForPop; + private String domainForPop; + private IAcsClient iAcsClient; + private Long ownerId; + private final static String PRODUCT_NAME = "Dybaseapi"; + private long bufferTime = 1000 * 60 * 2;// 过期时间小于2分钟则重新获取,防止服务器时间误差 + private final Object lock = new Object(); + private ConcurrentMap tokenMap = new ConcurrentHashMap(); + + public TokenGetterForAlicom(String accessKeyId, String accessKeySecret, + String endpointNameForPop, String regionIdForPop, String domainForPop, + Long ownerId) throws ClientException { + this.accessKeyId = accessKeyId; + this.accessKeySecret = accessKeySecret; + this.endpointNameForPop = endpointNameForPop; + this.regionIdForPop = regionIdForPop; + this.domainForPop = domainForPop; + this.ownerId = ownerId; + init(); + } + + private void init() throws ClientException { + DefaultProfile.addEndpoint(endpointNameForPop, regionIdForPop, PRODUCT_NAME, + domainForPop); + IClientProfile profile = DefaultProfile.getProfile(regionIdForPop, accessKeyId, + accessKeySecret); + profile.getHttpClientConfig().setCompatibleMode(true); + iAcsClient = new DefaultAcsClient(profile); + } + + private TokenForAlicom getTokenFromRemote(String messageType) + throws ServerException, ClientException, ParseException { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + QueryTokenForMnsQueueRequest request = new QueryTokenForMnsQueueRequest(); + request.setAcceptFormat(FormatType.JSON); + request.setMessageType(messageType); + request.setOwnerId(ownerId); + request.setProtocol(ProtocolType.HTTPS); + request.setMethod(MethodType.POST); + QueryTokenForMnsQueueResponse response = iAcsClient.getAcsResponse(request); + String resultCode = response.getCode(); + if (resultCode != null && "OK".equals(resultCode)) { + QueryTokenForMnsQueueResponse.MessageTokenDTO dto = response + .getMessageTokenDTO(); + TokenForAlicom token = new TokenForAlicom(); + String timeStr = dto.getExpireTime(); + token.setMessageType(messageType); + token.setExpireTime(df.parse(timeStr).getTime()); + token.setToken(dto.getSecurityToken()); + token.setTempAccessKeyId(dto.getAccessKeyId()); + token.setTempAccessKeySecret(dto.getAccessKeySecret()); + return token; + } + else { + log.error("getTokenFromRemote_error,messageType:" + messageType + ",code:" + + response.getCode() + ",message:" + response.getMessage()); + throw new ServerException(response.getCode(), response.getMessage()); + } + } + + public TokenForAlicom getTokenByMessageType(String messageType, String queueName, + String mnsAccountEndpoint) + throws ServerException, ClientException, ParseException { + TokenForAlicom token = tokenMap.get(messageType); + Long now = System.currentTimeMillis(); + if (token == null || (token.getExpireTime() - now) < bufferTime) {// 过期时间小于2分钟则重新获取,防止服务器时间误差 + synchronized (lock) { + token = tokenMap.get(messageType); + if (token == null || (token.getExpireTime() - now) < bufferTime) { + TokenForAlicom oldToken = null; + if (token != null) { + oldToken = token; + } + token = getTokenFromRemote(messageType); + // 因为换token时需要重建client和关闭老的client,所以创建client的代码和创建token放在一起 + CloudAccount account = new CloudAccount(token.getTempAccessKeyId(), + token.getTempAccessKeySecret(), mnsAccountEndpoint, + token.getToken()); + MNSClient client = account.getMNSClient(); + CloudQueue queue = client.getQueueRef(queueName); + token.setClient(client); + token.setQueue(queue); + tokenMap.put(messageType, token); + if (oldToken != null) { + oldToken.closeClient(); + } + } + } + } + return token; + } +} diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/config/SmsAutoConfiguration.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/config/SmsAutoConfiguration.java new file mode 100644 index 00000000..08e75098 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/config/SmsAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.alicloud.context.AliCloudProperties; +import com.alibaba.alicloud.context.sms.SmsProperties; +import com.alibaba.alicloud.sms.ISmsService; +import com.alibaba.alicloud.sms.SmsInitializerEventListener; +import com.alibaba.alicloud.sms.SmsServiceImpl; + +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; + +/** + * @author pbting + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnClass(value = SendSmsRequest.class) +@ConditionalOnProperty(value = "spring.cloud.alicloud.sms.enable", matchIfMissing = true) +public class SmsAutoConfiguration { + + @Bean + public SmsServiceImpl smsService(AliCloudProperties aliCloudProperties, + SmsProperties smsProperties) { + return new SmsServiceImpl(aliCloudProperties, smsProperties); + } + + @Bean + public SmsInitializerEventListener smsInitializePostListener( + SmsProperties smsProperties, ISmsService smsService) { + return new SmsInitializerEventListener(smsProperties, smsService); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/EndpointManager.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/EndpointManager.java new file mode 100644 index 00000000..79354f24 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/EndpointManager.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.endpoint; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.ReentrantLock; + +import com.aliyuncs.dysmsapi.model.v20170525.SendBatchSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; + +/** + * + */ +public final class EndpointManager { + + private final static int BACKLOG_SIZE = 20; + + private final static ReentrantLock SEND_REENTRANT_LOCK = new ReentrantLock(true); + private final static ReentrantLock SEND_BATCH_REENTRANT_LOCK = new ReentrantLock( + true); + + private final static LinkedBlockingQueue SEND_SMS_REQUESTS = new LinkedBlockingQueue( + BACKLOG_SIZE); + private final static LinkedBlockingQueue SEND_BATCH_SMS_REQUESTS = new LinkedBlockingQueue( + BACKLOG_SIZE); + private final static LinkedBlockingQueue RECEIVE_MESSAGE_ENTITIES = new LinkedBlockingQueue( + BACKLOG_SIZE); + + public static void addSendSmsRequest(SendSmsRequest sendSmsRequest) { + if (SEND_SMS_REQUESTS.offer(sendSmsRequest)) { + return; + } + try { + SEND_REENTRANT_LOCK.lock(); + SEND_SMS_REQUESTS.poll(); + SEND_SMS_REQUESTS.offer(sendSmsRequest); + } + finally { + SEND_REENTRANT_LOCK.unlock(); + } + } + + public static void addSendBatchSmsRequest(SendBatchSmsRequest sendBatchSmsRequest) { + if (SEND_BATCH_SMS_REQUESTS.offer(sendBatchSmsRequest)) { + return; + } + try { + SEND_BATCH_REENTRANT_LOCK.lock(); + SEND_BATCH_SMS_REQUESTS.poll(); + SEND_BATCH_SMS_REQUESTS.offer(sendBatchSmsRequest); + } + finally { + SEND_BATCH_REENTRANT_LOCK.unlock(); + } + } + + public static void addReceiveMessageEntity( + ReceiveMessageEntity receiveMessageEntity) { + if (RECEIVE_MESSAGE_ENTITIES.offer(receiveMessageEntity)) { + return; + } + RECEIVE_MESSAGE_ENTITIES.poll(); + RECEIVE_MESSAGE_ENTITIES.offer(receiveMessageEntity); + } + + public static Map getSmsEndpointMessage() { + List sendSmsRequests = new LinkedList<>(); + List sendBatchSmsRequests = new LinkedList<>(); + List receiveMessageEntities = new LinkedList<>(); + try { + SEND_REENTRANT_LOCK.lock(); + SEND_BATCH_REENTRANT_LOCK.lock(); + sendSmsRequests.addAll(SEND_SMS_REQUESTS); + sendBatchSmsRequests.addAll(SEND_BATCH_SMS_REQUESTS); + } + finally { + SEND_REENTRANT_LOCK.unlock(); + SEND_BATCH_REENTRANT_LOCK.unlock(); + } + receiveMessageEntities.addAll(RECEIVE_MESSAGE_ENTITIES); + + Map endpointMessages = new HashMap<>(); + endpointMessages.put("send-sms-request", sendSmsRequests); + endpointMessages.put("send-batch-sms-request", sendBatchSmsRequests); + endpointMessages.put("message-listener", receiveMessageEntities); + + return endpointMessages; + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/ReceiveMessageEntity.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/ReceiveMessageEntity.java new file mode 100644 index 00000000..685fbe97 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/ReceiveMessageEntity.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.endpoint; + +import java.io.Serializable; + +import com.alibaba.alicloud.sms.base.MessageListener; + +/** + * @author pbting + */ +public class ReceiveMessageEntity implements Serializable { + private String messageType; + private String queueName; + private MessageListener messageListener; + + public ReceiveMessageEntity(String messageType, String queueName, + MessageListener messageListener) { + this.messageType = messageType; + this.queueName = queueName; + this.messageListener = messageListener; + } + + public String getMessageType() { + return messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + } + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public MessageListener getMessageListener() { + return messageListener; + } + + public void setMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpoint.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpoint.java new file mode 100644 index 00000000..57da7bd5 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpoint.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.endpoint; + +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +@Endpoint(id = "sms") +public class SmsEndpoint { + + @ReadOperation + public Map invoke() { + + return EndpointManager.getSmsEndpointMessage(); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpointAutoConfiguration.java b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpointAutoConfiguration.java new file mode 100644 index 00000000..cf8a6bb7 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/java/com/alibaba/alicloud/sms/endpoint/SmsEndpointAutoConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 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 + * + * http://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 com.alibaba.alicloud.sms.endpoint; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; + +@ConditionalOnWebApplication +@ConditionalOnClass(Endpoint.class) +public class SmsEndpointAutoConfiguration { + + @Bean + public SmsEndpoint smsEndpoint() { + return new SmsEndpoint(); + } +} \ No newline at end of file diff --git a/spring-cloud-alicloud-sms/src/main/resources/META-INF/spring.factories b/spring-cloud-alicloud-sms/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..a6c328d2 --- /dev/null +++ b/spring-cloud-alicloud-sms/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.alicloud.sms.config.SmsAutoConfiguration,\ + com.alibaba.alicloud.sms.endpoint.SmsEndpointAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-starter-alibaba/pom.xml b/spring-cloud-starter-alibaba/pom.xml new file mode 100644 index 00000000..5afd96b0 --- /dev/null +++ b/spring-cloud-starter-alibaba/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + spring-cloud-starter-alibaba + pom + Spring Cloud Alibaba Starters + Spring Cloud Alibaba Starters + + spring-cloud-starter-alibaba-nacos-config + spring-cloud-starter-alibaba-nacos-config-server + spring-cloud-starter-alibaba-nacos-discovery + spring-cloud-starter-alibaba-sentinel + spring-cloud-starter-alibaba-seata + spring-cloud-starter-stream-rocketmq + spring-cloud-starter-bus-rocketmq + spring-cloud-starter-dubbo + + \ No newline at end of file diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config-server/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config-server/pom.xml new file mode 100644 index 00000000..6cdb3f93 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config-server/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alibaba-nacos-config-server + Spring Cloud Starter Alibaba Nacos Config Server + + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-config + + + + diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config/pom.xml new file mode 100644 index 00000000..0e58cd6e --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-config/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alibaba-nacos-config + Spring Cloud Starter Alibaba Nacos Config + + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-config + + + + diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-discovery/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-discovery/pom.xml new file mode 100644 index 00000000..1479674b --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-nacos-discovery/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alibaba-nacos-discovery + Spring Cloud Starter Alibaba Nacos Discovery + + + + com.alibaba.cloud + spring-cloud-alibaba-nacos-discovery + + + + diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-seata/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-seata/pom.xml new file mode 100644 index 00000000..47cc5acd --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-seata/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alibaba-seata + Spring Cloud Starter Alibaba Seata + + + + com.alibaba.cloud + spring-cloud-alibaba-seata + + + + diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-sentinel/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-sentinel/pom.xml new file mode 100644 index 00000000..cc903895 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-alibaba-sentinel/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alibaba-sentinel + Spring Cloud Starter Alibaba Sentinel + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel + + + + diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/pom.xml new file mode 100644 index 00000000..44b1726f --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/pom.xml @@ -0,0 +1,33 @@ + + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + com.alibaba.cloud + spring-cloud-starter-bus-rocketmq + Spring Cloud Alibaba Bus RocketMQ + + + + + + com.alibaba.cloud + spring-cloud-stream-binder-rocketmq + + + + + org.springframework.cloud + spring-cloud-bus + + + + + \ No newline at end of file diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/java/org/springframework/cloud/bus/rocketmq/env/RocketMQBusEnvironmentPostProcessor.java b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/java/org/springframework/cloud/bus/rocketmq/env/RocketMQBusEnvironmentPostProcessor.java new file mode 100644 index 00000000..093dce41 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/java/org/springframework/cloud/bus/rocketmq/env/RocketMQBusEnvironmentPostProcessor.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.cloud.bus.rocketmq.env; + +import static org.springframework.cloud.bus.SpringCloudBusClient.INPUT; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.cloud.bus.BusEnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; + +/** + * The lowest precedence {@link EnvironmentPostProcessor} configures default RocketMQ Bus + * Properties that will be appended into {@link SpringApplication#defaultProperties} + * + * @author Mercy + * @see BusEnvironmentPostProcessor + * @since 0.2.1 + */ +public class RocketMQBusEnvironmentPostProcessor + implements EnvironmentPostProcessor, Ordered { + + /** + * The name of {@link PropertySource} of {@link SpringApplication#defaultProperties} + */ + private static final String PROPERTY_SOURCE_NAME = "defaultProperties"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + + addDefaultPropertySource(environment); + + } + + private void addDefaultPropertySource(ConfigurableEnvironment environment) { + + Map map = new HashMap(); + + configureDefaultProperties(map); + + addOrReplace(environment.getPropertySources(), map); + } + + private void configureDefaultProperties(Map source) { + // Required Properties + String groupBindingPropertyName = createBindingPropertyName(INPUT, "group"); + String broadcastingPropertyName = createRocketMQPropertyName(INPUT, + "broadcasting"); + source.put(groupBindingPropertyName, "rocketmq-bus-group"); + source.put(broadcastingPropertyName, "true"); + } + + private String createRocketMQPropertyName(String channel, String propertyName) { + return "spring.cloud.stream.rocketmq.bindings." + INPUT + ".consumer." + + propertyName; + } + + private String createBindingPropertyName(String channel, String propertyName) { + return "spring.cloud.stream.bindings." + channel + "." + propertyName; + } + + /** + * Copy from + * {@link BusEnvironmentPostProcessor#addOrReplace(MutablePropertySources, Map)} + * + * @param propertySources {@link MutablePropertySources} + * @param map Default RocketMQ Bus Properties + */ + private void addOrReplace(MutablePropertySources propertySources, + Map map) { + MapPropertySource target = null; + if (propertySources.contains(PROPERTY_SOURCE_NAME)) { + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + for (String key : map.keySet()) { + if (!target.containsProperty(key)) { + target.getSource().put(key, map.get(key)); + } + } + } + } + if (target == null) { + target = new MapPropertySource(PROPERTY_SOURCE_NAME, map); + } + if (!propertySources.contains(PROPERTY_SOURCE_NAME)) { + propertySources.addLast(target); + } + } + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE; + } +} diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c5a9fb95 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-bus-rocketmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# EnvironmentPostProcessor +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.cloud.bus.rocketmq.env.RocketMQBusEnvironmentPostProcessor \ No newline at end of file diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-dubbo/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-dubbo/pom.xml new file mode 100644 index 00000000..9d26ad96 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-dubbo/pom.xml @@ -0,0 +1,24 @@ + + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-starter-dubbo + Spring Cloud Starter Dubbo + + + + ${project.groupId} + spring-cloud-alibaba-dubbo + ${project.version} + + + + \ No newline at end of file diff --git a/spring-cloud-starter-alibaba/spring-cloud-starter-stream-rocketmq/pom.xml b/spring-cloud-starter-alibaba/spring-cloud-starter-stream-rocketmq/pom.xml new file mode 100644 index 00000000..1ad87f56 --- /dev/null +++ b/spring-cloud-starter-alibaba/spring-cloud-starter-stream-rocketmq/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + spring-cloud-starter-stream-rocketmq + Spring Cloud Starter Stream RocketMQ + + + + com.alibaba.cloud + spring-cloud-stream-binder-rocketmq + + + + diff --git a/spring-cloud-starter-alicloud/pom.xml b/spring-cloud-starter-alicloud/pom.xml new file mode 100644 index 00000000..498be0fe --- /dev/null +++ b/spring-cloud-starter-alicloud/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alicloud + pom + Spring Cloud Alibaba Cloud Starters + Spring Cloud Alibaba Cloud Starters + + spring-cloud-starter-alicloud-oss + spring-cloud-starter-alicloud-acm + spring-cloud-starter-alicloud-ans + spring-cloud-starter-alicloud-schedulerx + spring-cloud-starter-alicloud-sms + + \ No newline at end of file diff --git a/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-acm/pom.xml b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-acm/pom.xml new file mode 100644 index 00000000..6195e93c --- /dev/null +++ b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-acm/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alicloud + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alicloud-acm + Spring Cloud Starter Alibaba Cloud ACM + + + + com.alibaba.cloud + spring-cloud-alicloud-acm + + + + diff --git a/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-ans/pom.xml b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-ans/pom.xml new file mode 100644 index 00000000..6bd99648 --- /dev/null +++ b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-ans/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + com.alibaba.cloud + spring-cloud-starter-alicloud + 0.9.1.BUILD-SNAPSHOT + + + spring-cloud-starter-alicloud-ans + Spring Cloud Starter Alibaba Cloud ANS + + + + com.alibaba.cloud + spring-cloud-alicloud-ans + + + + \ No newline at end of file diff --git a/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-oss/pom.xml b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-oss/pom.xml new file mode 100644 index 00000000..4cad9b7c --- /dev/null +++ b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-oss/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alicloud + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alicloud-oss + Spring Cloud Starter Alibaba Cloud OSS + + + + com.alibaba.cloud + spring-cloud-alicloud-oss + + + com.aliyun.oss + aliyun-sdk-oss + + + + diff --git a/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-schedulerx/pom.xml b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-schedulerx/pom.xml new file mode 100644 index 00000000..dc9f7bfe --- /dev/null +++ b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-schedulerx/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alicloud + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alicloud-schedulerx + Spring Cloud Starter Alibaba Cloud SchedulerX + + + + com.alibaba.cloud + spring-cloud-alicloud-schedulerx + + + + diff --git a/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-sms/pom.xml b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-sms/pom.xml new file mode 100644 index 00000000..b53e0599 --- /dev/null +++ b/spring-cloud-starter-alicloud/spring-cloud-starter-alicloud-sms/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-starter-alicloud + 0.9.1.BUILD-SNAPSHOT + + spring-cloud-starter-alicloud-sms + Spring Cloud Starter Alibaba Cloud SMS + + + + com.alibaba.cloud + spring-cloud-alicloud-sms + + + + diff --git a/spring-cloud-stream-binder-rocketmq/pom.xml b/spring-cloud-stream-binder-rocketmq/pom.xml new file mode 100644 index 00000000..75979bd1 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/pom.xml @@ -0,0 +1,91 @@ + + + + + com.alibaba.cloud + spring-cloud-alibaba + 0.9.1.BUILD-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-stream-binder-rocketmq + Spring Cloud Alibaba RocketMQ Binder + + + + + org.springframework.cloud + spring-cloud-stream + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot + true + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + + org.springframework.boot + spring-boot-actuator + true + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + true + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + + diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderConstants.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderConstants.java new file mode 100644 index 00000000..72c97420 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderConstants.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq; + +/** + * @author Jim + */ +public interface RocketMQBinderConstants { + + /** + * Header key + */ + String ROCKET_TRANSACTIONAL_ARG = "TRANSACTIONAL_ARG"; + + /** + * Default value + */ + String DEFAULT_NAME_SERVER = "127.0.0.1:9876"; + + String DEFAULT_GROUP = "rocketmq_binder_default_group_name"; + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderUtils.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderUtils.java new file mode 100644 index 00000000..673d3370 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQBinderUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq; + +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; + +/** + * @author Jim + */ +public class RocketMQBinderUtils { + + public static RocketMQBinderConfigurationProperties mergeProperties( + RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties, + RocketMQProperties rocketMQProperties) { + RocketMQBinderConfigurationProperties result = new RocketMQBinderConfigurationProperties(); + if (StringUtils.isEmpty(rocketMQProperties.getNameServer())) { + result.setNameServer(rocketBinderConfigurationProperties.getNameServer()); + } + else { + result.setNameServer(rocketMQProperties.getNameServer()); + } + if (rocketMQProperties.getProducer() == null + || StringUtils.isEmpty(rocketMQProperties.getProducer().getAccessKey())) { + result.setAccessKey(rocketBinderConfigurationProperties.getAccessKey()); + } + else { + result.setAccessKey(rocketMQProperties.getProducer().getAccessKey()); + } + if (rocketMQProperties.getProducer() == null + || StringUtils.isEmpty(rocketMQProperties.getProducer().getSecretKey())) { + result.setSecretKey(rocketBinderConfigurationProperties.getSecretKey()); + } + else { + result.setSecretKey(rocketMQProperties.getProducer().getSecretKey()); + } + if (rocketMQProperties.getProducer() == null || StringUtils + .isEmpty(rocketMQProperties.getProducer().getCustomizedTraceTopic())) { + result.setCustomizedTraceTopic( + rocketBinderConfigurationProperties.getCustomizedTraceTopic()); + } + else { + result.setCustomizedTraceTopic( + rocketMQProperties.getProducer().getCustomizedTraceTopic()); + } + if (rocketMQProperties.getProducer() != null + && rocketMQProperties.getProducer().isEnableMsgTrace()) { + result.setEnableMsgTrace(Boolean.TRUE); + } + else { + result.setEnableMsgTrace( + rocketBinderConfigurationProperties.isEnableMsgTrace()); + } + return result; + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQMessageChannelBinder.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQMessageChannelBinder.java new file mode 100644 index 00000000..d4aaf88a --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQMessageChannelBinder.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.apache.rocketmq.spring.support.RocketMQUtil; +import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.integration.StaticMessageHeaderAccessor; +import org.springframework.integration.acks.AcknowledgmentCallback; +import org.springframework.integration.acks.AcknowledgmentCallback.Status; +import org.springframework.integration.core.MessageProducer; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.consuming.RocketMQListenerBindingContainer; +import com.alibaba.cloud.stream.binder.rocketmq.integration.RocketMQInboundChannelAdapter; +import com.alibaba.cloud.stream.binder.rocketmq.integration.RocketMQMessageHandler; +import com.alibaba.cloud.stream.binder.rocketmq.integration.RocketMQMessageSource; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.InstrumentationManager; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQExtendedBindingProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQProducerProperties; +import com.alibaba.cloud.stream.binder.rocketmq.provisioning.RocketMQTopicProvisioner; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jim + */ +public class RocketMQMessageChannelBinder extends + AbstractMessageChannelBinder, ExtendedProducerProperties, RocketMQTopicProvisioner> + implements + ExtendedPropertiesBinder { + + private RocketMQExtendedBindingProperties extendedBindingProperties = new RocketMQExtendedBindingProperties(); + + private final RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties; + private final RocketMQProperties rocketMQProperties; + private final InstrumentationManager instrumentationManager; + + private Map topicInUse = new HashMap<>(); + + public RocketMQMessageChannelBinder(RocketMQTopicProvisioner provisioningProvider, + RocketMQExtendedBindingProperties extendedBindingProperties, + RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties, + RocketMQProperties rocketMQProperties, + InstrumentationManager instrumentationManager) { + super(null, provisioningProvider); + this.extendedBindingProperties = extendedBindingProperties; + this.rocketBinderConfigurationProperties = rocketBinderConfigurationProperties; + this.rocketMQProperties = rocketMQProperties; + this.instrumentationManager = instrumentationManager; + } + + @Override + protected MessageHandler createProducerMessageHandler(ProducerDestination destination, + ExtendedProducerProperties producerProperties, + MessageChannel errorChannel) throws Exception { + if (producerProperties.getExtension().getEnabled()) { + + // if producerGroup is empty, using destination + String extendedProducerGroup = producerProperties.getExtension().getGroup(); + String producerGroup = StringUtils.isEmpty(extendedProducerGroup) + ? destination.getName() + : extendedProducerGroup; + + RocketMQBinderConfigurationProperties mergedProperties = RocketMQBinderUtils + .mergeProperties(rocketBinderConfigurationProperties, + rocketMQProperties); + + RocketMQTemplate rocketMQTemplate; + if (producerProperties.getExtension().getTransactional()) { + Map rocketMQTemplates = getBeanFactory() + .getBeansOfType(RocketMQTemplate.class); + if (rocketMQTemplates.size() == 0) { + throw new IllegalStateException( + "there is no RocketMQTemplate in Spring BeanFactory"); + } + else if (rocketMQTemplates.size() > 1) { + throw new IllegalStateException( + "there is more than 1 RocketMQTemplates in Spring BeanFactory"); + } + rocketMQTemplate = rocketMQTemplates.values().iterator().next(); + } + else { + rocketMQTemplate = new RocketMQTemplate(); + rocketMQTemplate.setObjectMapper(this.getApplicationContext() + .getBeansOfType(ObjectMapper.class).values().iterator().next()); + DefaultMQProducer producer; + String ak = mergedProperties.getAccessKey(); + String sk = mergedProperties.getSecretKey(); + if (!StringUtils.isEmpty(ak) && !StringUtils.isEmpty(sk)) { + RPCHook rpcHook = new AclClientRPCHook( + new SessionCredentials(ak, sk)); + producer = new DefaultMQProducer(producerGroup, rpcHook, + mergedProperties.isEnableMsgTrace(), + mergedProperties.getCustomizedTraceTopic()); + producer.setVipChannelEnabled(false); + producer.setInstanceName(RocketMQUtil.getInstanceName(rpcHook, + destination.getName() + "|" + UtilAll.getPid())); + } + else { + producer = new DefaultMQProducer(producerGroup); + producer.setVipChannelEnabled( + producerProperties.getExtension().getVipChannelEnabled()); + } + producer.setNamesrvAddr(mergedProperties.getNameServer()); + producer.setSendMsgTimeout( + producerProperties.getExtension().getSendMessageTimeout()); + producer.setRetryTimesWhenSendFailed( + producerProperties.getExtension().getRetryTimesWhenSendFailed()); + producer.setRetryTimesWhenSendAsyncFailed(producerProperties + .getExtension().getRetryTimesWhenSendAsyncFailed()); + producer.setCompressMsgBodyOverHowmuch(producerProperties.getExtension() + .getCompressMessageBodyThreshold()); + producer.setRetryAnotherBrokerWhenNotStoreOK( + producerProperties.getExtension().isRetryNextServer()); + producer.setMaxMessageSize( + producerProperties.getExtension().getMaxMessageSize()); + rocketMQTemplate.setProducer(producer); + } + + RocketMQMessageHandler messageHandler = new RocketMQMessageHandler( + rocketMQTemplate, destination.getName(), producerGroup, + producerProperties.getExtension().getTransactional(), + instrumentationManager); + messageHandler.setBeanFactory(this.getApplicationContext().getBeanFactory()); + messageHandler.setSync(producerProperties.getExtension().getSync()); + + if (errorChannel != null) { + messageHandler.setSendFailureChannel(errorChannel); + } + return messageHandler; + } + else { + throw new RuntimeException("Binding for channel " + destination.getName() + + " has been disabled, message can't be delivered"); + } + } + + @Override + protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, + String group, + ExtendedConsumerProperties consumerProperties) + throws Exception { + if (group == null || "".equals(group)) { + throw new RuntimeException( + "'group must be configured for channel " + destination.getName()); + } + + RocketMQListenerBindingContainer listenerContainer = new RocketMQListenerBindingContainer( + consumerProperties, rocketBinderConfigurationProperties, this); + listenerContainer.setConsumerGroup(group); + listenerContainer.setTopic(destination.getName()); + listenerContainer.setConsumeThreadMax(consumerProperties.getConcurrency()); + listenerContainer.setSuspendCurrentQueueTimeMillis( + consumerProperties.getExtension().getSuspendCurrentQueueTimeMillis()); + listenerContainer.setDelayLevelWhenNextConsume( + consumerProperties.getExtension().getDelayLevelWhenNextConsume()); + listenerContainer + .setNameServer(rocketBinderConfigurationProperties.getNameServer()); + + RocketMQInboundChannelAdapter rocketInboundChannelAdapter = new RocketMQInboundChannelAdapter( + listenerContainer, consumerProperties, instrumentationManager); + + topicInUse.put(destination.getName(), group); + + ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination, + group, consumerProperties); + if (consumerProperties.getMaxAttempts() > 1) { + rocketInboundChannelAdapter + .setRetryTemplate(buildRetryTemplate(consumerProperties)); + rocketInboundChannelAdapter + .setRecoveryCallback(errorInfrastructure.getRecoverer()); + } + else { + rocketInboundChannelAdapter + .setErrorChannel(errorInfrastructure.getErrorChannel()); + } + + return rocketInboundChannelAdapter; + } + + @Override + protected PolledConsumerResources createPolledConsumerResources(String name, + String group, ConsumerDestination destination, + ExtendedConsumerProperties consumerProperties) { + RocketMQMessageSource rocketMQMessageSource = new RocketMQMessageSource( + rocketBinderConfigurationProperties, consumerProperties, name, group); + return new PolledConsumerResources(rocketMQMessageSource, + registerErrorInfrastructure(destination, group, consumerProperties, + true)); + } + + @Override + protected MessageHandler getPolledConsumerErrorMessageHandler( + ConsumerDestination destination, String group, + ExtendedConsumerProperties properties) { + return message -> { + if (message.getPayload() instanceof MessagingException) { + AcknowledgmentCallback ack = StaticMessageHeaderAccessor + .getAcknowledgmentCallback( + ((MessagingException) message.getPayload()) + .getFailedMessage()); + if (ack != null) { + if (properties.getExtension().shouldRequeue()) { + ack.acknowledge(Status.REQUEUE); + } + else { + ack.acknowledge(Status.REJECT); + } + } + } + }; + } + + @Override + public RocketMQConsumerProperties getExtendedConsumerProperties(String channelName) { + return extendedBindingProperties.getExtendedConsumerProperties(channelName); + } + + @Override + public RocketMQProducerProperties getExtendedProducerProperties(String channelName) { + return extendedBindingProperties.getExtendedProducerProperties(channelName); + } + + public Map getTopicInUse() { + return topicInUse; + } + + @Override + public String getDefaultsPrefix() { + return extendedBindingProperties.getDefaultsPrefix(); + } + + @Override + public Class getExtendedPropertiesEntryClass() { + return extendedBindingProperties.getExtendedPropertiesEntryClass(); + } + + public void setExtendedBindingProperties( + RocketMQExtendedBindingProperties extendedBindingProperties) { + this.extendedBindingProperties = extendedBindingProperties; + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/actuator/RocketMQBinderHealthIndicator.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/actuator/RocketMQBinderHealthIndicator.java new file mode 100644 index 00000000..c7539822 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/actuator/RocketMQBinderHealthIndicator.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.actuator; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; + +import com.alibaba.cloud.stream.binder.rocketmq.metrics.Instrumentation; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.InstrumentationManager; + +/** + * @author Timur Valiev + * @author Jim + */ +public class RocketMQBinderHealthIndicator extends AbstractHealthIndicator { + + @Autowired + private InstrumentationManager instrumentationManager; + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + if (instrumentationManager.getHealthInstrumentations().stream() + .allMatch(Instrumentation::isUp)) { + builder.up(); + return; + } + if (instrumentationManager.getHealthInstrumentations().stream() + .allMatch(Instrumentation::isOutOfService)) { + builder.outOfService(); + return; + } + builder.down(); + instrumentationManager.getHealthInstrumentations().stream() + .filter(instrumentation -> !instrumentation.isStarted()) + .forEach(instrumentation1 -> builder + .withException(instrumentation1.getStartException())); + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderAutoConfiguration.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderAutoConfiguration.java new file mode 100644 index 00000000..66a26ec1 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderAutoConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.config; + +import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration; +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import com.alibaba.cloud.stream.binder.rocketmq.RocketMQMessageChannelBinder; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.InstrumentationManager; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQExtendedBindingProperties; +import com.alibaba.cloud.stream.binder.rocketmq.provisioning.RocketMQTopicProvisioner; + +/** + * @author Timur Valiev + * @author Jim + */ +@Configuration +@Import({ RocketMQAutoConfiguration.class, + RocketMQBinderHealthIndicatorAutoConfiguration.class }) +@EnableConfigurationProperties({ RocketMQBinderConfigurationProperties.class, + RocketMQExtendedBindingProperties.class }) +public class RocketMQBinderAutoConfiguration { + + private final RocketMQExtendedBindingProperties extendedBindingProperties; + + private final RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties; + + @Autowired(required = false) + private RocketMQProperties rocketMQProperties = new RocketMQProperties(); + + @Autowired + public RocketMQBinderAutoConfiguration( + RocketMQExtendedBindingProperties extendedBindingProperties, + RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties) { + this.extendedBindingProperties = extendedBindingProperties; + this.rocketBinderConfigurationProperties = rocketBinderConfigurationProperties; + } + + @Bean + public RocketMQTopicProvisioner provisioningProvider() { + return new RocketMQTopicProvisioner(); + } + + @Bean + public RocketMQMessageChannelBinder rocketMessageChannelBinder( + RocketMQTopicProvisioner provisioningProvider, + InstrumentationManager instrumentationManager) { + RocketMQMessageChannelBinder binder = new RocketMQMessageChannelBinder( + provisioningProvider, extendedBindingProperties, + rocketBinderConfigurationProperties, rocketMQProperties, + instrumentationManager); + binder.setExtendedBindingProperties(extendedBindingProperties); + return binder; + } + + @Bean + public InstrumentationManager instrumentationManager() { + return new InstrumentationManager(); + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderHealthIndicatorAutoConfiguration.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderHealthIndicatorAutoConfiguration.java new file mode 100644 index 00000000..cdc3537a --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQBinderHealthIndicatorAutoConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.config; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.stream.binder.rocketmq.actuator.RocketMQBinderHealthIndicator; + +/** + * @author Jim + */ +@Configuration +@ConditionalOnClass(Endpoint.class) +public class RocketMQBinderHealthIndicatorAutoConfiguration { + + @Bean + public RocketMQBinderHealthIndicator rocketBinderHealthIndicator() { + return new RocketMQBinderHealthIndicator(); + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQComponent4BinderAutoConfiguration.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQComponent4BinderAutoConfiguration.java new file mode 100644 index 00000000..33186962 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/config/RocketMQComponent4BinderAutoConfiguration.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.config; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration; +import org.apache.rocketmq.spring.config.RocketMQConfigUtils; +import org.apache.rocketmq.spring.config.RocketMQTransactionAnnotationProcessor; +import org.apache.rocketmq.spring.config.TransactionHandlerRegistry; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.RocketMQBinderConstants; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jim + */ +@Configuration +@AutoConfigureAfter(RocketMQAutoConfiguration.class) +@ConditionalOnMissingBean(DefaultMQProducer.class) +public class RocketMQComponent4BinderAutoConfiguration { + + private final Environment environment; + + public RocketMQComponent4BinderAutoConfiguration(Environment environment) { + this.environment = environment; + } + + @Bean + @ConditionalOnMissingBean(DefaultMQProducer.class) + public DefaultMQProducer defaultMQProducer() { + DefaultMQProducer producer; + String configNameServer = environment.resolveRequiredPlaceholders( + "${spring.cloud.stream.rocketmq.binder.name-server:${rocketmq.producer.name-server:}}"); + String ak = environment.resolveRequiredPlaceholders( + "${spring.cloud.stream.rocketmq.binder.access-key:${rocketmq.producer.access-key:}}"); + String sk = environment.resolveRequiredPlaceholders( + "${spring.cloud.stream.rocketmq.binder.secret-key:${rocketmq.producer.secret-key:}}"); + if (!StringUtils.isEmpty(ak) && !StringUtils.isEmpty(sk)) { + producer = new DefaultMQProducer(RocketMQBinderConstants.DEFAULT_GROUP, + new AclClientRPCHook(new SessionCredentials(ak, sk))); + producer.setVipChannelEnabled(false); + } + else { + producer = new DefaultMQProducer(RocketMQBinderConstants.DEFAULT_GROUP); + } + if (StringUtils.isEmpty(configNameServer)) { + configNameServer = RocketMQBinderConstants.DEFAULT_NAME_SERVER; + } + producer.setNamesrvAddr(configNameServer); + return producer; + } + + @Bean(destroyMethod = "destroy") + @ConditionalOnMissingBean + public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer, + ObjectMapper objectMapper) { + RocketMQTemplate rocketMQTemplate = new RocketMQTemplate(); + rocketMQTemplate.setProducer(mqProducer); + rocketMQTemplate.setObjectMapper(objectMapper); + return rocketMQTemplate; + } + + @Bean + @ConditionalOnBean(RocketMQTemplate.class) + @ConditionalOnMissingBean(TransactionHandlerRegistry.class) + public TransactionHandlerRegistry transactionHandlerRegistry( + RocketMQTemplate template) { + return new TransactionHandlerRegistry(template); + } + + @Bean(name = RocketMQConfigUtils.ROCKETMQ_TRANSACTION_ANNOTATION_PROCESSOR_BEAN_NAME) + @ConditionalOnBean(TransactionHandlerRegistry.class) + public static RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor( + TransactionHandlerRegistry transactionHandlerRegistry) { + return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry); + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQListenerBindingContainer.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQListenerBindingContainer.java new file mode 100644 index 00000000..1a11324f --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQListenerBindingContainer.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.consuming; + +import java.util.List; +import java.util.Objects; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.MessageModel; +import org.apache.rocketmq.spring.annotation.SelectorType; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener; +import org.apache.rocketmq.spring.support.RocketMQListenerContainer; +import org.apache.rocketmq.spring.support.RocketMQUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.context.SmartLifecycle; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.RocketMQMessageChannelBinder; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties; + +/** + * @author Jim + */ +public class RocketMQListenerBindingContainer + implements InitializingBean, RocketMQListenerContainer, SmartLifecycle { + + private final static Logger log = LoggerFactory + .getLogger(RocketMQListenerBindingContainer.class); + + private long suspendCurrentQueueTimeMillis = 1000; + + /** + * Message consume retry strategy
+ * -1,no retry,put into DLQ directly
+ * 0,broker control retry frequency
+ * >0,client control retry frequency. + */ + private int delayLevelWhenNextConsume = 0; + + private String nameServer; + + private String consumerGroup; + + private String topic; + + private int consumeThreadMax = 64; + + private String charset = "UTF-8"; + + private RocketMQListener rocketMQListener; + + private DefaultMQPushConsumer consumer; + + private boolean running; + + private final ExtendedConsumerProperties rocketMQConsumerProperties; + + private final RocketMQMessageChannelBinder rocketMQMessageChannelBinder; + private final RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties; + + // The following properties came from RocketMQConsumerProperties. + private ConsumeMode consumeMode; + private SelectorType selectorType; + private String selectorExpression; + private MessageModel messageModel; + + public RocketMQListenerBindingContainer( + ExtendedConsumerProperties rocketMQConsumerProperties, + RocketMQBinderConfigurationProperties rocketBinderConfigurationProperties, + RocketMQMessageChannelBinder rocketMQMessageChannelBinder) { + this.rocketMQConsumerProperties = rocketMQConsumerProperties; + this.rocketBinderConfigurationProperties = rocketBinderConfigurationProperties; + this.rocketMQMessageChannelBinder = rocketMQMessageChannelBinder; + this.consumeMode = rocketMQConsumerProperties.getExtension().getOrderly() + ? ConsumeMode.ORDERLY + : ConsumeMode.CONCURRENTLY; + if (StringUtils.isEmpty(rocketMQConsumerProperties.getExtension().getSql())) { + this.selectorType = SelectorType.TAG; + this.selectorExpression = rocketMQConsumerProperties.getExtension().getTags(); + } + else { + this.selectorType = SelectorType.SQL92; + this.selectorExpression = rocketMQConsumerProperties.getExtension().getSql(); + } + this.messageModel = rocketMQConsumerProperties.getExtension().getBroadcasting() + ? MessageModel.BROADCASTING + : MessageModel.CLUSTERING; + } + + @Override + public void setupMessageListener(RocketMQListener rocketMQListener) { + this.rocketMQListener = rocketMQListener; + } + + @Override + public void destroy() throws Exception { + this.setRunning(false); + if (Objects.nonNull(consumer)) { + consumer.shutdown(); + } + log.info("container destroyed, {}", this.toString()); + } + + @Override + public void afterPropertiesSet() throws Exception { + initRocketMQPushConsumer(); + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + stop(); + callback.run(); + } + + @Override + public void start() { + if (this.isRunning()) { + throw new IllegalStateException( + "container already running. " + this.toString()); + } + + try { + consumer.start(); + } + catch (MQClientException e) { + throw new IllegalStateException("Failed to start RocketMQ push consumer", e); + } + this.setRunning(true); + + log.info("running container: {}", this.toString()); + } + + @Override + public void stop() { + if (this.isRunning()) { + if (Objects.nonNull(consumer)) { + consumer.shutdown(); + } + setRunning(false); + } + } + + @Override + public boolean isRunning() { + return running; + } + + private void setRunning(boolean running) { + this.running = running; + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE; + } + + private void initRocketMQPushConsumer() throws MQClientException { + Assert.notNull(rocketMQListener, "Property 'rocketMQListener' is required"); + Assert.notNull(consumerGroup, "Property 'consumerGroup' is required"); + Assert.notNull(nameServer, "Property 'nameServer' is required"); + Assert.notNull(topic, "Property 'topic' is required"); + + String ak = rocketBinderConfigurationProperties.getAccessKey(); + String sk = rocketBinderConfigurationProperties.getSecretKey(); + if (!StringUtils.isEmpty(ak) && !StringUtils.isEmpty(sk)) { + RPCHook rpcHook = new AclClientRPCHook(new SessionCredentials(ak, sk)); + consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, + new AllocateMessageQueueAveragely(), + rocketBinderConfigurationProperties.isEnableMsgTrace(), + rocketBinderConfigurationProperties.getCustomizedTraceTopic()); + consumer.setInstanceName(RocketMQUtil.getInstanceName(rpcHook, + topic + "|" + UtilAll.getPid())); + consumer.setVipChannelEnabled(false); + } + else { + consumer = new DefaultMQPushConsumer(consumerGroup, + rocketBinderConfigurationProperties.isEnableMsgTrace(), + rocketBinderConfigurationProperties.getCustomizedTraceTopic()); + } + + consumer.setNamesrvAddr(nameServer); + consumer.setConsumeThreadMax(rocketMQConsumerProperties.getConcurrency()); + consumer.setConsumeThreadMin(rocketMQConsumerProperties.getConcurrency()); + + switch (messageModel) { + case BROADCASTING: + consumer.setMessageModel( + org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING); + break; + case CLUSTERING: + consumer.setMessageModel( + org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING); + break; + default: + throw new IllegalArgumentException("Property 'messageModel' was wrong."); + } + + switch (selectorType) { + case TAG: + consumer.subscribe(topic, selectorExpression); + break; + case SQL92: + consumer.subscribe(topic, MessageSelector.bySql(selectorExpression)); + break; + default: + throw new IllegalArgumentException("Property 'selectorType' was wrong."); + } + + switch (consumeMode) { + case ORDERLY: + consumer.setMessageListener(new DefaultMessageListenerOrderly()); + break; + case CONCURRENTLY: + consumer.setMessageListener(new DefaultMessageListenerConcurrently()); + break; + default: + throw new IllegalArgumentException("Property 'consumeMode' was wrong."); + } + + if (rocketMQListener instanceof RocketMQPushConsumerLifecycleListener) { + ((RocketMQPushConsumerLifecycleListener) rocketMQListener) + .prepareStart(consumer); + } + + } + + @Override + public String toString() { + return "RocketMQListenerBindingContainer{" + "consumerGroup='" + consumerGroup + + '\'' + ", nameServer='" + nameServer + '\'' + ", topic='" + topic + '\'' + + ", consumeMode=" + consumeMode + ", selectorType=" + selectorType + + ", selectorExpression='" + selectorExpression + '\'' + ", messageModel=" + + messageModel + '}'; + } + + public long getSuspendCurrentQueueTimeMillis() { + return suspendCurrentQueueTimeMillis; + } + + public void setSuspendCurrentQueueTimeMillis(long suspendCurrentQueueTimeMillis) { + this.suspendCurrentQueueTimeMillis = suspendCurrentQueueTimeMillis; + } + + public int getDelayLevelWhenNextConsume() { + return delayLevelWhenNextConsume; + } + + public void setDelayLevelWhenNextConsume(int delayLevelWhenNextConsume) { + this.delayLevelWhenNextConsume = delayLevelWhenNextConsume; + } + + public String getNameServer() { + return nameServer; + } + + public void setNameServer(String nameServer) { + this.nameServer = nameServer; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public int getConsumeThreadMax() { + return consumeThreadMax; + } + + public void setConsumeThreadMax(int consumeThreadMax) { + this.consumeThreadMax = consumeThreadMax; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public RocketMQListener getRocketMQListener() { + return rocketMQListener; + } + + public void setRocketMQListener(RocketMQListener rocketMQListener) { + this.rocketMQListener = rocketMQListener; + } + + public DefaultMQPushConsumer getConsumer() { + return consumer; + } + + public void setConsumer(DefaultMQPushConsumer consumer) { + this.consumer = consumer; + } + + public ExtendedConsumerProperties getRocketMQConsumerProperties() { + return rocketMQConsumerProperties; + } + + public ConsumeMode getConsumeMode() { + return consumeMode; + } + + public SelectorType getSelectorType() { + return selectorType; + } + + public String getSelectorExpression() { + return selectorExpression; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public class DefaultMessageListenerConcurrently + implements MessageListenerConcurrently { + + @SuppressWarnings("unchecked") + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + for (MessageExt messageExt : msgs) { + log.debug("received msg: {}", messageExt); + try { + long now = System.currentTimeMillis(); + rocketMQListener + .onMessage(RocketMQUtil.convertToSpringMessage(messageExt)); + long costTime = System.currentTimeMillis() - now; + log.debug("consume {} cost: {} ms", messageExt.getMsgId(), costTime); + } + catch (Exception e) { + log.warn("consume message failed. messageExt:{}", messageExt, e); + context.setDelayLevelWhenNextConsume(delayLevelWhenNextConsume); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + + public class DefaultMessageListenerOrderly implements MessageListenerOrderly { + + @SuppressWarnings("unchecked") + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, + ConsumeOrderlyContext context) { + for (MessageExt messageExt : msgs) { + log.debug("received msg: {}", messageExt); + try { + long now = System.currentTimeMillis(); + rocketMQListener + .onMessage(RocketMQUtil.convertToSpringMessage(messageExt)); + long costTime = System.currentTimeMillis() - now; + log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime); + } + catch (Exception e) { + log.warn("consume message failed. messageExt:{}", messageExt, e); + context.setSuspendCurrentQueueTimeMillis( + suspendCurrentQueueTimeMillis); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + } + + return ConsumeOrderlyStatus.SUCCESS; + } + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQMessageQueueChooser.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQMessageQueueChooser.java new file mode 100644 index 00000000..62724827 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/consuming/RocketMQMessageQueueChooser.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.stream.binder.rocketmq.consuming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * @author Jim + */ +public class RocketMQMessageQueueChooser { + + private volatile int queueIndex = 0; + + private volatile List messageQueues; + + public MessageQueue choose() { + return messageQueues.get(queueIndex); + } + + public int requeue() { + if (queueIndex - 1 < 0) { + this.queueIndex = messageQueues.size() - 1; + } + else { + this.queueIndex = this.queueIndex - 1; + } + return this.queueIndex; + } + + public void increment() { + this.queueIndex = (this.queueIndex + 1) % messageQueues.size(); + } + + public void reset(Set queueSet) { + this.messageQueues = null; + this.messageQueues = new ArrayList<>(queueSet); + this.queueIndex = 0; + } + + public List getMessageQueues() { + return messageQueues; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQInboundChannelAdapter.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQInboundChannelAdapter.java new file mode 100644 index 00000000..eeb8d3ef --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQInboundChannelAdapter.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.integration; + +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.integration.endpoint.MessageProducerSupport; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; + +import com.alibaba.cloud.stream.binder.rocketmq.consuming.RocketMQListenerBindingContainer; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.Instrumentation; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.InstrumentationManager; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties; + +/** + * @author Jim + */ +public class RocketMQInboundChannelAdapter extends MessageProducerSupport { + + private static final Logger log = LoggerFactory + .getLogger(RocketMQInboundChannelAdapter.class); + + private RetryTemplate retryTemplate; + + private RecoveryCallback recoveryCallback; + + private RocketMQListenerBindingContainer rocketMQListenerContainer; + + private final ExtendedConsumerProperties consumerProperties; + + private final InstrumentationManager instrumentationManager; + + public RocketMQInboundChannelAdapter( + RocketMQListenerBindingContainer rocketMQListenerContainer, + ExtendedConsumerProperties consumerProperties, + InstrumentationManager instrumentationManager) { + this.rocketMQListenerContainer = rocketMQListenerContainer; + this.consumerProperties = consumerProperties; + this.instrumentationManager = instrumentationManager; + } + + @Override + protected void onInit() { + if (consumerProperties == null + || !consumerProperties.getExtension().getEnabled()) { + return; + } + super.onInit(); + if (this.retryTemplate != null) { + Assert.state(getErrorChannel() == null, + "Cannot have an 'errorChannel' property when a 'RetryTemplate' is " + + "provided; use an 'ErrorMessageSendingRecoverer' in the 'recoveryCallback' property to " + + "send an error message when retries are exhausted"); + } + + BindingRocketMQListener listener = new BindingRocketMQListener(); + rocketMQListenerContainer.setRocketMQListener(listener); + + if (retryTemplate != null) { + this.retryTemplate.registerListener(listener); + } + + try { + rocketMQListenerContainer.afterPropertiesSet(); + + } + catch (Exception e) { + log.error("rocketMQListenerContainer init error: " + e.getMessage(), e); + throw new IllegalArgumentException( + "rocketMQListenerContainer init error: " + e.getMessage(), e); + } + + instrumentationManager.addHealthInstrumentation( + new Instrumentation(rocketMQListenerContainer.getTopic() + + rocketMQListenerContainer.getConsumerGroup())); + } + + @Override + protected void doStart() { + if (consumerProperties == null + || !consumerProperties.getExtension().getEnabled()) { + return; + } + try { + rocketMQListenerContainer.start(); + instrumentationManager + .getHealthInstrumentation(rocketMQListenerContainer.getTopic() + + rocketMQListenerContainer.getConsumerGroup()) + .markStartedSuccessfully(); + } + catch (Exception e) { + instrumentationManager + .getHealthInstrumentation(rocketMQListenerContainer.getTopic() + + rocketMQListenerContainer.getConsumerGroup()) + .markStartFailed(e); + log.error("RocketMQTemplate startup failed, Caused by " + e.getMessage()); + throw new MessagingException(MessageBuilder.withPayload( + "RocketMQTemplate startup failed, Caused by " + e.getMessage()) + .build(), e); + } + } + + @Override + protected void doStop() { + rocketMQListenerContainer.stop(); + } + + public void setRetryTemplate(RetryTemplate retryTemplate) { + this.retryTemplate = retryTemplate; + } + + public void setRecoveryCallback(RecoveryCallback recoveryCallback) { + this.recoveryCallback = recoveryCallback; + } + + protected class BindingRocketMQListener + implements RocketMQListener, RetryListener { + + @Override + public void onMessage(Message message) { + boolean enableRetry = RocketMQInboundChannelAdapter.this.retryTemplate != null; + if (enableRetry) { + RocketMQInboundChannelAdapter.this.retryTemplate.execute(context -> { + RocketMQInboundChannelAdapter.this.sendMessage(message); + return null; + }, (RecoveryCallback) RocketMQInboundChannelAdapter.this.recoveryCallback); + } + else { + RocketMQInboundChannelAdapter.this.sendMessage(message); + } + } + + @Override + public boolean open(RetryContext context, + RetryCallback callback) { + return true; + } + + @Override + public void close(RetryContext context, + RetryCallback callback, Throwable throwable) { + } + + @Override + public void onError(RetryContext context, + RetryCallback callback, Throwable throwable) { + + } + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageHandler.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageHandler.java new file mode 100644 index 00000000..6aa0e0fe --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageHandler.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.integration; + +import java.util.Optional; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.apache.rocketmq.spring.support.RocketMQHeaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.Lifecycle; +import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.integration.support.DefaultErrorMessageStrategy; +import org.springframework.integration.support.ErrorMessageStrategy; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.RocketMQBinderConstants; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.Instrumentation; +import com.alibaba.cloud.stream.binder.rocketmq.metrics.InstrumentationManager; + +/** + * @author Jim + */ +public class RocketMQMessageHandler extends AbstractMessageHandler implements Lifecycle { + + private final static Logger log = LoggerFactory + .getLogger(RocketMQMessageHandler.class); + + private ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy(); + + private MessageChannel sendFailureChannel; + + private final RocketMQTemplate rocketMQTemplate; + + private final Boolean transactional; + + private final String destination; + + private final String groupName; + + private final InstrumentationManager instrumentationManager; + + private boolean sync = false; + + private volatile boolean running = false; + + public RocketMQMessageHandler(RocketMQTemplate rocketMQTemplate, String destination, + String groupName, Boolean transactional, + InstrumentationManager instrumentationManager) { + this.rocketMQTemplate = rocketMQTemplate; + this.destination = destination; + this.groupName = groupName; + this.transactional = transactional; + this.instrumentationManager = instrumentationManager; + } + + @Override + public void start() { + if (!transactional) { + instrumentationManager + .addHealthInstrumentation(new Instrumentation(destination)); + try { + rocketMQTemplate.afterPropertiesSet(); + instrumentationManager.getHealthInstrumentation(destination) + .markStartedSuccessfully(); + } + catch (Exception e) { + instrumentationManager.getHealthInstrumentation(destination) + .markStartFailed(e); + log.error("RocketMQTemplate startup failed, Caused by " + e.getMessage()); + throw new MessagingException(MessageBuilder.withPayload( + "RocketMQTemplate startup failed, Caused by " + e.getMessage()) + .build(), e); + } + } + running = true; + } + + @Override + public void stop() { + if (!transactional) { + rocketMQTemplate.destroy(); + } + running = false; + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + protected void handleMessageInternal(org.springframework.messaging.Message message) + throws Exception { + try { + final StringBuilder topicWithTags = new StringBuilder(destination); + String tags = Optional + .ofNullable(message.getHeaders().get(RocketMQHeaders.TAGS)).orElse("") + .toString(); + if (!StringUtils.isEmpty(tags)) { + topicWithTags.append(":").append(tags); + } + SendResult sendRes = null; + if (transactional) { + sendRes = rocketMQTemplate.sendMessageInTransaction(groupName, + topicWithTags.toString(), message, message.getHeaders() + .get(RocketMQBinderConstants.ROCKET_TRANSACTIONAL_ARG)); + log.debug("transactional send to topic " + topicWithTags + " " + sendRes); + } + else { + int delayLevel = 0; + try { + Object delayLevelObj = message.getHeaders() + .getOrDefault(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 0); + if (delayLevelObj instanceof Number) { + delayLevel = ((Number) delayLevelObj).intValue(); + } + else if (delayLevelObj instanceof String) { + delayLevel = Integer.parseInt((String) delayLevelObj); + } + } + catch (Exception e) { + // ignore + } + if (sync) { + sendRes = rocketMQTemplate.syncSend(topicWithTags.toString(), message, + rocketMQTemplate.getProducer().getSendMsgTimeout(), + delayLevel); + log.debug("sync send to topic " + topicWithTags + " " + sendRes); + } + else { + rocketMQTemplate.asyncSend(topicWithTags.toString(), message, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.debug("async send to topic " + topicWithTags + " " + + sendResult); + } + + @Override + public void onException(Throwable e) { + log.error( + "RocketMQ Message hasn't been sent. Caused by " + + e.getMessage()); + if (getSendFailureChannel() != null) { + getSendFailureChannel().send( + RocketMQMessageHandler.this.errorMessageStrategy + .buildErrorMessage( + new MessagingException( + message, e), + null)); + } + } + }); + } + } + if (sendRes != null && !sendRes.getSendStatus().equals(SendStatus.SEND_OK)) { + if (getSendFailureChannel() != null) { + this.getSendFailureChannel().send(message); + } + else { + throw new MessagingException(message, + new MQClientException("message hasn't been sent", null)); + } + } + } + catch (Exception e) { + log.error("RocketMQ Message hasn't been sent. Caused by " + e.getMessage()); + if (getSendFailureChannel() != null) { + getSendFailureChannel().send(this.errorMessageStrategy + .buildErrorMessage(new MessagingException(message, e), null)); + } + else { + throw new MessagingException(message, e); + } + } + + } + + /** + * Set the failure channel. After a send failure, an {@link ErrorMessage} will be sent + * to this channel with a payload of a {@link MessagingException} with the failed + * message and cause. + * @param sendFailureChannel the failure channel. + * @since 0.2.2 + */ + public void setSendFailureChannel(MessageChannel sendFailureChannel) { + this.sendFailureChannel = sendFailureChannel; + } + + /** + * Set the error message strategy implementation to use when sending error messages + * after send failures. Cannot be null. + * @param errorMessageStrategy the implementation. + * @since 0.2.2 + */ + public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { + Assert.notNull(errorMessageStrategy, "'errorMessageStrategy' cannot be null"); + this.errorMessageStrategy = errorMessageStrategy; + } + + public MessageChannel getSendFailureChannel() { + return sendFailureChannel; + } + + public void setSync(boolean sync) { + this.sync = sync; + } +} \ No newline at end of file diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageSource.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageSource.java new file mode 100644 index 00000000..1d1ec5d6 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/integration/RocketMQMessageSource.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2018 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 com.alibaba.cloud.stream.binder.rocketmq.integration; + +import java.util.List; +import java.util.Set; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.MessageQueueListener; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.spring.support.RocketMQUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.context.Lifecycle; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.integration.acks.AcknowledgmentCallback; +import org.springframework.integration.acks.AcknowledgmentCallbackFactory; +import org.springframework.integration.endpoint.AbstractMessageSource; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.alibaba.cloud.stream.binder.rocketmq.consuming.RocketMQMessageQueueChooser; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties; + +/** + * @author Jim + */ +public class RocketMQMessageSource extends AbstractMessageSource + implements DisposableBean, Lifecycle { + + private final static Logger log = LoggerFactory + .getLogger(RocketMQMessageSource.class); + + private final RocketMQCallbackFactory ackCallbackFactory; + + private final RocketMQBinderConfigurationProperties rocketMQBinderConfigurationProperties; + + private final ExtendedConsumerProperties rocketMQConsumerProperties; + + private final String topic; + + private final String group; + + private final Object consumerMonitor = new Object(); + + private DefaultMQPullConsumer consumer; + + private boolean running; + + private MessageSelector messageSelector; + + private RocketMQMessageQueueChooser messageQueueChooser = new RocketMQMessageQueueChooser(); + + public RocketMQMessageSource( + RocketMQBinderConfigurationProperties rocketMQBinderConfigurationProperties, + ExtendedConsumerProperties rocketMQConsumerProperties, + String topic, String group) { + this(new RocketMQCallbackFactory(), rocketMQBinderConfigurationProperties, + rocketMQConsumerProperties, topic, group); + } + + public RocketMQMessageSource(RocketMQCallbackFactory ackCallbackFactory, + RocketMQBinderConfigurationProperties rocketMQBinderConfigurationProperties, + ExtendedConsumerProperties rocketMQConsumerProperties, + String topic, String group) { + this.ackCallbackFactory = ackCallbackFactory; + this.rocketMQBinderConfigurationProperties = rocketMQBinderConfigurationProperties; + this.rocketMQConsumerProperties = rocketMQConsumerProperties; + this.topic = topic; + this.group = group; + } + + @Override + public synchronized void start() { + if (this.isRunning()) { + throw new IllegalStateException( + "pull consumer already running. " + this.toString()); + } + try { + consumer = new DefaultMQPullConsumer(group); + consumer.setNamesrvAddr( + rocketMQBinderConfigurationProperties.getNameServer()); + consumer.setConsumerPullTimeoutMillis( + rocketMQConsumerProperties.getExtension().getPullTimeout()); + consumer.setMessageModel(MessageModel.CLUSTERING); + + String tags = rocketMQConsumerProperties.getExtension().getTags(); + String sql = rocketMQConsumerProperties.getExtension().getSql(); + + if (!StringUtils.isEmpty(tags) && !StringUtils.isEmpty(sql)) { + messageSelector = MessageSelector.byTag(tags); + } + else if (!StringUtils.isEmpty(tags)) { + messageSelector = MessageSelector.byTag(tags); + } + else if (!StringUtils.isEmpty(sql)) { + messageSelector = MessageSelector.bySql(sql); + } + + consumer.registerMessageQueueListener(topic, new MessageQueueListener() { + @Override + public void messageQueueChanged(String topic, Set mqAll, + Set mqDivided) { + log.info( + "messageQueueChanged, topic='{}', mqAll=`{}`, mqDivided=`{}`", + topic, mqAll, mqDivided); + switch (consumer.getMessageModel()) { + case BROADCASTING: + RocketMQMessageSource.this.resetMessageQueues(mqAll); + break; + case CLUSTERING: + RocketMQMessageSource.this.resetMessageQueues(mqDivided); + break; + default: + break; + } + } + }); + consumer.start(); + } + catch (MQClientException e) { + log.error("DefaultMQPullConsumer startup error: " + e.getMessage(), e); + } + this.setRunning(true); + } + + @Override + public synchronized void stop() { + if (this.isRunning()) { + this.setRunning(false); + consumer.shutdown(); + } + } + + @Override + public synchronized boolean isRunning() { + return running; + } + + @Override + protected synchronized Object doReceive() { + if (messageQueueChooser.getMessageQueues() == null + || messageQueueChooser.getMessageQueues().size() == 0) { + return null; + } + try { + int count = 0; + while (count < messageQueueChooser.getMessageQueues().size()) { + MessageQueue messageQueue; + synchronized (this.consumerMonitor) { + messageQueue = messageQueueChooser.choose(); + messageQueueChooser.increment(); + } + + long offset = consumer.fetchConsumeOffset(messageQueue, + rocketMQConsumerProperties.getExtension().isFromStore()); + + log.debug("topic='{}', group='{}', messageQueue='{}', offset now='{}'", + this.topic, this.group, messageQueue, offset); + + PullResult pullResult; + if (messageSelector != null) { + pullResult = consumer.pull(messageQueue, messageSelector, offset, 1); + } + else { + pullResult = consumer.pull(messageQueue, (String) null, offset, 1); + } + + if (pullResult.getPullStatus() == PullStatus.FOUND) { + List messageExtList = pullResult.getMsgFoundList(); + + Message message = RocketMQUtil + .convertToSpringMessage(messageExtList.get(0)); + + AcknowledgmentCallback ackCallback = this.ackCallbackFactory + .createCallback(new RocketMQAckInfo(messageQueue, pullResult, + consumer, offset)); + + Message messageResult = MessageBuilder.fromMessage(message).setHeader( + IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, + ackCallback).build(); + return messageResult; + } + else { + log.debug("messageQueue='{}' PullResult='{}' with topic `{}`", + messageQueueChooser.getMessageQueues(), + pullResult.getPullStatus(), topic); + } + count++; + } + } + catch (Exception e) { + log.error("Consumer pull error: " + e.getMessage(), e); + } + return null; + } + + @Override + public String getComponentType() { + return "rocketmq:message-source"; + } + + public synchronized void setRunning(boolean running) { + this.running = running; + } + + public synchronized void resetMessageQueues(Set queueSet) { + log.info("resetMessageQueues, topic='{}', messageQueue=`{}`", topic, queueSet); + synchronized (this.consumerMonitor) { + this.messageQueueChooser.reset(queueSet); + } + } + + public static class RocketMQCallbackFactory + implements AcknowledgmentCallbackFactory { + + @Override + public AcknowledgmentCallback createCallback(RocketMQAckInfo info) { + return new RocketMQAckCallback(info); + } + + } + + public static class RocketMQAckCallback implements AcknowledgmentCallback { + + private final RocketMQAckInfo ackInfo; + + private boolean acknowledged; + + private boolean autoAckEnabled = true; + + public RocketMQAckCallback(RocketMQAckInfo ackInfo) { + this.ackInfo = ackInfo; + } + + protected void setAcknowledged(boolean acknowledged) { + this.acknowledged = acknowledged; + } + + @Override + public boolean isAcknowledged() { + return this.acknowledged; + } + + @Override + public void noAutoAck() { + this.autoAckEnabled = false; + } + + @Override + public boolean isAutoAck() { + return this.autoAckEnabled; + } + + @Override + public void acknowledge(Status status) { + Assert.notNull(status, "'status' cannot be null"); + if (this.acknowledged) { + throw new IllegalStateException("Already acknowledged"); + } + log.debug("acknowledge(" + status.name() + ") for " + this); + synchronized (this.ackInfo.getConsumerMonitor()) { + try { + switch (status) { + case ACCEPT: + case REJECT: + ackInfo.getConsumer().updateConsumeOffset( + ackInfo.getMessageQueue(), + ackInfo.getPullResult().getNextBeginOffset()); + log.debug("messageQueue='{}' offset update to `{}`", + ackInfo.getMessageQueue(), String.valueOf( + ackInfo.getPullResult().getNextBeginOffset())); + break; + case REQUEUE: + // decrease index and update offset of messageQueue of ackInfo + int oldIndex = ackInfo.getMessageQueueChooser().requeue(); + ackInfo.getConsumer().updateConsumeOffset( + ackInfo.getMessageQueue(), ackInfo.getOldOffset()); + log.debug( + "messageQueue='{}' offset requeue to index:`{}`, oldOffset:'{}'", + ackInfo.getMessageQueue(), oldIndex, + ackInfo.getOldOffset()); + break; + default: + break; + } + } + catch (MQClientException e) { + log.error("acknowledge error: " + e.getErrorMessage(), e); + } + finally { + this.acknowledged = true; + } + } + } + + @Override + public String toString() { + return "RocketMQAckCallback{" + "ackInfo=" + ackInfo + ", acknowledged=" + + acknowledged + ", autoAckEnabled=" + autoAckEnabled + '}'; + } + } + + public class RocketMQAckInfo { + + private final MessageQueue messageQueue; + + private final PullResult pullResult; + + private final DefaultMQPullConsumer consumer; + + private final long oldOffset; + + public RocketMQAckInfo(MessageQueue messageQueue, PullResult pullResult, + DefaultMQPullConsumer consumer, long oldOffset) { + this.messageQueue = messageQueue; + this.pullResult = pullResult; + this.consumer = consumer; + this.oldOffset = oldOffset; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public PullResult getPullResult() { + return pullResult; + } + + public DefaultMQPullConsumer getConsumer() { + return consumer; + } + + public RocketMQMessageQueueChooser getMessageQueueChooser() { + return RocketMQMessageSource.this.messageQueueChooser; + } + + public long getOldOffset() { + return oldOffset; + } + + public Object getConsumerMonitor() { + return RocketMQMessageSource.this.consumerMonitor; + } + + @Override + public String toString() { + return "RocketMQAckInfo{" + "messageQueue=" + messageQueue + ", pullResult=" + + pullResult + ", consumer=" + consumer + ", oldOffset=" + oldOffset + + '}'; + } + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/Instrumentation.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/Instrumentation.java new file mode 100644 index 00000000..f62b018e --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/Instrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.metrics; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Timur Valiev + * @author Jim + */ +public class Instrumentation { + private final String name; + protected final AtomicBoolean started = new AtomicBoolean(false); + protected Exception startException = null; + + public Instrumentation(String name) { + this.name = name; + } + + public boolean isDown() { + return startException != null; + } + + public boolean isUp() { + return started.get(); + } + + public boolean isOutOfService() { + return !started.get() && startException == null; + } + + public void markStartedSuccessfully() { + started.set(true); + } + + public void markStartFailed(Exception e) { + started.set(false); + startException = e; + } + + public String getName() { + return name; + } + + public boolean isStarted() { + return started.get(); + } + + public Exception getStartException() { + return startException; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/InstrumentationManager.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/InstrumentationManager.java new file mode 100644 index 00000000..1152bf54 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/metrics/InstrumentationManager.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.metrics; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @author Timur Valiev + * @author Jim + */ +public class InstrumentationManager { + + private final Map runtime = new ConcurrentHashMap<>(); + + private final Map healthInstrumentations = new HashMap<>(); + + public Set getHealthInstrumentations() { + return healthInstrumentations.entrySet().stream().map(Map.Entry::getValue) + .collect(Collectors.toSet()); + } + + public void addHealthInstrumentation(Instrumentation instrumentation) { + healthInstrumentations.put(instrumentation.getName(), instrumentation); + } + + public Instrumentation getHealthInstrumentation(String key) { + return healthInstrumentations.get(key); + } + + public Map getRuntime() { + return runtime; + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBinderConfigurationProperties.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBinderConfigurationProperties.java new file mode 100644 index 00000000..4fda8c8f --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBinderConfigurationProperties.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.properties; + +import org.apache.rocketmq.common.MixAll; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.alibaba.cloud.stream.binder.rocketmq.RocketMQBinderConstants; + +/** + * @author Timur Valiev + * @author Jim + */ +@ConfigurationProperties(prefix = "spring.cloud.stream.rocketmq.binder") +public class RocketMQBinderConfigurationProperties { + + /** + * The name server for rocketMQ, formats: `host:port;host:port`. + */ + private String nameServer = RocketMQBinderConstants.DEFAULT_NAME_SERVER; + + /** + * The property of "access-key". + */ + private String accessKey; + + /** + * The property of "secret-key". + */ + private String secretKey; + + /** + * Switch flag instance for message trace. + */ + private boolean enableMsgTrace = true; + + /** + * The name value of message trace topic.If you don't config,you can use the default + * trace topic name. + */ + private String customizedTraceTopic = MixAll.RMQ_SYS_TRACE_TOPIC; + + public String getNameServer() { + return nameServer; + } + + public void setNameServer(String nameServer) { + this.nameServer = nameServer; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public boolean isEnableMsgTrace() { + return enableMsgTrace; + } + + public void setEnableMsgTrace(boolean enableMsgTrace) { + this.enableMsgTrace = enableMsgTrace; + } + + public String getCustomizedTraceTopic() { + return customizedTraceTopic; + } + + public void setCustomizedTraceTopic(String customizedTraceTopic) { + this.customizedTraceTopic = customizedTraceTopic; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBindingProperties.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBindingProperties.java new file mode 100644 index 00000000..0829325d --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQBindingProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.properties; + +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; + +/** + * @author Timur Valiev + * @author Jim + */ +public class RocketMQBindingProperties implements BinderSpecificPropertiesProvider { + + private RocketMQConsumerProperties consumer = new RocketMQConsumerProperties(); + + private RocketMQProducerProperties producer = new RocketMQProducerProperties(); + + @Override + public RocketMQConsumerProperties getConsumer() { + return consumer; + } + + public void setConsumer(RocketMQConsumerProperties consumer) { + this.consumer = consumer; + } + + @Override + public RocketMQProducerProperties getProducer() { + return producer; + } + + public void setProducer(RocketMQProducerProperties producer) { + this.producer = producer; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQConsumerProperties.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQConsumerProperties.java new file mode 100644 index 00000000..9554e580 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQConsumerProperties.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.properties; + +import org.apache.rocketmq.client.consumer.MQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +/** + * @author Timur Valiev + * @author Jim + */ +public class RocketMQConsumerProperties { + + /** + * using '||' to split tag {@link MQPushConsumer#subscribe(String, String)} + */ + private String tags; + + /** + * {@link MQPushConsumer#subscribe(String, MessageSelector)} + * {@link MessageSelector#bySql(String)} + */ + private String sql; + + /** + * {@link MessageModel#BROADCASTING} + */ + private Boolean broadcasting = false; + + /** + * if orderly is true, using {@link MessageListenerOrderly} else if orderly if false, + * using {@link MessageListenerConcurrently} + */ + private Boolean orderly = false; + + /** + * for concurrently listener. message consume retry strategy. see + * {@link ConsumeConcurrentlyContext#delayLevelWhenNextConsume}. -1 means dlq(or + * discard, see {@link this#shouldRequeue}), others means requeue + */ + private int delayLevelWhenNextConsume = 0; + + /** + * for orderly listener. next retry delay time + */ + private long suspendCurrentQueueTimeMillis = 1000; + + private Boolean enabled = true; + + // ------------ For Pull Consumer ------------ + + private long pullTimeout = 10 * 1000; + + private boolean fromStore; + + // ------------ For Pull Consumer ------------ + + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } + + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } + + public Boolean getOrderly() { + return orderly; + } + + public void setOrderly(Boolean orderly) { + this.orderly = orderly; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Boolean getBroadcasting() { + return broadcasting; + } + + public void setBroadcasting(Boolean broadcasting) { + this.broadcasting = broadcasting; + } + + public int getDelayLevelWhenNextConsume() { + return delayLevelWhenNextConsume; + } + + public void setDelayLevelWhenNextConsume(int delayLevelWhenNextConsume) { + this.delayLevelWhenNextConsume = delayLevelWhenNextConsume; + } + + public long getSuspendCurrentQueueTimeMillis() { + return suspendCurrentQueueTimeMillis; + } + + public void setSuspendCurrentQueueTimeMillis(long suspendCurrentQueueTimeMillis) { + this.suspendCurrentQueueTimeMillis = suspendCurrentQueueTimeMillis; + } + + public long getPullTimeout() { + return pullTimeout; + } + + public void setPullTimeout(long pullTimeout) { + this.pullTimeout = pullTimeout; + } + + public boolean isFromStore() { + return fromStore; + } + + public void setFromStore(boolean fromStore) { + this.fromStore = fromStore; + } + + public boolean shouldRequeue() { + return delayLevelWhenNextConsume != -1; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQExtendedBindingProperties.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQExtendedBindingProperties.java new file mode 100644 index 00000000..3583873d --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQExtendedBindingProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; + +/** + * @author Timur Valiev + * @author Jim + */ +@ConfigurationProperties("spring.cloud.stream.rocketmq") +public class RocketMQExtendedBindingProperties extends + AbstractExtendedBindingProperties { + + private static final String DEFAULTS_PREFIX = "spring.cloud.stream.rocketmq.default"; + + @Override + public String getDefaultsPrefix() { + return DEFAULTS_PREFIX; + } + + @Override + public Class getExtendedPropertiesEntryClass() { + return RocketMQBindingProperties.class; + } +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQProducerProperties.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQProducerProperties.java new file mode 100644 index 00000000..2d4d197e --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/properties/RocketMQProducerProperties.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.properties; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; + +/** + * @author Timur Valiev + * @author Jim + */ +public class RocketMQProducerProperties { + + private Boolean enabled = true; + + /** + * Name of producer. + */ + private String group; + + /** + * Maximum allowed message size in bytes {@link DefaultMQProducer#maxMessageSize}. + */ + private Integer maxMessageSize = 1024 * 1024 * 4; + + private Boolean transactional = false; + + private Boolean sync = false; + + private Boolean vipChannelEnabled = true; + + /** + * Millis of send message timeout. + */ + private int sendMessageTimeout = 3000; + + /** + * Compress message body threshold, namely, message body larger than 4k will be + * compressed on default. + */ + private int compressMessageBodyThreshold = 1024 * 4; + + /** + * Maximum number of retry to perform internally before claiming sending failure in + * synchronous mode. This may potentially cause message duplication which is up to + * application developers to resolve. + */ + private int retryTimesWhenSendFailed = 2; + + /** + *

+ * Maximum number of retry to perform internally before claiming sending failure in + * asynchronous mode. + *

+ * This may potentially cause message duplication which is up to application + * developers to resolve. + */ + private int retryTimesWhenSendAsyncFailed = 2; + + /** + * Indicate whether to retry another broker on sending failure internally. + */ + private boolean retryNextServer = false; + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Integer getMaxMessageSize() { + return maxMessageSize; + } + + public void setMaxMessageSize(Integer maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + public Boolean getTransactional() { + return transactional; + } + + public void setTransactional(Boolean transactional) { + this.transactional = transactional; + } + + public Boolean getSync() { + return sync; + } + + public void setSync(Boolean sync) { + this.sync = sync; + } + + public Boolean getVipChannelEnabled() { + return vipChannelEnabled; + } + + public void setVipChannelEnabled(Boolean vipChannelEnabled) { + this.vipChannelEnabled = vipChannelEnabled; + } + + public int getSendMessageTimeout() { + return sendMessageTimeout; + } + + public void setSendMessageTimeout(int sendMessageTimeout) { + this.sendMessageTimeout = sendMessageTimeout; + } + + public int getCompressMessageBodyThreshold() { + return compressMessageBodyThreshold; + } + + public void setCompressMessageBodyThreshold(int compressMessageBodyThreshold) { + this.compressMessageBodyThreshold = compressMessageBodyThreshold; + } + + public int getRetryTimesWhenSendFailed() { + return retryTimesWhenSendFailed; + } + + public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) { + this.retryTimesWhenSendFailed = retryTimesWhenSendFailed; + } + + public int getRetryTimesWhenSendAsyncFailed() { + return retryTimesWhenSendAsyncFailed; + } + + public void setRetryTimesWhenSendAsyncFailed(int retryTimesWhenSendAsyncFailed) { + this.retryTimesWhenSendAsyncFailed = retryTimesWhenSendAsyncFailed; + } + + public boolean isRetryNextServer() { + return retryNextServer; + } + + public void setRetryNextServer(boolean retryNextServer) { + this.retryNextServer = retryNextServer; + } + +} \ No newline at end of file diff --git a/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/provisioning/RocketMQTopicProvisioner.java b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/provisioning/RocketMQTopicProvisioner.java new file mode 100644 index 00000000..95f0b5fb --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/java/com/alibaba/cloud/stream/binder/rocketmq/provisioning/RocketMQTopicProvisioner.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq.provisioning; + +import org.apache.rocketmq.client.Validators; +import org.apache.rocketmq.client.exception.MQClientException; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.cloud.stream.provisioning.ProvisioningException; +import org.springframework.cloud.stream.provisioning.ProvisioningProvider; + +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQProducerProperties; + +/** + * @author Timur Valiev + * @author Jim + */ +public class RocketMQTopicProvisioner implements + ProvisioningProvider, ExtendedProducerProperties> { + + @Override + public ProducerDestination provisionProducerDestination(String name, + ExtendedProducerProperties properties) + throws ProvisioningException { + checkTopic(name); + return new RocketProducerDestination(name); + } + + @Override + public ConsumerDestination provisionConsumerDestination(String name, String group, + ExtendedConsumerProperties properties) + throws ProvisioningException { + checkTopic(name); + return new RocketConsumerDestination(name); + } + + private void checkTopic(String topic) { + try { + Validators.checkTopic(topic); + } + catch (MQClientException e) { + throw new AssertionError(e); + } + } + + private static final class RocketProducerDestination implements ProducerDestination { + + private final String producerDestinationName; + + RocketProducerDestination(String destinationName) { + this.producerDestinationName = destinationName; + } + + @Override + public String getName() { + return producerDestinationName; + } + + @Override + public String getNameForPartition(int partition) { + return producerDestinationName; + } + + } + + private static final class RocketConsumerDestination implements ConsumerDestination { + + private final String consumerDestinationName; + + RocketConsumerDestination(String consumerDestinationName) { + this.consumerDestinationName = consumerDestinationName; + } + + @Override + public String getName() { + return this.consumerDestinationName; + } + + } + +} diff --git a/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.binders b/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.binders new file mode 100644 index 00000000..2e5b9953 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.binders @@ -0,0 +1 @@ +rocketmq:com.alibaba.cloud.stream.binder.rocketmq.config.RocketMQBinderAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.factories b/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..82d344e0 --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.stream.binder.rocketmq.config.RocketMQComponent4BinderAutoConfiguration diff --git a/spring-cloud-stream-binder-rocketmq/src/test/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQAutoConfigurationTests.java b/spring-cloud-stream-binder-rocketmq/src/test/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQAutoConfigurationTests.java new file mode 100644 index 00000000..cdecbeab --- /dev/null +++ b/spring-cloud-stream-binder-rocketmq/src/test/java/com/alibaba/cloud/stream/binder/rocketmq/RocketMQAutoConfigurationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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 + * + * http://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 com.alibaba.cloud.stream.binder.rocketmq; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.alibaba.cloud.stream.binder.rocketmq.config.RocketMQBinderAutoConfiguration; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQBinderConfigurationProperties; +import com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQExtendedBindingProperties; + +/** + * @author Jim + */ +public class RocketMQAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(RocketMQBinderAutoConfiguration.class)) + .withPropertyValues( + "spring.cloud.stream.rocketmq.binder.name-server=127.0.0.1:9876", + "spring.cloud.stream.bindings.output.destination=TopicOrderTest", + "spring.cloud.stream.bindings.output.content-type=application/json", + "spring.cloud.stream.bindings.input1.destination=TopicOrderTest", + "spring.cloud.stream.bindings.input1.content-type=application/json", + "spring.cloud.stream.bindings.input1.group=test-group1", + "spring.cloud.stream.rocketmq.bindings.input1.consumer.orderly=true", + "spring.cloud.stream.bindings.input1.consumer.maxAttempts=1", + "spring.cloud.stream.bindings.input2.destination=TopicOrderTest", + "spring.cloud.stream.bindings.input2.content-type=application/json", + "spring.cloud.stream.bindings.input2.group=test-group2", + "spring.cloud.stream.rocketmq.bindings.input2.consumer.orderly=false", + "spring.cloud.stream.rocketmq.bindings.input2.consumer.tags=tag1"); + + @Test + public void testProperties() { + this.contextRunner.run(context -> { + RocketMQBinderConfigurationProperties binderConfigurationProperties = context + .getBean(RocketMQBinderConfigurationProperties.class); + assertThat(binderConfigurationProperties.getNameServer()) + .isEqualTo("127.0.0.1:9876"); + RocketMQExtendedBindingProperties bindingProperties = context + .getBean(RocketMQExtendedBindingProperties.class); + assertThat( + bindingProperties.getExtendedConsumerProperties("input2").getTags()) + .isEqualTo("tag1"); + assertThat(bindingProperties.getExtendedConsumerProperties("input2") + .getOrderly()).isFalse(); + assertThat(bindingProperties.getExtendedConsumerProperties("input1") + .getOrderly()).isTrue(); + }); + } + +} \ No newline at end of file