diff --git a/README.md b/README.md index e70186be..14287f9d 100644 --- a/README.md +++ b/README.md @@ -7,52 +7,11 @@ -#### 多版本控制 -* [spring-cloud-bamboo](spring-cloud-bamboo/README.md) -* spring-cloud-start-multi-version -* [spring-cloud-mult-version-samples](spring-cloud-mult-version-samples/README.md) - - - -#### 灰度发布 -* [spring-cloud-gray-core](spring-cloud-gray-core/README.md) -* spring-cloud-gray-client -* spring-cloud-gray-server -* spring-cloud-start-gray -* spring-cloud-start-gray-server -* [spring-cloud-gray-samples](spring-cloud-gray-samples/README.md) -* [spring-cloud-gray-zookeeper-samples](spring-cloud-gray-zookeeper-samples/README.md) - -#### maven 依赖 -jar包已经上传到maven中央库,可以通过maven直接从中央库下载 -```xml - - - cn.springcloud.gray - spring-cloud-starter-multi-version - {version} - - - - - cn.springcloud.gray - spring-cloud-starter-gray - {version} - - - - - cn.springcloud.gray - spring-cloud-starter-gray-server - {version} - -``` - #### 不足 -gray目前只有灰度管理的基本功能, 像数据持久化,高可用,推送灰度调整消息等, 都没有实现。 也没有界面化, 仅仅只有接口列表。 +没有界面化, 仅仅只有接口列表。 #### 扩展思考 -gray目前仅仅只支持spring cloud eureka, 但是在spring cloud中,eureka只是做为其中一个注册中心, 如果要做spring cloud的灰度管理, 就还需要兼容其他的注册中心, 比如zookeeper, consul等。 +gray目前仅仅只支持spring cloud eureka, 但是在spring cloud中,eureka只是做为其中一个注册中心, 如果要做spring cloud的灰度管理, 就还需要兼容其他的注册中心, 比如zookeeper, consul, nacos等。 diff --git a/pom.xml b/pom.xml index 69cd894a..c8d3982d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,30 +4,29 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - cn.springcloud.gray - spring-cloud-gray - pom - 1.1.0 org.springframework.boot spring-boot-starter-parent 1.5.4.RELEASE + cn.springcloud.gray + spring-cloud-gray + pom + 2.0.0 spring-cloud-gray-dependencies - spring-cloud-bamboo - spring-cloud-starter-multi-version - spring-cloud-mult-version-samples - spring-cloud-gray-core spring-cloud-gray-server spring-cloud-gray-client - spring-cloud-starter-gray + spring-cloud-starter-gray-client spring-cloud-starter-gray-server spring-cloud-gray-samples - spring-cloud-gray-zookeeper-samples + spring-cloud-gray-client-netflix + spring-cloud-gray-utils + spring-cloud-gray-starter-dependencies + spring-cloud-gray-webui @@ -56,11 +55,15 @@ 1.8 1.8 1.8 - Dalston.SR5 - 2.7.0 - - + Edgware.SR5 + 2.9.2 + 1.18.8 + 1.7.26 + 3.5 + 27.0.1-jre + 1.1.0.Final + @@ -73,13 +76,13 @@ import - - cn.springcloud.gray - spring-cloud-gray-dependencies - ${project.version} - pom - import - + + + + + + + @@ -93,6 +96,44 @@ + + + cn.springcloud.gray + spring-cloud-gray-core + ${project.version} + + + cn.springcloud.gray + spring-cloud-gray-client + ${project.version} + + + cn.springcloud.gray + spring-cloud-gray-client-netflix + ${project.version} + + + cn.springcloud.gray + spring-cloud-gray-utils + ${project.version} + + + cn.springcloud.gray + spring-cloud-starter-gray-client + ${project.version} + + + cn.springcloud.gray + spring-cloud-gray-server + ${project.version} + + + cn.springcloud.gray + spring-cloud-starter-gray-server + ${project.version} + + + io.springfox @@ -109,12 +150,84 @@ springfox-bean-validators ${springfox.version} + + org.projectlombok + lombok + ${lombok.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + com.google.guava + guava + ${guava.version} + + + + + + + + + + + + + + commons-io + commons-io + 2.4 + + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + spring-boot:run + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + diff --git a/spring-cloud-bamboo/README.md b/spring-cloud-bamboo/README.md deleted file mode 100644 index 5d310c87..00000000 --- a/spring-cloud-bamboo/README.md +++ /dev/null @@ -1,334 +0,0 @@ -## 多版本控制 - -该项目是在spring-cloud-ribbon的基础上进行扩展,以实现接口的多个版本的调用及负载均衡,支持feign方式和断路器(spring-cloud-hystrix)。 - - -##### 场景 -服务A部署了两个实例 serivceA-1,serviceA-2, spring cloud ribbon默认是轮询的方式将请求分别转到两个实例上。如果由于业务原因,服务需要从1.0升级到2.0。 - -场景1:将所有服务实例平缓的过度到2.0。 -场景2:2.0的服务实例需要兼容1.0的服务接口。 - - - -##### 思路 -在spring cloud微服务体系中,服务的请求来源无外乎两个方面: -来源1:外部请求通过网关(zuul)转发而来。 -来源2:内部服务之间的调用请求。 -不论网关转发过来的请求,还是内部服务调用过来的请求,都需要ribbon做负载均衡,所以可以扩展ribbon的负载均衡策略从而实现不同版本的请求转发到不同的服务实例上。 - -![http request case](../doc/img/u-http-request-case.png) - -网关的转发过程是:zuul > hystrix > ribbon -内部服务调用的过程有两种: -RestTemplate > hystrix > ribbon -Feign > hystrix > ribbon - -而其中hystrix有一个线程池隔离的能力,会创建另一个线程去请求服务,拥有更好的控制并发访问量、以及服务降级等能力,但是会出现一个问题,就是线程变量(ThreadLocal)的传递问题,这可以通过com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault对象解决。 - - - - -##### 代码设计 -虽然整个项目实现起来代码量不少, 但是在接口设计上, 却只有三个简单的接口负责数据传递,路由的逻辑依然是封装在实现了IRule接口的实现类中(后面分析)。 - -![ribbon connection point class designer](../doc/img/cd-ribbon-connection-point.png) - -* BambooRibbonConnectionPoint -这个接口是负责将bamboo跟ribbon连接起来的,将请求的信息, 以及根据业务需要添加的一些路由信息,和获取请求接口的目标版本,还有触发执行LoadBanceRequestTrigger等,都是由该接口的实现类DefaultRibbonConnectionPoint负责实现。 -```java -public class DefaultRibbonConnectionPoint implements BambooRibbonConnectionPoint, ApplicationContextAware { - ... - @Override - public void executeConnectPoint(ConnectPointContext connectPointContext) { - ConnectPointContext.contextLocal.set(connectPointContext); - BambooRequest bambooRequest = connectPointContext.getBambooRequest(); - String requestVersion = versionExtractor.extractVersion(bambooRequest); - BambooRequestContext.initRequestContext(bambooRequest, requestVersion); - executeBeforeReuqestTrigger(); - } - - @Override - public void shutdownconnectPoint() { - try { - executeAfterReuqestTrigger(); - } catch (Exception e) { - ConnectPointContext.getContextLocal().setExcption(e); - } finally { - curRequestTriggers.remove(); - ConnectPointContext.contextLocal.remove(); - BambooRequestContext.shutdownRequestContext(); - } - } - ... -} -``` - - -* RequestVersionExtractor -这个接口负责获取请求需要访问的目标接口的版本。比如有些接口版本是放在路径上,如:/v1/api/test/get。也有放在uri参数中:/api/test/get?v=1。也有可能放到header中,所以在bamboo抽象出来一个接口, 具体的实现由开发者根据业务去实现。 - - -* LoadBalanceRequestTrigger -Ribbon请求的触发器,在ribbon请求发起时, 会被执行。这个接口有三个方法,分别是判断是否需要执行的方法(shouldExecute),以及请求之前执行(before)和请求完成之后执行(after),如果出现异常,after方法依然会被执行。 - - -##### 代码实现 -上面三个接口只是简单的实现了获取请求的目标版本、触发ribbon请求的触发器,以及将信息向下一步传递。在这一段中,将介绍如何与zuul、feign、RestTemplate以及ribbon和hystrix衔接起来。 - -* RestTemplate衔接 -ClientHttpRequestInterceptor是RestTemplate的拦截器接口,可以通过这个接口添加bamboo的逻辑, 从而将RestTemplate和bamboo衔接起来。 -BambooClientHttpRequestIntercptor是ClientHttpRequestInterceptor接口的实现类,它加入了bamboo的逻辑。 -```java -/** - * 用于@LoadBalance 标记的 RestTemplate,主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooClientHttpRequestIntercptor implements ClientHttpRequestInterceptor { - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - - URI uri = request.getURI(); - BambooRequest bambooRequest = BambooRequest.builder() - .serviceId(uri.getHost()) - .uri(uri.getPath()) - .ip(RequestIpKeeper.getRequestIp()) - .addMultiHeaders(request.getHeaders()) - .addMultiParams(WebUtils.getQueryParams(uri.getQuery())) - .build(); - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(bambooRequest).build(); - try { - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return execution.execute(request, body); - } finally { - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - } - } -} -``` - -* Feign衔接 -BambooFeignClient类实现了feign.Client接口, 该类是一个代理类,主要的Feign的调用逻辑依然由被代理的类去执行,在该类中添加了bamboo的逻辑,从而将Feign和bamboo衔接起来。 -```java -/** - * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooFeignClient implements Client { - - private Client delegate; - - - public BambooFeignClient(Client delegate) { - this.delegate = delegate; - } - - @Override - public Response execute(Request request, Request.Options options) throws IOException { - URI uri = URI.create(request.url()); - BambooRequest.Builder builder = BambooRequest.builder() - .serviceId(uri.getHost()) - .uri(uri.getPath()) - .ip(RequestIpKeeper.getRequestIp()) - .addMultiParams(WebUtils.getQueryParams(uri.getQuery())); - - request.headers().entrySet().forEach(entry ->{ - for (String v : entry.getValue()) { - builder.addHeader(entry.getKey(), v); - } - }); - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(builder.build()).build(); - - try { - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return delegate.execute(request, options); - }finally { - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - } - } -} -``` - - -* Zuul衔接 -实现两个ZuulFilter接口,分别是pre和post类型,将bamboo的逻辑加入其中。Pre类型的ZuulFilter获取请求信息,并执行LoadBalanceRequestTrigger#before方法。Post类型的ZuulFilter执行LoadBalanceRequestTrigger#after方法,并清除存在ThradLocal中的相关信息。 -```java -/** - * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooPreZuulFilter extends ZuulFilter { - @Override - public String filterType() { - return FilterConstants.PRE_TYPE; - } - - @Override - public int filterOrder() { - return 10000; - } - - @Override - public boolean shouldFilter() { - return true; - } - - @Override - public Object run() { - RequestContext context = RequestContext.getCurrentContext(); - BambooRequest.Builder builder = BambooRequest.builder() - .serviceId((String)context.get(FilterConstants.SERVICE_ID_KEY)) - .uri((String)context.get(FilterConstants.REQUEST_URI_KEY)) - .ip(context.getZuulRequestHeaders().get(FilterConstants.X_FORWARDED_FOR_HEADER.toLowerCase())) - .addMultiParams(context.getRequestQueryParams()) - .addHeaders(context.getZuulRequestHeaders()) - .addHeaders(context.getOriginResponseHeaders().stream().collect(Collectors.toMap(Pair::first, Pair::second))); - context.getOriginResponseHeaders().forEach(pair-> builder.addHeader(pair.first(), pair.second())); - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(builder.build()).build(); - - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return null; - } - -} -``` -```java -/** - * 做一些善后工作。比如删除BambooRequestContext在ThreadLocal中的信息。 - */ -public class BambooPostZuulFilter extends ZuulFilter { - @Override - public String filterType() { - return FilterConstants.POST_TYPE; - } - - @Override - public int filterOrder() { - return 0; - } - - @Override - public boolean shouldFilter() { - return true; - } - - @Override - public Object run() { -// BambooRequestContext.shutdownRequestContext(); - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - return null; - } -} -``` - -* Hystrix衔接 -Hystrix实现降级、断路器等功能,但是在使用线程池隔离时,ThreadLocal存储的信息如何传递下去呢?使用HystrixRequestVariableDefault可以解决这个问题。可以查看com.netflix.hystrix.strategy.concurrency包下的HystrixContexSchedulerAction、HystrixContextCallable、HystrixContextRunnable,它们都有一段相同功能的代码 -```java -public class HystrixContextRunnable implements Runnable { - - private final Callable actual; - private final HystrixRequestContext parentThreadState; - - //... - - @Override - public void run() { - HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread(); - try { - // set the state of this thread to that of its parent - HystrixRequestContext.setContextOnCurrentThread(parentThreadState); - // execute actual Callable with the state of the parent - try { - actual.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } finally { - // restore this thread back to its original state - HystrixRequestContext.setContextOnCurrentThread(existingState); - } - } - -} -``` -parentThreadState也是一个HystrixRequestContext对象,它是在hystrix创建线程之前的,也就是处理http请求的线程的HystrixRequestContext对象,我们一般也是维护这个对象。在使用线程池隔离时,hystrix会将parentThreadState中的信息复到到新线程中,实现跨线程的数据传递,从而在后面的逻辑中可以获取到parentThreadState中维护的信息,包括ribbon的路由信息。在bamboo中,将一步骤的逻辑放到BambooRequestContext中,将BambooRequestContext实例本身传递下去。 -```java -public class BambooRequestContext { - - private static final Logger log = LoggerFactory.getLogger(BambooRequestContext.class); - - private static final HystrixRequestVariableDefault CURRENT_CONTEXT = new HystrixRequestVariableDefault(); - - - private final String apiVersion; - private final BambooRequest bambooRequest; - private Map params; - - - private BambooRequestContext(BambooRequest bambooRequest, String apiVersion) { - params = new HashMap<>(); - this.apiVersion = apiVersion; - this.bambooRequest = bambooRequest; - } - - - public static BambooRequestContext currentRequestCentxt() { - return CURRENT_CONTEXT.get(); - } - - public static void initRequestContext(BambooRequest bambooRequest, String apiVersion) { - if (!HystrixRequestContext.isCurrentThreadInitialized()) { - HystrixRequestContext.initializeContext(); - } - CURRENT_CONTEXT.set(new BambooRequestContext(bambooRequest, apiVersion)); - } - - public static void shutdownRequestContext() { - if (HystrixRequestContext.isCurrentThreadInitialized()) { - HystrixRequestContext.getContextForCurrentThread().shutdown(); - } - } - - //忽略setter/getter -} -``` - -* Ribbon 路由规则 -Bamboo中的BambooZoneAvoidanceRule继承了ZoneAvoidanceRule,所以它会有ZvoidanceRule的一切特性,在此基础上,还加入了版本过滤的逻辑,这个逻辑主要是由BambooApiVersionPredicate实现。从BambooRequestContext中获取请求的接口的版本,如果有该没有获取到版本,就返回true;如果有获取到版本,就获取服务实例的metadata中的version信息,并进行匹配校验,返回结果。 -```java -public class BambooApiVersionPredicate extends AbstractServerPredicate { - - - public BambooApiVersionPredicate(BambooZoneAvoidanceRule rule) { - super(rule); - } - - @Override - public boolean apply(PredicateKey input) { - BambooLoadBalancerKey loadBalancerKey = getBambooLoadBalancerKey(input); - if (loadBalancerKey != null && !StringUtils.isEmpty(loadBalancerKey.getApiVersion())) { - Map serverMetadata = ((BambooZoneAvoidanceRule) this.rule) - .getServerMetadata(loadBalancerKey.getServiceId(), input.getServer()); - String versions = serverMetadata.get("versions"); - return matchVersion(versions, loadBalancerKey.getApiVersion()); - } - return true; - } - - private BambooLoadBalancerKey getBambooLoadBalancerKey(PredicateKey input) { - if(BambooRequestContext.currentRequestCentxt()!=null){ - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); - String apiVersion = bambooRequestContext.getApiVersion(); - if(!StringUtils.isEmpty(apiVersion)){ - return BambooLoadBalancerKey.builder().apiVersion(apiVersion) - .serviceId(bambooRequestContext.getServiceId()).build(); - } - } - return null; - } - //... -} -``` - -##### 使用说明 -多版本控制 --> [spring-cloud-mult-version-samples](../spring-cloud-mult-version-samples/README.md) \ No newline at end of file diff --git a/spring-cloud-bamboo/pom.xml b/spring-cloud-bamboo/pom.xml deleted file mode 100644 index a731a9c0..00000000 --- a/spring-cloud-bamboo/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - spring-cloud-gray - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo - - - jar - - - - - org.springframework.boot - spring-boot-starter-web - provided - - - org.springframework.boot - spring-boot-starter-test - provided - - - org.springframework.cloud - spring-cloud-starter-eureka - provided - - - org.springframework.cloud - spring-cloud-starter-feign - provided - - - org.springframework.cloud - spring-cloud-starter-zuul - provided - - - - org.springframework.cloud - spring-cloud-netflix-core - provided - - - - - - \ No newline at end of file diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooAppContext.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooAppContext.java deleted file mode 100644 index 376fe6fe..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooAppContext.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.springcloud.bamboo; - - -import cn.springcloud.bamboo.ribbon.EurekaServerExtractor; - -public class BambooAppContext { - - private static BambooRibbonConnectionPoint defaultConnectionPoint; - private static EurekaServerExtractor eurekaServerExtractor; - private static String localIp; - - public static BambooRibbonConnectionPoint getBambooRibbonConnectionPoint(){ - return defaultConnectionPoint; - } - - - static void setDefaultConnectionPoint(BambooRibbonConnectionPoint connectionPoint){ - BambooAppContext.defaultConnectionPoint = connectionPoint; - } - - public static EurekaServerExtractor getEurekaServerExtractor() { - return eurekaServerExtractor; - } - - static void setEurekaServerExtractor(EurekaServerExtractor eurekaServerExtractor) { - BambooAppContext.eurekaServerExtractor = eurekaServerExtractor; - } - - public static String getLocalIp() { - return localIp; - } - - static void setLocalIp(String localIp) { - BambooAppContext.localIp = localIp; - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooConstants.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooConstants.java deleted file mode 100644 index 382b1e6f..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooConstants.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.springcloud.bamboo; - -public class BambooConstants { - - - /** - * 初始化Bean的顺序标准值 - */ - public static final int INITIALIZING_ORDER = 100000; -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooInitializingBean.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooInitializingBean.java deleted file mode 100644 index 578be146..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooInitializingBean.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.springcloud.bamboo; - -import cn.springcloud.bamboo.ribbon.EurekaServerExtractor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -import java.net.InetAddress; -import java.net.UnknownHostException; - - -/** - * Bamboo相关依赖的初始化工作 - */ -public class BambooInitializingBean implements InitializingBean, ApplicationContextAware { - - private static final Logger log = LoggerFactory.getLogger(BambooInitializingBean.class); - - - private ApplicationContext ctx; - - @Override - public void afterPropertiesSet() { - BambooAppContext.setDefaultConnectionPoint(ctx.getBean(BambooRibbonConnectionPoint.class)); - BambooAppContext.setEurekaServerExtractor(ctx.getBean(EurekaServerExtractor.class)); - setLocalIp(); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.ctx = applicationContext; - } - - - /** - * 设置本机ip - */ - private void setLocalIp(){ - try { - BambooAppContext.setLocalIp(InetAddress.getLocalHost().getHostAddress()); - } catch (UnknownHostException e) { - log.error("[IpHelper-getIpAddr] IpHelper error.", e); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequest.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequest.java deleted file mode 100644 index 246103a2..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequest.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.springcloud.bamboo; - -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import java.util.List; -import java.util.Map; - -public class BambooRequest { - private final String uri; - private final String serviceId; - private final String ip; - private final MultiValueMap params; - private final MultiValueMap headers; - private final RequestBody requestBody; - - - private BambooRequest( - String uri, String serviceId, String ip, MultiValueMap params, - MultiValueMap headers, RequestBody requestBody) { - this.uri = uri; - this.serviceId = serviceId; - this.params = params; - this.headers = headers; - this.ip = ip; - this.requestBody = requestBody; - - } - - - public static Builder builder(){ - return new Builder(); - } - - public static class Builder{ - private String uri; - private String serviceId; - private String ip; - private MultiValueMap params = new LinkedMultiValueMap<>(); - private MultiValueMap headers = new LinkedMultiValueMap<>(); - private RequestBody requestBody; - private Builder(){ - - } - - - - public Builder ip(String ip){ - this.ip = ip; - return this; - } - - public Builder uri(String uri){ - this.uri = uri; - return this; - } - - public Builder serviceId(String serviceId){ - this.serviceId = serviceId; - return this; - } - - public Builder params(MultiValueMap params){ - if(params!=null) { - this.params = params; - } - return this; - } - - public Builder addParams(Map params){ - if(params!=null) { - this.params.setAll(params); - } - return this; - } - - public Builder addParameter(String key, String value){ - this.params.add(key, value); - return this; - } - - public Builder addMultiParams(Map> params){ - if(params!=null) { - this.params.putAll(params); - } - return this; - } - - public Builder headers(MultiValueMap headers){ - if(headers!=null) { - this.headers = headers; - } - return this; - } - - public Builder addHeaders(Map headers){ - if(headers!=null) { - this.headers.setAll(headers); - } - return this; - } - - public Builder addMultiHeaders(Map> headers){ - if(headers!=null) { - this.headers.putAll(headers); - } - return this; - } - - public Builder addHeader(String key, String value){ - this.headers.add(key, value); - return this; - } - - public Builder requestBody(RequestBody requestBody){ - this.requestBody = requestBody; - return this; - } - - public Builder requestBody(byte[] body){ - this.requestBody = new BytesRequestBody(body); - return this; - } - - - public BambooRequest build() { - return new BambooRequest(uri, serviceId, ip, params, headers, requestBody); - } - } - - - - - - public String getUri() { - return uri; - } - - public String getServiceId() { - return serviceId; - } - - public MultiValueMap getParams() { - return params; - } - - public MultiValueMap getHeaders() { - return headers; - } - - - public String getIp() { - return ip; - } - - public RequestBody getRequestBody() { - return requestBody; - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequestContext.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequestContext.java deleted file mode 100644 index f10ac3b7..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRequestContext.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.springcloud.bamboo; - -import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; -import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -public class BambooRequestContext { - - private static final Logger log = LoggerFactory.getLogger(BambooRequestContext.class); - - private static final HystrixRequestVariableDefault CURRENT_CONTEXT = new HystrixRequestVariableDefault(); - - - private final String apiVersion; - private final BambooRequest bambooRequest; - private Map params; - - - private BambooRequestContext(BambooRequest bambooRequest, String apiVersion) { - params = new HashMap<>(); - this.apiVersion = apiVersion; - this.bambooRequest = bambooRequest; - } - - - public static BambooRequestContext currentRequestCentxt() { - return CURRENT_CONTEXT.get(); - } - - public static void initRequestContext(BambooRequest bambooRequest, String apiVersion) { - if (!HystrixRequestContext.isCurrentThreadInitialized()) { - HystrixRequestContext.initializeContext(); - } - CURRENT_CONTEXT.set(new BambooRequestContext(bambooRequest, apiVersion)); - } - - public static void shutdownRequestContext() { - if (HystrixRequestContext.isCurrentThreadInitialized()) { - HystrixRequestContext.getContextForCurrentThread().shutdown(); - } - } - - - public String getApiVersion() { - return apiVersion; - } - - public String getServiceId() { - return bambooRequest.getServiceId(); - } - - - public void addParameter(String key, Object value){ - params.put(key, value); - } - - public Object getParameter(String key){ - return params.get(key); - } - - - public String getStrParameter(String key){ - return (String) params.get(key); - } - - public Integer getIntegerParameter(String key){ - return (Integer) params.get(key); - } - - - public Long getLongParameter(String key){ - return (Long) params.get(key); - } - - public Boolean getBooleanParameter(String key){ - return (Boolean) params.get(key); - } - - public BambooRequest getBambooRequest() { - return bambooRequest; - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRibbonConnectionPoint.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRibbonConnectionPoint.java deleted file mode 100644 index 2178eedd..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BambooRibbonConnectionPoint.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.springcloud.bamboo; - - -/** - * 个接口是负责将bamboo跟ribbon连接起来的,将请求的信息, 以及根据业务需要添加的一些路由信息,和获取请求接口的目标版本, - * 还有触发执行LoadBanceRequestTrigger等,都是由该接口的实现类DefaultRibbonConnectionPoint负责实现。 - */ -public interface BambooRibbonConnectionPoint { - - - void executeConnectPoint(ConnectPointContext connectPointContext); - - - void shutdownconnectPoint(); - - -// void executeBeforeReuqestTrigger(); - - -// void executeAfterReuqestTrigger(); - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BytesRequestBody.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BytesRequestBody.java deleted file mode 100644 index 600bda07..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/BytesRequestBody.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.springcloud.bamboo; - -import java.io.UnsupportedEncodingException; - -public class BytesRequestBody implements RequestBody{ - - private static final String DEFAULT_CHARSET = "UTF-8"; - - private byte[] body; - - public BytesRequestBody(byte[] body) { - this.body = body; - } - - @Override - public byte[] getBody() { - return body; - } - - @Override - public String getBodyString() { - return getBodyString(DEFAULT_CHARSET); - } - - @Override - public String getBodyString(String charset) { - try { - return new String(body, charset); - }catch (UnsupportedEncodingException e){ - throw new RuntimeException(e.getMessage(), e); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ConnectPointContext.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ConnectPointContext.java deleted file mode 100644 index 0f70ef1a..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ConnectPointContext.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.springcloud.bamboo; - -public class ConnectPointContext { - - static final ThreadLocal contextLocal = new ThreadLocal<>(); - - - private BambooRequest bambooRequest; - private Throwable excption; - - private ConnectPointContext(BambooRequest bambooRequest) { - this.bambooRequest = bambooRequest; - } - - public BambooRequest getBambooRequest() { - return bambooRequest; - } - - void setBambooRequest(BambooRequest bambooRequest) { - this.bambooRequest = bambooRequest; - } - - - public Throwable getExcption() { - return excption; - } - - void setExcption(Throwable excption) { - this.excption = excption; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private BambooRequest bambooRequest; - - private Builder() { - - } - - - public Builder bambooRequest(BambooRequest bambooRequest) { - this.bambooRequest = bambooRequest; - return this; - } - - public ConnectPointContext build() { - return new ConnectPointContext(bambooRequest); - } - } - - - public static ConnectPointContext getContextLocal() { - return contextLocal.get(); - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/DefaultRibbonConnectionPoint.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/DefaultRibbonConnectionPoint.java deleted file mode 100644 index 2712a19f..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/DefaultRibbonConnectionPoint.java +++ /dev/null @@ -1,94 +0,0 @@ -package cn.springcloud.bamboo; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class DefaultRibbonConnectionPoint implements BambooRibbonConnectionPoint, ApplicationContextAware { - - private RequestVersionExtractor versionExtractor; - private ApplicationContext ctx; - private static ThreadLocal> curRequestTriggers = new ThreadLocal(); - private List requestTriggerList; - - public DefaultRibbonConnectionPoint(RequestVersionExtractor versionExtractor) { - this(versionExtractor, null); - } - - public DefaultRibbonConnectionPoint(RequestVersionExtractor versionExtractor, List requestTriggerList) { - this.versionExtractor = versionExtractor; - this.requestTriggerList = requestTriggerList; - } - - @Override - public void executeConnectPoint(ConnectPointContext connectPointContext) { - ConnectPointContext.contextLocal.set(connectPointContext); - BambooRequest bambooRequest = connectPointContext.getBambooRequest(); - String requestVersion = versionExtractor.extractVersion(bambooRequest); - BambooRequestContext.initRequestContext(bambooRequest, requestVersion); - executeBeforeReuqestTrigger(); - } - - @Override - public void shutdownconnectPoint() { - try { - executeAfterReuqestTrigger(); - } catch (Exception e) { - ConnectPointContext.getContextLocal().setExcption(e); - } finally { - curRequestTriggers.remove(); - ConnectPointContext.contextLocal.remove(); - BambooRequestContext.shutdownRequestContext(); - } - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.ctx = applicationContext; - } - - private List chooseRequestTrigger() { - if (curRequestTriggers.get() != null) { - return curRequestTriggers.get(); - } - - Collection triggers; - - if (requestTriggerList != null) { - triggers = requestTriggerList; - } else { - triggers = ctx.getBeansOfType(LoadBalanceRequestTrigger.class).values(); - } - - List requestTriggers = new ArrayList<>(); - triggers.forEach(trigger -> { - if (trigger.shouldExecute()) { - requestTriggers.add(trigger); - } - }); - curRequestTriggers.set(requestTriggers); - return requestTriggers; - } - - - protected void executeBeforeReuqestTrigger() { - ConnectPointContext connectPointContext = ConnectPointContext.getContextLocal(); - List requestTriggers = chooseRequestTrigger(); - if (requestTriggers != null && !requestTriggers.isEmpty()) { - requestTriggers.forEach(trigger -> trigger.before(connectPointContext)); - } - } - - - protected void executeAfterReuqestTrigger() { - ConnectPointContext connectPointContext = ConnectPointContext.getContextLocal(); - List requestTriggers = chooseRequestTrigger(); - if (requestTriggers != null && !requestTriggers.isEmpty()) { - requestTriggers.forEach(trigger -> trigger.after(connectPointContext)); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/LoadBalanceRequestTrigger.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/LoadBalanceRequestTrigger.java deleted file mode 100644 index 00e2c318..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/LoadBalanceRequestTrigger.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.springcloud.bamboo; - -/** - * Ribbon请求的触发器,在ribbon请求发起时, 会被执行 - */ -public interface LoadBalanceRequestTrigger { - - - /** - * 判断是否需要执行的方法 - * - * @return boolean - */ - boolean shouldExecute(); - - - /** - * 请求之前执行 - * - * @param connectPointContext 连接点上下文 - */ - void before(ConnectPointContext connectPointContext); - - - /** - * 请求完成之后执行 - * 如果出现异常,该方法依然会被执行 - * - * @param connectPointContext 连接点上下文 - */ - void after(ConnectPointContext connectPointContext); -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestBody.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestBody.java deleted file mode 100644 index 0e928b9a..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestBody.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.springcloud.bamboo; - - -import javax.servlet.http.HttpServletRequest; - -/** - * 请求的body - * 如读取 {@link HttpServletRequest#getInputStream()}获取得到的内容 - */ -public interface RequestBody { - - - /** - * 返回byte数组类型的request body - * @return 请求体内容 - */ - byte[] getBody(); - - - /** - * 返回字符串类型的request body - * @return 请求体内容 - */ - String getBodyString(); - - - /** - * 将byte数组类型的request body转换成字符串类型的request body并返回 - * @param charset 字符串编码 - * @return 请求体内容 - */ - String getBodyString(String charset); -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestVersionExtractor.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestVersionExtractor.java deleted file mode 100644 index 0653ac47..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/RequestVersionExtractor.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.springcloud.bamboo; - - -/** - * 这个接口负责获取请求需要访问的目标接口的版本。 - * 比如有些接口版本是放在路径上,如:/v1/api/test/get。也有放在uri参数中:/api/test/get?v=1。 - * 也有可能放到header中,所以在bamboo抽象出来一个接口, 具体的实现由开发者根据业务去实现。 - */ -public interface RequestVersionExtractor { - - String extractVersion(BambooRequest bambooRequest); - - - /** - * 默认从querystring中获取:/api/test?version=1 - */ - class Default implements RequestVersionExtractor { - private static final String VERSION = "version"; - - @Override - public String extractVersion(BambooRequest bambooRequest) { - return bambooRequest.getParams().getFirst(VERSION); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooAutoConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooAutoConfiguration.java deleted file mode 100644 index 93e78fdb..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooAutoConfiguration.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.springcloud.bamboo.autoconfig; - -import cn.springcloud.bamboo.*; -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import cn.springcloud.bamboo.feign.config.BambooFeignConfiguration; -import cn.springcloud.bamboo.ribbon.BambooClientHttpRequestIntercptor; -import cn.springcloud.bamboo.ribbon.EurekaServerExtractor; -import cn.springcloud.bamboo.zuul.config.BambooZuulConfiguration; -import org.springframework.beans.factory.annotation.Autowired; -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.loadbalancer.LoadBalanced; -import org.springframework.cloud.netflix.ribbon.RibbonClients; -import org.springframework.cloud.netflix.ribbon.SpringClientFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.Order; -import org.springframework.web.client.RestTemplate; - -import java.util.Collections; -import java.util.List; - -/** - * Created by saleson on 2017/11/9. - */ -@Configuration -@EnableConfigurationProperties({BambooProperties.class}) -@AutoConfigureBefore({BambooFeignConfiguration.class, BambooZuulConfiguration.class}) -@Import(BambooWebConfiguration.class) -//@RibbonClients(defaultConfiguration = {BambooExtConfigration.class}) -@RibbonClients(defaultConfiguration = BambooRibbonClientsConfiguration.class) -public class BambooAutoConfiguration { - - - public static class UnUseBambooIRule { - - } - - -// @Autowired(required = false) -// private IClientConfig config; - - @Autowired - private SpringClientFactory springClientFactory; - @Autowired - private BambooProperties bambooProperties; - - - @Bean - @LoadBalanced - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new BambooClientHttpRequestIntercptor(bambooProperties)); - return restTemplate; - } - - @Bean - @ConditionalOnMissingBean - public EurekaServerExtractor eurekaServerExtractor() { - return new EurekaServerExtractor(springClientFactory); - } - - -// @Bean -// @ConditionalOnMissingBean(value = {BambooAutoConfiguration.UnUseBambooIRule.class}) -// public IRule ribbonRule() { -// BambooZoneAvoidanceRule rule = new BambooZoneAvoidanceRule(); -// rule.initWithNiwsConfig(config); -// return rule; -// } - - @Bean - @ConditionalOnMissingBean - public RequestVersionExtractor requestVersionExtractor() { - return new RequestVersionExtractor.Default(); - } - - - @Bean - @ConditionalOnMissingBean - public BambooRibbonConnectionPoint bambooRibbonConnectionPoint( - RequestVersionExtractor requestVersionExtractor, - @Autowired(required = false) List requestTriggerList) { - if (requestTriggerList != null) { - requestTriggerList = Collections.EMPTY_LIST; - } - return new DefaultRibbonConnectionPoint(requestVersionExtractor, requestTriggerList); - } - - @Bean - @Order(value = BambooConstants.INITIALIZING_ORDER) - public BambooInitializingBean bambooInitializingBean() { - return new BambooInitializingBean(); - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooRibbonClientsConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooRibbonClientsConfiguration.java deleted file mode 100644 index 53f14528..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooRibbonClientsConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.springcloud.bamboo.autoconfig; - - -import cn.springcloud.bamboo.ribbon.loadbalancer.BambooZoneAvoidanceRule; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.IRule; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class BambooRibbonClientsConfiguration { - - @Autowired(required = false) - private IClientConfig config; - - @Bean - @ConditionalOnMissingBean(value = {BambooAutoConfiguration.UnUseBambooIRule.class}) - public IRule ribbonRule() { - BambooZoneAvoidanceRule rule = new BambooZoneAvoidanceRule(); - rule.initWithNiwsConfig(config); - return rule; - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooWebConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooWebConfiguration.java deleted file mode 100644 index d013807e..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/BambooWebConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.springcloud.bamboo.autoconfig; - -import cn.springcloud.bamboo.web.IpKeepInterceptor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; - -@Configuration -public class BambooWebConfiguration extends WebMvcConfigurerAdapter { - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new IpKeepInterceptor()); - super.addInterceptors(registry); - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/properties/BambooProperties.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/properties/BambooProperties.java deleted file mode 100644 index 08bf2927..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/autoconfig/properties/BambooProperties.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.springcloud.bamboo.autoconfig.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("multiversion") -public class BambooProperties { - - private BambooRequest bambooRequest = new BambooRequest(); - - public BambooRequest getBambooRequest() { - return bambooRequest; - } - - public void setBambooRequest(BambooRequest bambooRequest) { - this.bambooRequest = bambooRequest; - } - - public static class BambooRequest { - - private boolean loadBody = false; - - /** - * 是否读取并加载请求的body数据 - * - * @return - */ - public boolean isLoadBody() { - return loadBody; - } - - public void setLoadBody(boolean loadBody) { - this.loadBody = loadBody; - } - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/BambooFeignClient.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/BambooFeignClient.java deleted file mode 100644 index 9659a7c4..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/BambooFeignClient.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.springcloud.bamboo.feign; - -import cn.springcloud.bamboo.BambooAppContext; -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.bamboo.ConnectPointContext; -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import cn.springcloud.bamboo.utils.WebUtils; -import cn.springcloud.bamboo.web.RequestIpKeeper; -import feign.Client; -import feign.Request; -import feign.Response; - -import java.io.IOException; -import java.net.URI; - -/** - * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooFeignClient implements Client { - - private Client delegate; - private BambooProperties bambooProperties; - - public BambooFeignClient(BambooProperties bambooProperties, Client delegate) { - this.delegate = delegate; - this.bambooProperties = bambooProperties; - } - - @Override - public Response execute(Request request, Request.Options options) throws IOException { - URI uri = URI.create(request.url()); - BambooRequest.Builder builder = BambooRequest.builder() - .serviceId(uri.getHost()) - .uri(uri.getPath()) - .ip(RequestIpKeeper.getRequestIp()) - .addMultiParams(WebUtils.getQueryParams(uri.getQuery())); - if(bambooProperties.getBambooRequest().isLoadBody()){ - builder.requestBody(request.body()); - } - - - request.headers().entrySet().forEach(entry ->{ - for (String v : entry.getValue()) { - builder.addHeader(entry.getKey(), v); - } - }); - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(builder.build()).build(); - - try { - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return delegate.execute(request, options); - }finally { - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignClientsConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignClientsConfiguration.java deleted file mode 100644 index fd591974..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignClientsConfiguration.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.springcloud.bamboo.feign.config; - -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import cn.springcloud.bamboo.feign.BambooFeignClient; -import feign.Client; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class BambooFeignClientsConfiguration { - - - @Autowired - private Client feignClient; - @Autowired - private BambooProperties bambooProperties; - - @Bean - public Client bambooFeignClient() { - return new BambooFeignClient(bambooProperties, feignClient); - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignConfiguration.java deleted file mode 100644 index c2a23416..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/feign/config/BambooFeignConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.springcloud.bamboo.feign.config; - -import com.netflix.loadbalancer.ILoadBalancer; -import feign.Feign; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.netflix.feign.EnableFeignClients; -import org.springframework.context.annotation.Configuration; - -/** - * Created by saleson on 2017/11/9. - */ -@ConditionalOnClass(value = {ILoadBalancer.class, Feign.class}) -@Configuration -@EnableFeignClients(defaultConfiguration = {BambooFeignClientsConfiguration.class}) -public class BambooFeignConfiguration { - - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/BambooClientHttpRequestIntercptor.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/BambooClientHttpRequestIntercptor.java deleted file mode 100644 index 1c9cc1a1..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/BambooClientHttpRequestIntercptor.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.springcloud.bamboo.ribbon; - -import cn.springcloud.bamboo.BambooAppContext; -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.bamboo.ConnectPointContext; -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import cn.springcloud.bamboo.utils.WebUtils; -import cn.springcloud.bamboo.web.RequestIpKeeper; -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; - - -/** - * 用于@LoadBalance 标记的 RestTemplate,主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooClientHttpRequestIntercptor implements ClientHttpRequestInterceptor { - - private BambooProperties bambooProperties; - - public BambooClientHttpRequestIntercptor(BambooProperties bambooProperties) { - this.bambooProperties = bambooProperties; - } - - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - - URI uri = request.getURI(); - BambooRequest.Builder bambooReqBuilder = BambooRequest.builder() - .serviceId(uri.getHost()) - .uri(uri.getPath()) - .ip(RequestIpKeeper.getRequestIp()) - .addMultiHeaders(request.getHeaders()) - .addMultiParams(WebUtils.getQueryParams(uri.getQuery())); - - if(bambooProperties.getBambooRequest().isLoadBody()) { - bambooReqBuilder.requestBody(body); - } - - BambooRequest bambooRequest = bambooReqBuilder.build(); - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(bambooRequest).build(); - try { - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return execution.execute(request, body); - } finally { - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - } - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/EurekaServerExtractor.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/EurekaServerExtractor.java deleted file mode 100644 index e76f3420..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/EurekaServerExtractor.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.springcloud.bamboo.ribbon; - -import com.netflix.loadbalancer.Server; -import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; -import org.springframework.cloud.netflix.ribbon.ServerIntrospector; -import org.springframework.cloud.netflix.ribbon.SpringClientFactory; - -import java.util.Map; - -/** - * 获取服务实例的相关信息 - */ -public class EurekaServerExtractor { - - - private SpringClientFactory clientFactory; - - public EurekaServerExtractor(SpringClientFactory clientFactory) { - this.clientFactory = clientFactory; - } - - public ServerIntrospector serverIntrospector(String serviceId) { - ServerIntrospector serverIntrospector = this.clientFactory.getInstance(serviceId, - ServerIntrospector.class); - if (serverIntrospector == null) { - serverIntrospector = new DefaultServerIntrospector(); - } - return serverIntrospector; - } - - /** - * 获取实例的metadata信息 - * - * @param serviceId 服务id - * @param server ribbon服务器(服务实例) - * @return 服务实例的metadata信息 - */ - public Map getServerMetadata(String serviceId, Server server) { - return serverIntrospector(serviceId).getMetadata(server); - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooApiVersionPredicate.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooApiVersionPredicate.java deleted file mode 100644 index bc226f47..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooApiVersionPredicate.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.springcloud.bamboo.ribbon.loadbalancer; - -import cn.springcloud.bamboo.BambooRequestContext; -import com.netflix.loadbalancer.AbstractServerPredicate; -import com.netflix.loadbalancer.PredicateKey; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang3.StringUtils; - -import java.util.Map; - -/** - * Created by saleson on 2017/11/10. - */ -public class BambooApiVersionPredicate extends AbstractServerPredicate { - - - public BambooApiVersionPredicate(BambooZoneAvoidanceRule rule) { - super(rule); - } - - @Override - public boolean apply(PredicateKey input) { - BambooLoadBalancerKey loadBalancerKey = getBambooLoadBalancerKey(input); - if (loadBalancerKey != null && !StringUtils.isEmpty(loadBalancerKey.getApiVersion())) { - Map serverMetadata = ((BambooZoneAvoidanceRule) this.rule) - .getServerMetadata(loadBalancerKey.getServiceId(), input.getServer()); - String versions = serverMetadata.get("versions"); - return matchVersion(versions, loadBalancerKey.getApiVersion()); - } - return true; - } - - private BambooLoadBalancerKey getBambooLoadBalancerKey(PredicateKey input) { - /*if (input.getLoadBalancerKey() != null && input.getLoadBalancerKey() instanceof BambooLoadBalancerKey) { - return (BambooLoadBalancerKey) input.getLoadBalancerKey(); - } else if (BambooRequestContext.instance() != null) { - return BambooRequestContext.instance().getLoadBalancerKey(); - } else */if(BambooRequestContext.currentRequestCentxt()!=null){ - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); - String apiVersion = bambooRequestContext.getApiVersion(); - if(!StringUtils.isEmpty(apiVersion)){ - return BambooLoadBalancerKey.builder().apiVersion(apiVersion) - .serviceId(bambooRequestContext.getServiceId()).build(); - } - } - return null; - } - - /** - * 匹配api version - * @param serverVersions - * @param apiVersion - * @return - */ - private boolean matchVersion(String serverVersions, String apiVersion) { - if (StringUtils.isEmpty(serverVersions)) { - return false; - } - String[] versions = StringUtils.split(serverVersions, ","); - return ArrayUtils.contains(versions, apiVersion); - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooLoadBalancerKey.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooLoadBalancerKey.java deleted file mode 100644 index 05296998..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooLoadBalancerKey.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.springcloud.bamboo.ribbon.loadbalancer; - -/** - * Created by saleson on 2017/11/9. - */ -public class BambooLoadBalancerKey { - - - private String serviceId; - private String apiVersion; - - - private BambooLoadBalancerKey() { - - } - - - public String getApiVersion() { - return apiVersion; - } - - public String getServiceId() { - return serviceId; - } - - private void setApiVersion(String apiVersion) { - this.apiVersion = apiVersion; - } - - public static Builder builder() { - return new Builder(); - } - - - public static class Builder { - - private BambooLoadBalancerKey build = new BambooLoadBalancerKey(); - - public Builder apiVersion(String apiVersion) { - build.apiVersion = apiVersion; - return this; - } - - public Builder serviceId(String serviceId) { - build.serviceId = serviceId; - return this; - } - - public BambooLoadBalancerKey build() { - return build; - } - - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooZoneAvoidanceRule.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooZoneAvoidanceRule.java deleted file mode 100644 index 15bb835d..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/ribbon/loadbalancer/BambooZoneAvoidanceRule.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.springcloud.bamboo.ribbon.loadbalancer; - -import cn.springcloud.bamboo.BambooAppContext; -import com.netflix.loadbalancer.AbstractServerPredicate; -import com.netflix.loadbalancer.CompositePredicate; -import com.netflix.loadbalancer.Server; -import com.netflix.loadbalancer.ZoneAvoidanceRule; - -import java.util.Map; - -/** - * Created by saleson on 2017/11/9. - */ -public class BambooZoneAvoidanceRule extends ZoneAvoidanceRule { - - protected CompositePredicate bambooCompositePredicate; - - public BambooZoneAvoidanceRule() { - super(); - BambooApiVersionPredicate apiVersionPredicate = new BambooApiVersionPredicate(this); - bambooCompositePredicate = CompositePredicate.withPredicates(super.getPredicate(), - apiVersionPredicate).build(); - } - - @Override - public AbstractServerPredicate getPredicate() { - return bambooCompositePredicate; - } - - - public Map getServerMetadata(String serviceId, Server server) { - return BambooAppContext.getEurekaServerExtractor().getServerMetadata(serviceId, server); - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/IpKeepInterceptor.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/IpKeepInterceptor.java deleted file mode 100644 index ef37e113..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/IpKeepInterceptor.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.springcloud.bamboo.web; - -import cn.springcloud.bamboo.utils.WebUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.net.InetAddress; -import java.net.UnknownHostException; - - -/** - * 获取请求的真实ip,并保存到当前线程中。 - * @see RequestIpKeeper - */ -public class IpKeepInterceptor extends HandlerInterceptorAdapter { - - private static final Logger log = LoggerFactory.getLogger(IpKeepInterceptor.class); - - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - //获取ip - String ip = WebUtils.getIpAddr(request); - //保存 - RequestIpKeeper.instance().setIp(ip); - return super.preHandle(request, response, handler); - } - - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - //清除ThreadLocal - RequestIpKeeper.instance().clear(); - super.afterCompletion(request, response, handler, ex); - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/RequestIpKeeper.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/RequestIpKeeper.java deleted file mode 100644 index fad61f48..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/web/RequestIpKeeper.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.springcloud.bamboo.web; - -import cn.springcloud.bamboo.BambooAppContext; -import org.apache.commons.lang3.StringUtils; - -public final class RequestIpKeeper { - - private static final ThreadLocal ipLocal = new ThreadLocal<>(); - - - private static RequestIpKeeper INSTANCE = new RequestIpKeeper(); - - private RequestIpKeeper() { - - } - - public static RequestIpKeeper instance() { - return INSTANCE; - } - - - void setIp(String ip) { - ipLocal.set(ip); - } - - - public String getIp() { - return ipLocal.get(); - } - - - public void clear() { - ipLocal.remove(); - } - - - public static String getRequestIp() { - String ip = instance().getIp(); - if (StringUtils.isEmpty(ip)) { - ip = BambooAppContext.getLocalIp(); - } - return ip; - } - -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/config/BambooZuulConfiguration.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/config/BambooZuulConfiguration.java deleted file mode 100644 index 6ca5827b..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/config/BambooZuulConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.springcloud.bamboo.zuul.config; - -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import cn.springcloud.bamboo.zuul.filter.BambooPostZuulFilter; -import cn.springcloud.bamboo.zuul.filter.BambooPreZuulFilter; -import com.netflix.zuul.http.ZuulServlet; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConditionalOnClass(value = ZuulServlet.class) -public class BambooZuulConfiguration { - @Autowired - private BambooProperties bambooProperties; - - @Bean - public BambooPreZuulFilter bambooPreZuulFilter(){ - return new BambooPreZuulFilter(bambooProperties); - } - - @Bean - public BambooPostZuulFilter bambooPostZuulFilter(){ - return new BambooPostZuulFilter(); - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPostZuulFilter.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPostZuulFilter.java deleted file mode 100644 index 06f1bfbe..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPostZuulFilter.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.springcloud.bamboo.zuul.filter; - -import cn.springcloud.bamboo.BambooAppContext; -import com.netflix.zuul.ZuulFilter; -import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; - -/** - * 做一些善后工作。比如删除BambooRequestContext在ThreadLocal中的信息。 - */ -public class BambooPostZuulFilter extends ZuulFilter { - @Override - public String filterType() { - return FilterConstants.POST_TYPE; - } - - @Override - public int filterOrder() { - return 0; - } - - @Override - public boolean shouldFilter() { - return true; - } - - @Override - public Object run() { -// BambooRequestContext.shutdownRequestContext(); - BambooAppContext.getBambooRibbonConnectionPoint().shutdownconnectPoint(); - return null; - } -} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPreZuulFilter.java b/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPreZuulFilter.java deleted file mode 100755 index 819f2091..00000000 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/zuul/filter/BambooPreZuulFilter.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.springcloud.bamboo.zuul.filter; - -import cn.springcloud.bamboo.BambooAppContext; -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.bamboo.ConnectPointContext; -import cn.springcloud.bamboo.autoconfig.properties.BambooProperties; -import com.netflix.util.Pair; -import com.netflix.zuul.ZuulFilter; -import com.netflix.zuul.context.RequestContext; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; - -import javax.servlet.http.HttpServletRequest; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Enumeration; -import java.util.stream.Collectors; - -/** - * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 - */ -public class BambooPreZuulFilter extends ZuulFilter { - - private static final Logger log = LoggerFactory.getLogger(BambooPreZuulFilter.class); - - private BambooProperties bambooProperties; - - public BambooPreZuulFilter(BambooProperties bambooProperties) { - this.bambooProperties = bambooProperties; - } - - @Override - public String filterType() { - return FilterConstants.PRE_TYPE; - } - - @Override - public int filterOrder() { - return 10000; - } - - @Override - public boolean shouldFilter() { - return true; - } - - @Override - public Object run() { - RequestContext context = RequestContext.getCurrentContext(); - BambooRequest.Builder builder = BambooRequest.builder() - .serviceId((String)context.get(FilterConstants.SERVICE_ID_KEY)) - .uri((String)context.get(FilterConstants.REQUEST_URI_KEY)) - .ip(context.getZuulRequestHeaders().get(FilterConstants.X_FORWARDED_FOR_HEADER.toLowerCase())) - .addMultiParams(context.getRequestQueryParams()) - .addHeaders(context.getZuulRequestHeaders()); - - // add http server request header - HttpServletRequest servletRequest = context.getRequest(); - Enumeration headerNames = servletRequest.getHeaderNames(); - while(headerNames.hasMoreElements()){ - String headerName = headerNames.nextElement(); - builder.addHeader(headerName, servletRequest.getHeader(headerName)); - } - - if(bambooProperties.getBambooRequest().isLoadBody()) { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(context.getRequest().getInputStream())); - byte[] reqBody = IOUtils.toByteArray(reader); - builder.requestBody(reqBody); - } catch (IOException e) { - String errorMsg = "获取request body出现异常"; - log.error(errorMsg, e); - throw new RuntimeException(errorMsg, e); - } - } - - ConnectPointContext connectPointContext = ConnectPointContext.builder().bambooRequest(builder.build()).build(); - - BambooAppContext.getBambooRibbonConnectionPoint().executeConnectPoint(connectPointContext); - return null; - } - -} diff --git a/spring-cloud-gray-client-netflix/pom.xml b/spring-cloud-gray-client-netflix/pom.xml new file mode 100644 index 00000000..97a17ae4 --- /dev/null +++ b/spring-cloud-gray-client-netflix/pom.xml @@ -0,0 +1,101 @@ + + + + spring-cloud-gray + cn.springcloud.gray + 2.0.0 + + 4.0.0 + + spring-cloud-gray-client-netflix + + + + cn.springcloud.gray + spring-cloud-gray-utils + + + cn.springcloud.gray + spring-cloud-gray-core + + + cn.springcloud.gray + spring-cloud-gray-client + + + + org.springframework + spring-web + + + io.github.openfeign + feign-hystrix + + + io.github.openfeign + feign-core + + + org.springframework.cloud + spring-cloud-netflix-core + + + org.springframework.cloud + spring-cloud-context + + + + org.springframework.cloud + spring-cloud-netflix-eureka-client + + + com.netflix.eureka + eureka-client + + + com.netflix.ribbon + ribbon-eureka + + + com.netflix.ribbon + ribbon-loadbalancer + + + com.netflix.ribbon + ribbon-core + + + com.netflix.zuul + zuul-core + + + org.projectlombok + lombok + + + javax.servlet + javax.servlet-api + + + org.slf4j + slf4j-api + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + + + + \ No newline at end of file diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/EurekaServerExplainer.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/EurekaServerExplainer.java new file mode 100644 index 00000000..5ffd5462 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/EurekaServerExplainer.java @@ -0,0 +1,50 @@ +package cn.springcloud.gray.client.netflix; + +import cn.springcloud.gray.servernode.ServerExplainer; +import cn.springcloud.gray.servernode.ServerSpec; +import com.netflix.loadbalancer.Server; +import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; +import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; + +import java.util.Map; + +public class EurekaServerExplainer implements ServerExplainer { + + private SpringClientFactory springClientFactory; + + public EurekaServerExplainer(SpringClientFactory springClientFactory) { + this.springClientFactory = springClientFactory; + } + + @Override + public ServerSpec apply(Server server) { + Map metadata = getServerMetadata(server.getMetaInfo().getServiceIdForDiscovery(), server); + return ServerSpec.builder().instanceId(server.getMetaInfo().getInstanceId()) + .serviceId(server.getMetaInfo().getServiceIdForDiscovery()) + .metadatas(metadata).build(); + } + + public ServerIntrospector serverIntrospector(String serviceId) { + if (springClientFactory == null) { + return new DefaultServerIntrospector(); + } + ServerIntrospector serverIntrospector = this.springClientFactory.getInstance(serviceId, + ServerIntrospector.class); + if (serverIntrospector == null) { + serverIntrospector = new DefaultServerIntrospector(); + } + return serverIntrospector; + } + + /** + * 获取实例的metadata信息 + * + * @param serviceId 服务id + * @param server ribbon服务器(服务实例) + * @return 服务实例的metadata信息 + */ + public Map getServerMetadata(String serviceId, Server server) { + return serverIntrospector(serviceId).getMetadata(server); + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/GrayClientHolder.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/GrayClientHolder.java new file mode 100644 index 00000000..3635a545 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/GrayClientHolder.java @@ -0,0 +1,37 @@ +package cn.springcloud.gray.client.netflix; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.servernode.ServerExplainer; +import cn.springcloud.gray.request.RequestLocalStorage; +import com.netflix.loadbalancer.Server; + +public class GrayClientHolder { + + private static GrayManager grayManager; + private static RequestLocalStorage requestLocalStorage; + private static ServerExplainer serverExplainer; + + public static GrayManager getGrayManager() { + return grayManager; + } + + public static void setGrayManager(GrayManager grayManager) { + GrayClientHolder.grayManager = grayManager; + } + + public static RequestLocalStorage getRequestLocalStorage() { + return requestLocalStorage; + } + + public static void setRequestLocalStorage(RequestLocalStorage requestLocalStorage) { + GrayClientHolder.requestLocalStorage = requestLocalStorage; + } + + public static ServerExplainer getServerExplainer() { + return serverExplainer; + } + + public static void setServerExplainer(ServerExplainer serverExplainer) { + GrayClientHolder.serverExplainer = serverExplainer; + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientEurekaAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/GrayClientEurekaAutoConfiguration.java similarity index 56% rename from spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientEurekaAutoConfiguration.java rename to spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/GrayClientEurekaAutoConfiguration.java index 200f7080..1d74e9ff 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientEurekaAutoConfiguration.java +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/GrayClientEurekaAutoConfiguration.java @@ -1,11 +1,13 @@ -package cn.springcloud.gray.client.config; +package cn.springcloud.gray.client.netflix.configuration; import cn.springcloud.gray.InstanceLocalInfo; +import cn.springcloud.gray.client.netflix.EurekaServerExplainer; import com.netflix.discovery.EurekaClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -17,15 +19,29 @@ @ConditionalOnBean(EurekaClient.class) public class GrayClientEurekaAutoConfiguration { + + @Autowired + private SpringClientFactory springClientFactory; + @Bean @ConditionalOnMissingBean public InstanceLocalInfo instanceLocalInfo(@Autowired EurekaRegistration registration) { String instanceId = registration.getInstanceConfig().getInstanceId(); - InstanceLocalInfo localInfo = new InstanceLocalInfo(); - localInfo.setInstanceId(instanceId); - localInfo.setServiceId(registration.getServiceId()); - localInfo.setGray(false); - return localInfo; + return InstanceLocalInfo.builder() + .instanceId(instanceId) + .serviceId(registration.getServiceId()) + .host(registration.getHost()) + .port(registration.getPort()) + .build(); } + + + @Bean + @ConditionalOnMissingBean + public EurekaServerExplainer eurekaServerExplainer() { + return new EurekaServerExplainer(springClientFactory); + } + + } diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/HystrixGrayAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/HystrixGrayAutoConfiguration.java new file mode 100644 index 00000000..a39727c9 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/HystrixGrayAutoConfiguration.java @@ -0,0 +1,61 @@ +package cn.springcloud.gray.client.netflix.configuration; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.client.netflix.hystrix.HystrixRequestLocalStorage; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import cn.springcloud.gray.request.RequestLocalStorage; +import cn.springcloud.gray.web.GrayTrackFilter; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; +import feign.hystrix.HystrixFeign; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; + +@Configuration +@ConditionalOnClass({HystrixCommand.class, HystrixFeign.class}) +public class HystrixGrayAutoConfiguration { + + + @Autowired + private GrayManager grayManager; + + + @Bean + public RequestLocalStorage requestLocalStorage() { + return new HystrixRequestLocalStorage(); + } + + + @Bean + public GrayTrackFilter grayTrackFilter( + RequestLocalStorage requestLocalStorage, + List> trackors) { + return new GrayTrackFilter(requestLocalStorage, trackors) { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!HystrixRequestContext.isCurrentThreadInitialized()) { + HystrixRequestContext.initializeContext(); + } + try { + super.doFilter(request, response, chain); + } finally { + if (HystrixRequestContext.isCurrentThreadInitialized()) { + HystrixRequestContext.getContextForCurrentThread().shutdown(); + } + } + } + }; + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/NetflixGrayAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/NetflixGrayAutoConfiguration.java new file mode 100644 index 00000000..e0e29d80 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/configuration/NetflixGrayAutoConfiguration.java @@ -0,0 +1,31 @@ +package cn.springcloud.gray.client.netflix.configuration; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.client.netflix.connectionpoint.DefaultRibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.ribbon.configuration.GrayRibbonClientsConfiguration; +import cn.springcloud.gray.request.RequestLocalStorage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.netflix.ribbon.RibbonClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RibbonClients(defaultConfiguration = GrayRibbonClientsConfiguration.class) +public class NetflixGrayAutoConfiguration { + + + @Autowired + private GrayManager grayManager; + @Autowired + private RequestLocalStorage requestLocalStorage; + + + @Bean + @ConditionalOnMissingBean + public RibbonConnectionPoint ribbonConnectionPoint() { + return new DefaultRibbonConnectionPoint(grayManager, requestLocalStorage); + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/ConnectPointContext.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/ConnectPointContext.java new file mode 100644 index 00000000..1c046810 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/ConnectPointContext.java @@ -0,0 +1,35 @@ +package cn.springcloud.gray.client.netflix.connectionpoint; + +import cn.springcloud.gray.request.GrayRequest; +import lombok.*; + + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ConnectPointContext { + + private static final ThreadLocal contextLocal = new ThreadLocal<>(); + + + private GrayRequest grayRequest; + @Setter + private Throwable throwable; + + private String interceptroType; + + + static void setContextLocal(ConnectPointContext cxt) { + contextLocal.set(cxt); + } + + static void removeContextLocal() { + contextLocal.remove(); + } + + + public static ConnectPointContext getContextLocal() { + return contextLocal.get(); + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/DefaultRibbonConnectionPoint.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/DefaultRibbonConnectionPoint.java new file mode 100644 index 00000000..df4c4ead --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/DefaultRibbonConnectionPoint.java @@ -0,0 +1,60 @@ +package cn.springcloud.gray.client.netflix.connectionpoint; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.RequestLocalStorage; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class DefaultRibbonConnectionPoint implements RibbonConnectionPoint { + + private GrayManager grayManager; + private RequestLocalStorage requestLocalStorage; + + public DefaultRibbonConnectionPoint(GrayManager grayManager, RequestLocalStorage requestLocalStorage) { + this.grayManager = grayManager; + this.requestLocalStorage = requestLocalStorage; + } + + @Override + public void executeConnectPoint(ConnectPointContext connectPointContext) { + ConnectPointContext.setContextLocal(connectPointContext); + GrayRequest grayRequest = connectPointContext.getGrayRequest(); + grayRequest.setGrayTrackInfo(requestLocalStorage.getGrayTrackInfo()); + requestLocalStorage.setGrayRequest(grayRequest); + + List interceptors = grayManager.getRequeestInterceptors(connectPointContext.getInterceptroType()); + interceptors.forEach(interceptor -> { + if (interceptor.shouldIntercept()) { + if (!interceptor.pre(grayRequest)) { + return; + } + } + }); + + } + + @Override + public void shutdownconnectPoint(ConnectPointContext connectPointContext) { + List interceptors = grayManager.getRequeestInterceptors(connectPointContext.getInterceptroType()); + interceptors.forEach(interceptor -> { + if (interceptor.shouldIntercept()) { + if (!interceptor.after(connectPointContext.getGrayRequest())) { + return; + } + } + }); + ConnectPointContext.removeContextLocal(); + requestLocalStorage.removeGrayTrackInfo(); + requestLocalStorage.removeGrayRequest(); + + } + + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/RibbonConnectionPoint.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/RibbonConnectionPoint.java new file mode 100644 index 00000000..46cd21ee --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/connectionpoint/RibbonConnectionPoint.java @@ -0,0 +1,32 @@ +package cn.springcloud.gray.client.netflix.connectionpoint; + + +import java.io.IOException; + +public interface RibbonConnectionPoint { + + + default T execute(ConnectPointContext connectPointContext, Supplier supplier) throws IOException { + try { + executeConnectPoint(connectPointContext); + return supplier.get(); + } catch (Exception e) { + connectPointContext.setThrowable(e); + throw e; + } finally { + shutdownconnectPoint(connectPointContext); + } + } + + + void executeConnectPoint(ConnectPointContext connectPointContext); + + + void shutdownconnectPoint(ConnectPointContext connectPointContext); + + + public interface Supplier { + T get() throws IOException; + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/constants/GrayNetflixClientConstants.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/constants/GrayNetflixClientConstants.java new file mode 100644 index 00000000..450077f8 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/constants/GrayNetflixClientConstants.java @@ -0,0 +1,9 @@ +package cn.springcloud.gray.client.netflix.constants; + +public class GrayNetflixClientConstants { + + public static final String INTERCEPTRO_TYPE_ALL = "all"; + public static final String INTERCEPTRO_TYPE_FEIGN = "feign"; + public static final String INTERCEPTRO_TYPE_ZUUL = "zuul"; + public static final String INTERCEPTRO_TYPE_RESTTEMPLATE = "rest"; +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/FeignRequestInterceptor.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/FeignRequestInterceptor.java new file mode 100644 index 00000000..3f45cd2c --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/FeignRequestInterceptor.java @@ -0,0 +1,64 @@ +package cn.springcloud.gray.client.netflix.feign; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.GrayTrackInfo; +import feign.Request; +import org.apache.commons.lang3.StringUtils; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +public class FeignRequestInterceptor implements RequestInterceptor { + @Override + public String interceptroType() { + return GrayNetflixClientConstants.INTERCEPTRO_TYPE_FEIGN; + } + + @Override + public boolean shouldIntercept() { + return true; + } + + @Override + public boolean pre(GrayRequest request) { + Request feignRequest = (Request) (request.getAttribute(GrayFeignClient.GRAY_REQUEST_ATTRIBUTE_NAME_FEIGN_REQUEST)); + GrayHttpTrackInfo grayTrack = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (grayTrack != null) { + if (StringUtils.isNotEmpty(grayTrack.getUri())) { + feignRequest.headers().put(GrayHttpTrackInfo.GRAY_TRACK_URI, Arrays.asList(grayTrack.getUri())); + } + if (StringUtils.isNotEmpty(grayTrack.getTraceIp())) { + feignRequest.headers().put(GrayHttpTrackInfo.GRAY_TRACK_TRACE_IP, Arrays.asList(grayTrack.getTraceIp())); + } + if (StringUtils.isNotEmpty(grayTrack.getMethod())) { + feignRequest.headers().put(GrayHttpTrackInfo.GRAY_TRACK_METHOD, Arrays.asList(grayTrack.getMethod())); + } + if (grayTrack.getParameters() != null && !grayTrack.getParameters().isEmpty()) { + grayTrack.getParameters().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + feignRequest.headers().put(name, entry.getValue()); + }); + } + if (grayTrack.getHeaders() != null && !grayTrack.getHeaders().isEmpty()) { + grayTrack.getHeaders().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + feignRequest.headers().put(name, entry.getValue()); + }); + } + } + return true; + } + + @Override + public boolean after(GrayRequest request) { + return true; + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayFeignClient.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayFeignClient.java new file mode 100644 index 00000000..7107f1a9 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayFeignClient.java @@ -0,0 +1,75 @@ +package cn.springcloud.gray.client.netflix.feign; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.config.properties.GrayTrackProperties; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.connectionpoint.ConnectPointContext; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.utils.WebUtils; +import feign.Client; +import feign.Request; +import feign.Response; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +import java.io.IOException; +import java.net.URI; + +/** + * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 + */ +public class GrayFeignClient implements Client { + + + public static final String GRAY_REQUEST_ATTRIBUTE_NAME_FEIGN_REQUEST = "feign.request"; + public static final String GRAY_REQUEST_ATTRIBUTE_NAME_FEIGN_REQUEST_OPTIONS = "feign.request.options"; + + + private GrayRequestProperties grayRequestProperties; + private GrayTrackProperties grayTrackProperties; + private RibbonConnectionPoint ribbonConnectionPoint; + private Client delegate; + + public GrayFeignClient( + Client delegate, RibbonConnectionPoint ribbonConnectionPoint, GrayRequestProperties grayRequestProperties) { + this.delegate = delegate; + this.grayRequestProperties = grayRequestProperties; + this.ribbonConnectionPoint = ribbonConnectionPoint; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + URI uri = URI.create(request.url()); + + + GrayHttpRequest grayRequest = new GrayHttpRequest(); + grayRequest.setUri(uri); + grayRequest.setServiceId(uri.getHost()); + grayRequest.setParameters(WebUtils.getQueryParams(uri.getQuery())); + grayRequest.addHeaders(request.headers()); + grayRequest.setMethod(request.method()); + + if (grayRequestProperties.isLoadBody()) { + grayRequest.setBody(request.body()); + } + + grayRequest.setAttribute(GRAY_REQUEST_ATTRIBUTE_NAME_FEIGN_REQUEST, request); + grayRequest.setAttribute(GRAY_REQUEST_ATTRIBUTE_NAME_FEIGN_REQUEST_OPTIONS, options); + ConnectPointContext connectPointContext = ConnectPointContext.builder() + .interceptroType(GrayNetflixClientConstants.INTERCEPTRO_TYPE_FEIGN) + .grayRequest(grayRequest).build(); + return ribbonConnectionPoint.execute(connectPointContext, () -> delegate.execute(request, options)); + +// try { +// ribbonConnectionPoint.executeConnectPoint(connectPointContext); +// return delegate.execute(request, options); +// } catch (Exception e) { +// connectPointContext.setThrowable(e); +// throw e; +// } finally { +// ribbonConnectionPoint.shutdownconnectPoint(connectPointContext); +// } + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayTrackFeignRequestInterceptor.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayTrackFeignRequestInterceptor.java new file mode 100644 index 00000000..a7c03f90 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/GrayTrackFeignRequestInterceptor.java @@ -0,0 +1,49 @@ +package cn.springcloud.gray.client.netflix.feign; + +import cn.springcloud.gray.request.*; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.apache.commons.lang3.StringUtils; + + +public class GrayTrackFeignRequestInterceptor implements RequestInterceptor { + + private RequestLocalStorage requestLocalStorage; + + + public GrayTrackFeignRequestInterceptor(RequestLocalStorage requestLocalStorage) { + this.requestLocalStorage = requestLocalStorage; + } + + @Override + public void apply(RequestTemplate template) { + GrayHttpTrackInfo grayTrack = (GrayHttpTrackInfo) requestLocalStorage.getGrayTrackInfo(); + if (grayTrack != null) { + if (StringUtils.isNotEmpty(grayTrack.getUri())) { + template.header(GrayHttpTrackInfo.GRAY_TRACK_URI, grayTrack.getUri()); + } + if (StringUtils.isNotEmpty(grayTrack.getTraceIp())) { + template.header(GrayHttpTrackInfo.GRAY_TRACK_TRACE_IP, grayTrack.getTraceIp()); + } + if (StringUtils.isNotEmpty(grayTrack.getMethod())) { + template.header(GrayHttpTrackInfo.GRAY_TRACK_METHOD, grayTrack.getMethod()); + } + if (grayTrack.getParameters() != null && !grayTrack.getParameters().isEmpty()) { + grayTrack.getParameters().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + template.header(name, entry.getValue()); + }); + } + if (grayTrack.getHeaders() != null && !grayTrack.getHeaders().isEmpty()) { + grayTrack.getHeaders().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + template.header(name, entry.getValue()); + }); + } + } + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignAutoConfiguration.java new file mode 100644 index 00000000..19d84092 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignAutoConfiguration.java @@ -0,0 +1,47 @@ +package cn.springcloud.gray.client.netflix.feign.configuration; + +import cn.springcloud.gray.client.netflix.configuration.HystrixGrayAutoConfiguration; +import cn.springcloud.gray.client.netflix.feign.GrayTrackFeignRequestInterceptor; +import cn.springcloud.gray.request.RequestLocalStorage; +import com.netflix.loadbalancer.ILoadBalancer; +import feign.Feign; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Created by saleson on 2017/11/9. + */ +@ConditionalOnClass(value = {ILoadBalancer.class, Feign.class}) +@Configuration +@EnableFeignClients(defaultConfiguration = {GrayFeignClientsConfiguration.class}) +public class GrayFeignAutoConfiguration { + + + @Configuration + @ConditionalOnProperty(value = "gray.request.track.enabled", matchIfMissing = true) + public static class GrayTrackFeignConfiguration { + + +// @Bean +// public FeignRequestInterceptor feignRequestInterceptor() { +// return new FeignRequestInterceptor(); +// } + + @Bean + public GrayTrackFeignRequestInterceptor grayTrackFeignRequestInterceptor(RequestLocalStorage requestLocalStorage) { + return new GrayTrackFeignRequestInterceptor(requestLocalStorage); + } + + } + + + @Configuration + @ConditionalOnProperty(value = "feign.hystrix.enabled") + public static class HystrixConfiguration extends HystrixGrayAutoConfiguration { + + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignClientsConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignClientsConfiguration.java new file mode 100644 index 00000000..ccec4ad5 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/feign/configuration/GrayFeignClientsConfiguration.java @@ -0,0 +1,27 @@ +package cn.springcloud.gray.client.netflix.feign.configuration; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.feign.GrayFeignClient; +import feign.Client; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GrayFeignClientsConfiguration { + + + @Autowired + private Client feignClient; + @Autowired + private RibbonConnectionPoint ribbonConnectionPoint; + @Autowired + private GrayRequestProperties grayRequestProperties; + + @Bean + public Client getFeignClient() { + return new GrayFeignClient(feignClient, ribbonConnectionPoint, grayRequestProperties); + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/hystrix/HystrixRequestLocalStorage.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/hystrix/HystrixRequestLocalStorage.java new file mode 100644 index 00000000..8f53a633 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/hystrix/HystrixRequestLocalStorage.java @@ -0,0 +1,44 @@ +package cn.springcloud.gray.client.netflix.hystrix; + +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.GrayTrackInfo; +import cn.springcloud.gray.request.RequestLocalStorage; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault; + +public class HystrixRequestLocalStorage implements RequestLocalStorage { + + + private static final HystrixRequestVariableDefault grayTrackInfoLocal = new HystrixRequestVariableDefault(); + private static final HystrixRequestVariableDefault rrayRequestLocal = new HystrixRequestVariableDefault(); + + + @Override + public void setGrayTrackInfo(GrayTrackInfo grayTrackInfo) { + grayTrackInfoLocal.set(grayTrackInfo); + } + + @Override + public void removeGrayTrackInfo() { + grayTrackInfoLocal.remove(); + } + + @Override + public GrayTrackInfo getGrayTrackInfo() { + return grayTrackInfoLocal.get(); + } + + @Override + public void setGrayRequest(GrayRequest grayRequest) { + rrayRequestLocal.set(grayRequest); + } + + @Override + public void removeGrayRequest() { + rrayRequestLocal.remove(); + } + + @Override + public GrayRequest getGrayRequest() { + return rrayRequestLocal.get(); + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/GrayClientHttpRequestIntercptor.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/GrayClientHttpRequestIntercptor.java new file mode 100644 index 00000000..22c231c1 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/GrayClientHttpRequestIntercptor.java @@ -0,0 +1,66 @@ +package cn.springcloud.gray.client.netflix.resttemplate; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.netflix.connectionpoint.ConnectPointContext; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.utils.WebUtils; +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; + + +/** + * 用于@LoadBalance 标记的 RestTemplate,主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 + */ +public class GrayClientHttpRequestIntercptor implements ClientHttpRequestInterceptor { + + public static final String GRAY_REQUEST_ATTRIBUTE_RESTTEMPLATE_REQUEST = "restTemplate.request"; + + private GrayRequestProperties grayRequestProperties; + private RibbonConnectionPoint ribbonConnectionPoint; + + public GrayClientHttpRequestIntercptor( + GrayRequestProperties grayRequestProperties, RibbonConnectionPoint ribbonConnectionPoint) { + this.grayRequestProperties = grayRequestProperties; + this.ribbonConnectionPoint = ribbonConnectionPoint; + } + + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + URI uri = request.getURI(); + GrayHttpRequest grayRequest = new GrayHttpRequest(); + grayRequest.setUri(uri); + grayRequest.setServiceId(uri.getHost()); + grayRequest.setParameters(WebUtils.getQueryParams(uri.getQuery())); + if (grayRequestProperties.isLoadBody()) { + grayRequest.setBody(body); + } + grayRequest.setMethod(request.getMethod().name()); + grayRequest.addHeaders(request.getHeaders()); + + grayRequest.setAttribute(GRAY_REQUEST_ATTRIBUTE_RESTTEMPLATE_REQUEST, request); + ConnectPointContext connectPointContext = ConnectPointContext.builder() + .interceptroType(GrayNetflixClientConstants.INTERCEPTRO_TYPE_RESTTEMPLATE) + .grayRequest(grayRequest).build(); + + return ribbonConnectionPoint.execute(connectPointContext, () -> execution.execute(request, body)); + +// try { +// ribbonConnectionPoint.executeConnectPoint(connectPointContext); +// return execution.execute(request, body); +// } catch (Exception e) { +// connectPointContext.setThrowable(e); +// throw e; +// } finally { +// ribbonConnectionPoint.shutdownconnectPoint(connectPointContext); +// } + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/RestTemplateRequestInterceptor.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/RestTemplateRequestInterceptor.java new file mode 100644 index 00000000..e712ebef --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/RestTemplateRequestInterceptor.java @@ -0,0 +1,65 @@ +package cn.springcloud.gray.client.netflix.resttemplate; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.GrayTrackInfo; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; + +import java.util.Arrays; + +public class RestTemplateRequestInterceptor implements RequestInterceptor { + @Override + public String interceptroType() { + return GrayNetflixClientConstants.INTERCEPTRO_TYPE_RESTTEMPLATE; + } + + @Override + public boolean shouldIntercept() { + return true; + } + + @Override + public boolean pre(GrayRequest request) { + GrayHttpTrackInfo grayTrack = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (grayTrack != null) { + HttpRequest httpRequest = (HttpRequest) request.getAttribute( + GrayClientHttpRequestIntercptor.GRAY_REQUEST_ATTRIBUTE_RESTTEMPLATE_REQUEST); + HttpHeaders httpHeaders = httpRequest.getHeaders(); + if (StringUtils.isNotEmpty(grayTrack.getUri())) { + httpHeaders.add(GrayHttpTrackInfo.GRAY_TRACK_URI, grayTrack.getUri()); + } + if (StringUtils.isNotEmpty(grayTrack.getTraceIp())) { + httpHeaders.add(GrayHttpTrackInfo.GRAY_TRACK_TRACE_IP, grayTrack.getTraceIp()); + } + if (StringUtils.isNotEmpty(grayTrack.getMethod())) { + httpHeaders.add(GrayHttpTrackInfo.GRAY_TRACK_METHOD, grayTrack.getMethod()); + } + if (grayTrack.getParameters() != null && !grayTrack.getParameters().isEmpty()) { + grayTrack.getParameters().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + httpHeaders.put(name, entry.getValue()); + }); + } + if (grayTrack.getHeaders() != null && !grayTrack.getHeaders().isEmpty()) { + grayTrack.getHeaders().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + httpHeaders.put(name, entry.getValue()); + }); + } + } + return true; + } + + @Override + public boolean after(GrayRequest request) { + return true; + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/configuration/GrayRestTemplateAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/configuration/GrayRestTemplateAutoConfiguration.java new file mode 100644 index 00000000..2138843f --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/resttemplate/configuration/GrayRestTemplateAutoConfiguration.java @@ -0,0 +1,58 @@ +package cn.springcloud.gray.client.netflix.resttemplate.configuration; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.resttemplate.GrayClientHttpRequestIntercptor; +import cn.springcloud.gray.client.netflix.resttemplate.RestTemplateRequestInterceptor; +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.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Configuration +@ConditionalOnClass(value = RestTemplate.class) +public class GrayRestTemplateAutoConfiguration { + + @Autowired + private GrayRequestProperties grayRequestProperties; + @Autowired + private RibbonConnectionPoint ribbonConnectionPoint; + + + @Bean + @LoadBalanced + @ConditionalOnMissingBean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); +// restTemplate.getInterceptors().add(grayClientHttpRequestIntercptor); + return restTemplate; + } + + + @Bean + public GrayClientHttpRequestIntercptor grayClientHttpRequestIntercptor(@LoadBalanced List restTemplates) { + GrayClientHttpRequestIntercptor intercptor = new GrayClientHttpRequestIntercptor( + grayRequestProperties, ribbonConnectionPoint); + restTemplates.forEach(restTemplate -> restTemplate.getInterceptors().add(intercptor)); + return intercptor; + } + + + @Configuration + @ConditionalOnProperty(value = "gray.request.track.enabled", matchIfMissing = true) + public static class GrayTrackRestTemplateConfiguration { + + + @Bean + public RestTemplateRequestInterceptor restTemplateRequestInterceptor() { + return new RestTemplateRequestInterceptor(); + } + + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayDecisionPredicate.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayDecisionPredicate.java new file mode 100644 index 00000000..ff319fcb --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayDecisionPredicate.java @@ -0,0 +1,45 @@ +package cn.springcloud.gray.client.netflix.ribbon; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.decision.GrayDecisionInputArgs; +import cn.springcloud.gray.servernode.ServerSpec; +import cn.springcloud.gray.request.GrayRequest; +import com.netflix.loadbalancer.AbstractServerPredicate; +import com.netflix.loadbalancer.PredicateKey; +import com.netflix.loadbalancer.Server; + +import java.util.List; + +public class GrayDecisionPredicate extends AbstractServerPredicate { + + public GrayDecisionPredicate(GrayLoadBalanceRule rule) { + super(rule); + } + + @Override + public boolean apply(PredicateKey input) { + + GrayLoadBalanceRule grayRule = getIRule(); + GrayRequest grayRequest = grayRule.getRequestLocalStorage().getGrayRequest(); + + Server server = input.getServer(); + String serviceId = grayRequest.getServiceId(); + String instanceId = server.getMetaInfo().getInstanceId(); + List grayDecisions = grayRule.getGrayManager().getGrayDecision(serviceId, instanceId); + + ServerSpec serverSpec = grayRule.getServerExplainer().apply(server); + + for (GrayDecision grayDecision : grayDecisions) { + if (grayDecision.test(GrayDecisionInputArgs.builder().grayRequest(grayRequest).server(serverSpec).build())) { + return true; + } + } + return false; + } + + + protected GrayLoadBalanceRule getIRule() { + return (GrayLoadBalanceRule) this.rule; + } + +} \ No newline at end of file diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayLoadBalanceRule.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayLoadBalanceRule.java new file mode 100644 index 00000000..aa3b84b0 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/GrayLoadBalanceRule.java @@ -0,0 +1,125 @@ +package cn.springcloud.gray.client.netflix.ribbon; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.client.netflix.GrayClientHolder; +import cn.springcloud.gray.model.GrayService; +import cn.springcloud.gray.servernode.ServerExplainer; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.RequestLocalStorage; +import com.google.common.base.Optional; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * 灰度发布的负载规则 + */ +@Slf4j +public class GrayLoadBalanceRule extends ZoneAvoidanceRule { + + protected CompositePredicate grayCompositePredicate; + protected GrayManager grayManager; + protected RequestLocalStorage requestLocalStorage; + protected ServerExplainer serverExplainer; + + + public GrayLoadBalanceRule() { + this(GrayClientHolder.getGrayManager(), GrayClientHolder.getRequestLocalStorage(), + GrayClientHolder.getServerExplainer()); + } + + public GrayLoadBalanceRule(GrayManager grayManager, RequestLocalStorage requestLocalStorage, + ServerExplainer serverExplainer) { + super(); + this.grayManager = grayManager; + this.requestLocalStorage = requestLocalStorage; + this.serverExplainer = serverExplainer; + init(); + } + +// public GrayLoadBalanceRule() { +// super(); +// init(); +// } + + protected void init() { + GrayDecisionPredicate grayPredicate = new GrayDecisionPredicate(this); + grayCompositePredicate = CompositePredicate.withPredicates(super.getPredicate(), + grayPredicate).build(); + } + + + @Override + public Server choose(Object key) { + ILoadBalancer lb = getLoadBalancer(); + GrayRequest grayRequest = requestLocalStorage.getGrayRequest(); + String serviceId = grayRequest.getServiceId(); + if (grayManager.hasGray(serviceId)) { + GrayService grayService = grayManager.getGrayService(serviceId); + List servers = lb.getAllServers(); + List grayServers = new ArrayList<>(grayService.getGrayInstances().size()); + List normalServers = new ArrayList<>(servers.size() - grayService.getGrayInstances().size()); + + for (Server server : servers) { +// ServerSpec serverSpec = serverExplainer.apply(server); + if (grayService.getGrayInstance(server.getMetaInfo().getInstanceId()) != null) { + grayServers.add(server); + } else { + normalServers.add(server); + } + } + Optional server = grayCompositePredicate.chooseRoundRobinAfterFiltering(grayServers, key); + if (server.isPresent()) { + return expect(server.get()); + } else { + return expect(choose(super.getPredicate(), normalServers, key)); + } + + } else { + return expect(super.choose(key)); + } + } + + @Override + public void initWithNiwsConfig(IClientConfig clientConfig) { + super.initWithNiwsConfig(clientConfig); + } + + @Override + public void setLoadBalancer(ILoadBalancer lb) { + super.setLoadBalancer(lb); + } + + private Server choose(AbstractServerPredicate serverPredicate, List servers, Object key) { + Optional server = serverPredicate.chooseRoundRobinAfterFiltering(servers, key); + if (server.isPresent()) { + return server.get(); + } else { + return null; + } + } + + + public GrayManager getGrayManager() { + return grayManager; + } + + public RequestLocalStorage getRequestLocalStorage() { + return requestLocalStorage; + } + + public ServerExplainer getServerExplainer() { + return serverExplainer; + } + + private Server expect(Server server) { + if (server != null) { + log.debug("找到server:{}", server.getId()); + } + return server; + } +} + diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/configuration/GrayRibbonClientsConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/configuration/GrayRibbonClientsConfiguration.java new file mode 100644 index 00000000..3d7d07d2 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/ribbon/configuration/GrayRibbonClientsConfiguration.java @@ -0,0 +1,40 @@ +package cn.springcloud.gray.client.netflix.ribbon.configuration; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.client.netflix.GrayClientHolder; +import cn.springcloud.gray.client.netflix.ribbon.GrayLoadBalanceRule; +import cn.springcloud.gray.servernode.ServerExplainer; +import cn.springcloud.gray.request.RequestLocalStorage; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.Server; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GrayRibbonClientsConfiguration implements InitializingBean { + + @Autowired + private GrayManager grayManager; + @Autowired + private RequestLocalStorage requestLocalStorage; + @Autowired + private ServerExplainer serverExplainer; + + @Bean + public IRule ribbonRule(@Autowired(required = false) IClientConfig config) { + GrayLoadBalanceRule rule = new GrayLoadBalanceRule(grayManager, requestLocalStorage, serverExplainer); + rule.initWithNiwsConfig(config); + return rule; + } + + + @Override + public void afterPropertiesSet() throws Exception { + GrayClientHolder.setGrayManager(grayManager); + GrayClientHolder.setRequestLocalStorage(requestLocalStorage); + GrayClientHolder.setServerExplainer(serverExplainer); + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPostZuulFilter.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPostZuulFilter.java new file mode 100644 index 00000000..2aa50fa3 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPostZuulFilter.java @@ -0,0 +1,41 @@ +package cn.springcloud.gray.client.netflix.zuul; + +import cn.springcloud.gray.client.netflix.connectionpoint.ConnectPointContext; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import com.netflix.zuul.ZuulFilter; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; + +/** + * 做一些善后工作。比如删除BambooRequestContext在ThreadLocal中的信息。 + */ +public class GrayPostZuulFilter extends ZuulFilter { + + + private RibbonConnectionPoint ribbonConnectionPoint; + + + public GrayPostZuulFilter(RibbonConnectionPoint ribbonConnectionPoint) { + this.ribbonConnectionPoint = ribbonConnectionPoint; + } + + @Override + public String filterType() { + return FilterConstants.POST_TYPE; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + ribbonConnectionPoint.shutdownconnectPoint(ConnectPointContext.getContextLocal()); + return null; + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPreZuulFilter.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPreZuulFilter.java new file mode 100755 index 00000000..caff7caf --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/GrayPreZuulFilter.java @@ -0,0 +1,107 @@ +package cn.springcloud.gray.client.netflix.zuul; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.netflix.connectionpoint.ConnectPointContext; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.util.Enumeration; + +/** + * 主要作用是用来获取request的相关信息,为后面的路由提供数据基础。 + */ +public class GrayPreZuulFilter extends ZuulFilter { + + private static final Logger log = LoggerFactory.getLogger(GrayPreZuulFilter.class); + + public static final String GRAY_REQUEST_ATTRIBUTE_NAME_ZUUL_REQUEST = "zuul.request"; + public static final String GRAY_REQUEST_ATTRIBUTE_NAME_ZUUL_REQUEST_CONTEXT = "zuul.requestContext"; + + private GrayRequestProperties grayRequestProperties; + private RibbonConnectionPoint ribbonConnectionPoint; + + public GrayPreZuulFilter(GrayRequestProperties grayRequestProperties, RibbonConnectionPoint ribbonConnectionPoint) { + this.grayRequestProperties = grayRequestProperties; + this.ribbonConnectionPoint = ribbonConnectionPoint; + } + + @Override + public String filterType() { + return FilterConstants.PRE_TYPE; + } + + @Override + public int filterOrder() { + return 10000; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + HttpServletRequest servletRequest = context.getRequest(); + + GrayHttpRequest grayRequest = new GrayHttpRequest(); + URI uri = URI.create((String) context.get(FilterConstants.REQUEST_URI_KEY)); + grayRequest.setUri(uri); + grayRequest.setServiceId((String) context.get(FilterConstants.SERVICE_ID_KEY)); + grayRequest.addParameters(context.getRequestQueryParams()); + if (grayRequestProperties.isLoadBody()) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(context.getRequest().getInputStream())); + byte[] reqBody = IOUtils.toByteArray(reader); + grayRequest.setBody(reqBody); + } catch (IOException e) { + String errorMsg = "获取request body出现异常"; + log.error(errorMsg, e); + } + } + + grayRequest.setMethod(servletRequest.getMethod()); + grayRequest.setHeaders(getHeaders(context)); + grayRequest.setAttribute(GRAY_REQUEST_ATTRIBUTE_NAME_ZUUL_REQUEST, servletRequest); + grayRequest.setAttribute(GRAY_REQUEST_ATTRIBUTE_NAME_ZUUL_REQUEST_CONTEXT, context); + //context.getZuulRequestHeaders().get(FilterConstants.X_FORWARDED_FOR_HEADER.toLowerCase()) + + ConnectPointContext connectPointContext = ConnectPointContext.builder() + .interceptroType(GrayNetflixClientConstants.INTERCEPTRO_TYPE_ZUUL) + .grayRequest(grayRequest).build(); + ribbonConnectionPoint.executeConnectPoint(connectPointContext); + return null; + } + + + private MultiValueMap getHeaders(RequestContext context) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + context.getZuulRequestHeaders().entrySet().forEach(entry -> { + headers.add(entry.getKey(), entry.getValue()); + }); + HttpServletRequest servletRequest = context.getRequest(); + Enumeration headerNames = servletRequest.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + headers.add(headerName, servletRequest.getHeader(headerName)); + } + return headers; + } + +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/ZuulRequestInterceptor.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/ZuulRequestInterceptor.java new file mode 100644 index 00000000..2e43668f --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/ZuulRequestInterceptor.java @@ -0,0 +1,68 @@ +package cn.springcloud.gray.client.netflix.zuul; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.client.netflix.constants.GrayNetflixClientConstants; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.GrayTrackInfo; +import com.netflix.zuul.context.RequestContext; +import org.apache.commons.lang3.StringUtils; + +public class ZuulRequestInterceptor implements RequestInterceptor { + @Override + public String interceptroType() { + return GrayNetflixClientConstants.INTERCEPTRO_TYPE_ZUUL; + } + + @Override + public boolean shouldIntercept() { + return true; + } + + @Override + public boolean pre(GrayRequest request) { + GrayHttpTrackInfo grayTrack = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (grayTrack != null) { + RequestContext context = (RequestContext) request.getAttribute( + GrayPreZuulFilter.GRAY_REQUEST_ATTRIBUTE_NAME_ZUUL_REQUEST_CONTEXT); + if (StringUtils.isNotEmpty(grayTrack.getUri())) { + context.addOriginResponseHeader(GrayHttpTrackInfo.GRAY_TRACK_URI, grayTrack.getUri()); + } + if (StringUtils.isNotEmpty(grayTrack.getTraceIp())) { + context.addOriginResponseHeader(GrayHttpTrackInfo.GRAY_TRACK_TRACE_IP, grayTrack.getTraceIp()); + } + if (StringUtils.isNotEmpty(grayTrack.getMethod())) { + context.addOriginResponseHeader(GrayHttpTrackInfo.GRAY_TRACK_METHOD, grayTrack.getMethod()); + } + if (grayTrack.getParameters() != null && !grayTrack.getParameters().isEmpty()) { + grayTrack.getParameters().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + context.addOriginResponseHeader(GrayHttpTrackInfo.GRAY_TRACK_METHOD, grayTrack.getMethod()); + + entry.getValue().forEach(v -> { + context.addOriginResponseHeader(name, v); + }); + }); + } + if (grayTrack.getHeaders() != null && !grayTrack.getHeaders().isEmpty()) { + grayTrack.getHeaders().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + entry.getValue().forEach(v -> { + context.addOriginResponseHeader(name, v); + }); + }); + } + + } + return true; + } + + @Override + public boolean after(GrayRequest request) { + return true; + } +} diff --git a/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/configuration/GrayZuulAutoConfiguration.java b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/configuration/GrayZuulAutoConfiguration.java new file mode 100644 index 00000000..66ccb1a7 --- /dev/null +++ b/spring-cloud-gray-client-netflix/src/main/java/cn/springcloud/gray/client/netflix/zuul/configuration/GrayZuulAutoConfiguration.java @@ -0,0 +1,55 @@ +package cn.springcloud.gray.client.netflix.zuul.configuration; + +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.client.netflix.configuration.HystrixGrayAutoConfiguration; +import cn.springcloud.gray.client.netflix.connectionpoint.RibbonConnectionPoint; +import cn.springcloud.gray.client.netflix.zuul.GrayPostZuulFilter; +import cn.springcloud.gray.client.netflix.zuul.GrayPreZuulFilter; +import cn.springcloud.gray.client.netflix.zuul.ZuulRequestInterceptor; +import com.netflix.zuul.http.ZuulServlet; +import org.springframework.beans.factory.annotation.Autowired; +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; + +@Configuration +@ConditionalOnClass(value = ZuulServlet.class) +public class GrayZuulAutoConfiguration { + + @Autowired + private GrayRequestProperties grayRequestProperties; + @Autowired + private RibbonConnectionPoint ribbonConnectionPoint; + + @Bean + public GrayPreZuulFilter grayPreZuulFilter() { + return new GrayPreZuulFilter(grayRequestProperties, ribbonConnectionPoint); + } + + @Bean + public GrayPostZuulFilter grayPostZuulFilter() { + return new GrayPostZuulFilter(ribbonConnectionPoint); + } + + + @Configuration + @ConditionalOnProperty(value = "gray.request.track.enabled", matchIfMissing = true) + public static class GrayTrackZuulConfiguration { + + + @Bean + public ZuulRequestInterceptor zuulRequestInterceptor() { + return new ZuulRequestInterceptor(); + } + + } + + + @Configuration + @ConditionalOnProperty(value = "zuul.ribbonIsolationStrategy", havingValue = "THREAD") + public static class HystrixConfiguration extends HystrixGrayAutoConfiguration { + + } + +} diff --git a/spring-cloud-gray-client/pom.xml b/spring-cloud-gray-client/pom.xml index 78b6e46b..ad4f1fb1 100644 --- a/spring-cloud-gray-client/pom.xml +++ b/spring-cloud-gray-client/pom.xml @@ -5,60 +5,72 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 spring-cloud-gray-client - - cn.springcloud.gray - spring-cloud-bamboo + org.springframework + spring-web - cn.springcloud.gray - spring-cloud-gray-core + org.springframework + spring-webmvc - org.springframework.boot - spring-boot-starter-web + spring-boot-autoconfigure provided - org.springframework.boot - spring-boot-starter-test - provided + cn.springcloud.gray + spring-cloud-gray-core - org.springframework.cloud - spring-cloud-starter-eureka - provided + cn.springcloud.gray + spring-cloud-gray-utils - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - provided + org.projectlombok + lombok - org.springframework.cloud - spring-cloud-starter-feign - provided + javax.servlet + javax.servlet-api + + + org.slf4j + slf4j-api + + + commons-collections + commons-collections + + + org.apache.commons + commons-lang3 + + + cn.springcloud.gray + spring-cloud-gray-utils + 2.0.0 + compile + + + org.springframework.boot + spring-boot-configuration-processor + true - - - - - - - - - - + + org.springframework.cloud + spring-cloud-stream + + \ No newline at end of file diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractCommunicableGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractCommunicableGrayManager.java new file mode 100644 index 00000000..15e63bd8 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractCommunicableGrayManager.java @@ -0,0 +1,43 @@ +package cn.springcloud.gray; + +import cn.springcloud.gray.communication.HttpInformationClient; +import cn.springcloud.gray.communication.InformationClient; +import cn.springcloud.gray.communication.RetryableInformationClient; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; + +import java.util.List; + +public abstract class AbstractCommunicableGrayManager extends SimpleGrayManager implements CommunicableGrayManager { + + private GrayClientConfig grayClientConfig; + private InformationClient informationClient; + + public AbstractCommunicableGrayManager(GrayClientConfig grayClientConfig, GrayDecisionFactoryKeeper grayDecisionFactoryKeeper, List requestInterceptors) { + super(grayDecisionFactoryKeeper, requestInterceptors); + this.grayClientConfig = grayClientConfig; + createInformationClient(); + } + + public GrayClientConfig getGrayClientConfig() { + return grayClientConfig; + } + + @Override + public InformationClient getGrayInformationClient() { + return informationClient; + } + + protected void createInformationClient() { + + GrayClientConfig clientConfig = getGrayClientConfig(); + InformationClient httpClient = new HttpInformationClient(clientConfig.getServerUrl()); + if (clientConfig.isRetryable()) { + informationClient = new RetryableInformationClient(Math.max(3, clientConfig.getRetryNumberOfRetries()), httpClient); + } else { + informationClient = httpClient; + } + + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractGrayManager.java index 83a51254..c14e5e3e 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractGrayManager.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/AbstractGrayManager.java @@ -1,14 +1,20 @@ package cn.springcloud.gray; -import cn.springcloud.gray.client.GrayClientAppContext; -import cn.springcloud.gray.core.*; +import cn.springcloud.gray.decision.GrayDecision; import cn.springcloud.gray.decision.MultiGrayDecision; +import cn.springcloud.gray.decision.factory.GrayDecisionFactory; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import cn.springcloud.gray.model.DecisionDefinition; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.PolicyDefinition; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.ListUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.OrderComparator; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** @@ -17,89 +23,89 @@ public abstract class AbstractGrayManager implements GrayManager { private static final Logger log = LoggerFactory.getLogger(AbstractGrayManager.class); - protected GrayDecisionFactory decisionFactory; - protected InformationClient client; - - public AbstractGrayManager(InformationClient client, GrayDecisionFactory decisionFactory) { - this.decisionFactory = decisionFactory; - this.client = client; - } + private GrayDecisionFactoryKeeper grayDecisionFactoryKeeper; + private Map> requestInterceptors = new HashMap<>(); - @Override - public boolean isOpen(String serviceId) { - GrayService grayService = grayService(serviceId); - return grayService != null - && grayService.isOpenGray(); + public AbstractGrayManager( + GrayDecisionFactoryKeeper grayDecisionFactoryKeeper, List requestInterceptors) { + initRequestInterceptors(requestInterceptors); + this.grayDecisionFactoryKeeper = grayDecisionFactoryKeeper; } - @Override - public List listGrayService() { - return client.listGrayService(); - } @Override - public GrayService grayService(String serviceId) { - return client.grayService(serviceId); + public List getRequeestInterceptors(String interceptroType) { + List list = requestInterceptors.get(interceptroType); + if (list == null) { + return ListUtils.EMPTY_LIST; + } + return list; } - @Override - public GrayInstance grayInstance(String serviceId, String instanceId) { - return client.grayInstance(serviceId, instanceId); - } @Override - public List grayDecision(GrayInstance instance) { - return grayDecision(instance.getServiceId(), instance.getInstanceId()); - } + public List getGrayDecision(GrayInstance instance) { + List policyDefinitions = instance.getPolicyDefinitions(); + List grayDecisions = new ArrayList<>(policyDefinitions.size()); - @Override - public List grayDecision(String serviceId, String instanceId) { - GrayInstance grayInstance = grayInstance(serviceId, instanceId); - if (grayInstance == null || !grayInstance.isOpenGray() - || grayInstance.getGrayPolicyGroups() == null - || grayInstance.getGrayPolicyGroups().isEmpty()) { - return Collections.emptyList(); - } - List policyGroups = grayInstance.getGrayPolicyGroups(); - List decisions = new ArrayList<>(policyGroups.size()); - for (GrayPolicyGroup policyGroup : policyGroups) { - if (!policyGroup.isEnable()) { + for (PolicyDefinition policyDefinition : policyDefinitions) { + if (CollectionUtils.isEmpty(policyDefinition.getList())) { continue; } - GrayDecision grayDecision = toGrayDecision(policyGroup); - if (grayDecision != GrayDecision.refuse()) { - decisions.add(grayDecision); + GrayDecision decision = createGrayDecision(policyDefinition); + if (decision != null) { + grayDecisions.add(decision); } } - return decisions; + + return grayDecisions; } @Override - public void serviceDownline() { - InstanceLocalInfo localInfo = GrayClientAppContext.getInstanceLocalInfo(); - if (localInfo.isGray()) { - log.debug("灰度服务下线..."); - client.serviceDownline(); - log.debug("灰度服务下线完成"); - } - serviceShutdown(); + public List getGrayDecision(String serviceId, String instanceId) { + return getGrayDecision(getGrayInstance(serviceId, instanceId)); } - protected abstract void serviceShutdown(); + private GrayDecision createGrayDecision(PolicyDefinition policyDefinition) { + MultiGrayDecision decision = new MultiGrayDecision(GrayDecision.allow()); + for (DecisionDefinition decisionDefinition : policyDefinition.getList()) { + decision = decision.and(grayDecisionFactoryKeeper.getGrayDecision(decisionDefinition)); + } + return decision; + } - private GrayDecision toGrayDecision(GrayPolicyGroup policyGroup) { - List policies = policyGroup.getList(); - if (policies == null || policies.isEmpty()) { - return GrayDecision.refuse(); + private void initRequestInterceptors(List requestInterceptors) { + if (requestInterceptors == null || requestInterceptors.isEmpty()) { + return; } - MultiGrayDecision decision = new MultiGrayDecision(GrayDecision.allow()); - policies.forEach(policy -> decision.and(decisionFactory.getDecision(policy))); - return decision; + List all = new ArrayList<>(); + for (RequestInterceptor interceptor : requestInterceptors) { + if (StringUtils.equals(interceptor.interceptroType(), "all")) { + all.add(interceptor); + } else { + List interceptors = this.requestInterceptors.get(interceptor.interceptroType()); + if (interceptors == null) { + interceptors = new ArrayList<>(); + this.requestInterceptors.put(interceptor.interceptroType(), interceptors); + } + interceptors.add(interceptor); + } + } + putTypeAllTo(all); } + private void putTypeAllTo(List all) { + if (all.isEmpty()) { + return; + } + requestInterceptors.values().forEach(list -> { + list.addAll(all); + OrderComparator.sort(list); + }); + } } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/BaseGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/BaseGrayManager.java deleted file mode 100644 index 31d16dcb..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/BaseGrayManager.java +++ /dev/null @@ -1,139 +0,0 @@ -package cn.springcloud.gray; - -import cn.springcloud.gray.client.GrayClientAppContext; -import cn.springcloud.gray.client.GrayClientConfig; -import cn.springcloud.gray.client.GrayOptionalArgs; -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * 在AbstractGrayManager基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。 - */ -public class BaseGrayManager extends AbstractGrayManager { - private static final Logger log = LoggerFactory.getLogger(BaseGrayManager.class); - private Map grayServiceMap; - private Timer updateTimer = new Timer("Gray-UpdateTimer", true); - private GrayClientConfig clientConfig; - - public BaseGrayManager(GrayOptionalArgs grayOptionalArgs) { - super(grayOptionalArgs.getInformationClient(), grayOptionalArgs.getDecisionFactory()); - clientConfig = grayOptionalArgs.getGrayClientConfig(); - grayServiceMap = new ConcurrentHashMap<>(); - } - - - @Override - public void openForWork() { - if (clientConfig.isGrayEnroll()) { - grayEnroll(); - } - log.info("拉取灰度列表"); - doUpdate(); - updateTimer.schedule(new UpdateTask(), - clientConfig.getServiceUpdateIntervalTimerInMs(), - clientConfig.getServiceUpdateIntervalTimerInMs()); - } - - @Override - public List listGrayService() { - if (grayServiceMap == null) { - List grayServices = super.listGrayService(); - if (grayServices == null) { - return null; - - } - updateGrayServices(grayServices); - } - return new ArrayList<>(grayServiceMap.values()); - } - - - @Override - public GrayService grayService(String serviceId) { - if (grayServiceMap == null) { - return super.grayService(serviceId); - } - return grayServiceMap.get(serviceId); - } - - @Override - public GrayInstance grayInstance(String serviceId, String instanceId) { - if (grayServiceMap == null) { - return super.grayInstance(serviceId, instanceId); - } - GrayService grayService = grayService(serviceId); - if (grayService != null) { - return grayService.getGrayInstance(instanceId); - } - return null; - } - - @Override - protected void serviceShutdown() { - updateTimer.cancel(); - } - - @Override - public void updateGrayServices(Collection grayServices) { - if (grayServices == null) { - return; - } - - Map grayMap = new HashMap<>(); - grayServices.forEach(grayService -> grayMap.put(grayService.getServiceId(), grayService)); - grayServiceMap = new ConcurrentHashMap(grayMap); - checkLocalGray(); - } - - - private void doUpdate() { - try { - log.debug("更新灰度服务列表..."); - updateGrayServices(client.listGrayService()); - } catch (Exception e) { - log.error("更新灰度服务列表失败", e); - } - } - - - private void grayEnroll() { - Thread t = new Thread(() -> { - - try { - Thread.sleep(clientConfig.grayEnrollDealyTimeInMs()); - } catch (InterruptedException e) { - } - log.info("灰度注册自身实例..."); - InstanceLocalInfo localInfo = GrayClientAppContext.getInstanceLocalInfo(); - try { - client.addGrayInstance(localInfo.getServiceId(), localInfo.getInstanceId()); - localInfo.setGray(true); - } catch (Exception e) { - log.error("自身实例灰度注册失败", e); - } - }, "GrayEnroll"); - t.start(); - } - - private void checkLocalGray() { - InstanceLocalInfo localInfo = GrayClientAppContext.getInstanceLocalInfo(); - GrayService grayService = grayServiceMap.get(localInfo.getServiceId()); - localInfo.setGray(grayService != null && grayService.getGrayInstance(localInfo.getInstanceId()) != null); - } - - class UpdateTask extends TimerTask { - - @Override - public void run() { - doUpdate(); - } - } - - -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/CommunicableGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/CommunicableGrayManager.java new file mode 100644 index 00000000..ddab3697 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/CommunicableGrayManager.java @@ -0,0 +1,9 @@ +package cn.springcloud.gray; + +import cn.springcloud.gray.communication.InformationClient; + +public interface CommunicableGrayManager extends GrayManager { + + InformationClient getGrayInformationClient(); + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/DefaultGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/DefaultGrayManager.java index 2fa44fe9..06ad5e1b 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/DefaultGrayManager.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/DefaultGrayManager.java @@ -1,12 +1,61 @@ package cn.springcloud.gray; -import cn.springcloud.gray.client.GrayOptionalArgs; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayService; +import lombok.extern.slf4j.Slf4j; -public class DefaultGrayManager extends BaseGrayManager { +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; - public DefaultGrayManager(GrayOptionalArgs grayOptionalArgs) { - super(grayOptionalArgs); +@Slf4j +public class DefaultGrayManager extends AbstractCommunicableGrayManager { + + private Timer updateTimer = new Timer("Gray-Update-Timer", true); + + public DefaultGrayManager(GrayClientConfig grayClientConfig, GrayDecisionFactoryKeeper grayDecisionFactoryKeeper, + List requestInterceptors) { + super(grayClientConfig, grayDecisionFactoryKeeper, requestInterceptors); + openForWork(); + } + + + public void openForWork() { + log.info("拉取灰度列表"); + doUpdate(); + updateTimer.schedule(new UpdateTask(), + getGrayClientConfig().getServiceUpdateIntervalTimerInMs(), + getGrayClientConfig().getServiceUpdateIntervalTimerInMs()); + } + + + private void doUpdate() { + try { + log.debug("更新灰度服务列表..."); + + List grayInstances = getGrayInformationClient().allGrayInstances(); + Map grayServices = new ConcurrentHashMap<>(); + grayInstances.forEach(instance -> { + updateGrayInstance(grayServices, instance); + }); + this.grayServices = grayServices; + } catch (Exception e) { + log.error("更新灰度服务列表失败", e); + } } + + class UpdateTask extends TimerTask { + + @Override + public void run() { + doUpdate(); + } + } + + } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientConfig.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayClientConfig.java similarity index 88% rename from spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientConfig.java rename to spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayClientConfig.java index 98041f81..6a2bb4c6 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientConfig.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayClientConfig.java @@ -1,7 +1,14 @@ -package cn.springcloud.gray.client; +package cn.springcloud.gray; public interface GrayClientConfig { + /** + * 运行类型:web + * + * @return + */ + String runenv(); + /** * 启动时是否灰度注册 diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayManager.java new file mode 100644 index 00000000..5372325f --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/GrayManager.java @@ -0,0 +1,47 @@ +package cn.springcloud.gray; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayService; + +import java.util.Collection; +import java.util.List; + + +/** + * 灰度客户端管理器,维护灰度列表,维护自身灰度状态,创建灰度决策对象。 + * 抽象实现类AbstractGrayManager实现了基础的获取灰度列表, 创建灰度决策对象的能力。 + * BaseGrayManger在其基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。 + */ +public interface GrayManager { + + + /** + * 判断指定的服务ID是否有灰度实例 + * + * @param serviceId 服务ID + * @return has gray instance if true + */ + boolean hasGray(String serviceId); + + Collection allGrayServices(); + + GrayService getGrayService(String serviceId); + + GrayInstance getGrayInstance(String serviceId, String instanceId); + + List getGrayDecision(GrayInstance instance); + + List getGrayDecision(String serviceId, String instanceId); + + void updateGrayInstance(GrayInstance instance); + + void closeGray(GrayInstance instance); + + void closeGray(String serviceId, String instanceId); + + List getRequeestInterceptors(String interceptroType); + + + void shutdown(); +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/HttpInformationClient.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/HttpInformationClient.java deleted file mode 100644 index d79e9c92..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/HttpInformationClient.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.springcloud.gray; - -import cn.springcloud.gray.client.GrayClientAppContext; -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayService; -import cn.springcloud.gray.core.InformationClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class HttpInformationClient implements InformationClient { - private static final Logger log = LoggerFactory.getLogger(HttpInformationClient.class); - private final String baseUrl; - private RestTemplate rest; - - public HttpInformationClient(String baseUrl, RestTemplate rest) { - this.baseUrl = baseUrl; - this.rest = rest; - } - - @Override - public List listGrayService() { - String url = this.baseUrl + "/gray/services/enable"; - ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { - }; - - try { - ResponseEntity> responseEntity = rest.exchange(url, HttpMethod.GET, null, typeRef); - return responseEntity.getBody(); - } catch (RuntimeException e) { - log.error("获取灰度服务列表失败", e); - throw e; - } - } - - @Override - public GrayService grayService(String serviceId) { - String url = this.baseUrl + "/gray/services/{serviceId}"; - Map params = new HashMap<>(); - params.put("serviceId", serviceId); - - try { - ResponseEntity responseEntity = rest.getForEntity(url, GrayService.class, params); - return responseEntity.getBody(); - } catch (RuntimeException e) { - log.error("获取灰度服务失败", e); - throw e; - } - } - - @Override - public GrayInstance grayInstance(String serviceId, String instanceId) { - String url = this.baseUrl + "/gray/services/{serviceId}/instance/?instanceId={instanceId}"; - - Map params = new HashMap<>(); - params.put("serviceId", serviceId); - params.put("instanceId", instanceId); - try { - ResponseEntity responseEntity = rest.getForEntity(url, GrayInstance.class, params); - return responseEntity.getBody(); - } catch (RuntimeException e) { - log.error("获取灰度服务实例失败", e); - throw e; - } - } - - @Override - public void addGrayInstance(String serviceId, String instanceId) { - GrayInstance grayInstance = new GrayInstance(); - grayInstance.setInstanceId(instanceId); - grayInstance.setServiceId(serviceId); - - String url = this.baseUrl + "/gray/services/{serviceId}/instance"; - try { - rest.postForEntity(url, grayInstance, null, serviceId); - } catch (RuntimeException e) { - log.error("灰度服务实例下线失败", e); - throw e; - } - } - - @Override - public void serviceDownline() { - InstanceLocalInfo localInfo = GrayClientAppContext.getInstanceLocalInfo(); - if (!localInfo.isGray()) { - return; - } - - serviceDownline(localInfo.getServiceId(), localInfo.getInstanceId()); - } - - @Override - public void serviceDownline(String serviceId, String instanceId) { - String url = this.baseUrl + "/gray/services/{serviceId}/instance/?instanceId={instanceId}"; - Map params = new HashMap<>(); - params.put("serviceId", serviceId); - try { - params.put("instanceId", instanceId); - rest.delete(url, params); - } catch (Exception e) { - log.error("灰度服务实例下线失败", e); - throw e; - } - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/InstanceLocalInfo.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/InstanceLocalInfo.java index 0bde8920..f079fcdf 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/InstanceLocalInfo.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/InstanceLocalInfo.java @@ -1,31 +1,18 @@ package cn.springcloud.gray; +import lombok.*; + + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder public class InstanceLocalInfo { private String serviceId; private String instanceId; + private String host; + private int port; + @Setter private boolean isGray; - public String getServiceId() { - return serviceId; - } - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public String getInstanceId() { - return instanceId; - } - - public void setInstanceId(String instanceId) { - this.instanceId = instanceId; - } - - public boolean isGray() { - return isGray; - } - - public void setGray(boolean gray) { - isGray = gray; - } } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RequestInterceptor.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RequestInterceptor.java new file mode 100644 index 00000000..4230b5a8 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RequestInterceptor.java @@ -0,0 +1,25 @@ +package cn.springcloud.gray; + + +import cn.springcloud.gray.request.GrayRequest; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * + */ +public interface RequestInterceptor extends Ordered { + + String interceptroType(); + + boolean shouldIntercept(); + + boolean pre(GrayRequest request); + + boolean after(GrayRequest request); + + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/SimpleGrayManager.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/SimpleGrayManager.java new file mode 100644 index 00000000..8bad6258 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/SimpleGrayManager.java @@ -0,0 +1,100 @@ +package cn.springcloud.gray; + +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayService; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class SimpleGrayManager extends AbstractGrayManager { + + + protected Map grayServices = new ConcurrentHashMap<>(); + + public SimpleGrayManager(GrayDecisionFactoryKeeper grayDecisionFactoryKeeper, List requestInterceptors) { + super(grayDecisionFactoryKeeper, requestInterceptors); + } + + + @Override + public boolean hasGray(String serviceId) { + GrayService grayService = grayServices.get(serviceId); + return grayService != null && !grayService.getGrayInstances().isEmpty(); + } + + @Override + public Collection allGrayServices() { + return grayServices.values(); + } + + @Override + public GrayService getGrayService(String serviceId) { + return grayServices.get(serviceId); + } + + @Override + public GrayInstance getGrayInstance(String serviceId, String instanceId) { + GrayService service = getGrayService(serviceId); + return service != null ? service.getGrayInstance(instanceId) : null; + } + + + @Override + public void updateGrayInstance(GrayInstance instance) { + if (instance == null || !instance.isGray()) { + return; + } + updateGrayInstance(grayServices, instance); + } + + protected void updateGrayInstance(Map grayServices, GrayInstance instance) { + GrayService service = getGrayService(instance.getServiceId()); + if (service == null) { + synchronized (this) { + service = getGrayService(instance.getServiceId()); + if (service == null) { + service = new GrayService(); + service.setServiceId(instance.getServiceId()); + grayServices.put(service.getServiceId(), service); + } + } + } else if (grayServices != this.grayServices) { + grayServices.put(service.getServiceId(), service); + } + log.debug("添加灰度实例, serviceId:{}, instanceId:{}", instance.getServiceId(), instance.getInstanceId()); + service.setGrayInstance(instance); + } + + @Override + public void closeGray(GrayInstance instance) { + GrayService service = getGrayService(instance.getServiceId()); + if (service == null) { + log.debug("没有找到灰度服务:{}, 所以无需删除实例:{} 的灰度状态", instance.getServiceId(), instance.getInstanceId()); + return; + } + log.debug("关闭实例的在灰度状态, serviceId:{}, instanceId:{}", instance.getServiceId(), instance.getInstanceId()); + service.removeGrayInstance(instance.getInstanceId()); + } + + @Override + public void closeGray(String serviceId, String instanceId) { + GrayService service = getGrayService(serviceId); + if (service == null) { + log.debug("没有找到灰度服务:{}, 所以无需删除实例:{} 的灰度状态", serviceId, instanceId); + return; + } + log.debug("关闭实例的在灰度状态, serviceId:{}, instanceId:{}", serviceId, instanceId); + service.removeGrayInstance(instanceId); + } + + + @Override + public void shutdown() { + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientAppContext.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientAppContext.java deleted file mode 100644 index fda98c44..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientAppContext.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.springcloud.gray.client; - -import cn.springcloud.gray.InstanceLocalInfo; -import cn.springcloud.gray.client.config.properties.GrayClientProperties; -import cn.springcloud.gray.core.GrayManager; - -public class GrayClientAppContext { - private static GrayManager grayManager; - private static InstanceLocalInfo instanceLocalInfo; - private static GrayClientProperties grayClientProperties; - - - public static GrayManager getGrayManager() { - return grayManager; - } - - static void setGrayManager(GrayManager grayManager) { - GrayClientAppContext.grayManager = grayManager; - } - - - public static InstanceLocalInfo getInstanceLocalInfo() { - return instanceLocalInfo; - } - - static void setInstanceLocalInfo(InstanceLocalInfo instanceLocalInfo) { - GrayClientAppContext.instanceLocalInfo = instanceLocalInfo; - } - - public static GrayClientProperties getGrayClientProperties() { - return grayClientProperties; - } - - static void setGrayClientProperties(GrayClientProperties grayClientProperties) { - GrayClientAppContext.grayClientProperties = grayClientProperties; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingBean.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingBean.java deleted file mode 100644 index 33488fb9..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingBean.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.springcloud.gray.client; - -import cn.springcloud.gray.InstanceLocalInfo; -import cn.springcloud.gray.client.config.properties.GrayClientProperties; -import cn.springcloud.gray.core.GrayManager; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -import javax.annotation.PreDestroy; - -public class GrayClientInitializingBean implements InitializingBean, ApplicationContextAware { - private ApplicationContext cxt; - - @Override - public void afterPropertiesSet() throws Exception { - GrayClientAppContext.setGrayManager(cxt.getBean(GrayManager.class)); - GrayClientAppContext.setInstanceLocalInfo(cxt.getBean(InstanceLocalInfo.class)); - GrayClientAppContext.setGrayClientProperties(cxt.getBean(GrayClientProperties.class)); - - startForWork(); - -// registrShutdownFunc(); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.cxt = applicationContext; - } - -// private void registrShutdownFunc(){ -// Runtime.getRuntime().addShutdownHook(new Thread(()->{ -// shutdown(); -// })); -// } - - @PreDestroy - public void shutdown() { - GrayClientAppContext.getGrayManager().serviceDownline(); - } - - private void startForWork() { - GrayClientAppContext.getGrayManager().openForWork(); - } -} \ No newline at end of file diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingDestroyBean.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingDestroyBean.java new file mode 100644 index 00000000..1f18174f --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayClientInitializingDestroyBean.java @@ -0,0 +1,63 @@ +package cn.springcloud.gray.client; + +import cn.springcloud.gray.CommunicableGrayManager; +import cn.springcloud.gray.GrayClientConfig; +import cn.springcloud.gray.InstanceLocalInfo; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; + + +@Slf4j +public class GrayClientInitializingDestroyBean implements InitializingBean { + + private CommunicableGrayManager grayManager; + private InstanceLocalInfo instanceLocalInfo; + private GrayClientConfig clientConfig; + + public GrayClientInitializingDestroyBean( + CommunicableGrayManager grayManager, GrayClientConfig clientConfig, InstanceLocalInfo instanceLocalInfo) { + this.grayManager = grayManager; + this.clientConfig = clientConfig; + this.instanceLocalInfo = instanceLocalInfo; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (clientConfig.isGrayEnroll()) { + if (clientConfig.grayEnrollDealyTimeInMs() > 0) { + Thread t = new Thread(() -> { + try { + Thread.sleep(clientConfig.grayEnrollDealyTimeInMs()); + } catch (InterruptedException e) { + } + log.info("灰度注册自身实例..."); + grayRegister(); + }, "GrayEnroll"); + t.start(); + } else { + grayRegister(); + } + } + } + + public void shutdown() { + if (instanceLocalInfo.isGray()) { + grayManager.getGrayInformationClient().serviceDownline( + instanceLocalInfo.getInstanceId()); + } + } + + private void grayRegister() { + GrayInstance grayInstance = new GrayInstance(); + grayInstance.setHost(instanceLocalInfo.getHost()); + grayInstance.setGrayStatus(GrayStatus.OPEN); + grayInstance.setInstanceId(instanceLocalInfo.getInstanceId()); + grayInstance.setServiceId(instanceLocalInfo.getServiceId()); + grayInstance.setPort(instanceLocalInfo.getPort()); + + grayManager.getGrayInformationClient().addGrayInstance(grayInstance); + instanceLocalInfo.setGray(true); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayOptionalArgs.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayOptionalArgs.java deleted file mode 100644 index d36221ef..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/GrayOptionalArgs.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.springcloud.gray.client; - -import cn.springcloud.gray.core.GrayDecisionFactory; -import cn.springcloud.gray.core.InformationClient; - -public class GrayOptionalArgs { - - private GrayClientConfig grayClientConfig; - private InformationClient informationClient; - private GrayDecisionFactory decisionFactory; - - - public GrayClientConfig getGrayClientConfig() { - return grayClientConfig; - } - - - public GrayDecisionFactory getDecisionFactory() { - return decisionFactory; - } - - - public void setGrayClientConfig(GrayClientConfig grayClientConfig) { - this.grayClientConfig = grayClientConfig; - } - - - public void setDecisionFactory(GrayDecisionFactory decisionFactory) { - this.decisionFactory = decisionFactory; - } - - - public InformationClient getInformationClient() { - return informationClient; - } - - public void setInformationClient(InformationClient informationClient) { - this.informationClient = informationClient; - } -} - - diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientAutoConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientAutoConfiguration.java index b3bab564..df498074 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientAutoConfiguration.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientAutoConfiguration.java @@ -1,87 +1,53 @@ package cn.springcloud.gray.client.config; -import cn.springcloud.bamboo.BambooConstants; -import cn.springcloud.bamboo.autoconfig.BambooAutoConfiguration; -import cn.springcloud.gray.DefaultGrayManager; -import cn.springcloud.gray.HttpInformationClient; -import cn.springcloud.gray.RetryableInformationClient; -import cn.springcloud.gray.client.GrayClientInitializingBean; -import cn.springcloud.gray.client.GrayOptionalArgs; +import cn.springcloud.gray.*; +import cn.springcloud.gray.client.GrayClientInitializingDestroyBean; import cn.springcloud.gray.client.config.properties.GrayClientProperties; -import cn.springcloud.gray.core.GrayDecisionFactory; -import cn.springcloud.gray.core.GrayManager; -import cn.springcloud.gray.core.InformationClient; -import cn.springcloud.gray.decision.DefaultGrayDecisionFactory; +import cn.springcloud.gray.client.config.properties.GrayRequestProperties; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import cn.springcloud.gray.request.RequestLocalStorage; +import cn.springcloud.gray.request.ThreadLocalRequestStorage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.netflix.ribbon.RibbonClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.web.client.RestTemplate; +import org.springframework.context.annotation.Import; + +import java.util.List; @Configuration -@EnableConfigurationProperties({GrayClientProperties.class}) +@EnableConfigurationProperties({GrayClientProperties.class, GrayRequestProperties.class}) @ConditionalOnBean(GrayClientMarkerConfiguration.GrayClientMarker.class) -@RibbonClients(defaultConfiguration = GrayRibbonClientsConfiguration.class) +@Import({GrayDecisionFactoryConfiguration.class, GrayTrackConfiguration.class}) public class GrayClientAutoConfiguration { - @Bean - public BambooAutoConfiguration.UnUseBambooIRule unUseBambooIRule() { - return new BambooAutoConfiguration.UnUseBambooIRule(); - } - - -// @Bean -// public IRule ribbonRule(@Autowired(required = false) IClientConfig config) { -// GrayLoadBalanceRule rule = new GrayLoadBalanceRule(); -// rule.initWithNiwsConfig(config); -// return rule; -// } + @Autowired + private GrayClientProperties grayClientProperties; - @Bean - @Order(value = BambooConstants.INITIALIZING_ORDER + 1) - public GrayClientInitializingBean grayClientInitializingBean() { - return new GrayClientInitializingBean(); - } @Bean @ConditionalOnMissingBean - public GrayDecisionFactory grayDecisionFactory() { - return new DefaultGrayDecisionFactory(); + public GrayManager grayManager(GrayDecisionFactoryKeeper grayDecisionFactoryKeeper, + @Autowired(required = false) List requestInterceptors) { + return new DefaultGrayManager(grayClientProperties, grayDecisionFactoryKeeper, requestInterceptors); } - @Configuration - @ConditionalOnProperty(prefix = "gray.client", value = "information-client", havingValue = "http", matchIfMissing - = true) - public static class HttpGrayManagerClientConfiguration { - @Autowired - private GrayClientProperties grayClientProperties; - - @Bean - public InformationClient informationClient() { - InformationClient client = new HttpInformationClient(grayClientProperties.getServerUrl(), new - RestTemplate()); - if (!grayClientProperties.isRetryable()) { - return client; - } - return new RetryableInformationClient(grayClientProperties.getRetryNumberOfRetries(), client); - } + @Bean + @ConditionalOnBean({CommunicableGrayManager.class, InstanceLocalInfo.class}) + public GrayClientInitializingDestroyBean grayClientInitializingDestroyBean( + CommunicableGrayManager grayManager, InstanceLocalInfo instanceLocalInfo) { + return new GrayClientInitializingDestroyBean(grayManager, grayClientProperties, instanceLocalInfo); + } - @Bean - public GrayManager grayManager(InformationClient informationClient, GrayDecisionFactory grayDecisionFactory) { - GrayOptionalArgs args = new GrayOptionalArgs(); - args.setDecisionFactory(grayDecisionFactory); - args.setGrayClientConfig(grayClientProperties); - args.setInformationClient(informationClient); - return new DefaultGrayManager(args); - } + @Bean + @ConditionalOnMissingBean + public RequestLocalStorage requestLocalStorage() { + return new ThreadLocalRequestStorage(); } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientMarkerConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientMarkerConfiguration.java index 75f5449f..917a7379 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientMarkerConfiguration.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientMarkerConfiguration.java @@ -12,6 +12,6 @@ public GrayClientMarker grayClientMarker() { return new GrayClientMarker(); } - class GrayClientMarker { + public class GrayClientMarker { } } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientZookeeperAutoConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientZookeeperAutoConfiguration.java deleted file mode 100644 index b3e29e9d..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayClientZookeeperAutoConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.springcloud.gray.client.config; - -import cn.springcloud.gray.InstanceLocalInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.client.serviceregistry.Registration; -import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryProperties; -import org.springframework.cloud.zookeeper.serviceregistry.ZookeeperRegistration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.naming.ConfigurationException; - -/** - * @Author: duozl - * @Date: 2018/6/5 18:18 - */ -@Configuration -@ConditionalOnBean(ZookeeperRegistration.class) -public class GrayClientZookeeperAutoConfiguration { - private static final String METADATA_KEY_INSTANCE_ID = "instanceId"; - - @Bean - @ConditionalOnMissingBean - public InstanceLocalInfo instanceLocalInfo(@Autowired Registration registration, - @Autowired ZookeeperDiscoveryProperties properties) - throws ConfigurationException { - String instanceId; - if (properties.getMetadata().containsKey(METADATA_KEY_INSTANCE_ID)) { - instanceId = properties.getMetadata().get(METADATA_KEY_INSTANCE_ID); - } else { - throw new ConfigurationException("Unable to find config spring.cloud.zookeeper.discovery.metadata" + - ".instanceId!"); - } - - InstanceLocalInfo localInfo = new InstanceLocalInfo(); - localInfo.setInstanceId(instanceId); - localInfo.setServiceId(registration.getServiceId()); - localInfo.setGray(false); - return localInfo; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayDecisionFactoryConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayDecisionFactoryConfiguration.java new file mode 100644 index 00000000..56068301 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayDecisionFactoryConfiguration.java @@ -0,0 +1,56 @@ +package cn.springcloud.gray.client.config; + +import cn.springcloud.gray.decision.DefaultGrayDecisionFactoryKeeper; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import cn.springcloud.gray.decision.factory.*; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.validation.Validator; + +import java.util.List; + +@Configuration +public class GrayDecisionFactoryConfiguration { + + + @Configuration + public static class WebGrayDecisionFactoryConfiguration { + + @Bean + public HttpHeaderGrayDecisionFactory httpHeaderGrayDecisionFactory() { + return new HttpHeaderGrayDecisionFactory(); + } + + @Bean + public HttpMethodGrayDecisionFactory httpMethodGrayDecisionFactory() { + return new HttpMethodGrayDecisionFactory(); + } + + @Bean + public HttpParameterGrayDecisionFactory httpParameterGrayDecisionFactory() { + return new HttpParameterGrayDecisionFactory(); + } + + @Bean + public TraceIpGrayDecisionFactory traceIpGrayDecisionFactory() { + return new TraceIpGrayDecisionFactory(); + } + } + + @Bean + @ConditionalOnMissingBean + public GrayDecisionFactoryKeeper grayDecisionFactoryKeeper( + List conversionServices, Validator validator, List decisionFactories) { + if (CollectionUtils.isNotEmpty(conversionServices)) { + return new DefaultGrayDecisionFactoryKeeper(conversionServices.get(0), validator, decisionFactories); + } + return new DefaultGrayDecisionFactoryKeeper(DefaultConversionService.getSharedInstance(), validator, decisionFactories); + + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayEventAutoConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayEventAutoConfiguration.java new file mode 100644 index 00000000..b22a63cb --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayEventAutoConfiguration.java @@ -0,0 +1,39 @@ +package cn.springcloud.gray.client.config; + +import cn.springcloud.gray.CommunicableGrayManager; +import cn.springcloud.gray.event.DefaultGrayEventListener; +import cn.springcloud.gray.event.GrayEventListener; +import cn.springcloud.gray.event.stream.StreamInput; +import cn.springcloud.gray.event.stream.StreamMessageListener; +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.stream.annotation.EnableBinding; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GrayEventAutoConfiguration { + + + @Bean + @ConditionalOnMissingBean + public GrayEventListener grayEventListener(CommunicableGrayManager grayManager) { + return new DefaultGrayEventListener(grayManager); + } + + + @Configuration + @ConditionalOnClass(EnableBinding.class) + @EnableBinding({StreamInput.class}) + @ConditionalOnProperty(value = "spring.cloud.stream.bindings.GrayEventInput.destination") + public static class StreamEventConfiguration { + + @Bean + public StreamMessageListener streamMessageListener(GrayEventListener grayEventListener) { + return new StreamMessageListener(grayEventListener); + } + + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayLoadAutoConfigration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayLoadAutoConfigration.java new file mode 100644 index 00000000..3da51f44 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayLoadAutoConfigration.java @@ -0,0 +1,36 @@ +package cn.springcloud.gray.client.config; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.client.config.properties.GrayLoadProperties; +import cn.springcloud.gray.model.GrayStatus; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +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; + +@Configuration +@ConditionalOnProperty(value = "gray.load.enabled", havingValue = "true") +@EnableConfigurationProperties({GrayLoadProperties.class}) +public class GrayLoadAutoConfigration { + + @Autowired + private GrayLoadProperties grayLoadProperties; + @Autowired + private GrayManager grayManager; + + + @Bean + public InitializingBean loadGrayInfoInitializing() { + return () -> { + grayLoadProperties.getGrayInstances().forEach(instance -> { + if (instance.getGrayStatus() == null) { + instance.setGrayStatus(GrayStatus.OPEN); + } + grayManager.updateGrayInstance(instance); + }); + }; + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayRibbonClientsConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayRibbonClientsConfiguration.java deleted file mode 100644 index 6ea1798f..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayRibbonClientsConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.springcloud.gray.client.config; - -import cn.springcloud.gray.ribbon.GrayLoadBalanceRule; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.IRule; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class GrayRibbonClientsConfiguration { - - - @Bean - public IRule ribbonRule(@Autowired(required = false) IClientConfig config) { - GrayLoadBalanceRule rule = new GrayLoadBalanceRule(); - rule.initWithNiwsConfig(config); - return rule; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayTrackConfiguration.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayTrackConfiguration.java new file mode 100644 index 00000000..d2d86046 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/GrayTrackConfiguration.java @@ -0,0 +1,137 @@ +package cn.springcloud.gray.client.config; + + +import cn.springcloud.gray.client.config.properties.GrayTrackProperties; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import cn.springcloud.gray.request.RequestLocalStorage; +import cn.springcloud.gray.web.GrayTrackFilter; +import cn.springcloud.gray.web.GrayTrackRequestInterceptor; +import cn.springcloud.gray.web.tracker.*; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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 org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@Configuration +@ConditionalOnProperty(value = "gray.request.track.enabled", matchIfMissing = true) +@EnableConfigurationProperties(GrayTrackProperties.class) +public class GrayTrackConfiguration { + + + @ConditionalOnProperty(value = "gray.client.runenv", havingValue = "web", matchIfMissing = true) + @Configuration + public static class GrayClientWebConfiguration extends WebMvcConfigurerAdapter { + + @Autowired + private GrayTrackProperties grayTrackProperties; + + @Autowired + private List> trackors; + + @Autowired + private RequestLocalStorage requestLocalStorage; + +// @Bean +// @ConditionalOnMissingBean +// public GrayTrackInterceptor grayTrackInterceptor() { +// return new GrayTrackInterceptor(requestLocalStorage, trackors); +// } + +// @Override +// public void addInterceptors(InterceptorRegistry registry) { +// InterceptorRegistration grayTrackRegistor = registry.addInterceptor(grayTrackInterceptor()); +// GrayTrackProperties.Web webProperties = grayTrackProperties.getWeb(); +// +// for (String pattern : webProperties.getPathPatterns()) { +// grayTrackRegistor.addPathPatterns(pattern); +// } +// for (String pattern : webProperties.getExcludePathPatterns()) { +// grayTrackRegistor.excludePathPatterns(pattern); +// } +// } + + @Bean + @ConditionalOnMissingBean + public GrayTrackFilter grayTrackFilter() { + return new GrayTrackFilter(requestLocalStorage, trackors); + } + + + @Bean + public FilterRegistrationBean companyUrlFilterRegister(GrayTrackFilter filter) { + GrayTrackProperties.Web webProperties = grayTrackProperties.getWeb(); + FilterRegistrationBean registration = new FilterRegistrationBean(); + //注入过滤器 + registration.setFilter(filter); + //拦截规则 + for (String pattern : webProperties.getPathPatterns()) { + registration.addUrlPatterns(pattern); + } + //过滤器名称 + registration.setName("GrayTrackFilter"); + //过滤器顺序 + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + + + @Bean + public HttpReceiveGrayTracker httpReceiveGrayTracker() { + return new HttpReceiveGrayTracker(); + } + + @Bean + @ConditionalOnProperty(value = "gray.request.track.web.need.headers") + public HttpHeaderGrayTracker httpHeaderGrayTracker() { + String header = grayTrackProperties.getWeb().getNeed().get(GrayTrackProperties.Web.NEED_HEADERS); + String[] headers = StringUtils.isEmpty(header) ? new String[]{} : header.split(","); + return new HttpHeaderGrayTracker(headers); + } + + @Bean + @ConditionalOnProperty(value = "gray.request.track.web.need.method", havingValue = "enable") + public HttpMethodGrayTracker httpMethodGrayTracker() { + return new HttpMethodGrayTracker(); + } + + @Bean + @ConditionalOnProperty(value = "gray.request.track.web.need.uri", havingValue = "enable") + public HttpURIGrayTracker httpURIGrayTracker() { + return new HttpURIGrayTracker(); + } + + @Bean + @ConditionalOnProperty(value = "gray.request.track.web.need.ip", havingValue = "enable") + public HttpIPGrayTracker httpIPGrayTracker() { + return new HttpIPGrayTracker(); + } + + @Bean + @ConditionalOnProperty(value = "gray.request.track.web.need.parameters", havingValue = "enable") + public HttpParameterGrayTracker httpParameterGrayTracker() { + String name = grayTrackProperties.getWeb().getNeed().get(GrayTrackProperties.Web.NEED_PARAMETERS); + String[] names = StringUtils.isEmpty(name) ? new String[]{} : name.split(","); + return new HttpParameterGrayTracker(names); + } + + + @Bean + public GrayTrackRequestInterceptor grayTrackRequestInterceptor() { + return new GrayTrackRequestInterceptor(grayTrackProperties); + } + + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayClientProperties.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayClientProperties.java index 6a03ac86..1885b0fa 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayClientProperties.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayClientProperties.java @@ -1,12 +1,13 @@ package cn.springcloud.gray.client.config.properties; -import cn.springcloud.gray.RetryableInformationClient; -import cn.springcloud.gray.client.GrayClientConfig; +import cn.springcloud.gray.communication.RetryableInformationClient; +import cn.springcloud.gray.GrayClientConfig; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("gray.client") public class GrayClientProperties implements GrayClientConfig { + private String runenv = "web"; private int serviceUpdateIntervalTimerInMs = 60000; @@ -28,6 +29,11 @@ public void setInformationClient(String informationClient) { this.informationClient = informationClient; } + @Override + public String runenv() { + return runenv; + } + @Override public boolean isGrayEnroll() { return instance.isGrayEnroll(); diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayLoadProperties.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayLoadProperties.java new file mode 100644 index 00000000..7a5a2485 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayLoadProperties.java @@ -0,0 +1,19 @@ +package cn.springcloud.gray.client.config.properties; + +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayStatus; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +@ConfigurationProperties("gray.load") +@Setter +@Getter +public class GrayLoadProperties { + private boolean enabled = false; + private List grayInstances = new ArrayList<>(); + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayRequestProperties.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayRequestProperties.java new file mode 100644 index 00000000..aeac6acd --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayRequestProperties.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.client.config.properties; + + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Setter +@Getter +@ConfigurationProperties(prefix = "gray.request") +public class GrayRequestProperties { + private boolean loadBody = false; + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayTrackProperties.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayTrackProperties.java new file mode 100644 index 00000000..cadad69b --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/client/config/properties/GrayTrackProperties.java @@ -0,0 +1,35 @@ +package cn.springcloud.gray.client.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + + +@Setter +@Getter +@ConfigurationProperties(prefix = "gray.request.track") +public class GrayTrackProperties { + private boolean enabled = true; + private String trackType = "web"; + private Web web = new Web(); + + @Setter + @Getter + public static class Web { + + public static final String NEED_URI = "uri"; + public static final String NEED_IP = "ip"; + public static final String NEED_METHOD = "method"; + public static final String NEED_HEADERS = "headers"; + public static final String NEED_PARAMETERS = "parameters"; + + private String[] pathPatterns = new String[]{"/*"}; + private String[] excludePathPatterns = new String[]{}; + + private Map need = new HashMap<>(); + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/HttpInformationClient.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/HttpInformationClient.java new file mode 100644 index 00000000..4d1cd12d --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/HttpInformationClient.java @@ -0,0 +1,81 @@ +package cn.springcloud.gray.communication; + +import cn.springcloud.gray.model.GrayInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HttpInformationClient implements InformationClient { + private static final Logger log = LoggerFactory.getLogger(HttpInformationClient.class); + private final String baseUrl; + private RestTemplate rest; + + public HttpInformationClient(String baseUrl) { + this(baseUrl, new RestTemplate()); + } + + public HttpInformationClient(String baseUrl, RestTemplate rest) { + this.baseUrl = baseUrl; + this.rest = rest; + } + + @Override + public List allGrayInstances() { + String url = this.baseUrl + "/gray/instances/enable"; + ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { + }; + + try { + ResponseEntity> responseEntity = rest.exchange(url, HttpMethod.GET, null, typeRef); + return responseEntity.getBody(); + } catch (RuntimeException e) { + log.error("获取灰度服务列表失败", e); + throw e; + } + } + + + @Override + public void addGrayInstance(GrayInstance grayInstance) { + String url = this.baseUrl + "/gray/instance/"; + try { + rest.postForEntity(url, grayInstance, null); + } catch (RuntimeException e) { + log.error("灰度服务实例下线失败", e); + throw e; + } + } + + @Override + public GrayInstance getGrayInstance(String serviceId, String instanceId) { + String url = this.baseUrl + "/gray/instance?serviceId={serviceId}&instanceId={instanceId}"; + try { + ResponseEntity responseEntity = + rest.getForEntity(url, GrayInstance.class, serviceId, instanceId); + return responseEntity.getBody(); + } catch (RuntimeException e) { + log.error("获取灰度实例", e); + throw e; + } + } + + @Override + public void serviceDownline(String instanceId) { + String url = this.baseUrl + "/gray/instance/{id}/switchStatus?switch=0"; + Map params = new HashMap<>(); + params.put("instanceId", instanceId); + try { + rest.delete(url, params); + } catch (Exception e) { + log.error("灰度服务实例下线失败", e); + throw e; + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClient.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClient.java new file mode 100644 index 00000000..e39febda --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClient.java @@ -0,0 +1,47 @@ +package cn.springcloud.gray.communication; + +import cn.springcloud.gray.model.GrayService; +import cn.springcloud.gray.model.GrayInstance; + +import java.util.List; + + +/** + * 该接口主要是负责和灰度服务端进行通信,获取灰度列表,编辑灰度实例等能力。其实现类HttpInformationClient默认使用http方式访问灰度服务端。 + * 子类InformationClientDecorator是一个适配器类,RetryableInformationClient继承了InformationClientDecorator类,实现了重试的功能。 + */ +public interface InformationClient { + + + /** + * 返回在灰度中注册的所有灰度实例(不包括非灰度实例) + * + * @return 所有的灰度实例 + */ + List allGrayInstances(); + + /** + * 注册灰度实例 + * + * @param grayInstance 服务实例id + */ + void addGrayInstance(GrayInstance grayInstance); + + /** + * 获取灰度实例的信息 + * + * @param serviceId 服务id + * @param instanceId 实例id + * @return + */ + GrayInstance getGrayInstance(String serviceId, String instanceId); + + + /** + * 灰度实例下线 + * + * @param instanceId 实例id + */ + void serviceDownline(String instanceId); + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClientDecorator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClientDecorator.java new file mode 100644 index 00000000..4e8e8992 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/InformationClientDecorator.java @@ -0,0 +1,95 @@ +package cn.springcloud.gray.communication; + +import cn.springcloud.gray.model.GrayInstance; + +import java.util.List; + + +/** + * InformationClientDecorator是一个适配器类 + */ +public abstract class InformationClientDecorator implements InformationClient { + + + public enum RequestType { + AddGrayInstance, + ServiceDownline, + AllGrayInstances, + GetGrayInstance + } + + + public interface RequestExecutor { + R execute(InformationClient delegate); + + RequestType getRequestType(); + } + + + protected abstract R execute(RequestExecutor requestExecutor); + + @Override + public GrayInstance getGrayInstance(String serviceId, String instanceId) { + return execute(new RequestExecutor() { + @Override + public GrayInstance execute(InformationClient delegate) { + return delegate.getGrayInstance(serviceId, instanceId); + } + + @Override + public RequestType getRequestType() { + return RequestType.GetGrayInstance; + } + }); + } + + @Override + public List allGrayInstances() { + return execute(new RequestExecutor>() { + @Override + public List execute(InformationClient delegate) { + return delegate.allGrayInstances(); + } + + @Override + public RequestType getRequestType() { + return RequestType.AllGrayInstances; + } + }); + } + + + @Override + public void addGrayInstance(GrayInstance grayInstance) { + + execute(new RequestExecutor() { + @Override + public Object execute(InformationClient delegate) { + delegate.addGrayInstance(grayInstance); + return null; + } + + @Override + public RequestType getRequestType() { + return RequestType.AddGrayInstance; + } + }); + } + + + @Override + public void serviceDownline(String instanceId) { + execute(new RequestExecutor() { + @Override + public Object execute(InformationClient delegate) { + delegate.serviceDownline(instanceId); + return null; + } + + @Override + public RequestType getRequestType() { + return RequestType.ServiceDownline; + } + }); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RetryableInformationClient.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/RetryableInformationClient.java similarity index 90% rename from spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RetryableInformationClient.java rename to spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/RetryableInformationClient.java index 6bd29f52..e18196a9 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/RetryableInformationClient.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/communication/RetryableInformationClient.java @@ -1,7 +1,5 @@ -package cn.springcloud.gray; +package cn.springcloud.gray.communication; -import cn.springcloud.gray.core.InformationClient; -import cn.springcloud.gray.core.InformationClientDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/ContextParameterDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/ContextParameterDecision.java deleted file mode 100644 index d394e4a9..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/ContextParameterDecision.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.springcloud.gray.decision; - - -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.bamboo.BambooRequestContext; -import cn.springcloud.gray.core.GrayDecision; - -import java.util.Map; -import java.util.Objects; - -public class ContextParameterDecision implements GrayDecision { - - - private final Map params; - - public ContextParameterDecision(Map params) { - if (params.isEmpty()) { - throw new NullPointerException("params must not be empty"); - } - this.params = params; - } - - @Override - public boolean test(BambooRequest bambooRequest) { - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); - for (Map.Entry entry : params.entrySet()) { - if (!Objects.equals(entry.getValue(), bambooRequestContext.getParameter(entry.getKey()))) { - return false; - } - } - return true; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactory.java deleted file mode 100644 index dbfefefb..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package cn.springcloud.gray.decision; - -import cn.springcloud.gray.core.GrayDecision; -import cn.springcloud.gray.core.GrayDecisionFactory; -import cn.springcloud.gray.core.GrayPolicy; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import java.util.Arrays; - -/** - * 默认的灰度决策工厂类,其默认实现类支持上述几种灰度决策的创建。 - */ -public class DefaultGrayDecisionFactory implements GrayDecisionFactory { - - - @Override - public GrayDecision getDecision(GrayPolicy grayPolicy) { - PolicyType policyType = PolicyType.valueOf(grayPolicy.getPolicyType()); - if (policyType == null) { - throw new IllegalArgumentException("not suppot"); - } - switch (policyType) { - case REQUEST_IP: - String ipstr = grayPolicy.getInfos().get(RequestIpDecision.IPS_KEY); - return new RequestIpDecision(Arrays.asList(ipstr.split(","))); - case REQUEST_HEADER: - MultiValueMap headers = new LinkedMultiValueMap<>(); - headers.setAll(grayPolicy.getInfos()); - return new RequestHeaderDecision(headers); - case REQUEST_PARAMETER: - MultiValueMap params = new LinkedMultiValueMap<>(); - params.setAll(grayPolicy.getInfos()); - return new RequestParameterDecision(params); - case CONTEXT_PARAMS: - return new ContextParameterDecision(grayPolicy.getInfos()); - default: - throw new IllegalArgumentException("not suppot"); - } - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactoryKeeper.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactoryKeeper.java new file mode 100644 index 00000000..3554fe5b --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/DefaultGrayDecisionFactoryKeeper.java @@ -0,0 +1,68 @@ +package cn.springcloud.gray.decision; + +import cn.springcloud.gray.decision.factory.GrayDecisionFactory; +import cn.springcloud.gray.model.DecisionDefinition; +import cn.springcloud.gray.utils.ConfigurationUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.validation.Validator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class DefaultGrayDecisionFactoryKeeper implements GrayDecisionFactoryKeeper, ApplicationContextAware { + + private ApplicationContext cxt; + private SpelExpressionParser parser = new SpelExpressionParser(); + private ConversionService conversionService; + private Validator validator; + + private Map grayDecisionFactories = new HashMap<>(); + + + public DefaultGrayDecisionFactoryKeeper(ConversionService conversionService, Validator validator, List decisionFactories) { + this.conversionService = conversionService; + this.validator = validator; + initGrayDecisionFactories(decisionFactories); + } + + + @Override + public GrayDecisionFactory getDecisionFactory(String name) { + return grayDecisionFactories.get(name); + } + + @Override + public GrayDecision getGrayDecision(DecisionDefinition decisionDefinition) { + GrayDecisionFactory factory = getDecisionFactory(decisionDefinition.getName()); + if (factory == null) { + log.error("没有找到灰度决定工厂:{}", decisionDefinition.getName()); + throw new NullPointerException("没有找到灰度决定工厂:" + decisionDefinition.getName()); + } + return factory.apply(configuration -> { + Map properties = ConfigurationUtils.normalize(decisionDefinition.getInfos(), parser, cxt); + ConfigurationUtils.bind(configuration, properties, + "", "", validator, + conversionService); + }); + } + + + private void initGrayDecisionFactories(List decisionFactories) { + decisionFactories.stream().forEach(factory -> { + grayDecisionFactories.put(factory.name(), factory); + }); + + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.cxt = applicationContext; + } +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecision.java similarity index 80% rename from spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecision.java rename to spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecision.java index c3c1965a..42530581 100644 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecision.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecision.java @@ -1,15 +1,14 @@ -package cn.springcloud.gray.core; +package cn.springcloud.gray.decision; -import cn.springcloud.bamboo.BambooRequest; - /** * 该接口是灰度决策,用来判断请求是否匹配灰度策略。 * 实现了ip匹配、request parameter匹配、request header匹配、BambooRequestContext中的参数匹配器以及合并匹配等多个匹配能力。 */ public interface GrayDecision { - boolean test(BambooRequest bambooRequest); + + boolean test(GrayDecisionInputArgs args); static AllowGraydecision allow() { @@ -36,7 +35,7 @@ private AllowGraydecision() { @Override - public boolean test(BambooRequest bambooRequest) { + public boolean test(GrayDecisionInputArgs args) { return true; } } @@ -55,7 +54,7 @@ private RefuseGraydecision() { } @Override - public boolean test(BambooRequest bambooRequest) { + public boolean test(GrayDecisionInputArgs args) { return false; } } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionFactoryKeeper.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionFactoryKeeper.java new file mode 100644 index 00000000..034760f3 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionFactoryKeeper.java @@ -0,0 +1,13 @@ +package cn.springcloud.gray.decision; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.decision.factory.GrayDecisionFactory; +import cn.springcloud.gray.model.DecisionDefinition; + +public interface GrayDecisionFactoryKeeper { + + GrayDecisionFactory getDecisionFactory(String name); + + + GrayDecision getGrayDecision(DecisionDefinition decisionDefinition); +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionInputArgs.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionInputArgs.java new file mode 100644 index 00000000..58106758 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/GrayDecisionInputArgs.java @@ -0,0 +1,15 @@ +package cn.springcloud.gray.decision; + +import cn.springcloud.gray.servernode.ServerSpec; +import cn.springcloud.gray.request.GrayRequest; +import lombok.*; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GrayDecisionInputArgs { + + private ServerSpec server; + private GrayRequest grayRequest; +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/MultiGrayDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/MultiGrayDecision.java index 5b2de3f1..61c9078e 100644 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/MultiGrayDecision.java +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/MultiGrayDecision.java @@ -1,10 +1,6 @@ package cn.springcloud.gray.decision; -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.gray.core.GrayDecision; - - /** * 组合从个灰度策略 */ @@ -25,8 +21,8 @@ public MultiGrayDecision and(GrayDecision other) { } @Override - public boolean test(BambooRequest bambooRequest) { - return decision.test(bambooRequest); + public boolean test(GrayDecisionInputArgs args) { + return decision.test(args); } diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/PolicyType.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/PolicyType.java deleted file mode 100644 index dd5c50cc..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/PolicyType.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.springcloud.gray.decision; - -public enum PolicyType { - - /** - * @see cn.springcloud.bamboo.BambooRequest#params - */ - REQUEST_PARAMETER, - /** - * @see cn.springcloud.bamboo.BambooRequest#headers - */ - REQUEST_HEADER, - - /** - * @see cn.springcloud.bamboo.BambooRequest#ip - */ - REQUEST_IP, - /** - * @see cn.springcloud.bamboo.BambooRequestContext#params - */ - CONTEXT_PARAMS -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestHeaderDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestHeaderDecision.java deleted file mode 100644 index 98bc7831..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestHeaderDecision.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.springcloud.gray.decision; - -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.gray.core.GrayDecision; -import org.apache.commons.collections.ListUtils; -import org.springframework.util.MultiValueMap; - -import java.util.List; -import java.util.Map; - -public class RequestHeaderDecision implements GrayDecision { - - - private final MultiValueMap headers; - - public RequestHeaderDecision(MultiValueMap headers) { - if (headers.isEmpty()) { - throw new NullPointerException("headers must not be empty"); - } - this.headers = headers; - } - - @Override - public boolean test(BambooRequest bambooRequest) { - for (Map.Entry> entry : headers.entrySet()) { - List values = bambooRequest.getHeaders().get(entry.getKey()); - if (!ListUtils.isEqualList(entry.getValue(), values)) { - return false; - } - } - return true; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestIpDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestIpDecision.java deleted file mode 100644 index ba98ef1f..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestIpDecision.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.springcloud.gray.decision; - - -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.gray.core.GrayDecision; - -import java.util.List; - -public class RequestIpDecision implements GrayDecision { - - public static final String IPS_KEY = "ips"; - - private final List ips; - - public RequestIpDecision(List ips) { - if (ips == null || ips.isEmpty()) { - throw new NullPointerException("ips must not be empty"); - } - this.ips = ips; - } - - @Override - public boolean test(BambooRequest bambooRequest) { - return ips.contains(bambooRequest.getIp()); - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestParameterDecision.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestParameterDecision.java deleted file mode 100644 index dcac3743..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/RequestParameterDecision.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.springcloud.gray.decision; - -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.gray.core.GrayDecision; -import org.apache.commons.collections.ListUtils; -import org.springframework.util.MultiValueMap; - -import java.util.List; -import java.util.Map; - -public class RequestParameterDecision implements GrayDecision { - - - private final MultiValueMap params; - - public RequestParameterDecision(MultiValueMap params) { - if (params.isEmpty()) { - throw new NullPointerException("params must not be empty"); - } - this.params = params; - } - - @Override - public boolean test(BambooRequest bambooRequest) { - for (Map.Entry> entry : params.entrySet()) { - List values = bambooRequest.getParams().get(entry.getKey()); - if (!ListUtils.isEqualList(entry.getValue(), values)) { - return false; - } - } - return true; - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAllComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAllComparator.java new file mode 100644 index 00000000..c4548d9d --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAllComparator.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.CollectionUtils; + +import java.util.Collection; +import java.util.List; + +public class CollectionStringContainAllComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return false; + } + if (CollectionUtils.isEmpty(another)) { + return false; + } + + return src.containsAll(another); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAnyComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAnyComparator.java new file mode 100644 index 00000000..6d3d05a7 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringContainAnyComparator.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.CollectionUtils; + +import java.util.Collection; +import java.util.List; + +public class CollectionStringContainAnyComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return false; + } + if (CollectionUtils.isEmpty(another)) { + return false; + } + + return CollectionUtils.containsAny(src, another); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringEqualComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringEqualComparator.java new file mode 100644 index 00000000..f80dae8b --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringEqualComparator.java @@ -0,0 +1,27 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.ListUtils; + +import java.util.*; + +public class CollectionStringEqualComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return CollectionUtils.isEmpty(another) ? true : false; + } + if (CollectionUtils.isEmpty(another)) { + return false; + } + + List srcSorted = new ArrayList<>(src); + Collections.sort(srcSorted); + List anotherSorted = new ArrayList<>(another); + Collections.sort(anotherSorted); + +// return CollectionUtils.isEqualCollection(); + return ListUtils.isEqualList(srcSorted, anotherSorted); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAllComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAllComparator.java new file mode 100644 index 00000000..1a33f463 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAllComparator.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.CollectionUtils; + +import java.util.Collection; +import java.util.List; + +public class CollectionStringNotContainAllComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return false; + } + if (CollectionUtils.isEmpty(another)) { + return false; + } + + return !CollectionUtils.containsAny(src, another); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAnyComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAnyComparator.java new file mode 100644 index 00000000..c99961bf --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringNotContainAnyComparator.java @@ -0,0 +1,26 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.CollectionUtils; + +import java.util.Collection; +import java.util.List; + +public class CollectionStringNotContainAnyComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return false; + } + if (CollectionUtils.isEmpty(another)) { + return false; + } + + for (String v : another) { + if (!src.contains(v)) { + return true; + } + } + return false; + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringUnEqualComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringUnEqualComparator.java new file mode 100644 index 00000000..173abae3 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CollectionStringUnEqualComparator.java @@ -0,0 +1,24 @@ +package cn.springcloud.gray.decision.compare; + +import org.apache.commons.collections.ListUtils; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class CollectionStringUnEqualComparator implements PredicateComparator> { + + @Override + public boolean test(Collection src, Collection another) { + if (CollectionUtils.isEmpty(src)) { + return CollectionUtils.isEmpty(another) ? false : true; + } + if (CollectionUtils.isEmpty(another)) { + return true; + } + + return !ListUtils.isEqualList(src, another); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/Comparators.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/Comparators.java new file mode 100644 index 00000000..e23d34a0 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/Comparators.java @@ -0,0 +1,24 @@ +package cn.springcloud.gray.decision.compare; + +import java.util.HashMap; +import java.util.Map; + +public class Comparators { + + + private static Map collectionStringComparators = new HashMap<>(); + + static { + collectionStringComparators.put(CompareMode.EQUAL, new CollectionStringEqualComparator()); + collectionStringComparators.put(CompareMode.UNEQUAL, new CollectionStringUnEqualComparator()); + collectionStringComparators.put(CompareMode.CONTAINS_ANY, new CollectionStringContainAnyComparator()); + collectionStringComparators.put(CompareMode.CONTAINS_ALL, new CollectionStringContainAllComparator()); + collectionStringComparators.put(CompareMode.NOT_CONTAINS_ALL, new CollectionStringNotContainAllComparator()); + collectionStringComparators.put(CompareMode.NOT_CONTAINS_ANY, new CollectionStringNotContainAnyComparator()); + } + + + public static PredicateComparator getCollectionStringComparator(CompareMode mode) { + return collectionStringComparators.get(mode); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CompareMode.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CompareMode.java new file mode 100644 index 00000000..701e6127 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/CompareMode.java @@ -0,0 +1,26 @@ +package cn.springcloud.gray.decision.compare; + +public enum CompareMode { + + EQUAL, + + UNEQUAL, + /** + * 全部包含 + */ + CONTAINS_ALL, + + /** + * 全部不包含 + */ + NOT_CONTAINS_ALL, + /** + * 至少包含其中一个 + */ + CONTAINS_ANY, + + /** + * 至少不包含其中一个 + */ + NOT_CONTAINS_ANY +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/PredicateComparator.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/PredicateComparator.java new file mode 100644 index 00000000..cebb0bc9 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/compare/PredicateComparator.java @@ -0,0 +1,6 @@ +package cn.springcloud.gray.decision.compare; + +public interface PredicateComparator { + + boolean test(T src, T another); +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/AbstractGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/AbstractGrayDecisionFactory.java new file mode 100644 index 00000000..1178a162 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/AbstractGrayDecisionFactory.java @@ -0,0 +1,20 @@ +package cn.springcloud.gray.decision.factory; + +import org.springframework.beans.BeanUtils; + +public abstract class AbstractGrayDecisionFactory implements GrayDecisionFactory { + + private Class configClass; + + protected AbstractGrayDecisionFactory(Class configClass) { + this.configClass = configClass; + } + + public Class getConfigClass() { + return configClass; + } + + public C newConfig() { + return BeanUtils.instantiateClass(getConfigClass()); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/CompareGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/CompareGrayDecisionFactory.java new file mode 100644 index 00000000..db00fdb0 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/CompareGrayDecisionFactory.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.compare.CompareMode; +import lombok.Getter; +import lombok.Setter; + +public abstract class CompareGrayDecisionFactory extends AbstractGrayDecisionFactory { + + + protected CompareGrayDecisionFactory(Class configClass) { + super(configClass); + } + + @Setter + @Getter + public static class CompareConfig { + private CompareMode compareMode; + + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/GrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/GrayDecisionFactory.java new file mode 100644 index 00000000..aa5602fb --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/GrayDecisionFactory.java @@ -0,0 +1,31 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.model.DecisionDefinition; +import cn.springcloud.gray.utils.NameUtils; +import org.springframework.beans.BeanUtils; + +import java.util.function.Consumer; + +/** + * 灰度决策的工厂类 + */ +public interface GrayDecisionFactory { + + + default String name() { + return NameUtils.normalizeFilterFactoryName(getClass()); + } + + default C newConfig() { + throw new UnsupportedOperationException("newConfig() not implemented"); + } + + default GrayDecision apply(Consumer consumer) { + C config = newConfig(); + consumer.accept(config); + return apply(config); + } + + GrayDecision apply(C configBean); +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpHeaderGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpHeaderGrayDecisionFactory.java new file mode 100644 index 00000000..930f074a --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpHeaderGrayDecisionFactory.java @@ -0,0 +1,44 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.decision.compare.Comparators; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class HttpHeaderGrayDecisionFactory + extends CompareGrayDecisionFactory { + + public HttpHeaderGrayDecisionFactory() { + super(Config.class); + } + + @Override + public GrayDecision apply(Config configBean) { + return args -> { + GrayHttpRequest grayRequest = (GrayHttpRequest) args.getGrayRequest(); + Map> headers = grayRequest.getHeaders(); + return Comparators.getCollectionStringComparator(configBean.getCompareMode()) + .test(headers.get(configBean.getHeader()), configBean.getValues()); + }; + } + + + @Setter + @Getter + public static class Config extends CompareGrayDecisionFactory.CompareConfig { + private String header; + private List values; + + public void setValues(List values) { + this.values = values; + Collections.sort(values); + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpMethodGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpMethodGrayDecisionFactory.java new file mode 100644 index 00000000..4d49163f --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpMethodGrayDecisionFactory.java @@ -0,0 +1,35 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpMethod; + +import java.util.Objects; + +public class HttpMethodGrayDecisionFactory extends AbstractGrayDecisionFactory { + + + public HttpMethodGrayDecisionFactory() { + super(Config.class); + } + + @Override + public GrayDecision apply(Config configBean) { + return args -> { + GrayHttpRequest grayRequest = (GrayHttpRequest) args.getGrayRequest(); + return StringUtils.endsWithIgnoreCase(grayRequest.getMethod(), configBean.getMethod().name()); + }; + } + + + @Setter + @Getter + public static class Config { + private HttpMethod method; + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpParameterGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpParameterGrayDecisionFactory.java new file mode 100644 index 00000000..4ef7f97a --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/HttpParameterGrayDecisionFactory.java @@ -0,0 +1,43 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.decision.compare.Comparators; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayRequest; +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class HttpParameterGrayDecisionFactory extends CompareGrayDecisionFactory { + + + public HttpParameterGrayDecisionFactory() { + super(Config.class); + } + + @Override + public GrayDecision apply(Config configBean) { + return args -> { + GrayHttpRequest grayRequest = (GrayHttpRequest) args.getGrayRequest(); + return Comparators.getCollectionStringComparator(configBean.getCompareMode()) + .test(grayRequest.getParameters().get(configBean.getName()), configBean.getValues()); + }; + } + + + @Setter + @Getter + public static class Config extends CompareGrayDecisionFactory.CompareConfig { + private String name; + private List values; + + public void setValues(List values) { + this.values = values; + Collections.sort(values); + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/TraceIpGrayDecisionFactory.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/TraceIpGrayDecisionFactory.java new file mode 100644 index 00000000..fbafa347 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/decision/factory/TraceIpGrayDecisionFactory.java @@ -0,0 +1,43 @@ +package cn.springcloud.gray.decision.factory; + +import cn.springcloud.gray.decision.GrayDecision; +import cn.springcloud.gray.request.GrayTrackInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TraceIpGrayDecisionFactory extends AbstractGrayDecisionFactory { + + + public TraceIpGrayDecisionFactory() { + super(Config.class); + } + + @Override + public GrayDecision apply(Config configBean) { + return args -> { + Pattern pat = Pattern.compile(configBean.getIp()); + GrayTrackInfo trackInfo = args.getGrayRequest().getGrayTrackInfo(); + if (trackInfo == null) { + return false; + } + String traceIp = trackInfo.getTraceIp(); + if (StringUtils.isEmpty(traceIp)) { + return false; + } + Matcher mat = pat.matcher(traceIp); + return mat.find(); + }; + } + + @Setter + @Getter + public static class Config { + private String ip; + + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/DefaultGrayEventListener.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/DefaultGrayEventListener.java new file mode 100644 index 00000000..68d06421 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/DefaultGrayEventListener.java @@ -0,0 +1,26 @@ +package cn.springcloud.gray.event; + +import cn.springcloud.gray.CommunicableGrayManager; +import cn.springcloud.gray.exceptions.EventException; +import cn.springcloud.gray.model.GrayInstance; + +public class DefaultGrayEventListener implements GrayEventListener { + + private CommunicableGrayManager grayManager; + + public DefaultGrayEventListener(CommunicableGrayManager grayManager) { + this.grayManager = grayManager; + } + + @Override + public void onEvent(GrayEventMsg msg) throws EventException { + switch (msg.getEventType()) { + case DOWN: + grayManager.closeGray(msg.getServiceId(), msg.getInstanceId()); + case UPDATE: + GrayInstance grayInstance = grayManager.getGrayInformationClient() + .getGrayInstance(msg.getServiceId(), msg.getInstanceId()); + grayManager.updateGrayInstance(grayInstance); + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamInput.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamInput.java new file mode 100755 index 00000000..748b1bd4 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamInput.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.event.stream; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +/** + * + */ +public interface StreamInput { + + String INPUT = "GrayEventInput"; + + @Input(INPUT) + SubscribableChannel input(); + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamMessageListener.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamMessageListener.java new file mode 100755 index 00000000..7e26efc4 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/event/stream/StreamMessageListener.java @@ -0,0 +1,29 @@ +package cn.springcloud.gray.event.stream; + +import cn.springcloud.gray.event.GrayEventListener; +import cn.springcloud.gray.event.GrayEventMsg; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.stream.annotation.StreamListener; + +import java.io.IOException; + + +public class StreamMessageListener { + + private static final Logger log = LoggerFactory.getLogger(StreamMessageListener.class); + + + private GrayEventListener grayEventListener; + + public StreamMessageListener(GrayEventListener grayEventListener) { + this.grayEventListener = grayEventListener; + } + + @StreamListener(StreamInput.INPUT) + public void receive(GrayEventMsg msg) throws IOException { + log.debug("接收到消息:{}", msg); + grayEventListener.onEvent(msg); + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/model/GrayService.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/model/GrayService.java new file mode 100644 index 00000000..791bef39 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/model/GrayService.java @@ -0,0 +1,57 @@ +package cn.springcloud.gray.model; + +import lombok.Getter; +import lombok.Setter; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * 注册的服务, 维护一个实例列表。 + */ +public class GrayService { + @Setter + @Getter + private String serviceId; + private Map grayInstances = new ConcurrentHashMap<>(); + + + public Collection getGrayInstances() { + return grayInstances.values(); + } + + + public boolean contains(String instanceId) { + return grayInstances.containsKey(instanceId); + } + + public void setGrayInstance(GrayInstance grayInstance) { + grayInstances.put(grayInstance.getInstanceId(), grayInstance); + } + + public GrayInstance removeGrayInstance(String instanceId) { + return grayInstances.remove(instanceId); + } + + + public GrayInstance getGrayInstance(String instanceId) { + return grayInstances.get(instanceId); + } + + public boolean isOpenGray() { + return getGrayInstances() != null + && !getGrayInstances().isEmpty() + && hasGrayInstance(); + } + + public boolean hasGrayInstance() { + for (GrayInstance grayInstance : getGrayInstances()) { + if (grayInstance.isGray()) { + return true; + } + } + return false; + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpRequest.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpRequest.java new file mode 100644 index 00000000..11cbf42a --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpRequest.java @@ -0,0 +1,40 @@ +package cn.springcloud.gray.request; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.LinkedMultiValueMap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class GrayHttpRequest extends GrayRequest { + + private Map> headers = new LinkedMultiValueMap<>(); + private String method; + private Map> parameters = new LinkedMultiValueMap<>(); + private byte[] body; + + + public void addHeaders(Map> headers) { + headers.forEach((k, v) -> { + Map> headerMap = (Map>) this.headers; + headerMap.put(k, v); + }); + } + + + public void addParameters(Map> parameters) { + parameters.forEach((k, v) -> { + Map> parameterMap = (Map>) this.parameters; + parameterMap.put(k, v); + }); + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpTrackInfo.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpTrackInfo.java new file mode 100644 index 00000000..a00ce53e --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayHttpTrackInfo.java @@ -0,0 +1,49 @@ +package cn.springcloud.gray.request; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.List; + +@Getter +public class GrayHttpTrackInfo extends GrayTrackInfo { + + public static final String GRAY_TRACK_HEADER_PREFIX = GRAY_TRACK_PREFIX + "header"; + + public static final String GRAY_TRACK_METHOD = GRAY_TRACK_PREFIX + "method"; + + public static final String GRAY_TRACK_PARAMETER_PREFIX = GRAY_TRACK_PREFIX + "param"; + + public static final String GRAY_TRACK_URI = GRAY_TRACK_PREFIX + "uri"; + + private HttpHeaders headers = new HttpHeaders(); + @Setter + private String method; + private MultiValueMap parameters = new LinkedMultiValueMap<>(); + @Setter + private String uri; + + public void addHeader(String name, String value) { + headers.add(name, value); + } + + public void setHeader(String name, List values) { + headers.put(name, values); + } + + + public void addParameter(String name, String value) { + parameters.add(name, value); + } + + public void setParameters(String name, List value) { + parameters.put(name, value); + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayInfoTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayInfoTracker.java new file mode 100644 index 00000000..adae8aec --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayInfoTracker.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.request; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +public interface GrayInfoTracker extends Ordered { + + void call(TRACK trackInfo, REQ request); + + @Override + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayRequest.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayRequest.java new file mode 100644 index 00000000..92233708 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayRequest.java @@ -0,0 +1,36 @@ +package cn.springcloud.gray.request; + + +import lombok.Getter; +import lombok.Setter; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +public class GrayRequest { + + @Setter + @Getter + private String serviceId; + + @Setter + @Getter + private URI uri; + + @Setter + @Getter + private GrayTrackInfo grayTrackInfo; + + + private Map attributes = new HashMap<>(32); + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayTrackInfo.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayTrackInfo.java new file mode 100644 index 00000000..fb0aa3a0 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/GrayTrackInfo.java @@ -0,0 +1,34 @@ +package cn.springcloud.gray.request; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Getter +public class GrayTrackInfo { + + public static final String GRAY_TRACK_SEPARATE = "__"; + + public static final String GRAY_TRACK_PREFIX = "_graytrack_"; + + public static final String GRAY_TRACK_TRACE_IP = GRAY_TRACK_PREFIX + "traceIP"; + + public static final String GRAY_TRACK_ATTRIBUTE_PREFIX = GRAY_TRACK_PREFIX + "attr"; + + @Setter + private String traceIp; + + private Map attributes = new HashMap<>(32); + + public String getAttribute(String name) { + return attributes.get(name); + } + + public void setAttribute(String name, String value) { + attributes.put(name, value); + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/RequestLocalStorage.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/RequestLocalStorage.java new file mode 100644 index 00000000..6a14758b --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/RequestLocalStorage.java @@ -0,0 +1,17 @@ +package cn.springcloud.gray.request; + +public interface RequestLocalStorage { + + + void setGrayTrackInfo(GrayTrackInfo grayTrackInfo); + + void removeGrayTrackInfo(); + + GrayTrackInfo getGrayTrackInfo(); + + void setGrayRequest(GrayRequest grayRequest); + + void removeGrayRequest(); + + GrayRequest getGrayRequest(); +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/ThreadLocalRequestStorage.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/ThreadLocalRequestStorage.java new file mode 100644 index 00000000..a2bdc6fd --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/request/ThreadLocalRequestStorage.java @@ -0,0 +1,36 @@ +package cn.springcloud.gray.request; + +public class ThreadLocalRequestStorage implements RequestLocalStorage { + private ThreadLocal grayRequestThreadLocal = new ThreadLocal<>(); + private ThreadLocal grayTrackInfoThreadLocal = new ThreadLocal<>(); + + @Override + public void setGrayTrackInfo(GrayTrackInfo grayTrackInfo) { + grayTrackInfoThreadLocal.set(grayTrackInfo); + } + + @Override + public void removeGrayTrackInfo() { + grayTrackInfoThreadLocal.remove(); + } + + @Override + public GrayTrackInfo getGrayTrackInfo() { + return grayTrackInfoThreadLocal.get(); + } + + @Override + public void setGrayRequest(GrayRequest grayRequest) { + grayRequestThreadLocal.set(grayRequest); + } + + @Override + public void removeGrayRequest() { + grayRequestThreadLocal.remove(); + } + + @Override + public GrayRequest getGrayRequest() { + return grayRequestThreadLocal.get(); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayDecisionPredicate.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayDecisionPredicate.java deleted file mode 100644 index a0b1e014..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayDecisionPredicate.java +++ /dev/null @@ -1,51 +0,0 @@ -package cn.springcloud.gray.ribbon; - -import cn.springcloud.bamboo.BambooAppContext; -import cn.springcloud.bamboo.BambooRequest; -import cn.springcloud.bamboo.BambooRequestContext; -import cn.springcloud.gray.core.GrayDecision; -import cn.springcloud.gray.utils.ServiceUtil; -import com.netflix.loadbalancer.AbstractServerPredicate; -import com.netflix.loadbalancer.PredicateKey; -import com.netflix.loadbalancer.Server; - -import java.util.List; -import java.util.Map; - -public class GrayDecisionPredicate extends AbstractServerPredicate { - - public GrayDecisionPredicate(GrayLoadBalanceRule rule) { - super(rule); - } - - @Override - public boolean apply(PredicateKey input) { - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); - if (bambooRequestContext == null || bambooRequestContext.getBambooRequest() == null) { - return false; - } - BambooRequest bambooRequest = bambooRequestContext.getBambooRequest(); - Server server = input.getServer(); - String serviceId = bambooRequest.getServiceId(); - Map serverMetadata = getServerMetadata(serviceId, server); - String instanceId = ServiceUtil.getInstanceId(server, serverMetadata); - - List grayDecisions = - getIRule().getGrayManager().grayDecision(serviceId, instanceId); - for (GrayDecision grayDecision : grayDecisions) { - if (grayDecision.test(bambooRequest)) { - return true; - } - } - return false; - } - - - protected GrayLoadBalanceRule getIRule() { - return (GrayLoadBalanceRule) this.rule; - } - - public Map getServerMetadata(String serviceId, Server server) { - return BambooAppContext.getEurekaServerExtractor().getServerMetadata(serviceId, server); - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayLoadBalanceRule.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayLoadBalanceRule.java deleted file mode 100644 index cca1bc77..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/ribbon/GrayLoadBalanceRule.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.springcloud.gray.ribbon; - -import cn.springcloud.bamboo.BambooAppContext; -import cn.springcloud.bamboo.BambooRequestContext; -import cn.springcloud.bamboo.ribbon.loadbalancer.BambooZoneAvoidanceRule; -import cn.springcloud.gray.client.GrayClientAppContext; -import cn.springcloud.gray.client.config.properties.GrayClientProperties; -import cn.springcloud.gray.core.GrayManager; -import cn.springcloud.gray.core.GrayService; -import cn.springcloud.gray.utils.ServiceUtil; -import com.google.common.base.Optional; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 灰度发布的负载规则 - */ -public class GrayLoadBalanceRule extends ZoneAvoidanceRule { - - protected CompositePredicate grayCompositePredicate; - protected ZoneAvoidanceRule subDelegate; - - public GrayLoadBalanceRule() { - super(); - init(); - } - - protected void init(){ - GrayDecisionPredicate apiVersionPredicate = new GrayDecisionPredicate(this); - GrayClientProperties grayClientProperties = GrayClientAppContext.getGrayClientProperties(); - //如果与多版本一起使用 - if(grayClientProperties.getInstance().isUseMultiVersion()){ - subDelegate = new BambooZoneAvoidanceRule(); - grayCompositePredicate = CompositePredicate.withPredicates(subDelegate.getPredicate(), - apiVersionPredicate).build(); - }else{ - grayCompositePredicate = CompositePredicate.withPredicates(super.getPredicate(), - apiVersionPredicate).build(); - } - } - - - @Override - public Server choose(Object key) { - ILoadBalancer lb = getLoadBalancer(); - BambooRequestContext requestContext = BambooRequestContext.currentRequestCentxt(); - String serviceId = requestContext.getServiceId(); - if (requestContext != null && getGrayManager().isOpen(serviceId)) { - GrayService grayService = getGrayManager().grayService(serviceId); - List servers = lb.getAllServers(); - List grayServers = new ArrayList<>(grayService.getGrayInstances().size()); - List normalServers = new ArrayList<>(servers.size() - grayService.getGrayInstances().size()); - for (Server server : servers) { - Map serverMetadata = getServerMetadata(serviceId, server); - String instanceId = ServiceUtil.getInstanceId(server, serverMetadata); - if (grayService.getGrayInstance(instanceId) != null) { - grayServers.add(server); - } else { - normalServers.add(server); - } - } - - Optional server = grayCompositePredicate.chooseRoundRobinAfterFiltering(grayServers, key); - if (server.isPresent()) { - return server.get(); - } else { - return choose(super.getPredicate(), normalServers, key); - } - }else if(subDelegate!=null){ - return subDelegate.choose(key); - }else{ - return super.choose(key); - } - } - - @Override - public void initWithNiwsConfig(IClientConfig clientConfig) { - super.initWithNiwsConfig(clientConfig); - if(subDelegate!=null){ - subDelegate.initWithNiwsConfig(clientConfig); - } - } - - @Override - public void setLoadBalancer(ILoadBalancer lb) { - super.setLoadBalancer(lb); - if(subDelegate!=null){ - subDelegate.setLoadBalancer(lb); - } - } - - private Server choose(AbstractServerPredicate serverPredicate, List servers, Object key) { - Optional server = serverPredicate.chooseRoundRobinAfterFiltering(servers, key); - if (server.isPresent()) { - return server.get(); - } else { - return null; - } - } - - public GrayManager getGrayManager() { - return GrayClientAppContext.getGrayManager(); - } - - public static Map getServerMetadata(String serviceId, Server server) { - return BambooAppContext.getEurekaServerExtractor().getServerMetadata(serviceId, server); - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerExplainer.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerExplainer.java new file mode 100644 index 00000000..e5f6a123 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerExplainer.java @@ -0,0 +1,9 @@ +package cn.springcloud.gray.servernode; + +public interface ServerExplainer { + + + ServerSpec apply(INSTANCE instance); + + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerSpec.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerSpec.java new file mode 100644 index 00000000..6cd90ba8 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/servernode/ServerSpec.java @@ -0,0 +1,32 @@ +package cn.springcloud.gray.servernode; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ServerSpec { + + + private String serviceId; + private String instanceId; + private URI uri; + private Map metadatas = new HashMap<>(); + + + public void setMetadata(String name, Object value) { + metadatas.put(name, value); + } + + public Object getMetadata(String name) { + return metadatas.get(name); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/NameUtils.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/NameUtils.java new file mode 100644 index 00000000..b6f7a318 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/NameUtils.java @@ -0,0 +1,37 @@ +package cn.springcloud.gray.utils; + +import cn.springcloud.gray.decision.factory.GrayDecisionFactory; + +public class NameUtils { + + + private NameUtils() { + throw new AssertionError("Must not instantiate utility class."); + } + + /** + * Generated name prefix. + */ + public static final String GENERATED_NAME_PREFIX = "_genkey_"; + + public static String generateName(int i) { + return GENERATED_NAME_PREFIX + i; + } + + + public static String normalizeFilterFactoryName( + Class clazz) { + return removeGarbage(clazz.getSimpleName() + .replace(GrayDecisionFactory.class.getSimpleName(), "")); + } + + private static String removeGarbage(String s) { + int garbageIdx = s.indexOf("$Mockito"); + if (garbageIdx > 0) { + return s.substring(0, garbageIdx); + } + + return s; + } + +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/ServiceUtil.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/ServiceUtil.java deleted file mode 100644 index 87b783cf..00000000 --- a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/utils/ServiceUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.springcloud.gray.utils; - -import com.netflix.loadbalancer.Server; -import org.springframework.cloud.zookeeper.discovery.ZookeeperServer; - -import java.util.Map; - -/** - * @Author: duozl - * @Date: 2018/6/5 15:56 - */ -public class ServiceUtil { - private static final String METADATA_KEY_INSTANCE_ID = "instanceId"; - - public static String getInstanceId(Server server, Map serverMetadata) { - try { - if (server instanceof ZookeeperServer) { - if (serverMetadata.containsKey(METADATA_KEY_INSTANCE_ID)) { - return serverMetadata.get(METADATA_KEY_INSTANCE_ID); - } else { - throw new IllegalStateException("Unable to find config spring.cloud.zookeeper.discovery.metadata" + - ".instanceId!"); - } - } - } catch (Throwable e) { - // do nothing,可能是类找不到等原因,如果引入了zookeeper的依赖,这个不会找不到 - } - return server.getMetaInfo().getInstanceId(); - } -} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackFilter.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackFilter.java new file mode 100644 index 00000000..931dc1cc --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackFilter.java @@ -0,0 +1,45 @@ +package cn.springcloud.gray.web; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import cn.springcloud.gray.request.RequestLocalStorage; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; + +public class GrayTrackFilter implements Filter { + + private RequestLocalStorage requestLocalStorage; + + private List> trackors; + + + public GrayTrackFilter(RequestLocalStorage requestLocalStorage, List> trackors) { + this.requestLocalStorage = requestLocalStorage; + this.trackors = trackors; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + GrayHttpTrackInfo webTrack = new GrayHttpTrackInfo(); + trackors.forEach(trackor -> trackor.call(webTrack, (HttpServletRequest) request)); + requestLocalStorage.setGrayTrackInfo(webTrack); + try { + chain.doFilter(request, response); + } finally { + requestLocalStorage.removeGrayTrackInfo(); + } + } + + @Override + public void destroy() { + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackRequestInterceptor.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackRequestInterceptor.java new file mode 100644 index 00000000..bf6e81f8 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/GrayTrackRequestInterceptor.java @@ -0,0 +1,106 @@ +package cn.springcloud.gray.web; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.client.config.properties.GrayTrackProperties; +import cn.springcloud.gray.request.GrayHttpRequest; +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayRequest; +import cn.springcloud.gray.request.GrayTrackInfo; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.function.Consumer; + +public class GrayTrackRequestInterceptor implements RequestInterceptor { + + private GrayTrackProperties grayTrackProperties; + private Map> handlers = new HashMap<>(); + + + public GrayTrackRequestInterceptor(GrayTrackProperties grayTrackProperties) { + this.grayTrackProperties = grayTrackProperties; + initHandlers(); + } + + @Override + public String interceptroType() { + return "all"; + } + + @Override + public boolean shouldIntercept() { + return true; + } + + @Override + public boolean pre(GrayRequest request) { + grayTrackProperties.getWeb().getNeed().keySet().forEach(k -> { + Optional.ofNullable(handlers.get(k)).ifPresent(h -> h.accept((GrayHttpRequest) request)); + }); + return true; + } + + @Override + public boolean after(GrayRequest request) { + return true; + } + + @Override + public int getOrder() { + return 100; + } + + private void initHandlers() { + handlers.put(GrayTrackProperties.Web.NEED_IP, request -> { + GrayHttpTrackInfo grayHttpTrackInfo = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (StringUtils.isNotEmpty(grayHttpTrackInfo.getTraceIp())) { + Map> h = (Map>) request.getHeaders(); + h.put(GrayHttpTrackInfo.GRAY_TRACK_TRACE_IP, Arrays.asList(grayHttpTrackInfo.getTraceIp())); + } + }); + + handlers.put(GrayTrackProperties.Web.NEED_URI, request -> { + GrayHttpTrackInfo grayHttpTrackInfo = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (StringUtils.isNotEmpty(grayHttpTrackInfo.getUri())) { + Map> h = (Map>) request.getHeaders(); + h.put(GrayHttpTrackInfo.GRAY_TRACK_URI, Arrays.asList(grayHttpTrackInfo.getUri())); + } + }); + + + handlers.put(GrayTrackProperties.Web.NEED_METHOD, request -> { + GrayHttpTrackInfo grayHttpTrackInfo = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (StringUtils.isNotEmpty(grayHttpTrackInfo.getUri())) { + Map> h = (Map>) request.getHeaders(); + h.put(GrayHttpTrackInfo.GRAY_TRACK_METHOD, Arrays.asList(grayHttpTrackInfo.getMethod())); + } + }); + + handlers.put(GrayTrackProperties.Web.NEED_HEADERS, request -> { + GrayHttpTrackInfo grayHttpTrackInfo = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (StringUtils.isNotEmpty(grayHttpTrackInfo.getUri())) { + Map> h = (Map>) request.getHeaders(); + grayHttpTrackInfo.getHeaders().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + h.put(name, entry.getValue()); + }); + } + }); + + handlers.put(GrayTrackProperties.Web.NEED_PARAMETERS, request -> { + GrayHttpTrackInfo grayHttpTrackInfo = (GrayHttpTrackInfo) request.getGrayTrackInfo(); + if (StringUtils.isNotEmpty(grayHttpTrackInfo.getUri())) { + Map> h = (Map>) request.getHeaders(); + grayHttpTrackInfo.getParameters().entrySet().forEach(entry -> { + String name = new StringBuffer().append(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX) + .append(GrayTrackInfo.GRAY_TRACK_SEPARATE) + .append(entry.getKey()).toString(); + h.put(name, entry.getValue()); + }); + } + }); + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/interceptor/GrayTrackInterceptor.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/interceptor/GrayTrackInterceptor.java new file mode 100644 index 00000000..4ecef8df --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/interceptor/GrayTrackInterceptor.java @@ -0,0 +1,44 @@ +package cn.springcloud.gray.web.interceptor; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import cn.springcloud.gray.request.GrayTrackInfo; +import cn.springcloud.gray.request.RequestLocalStorage; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @see {@link cn.springcloud.gray.web.GrayTrackFilter} + */ +@Deprecated +public class GrayTrackInterceptor extends HandlerInterceptorAdapter { + + + private RequestLocalStorage requestLocalStorage; + + private List> trackors; + + + public GrayTrackInterceptor(RequestLocalStorage requestLocalStorage, List> trackors) { + this.trackors = trackors; + this.requestLocalStorage = requestLocalStorage; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + GrayHttpTrackInfo webTrack = new GrayHttpTrackInfo(); + trackors.forEach(trackor -> trackor.call(webTrack, request)); + requestLocalStorage.setGrayTrackInfo(webTrack); + return super.preHandle(request, response, handler); + } + + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + requestLocalStorage.removeGrayTrackInfo(); + super.afterCompletion(request, response, handler, ex); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpGrayTracker.java new file mode 100644 index 00000000..662cfd6e --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpGrayTracker.java @@ -0,0 +1,9 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; + +import javax.servlet.http.HttpServletRequest; + +public interface HttpGrayTracker extends GrayInfoTracker { +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpHeaderGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpHeaderGrayTracker.java new file mode 100644 index 00000000..6e6c1a9a --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpHeaderGrayTracker.java @@ -0,0 +1,30 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +@Slf4j +public class HttpHeaderGrayTracker implements HttpGrayTracker { + + private String[] headers; + + public HttpHeaderGrayTracker(String[] headers) { + this.headers = headers; + } + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + for (String header : headers) { + Enumeration headerValues = request.getHeaders(header); + while (headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + trackInfo.addHeader(header, value); + log.debug("记录下header:{} -> {}", header, value); + } + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpIPGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpIPGrayTracker.java new file mode 100644 index 00000000..52405b53 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpIPGrayTracker.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import cn.springcloud.gray.utils.WebUtils; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.util.Enumeration; + +@Slf4j +public class HttpIPGrayTracker implements HttpGrayTracker { + + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + trackInfo.setTraceIp(WebUtils.getIpAddr(request)); + log.debug("记录下ip:{}", trackInfo.getTraceIp()); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpMethodGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpMethodGrayTracker.java new file mode 100644 index 00000000..f79e129a --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpMethodGrayTracker.java @@ -0,0 +1,20 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.http.HttpMethod; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + +@Slf4j +public class HttpMethodGrayTracker implements HttpGrayTracker { + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + trackInfo.setMethod(request.getMethod()); + log.debug("记录下方法:{}", request.getMethod()); + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpParameterGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpParameterGrayTracker.java new file mode 100644 index 00000000..fe944ac4 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpParameterGrayTracker.java @@ -0,0 +1,31 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayInfoTracker; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Enumeration; + +@Slf4j +public class HttpParameterGrayTracker implements HttpGrayTracker { + + private String[] names; + + public HttpParameterGrayTracker(String[] names) { + this.names = names; + } + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + for (String name : names) { + String[] values = request.getParameterValues(name); + if (ArrayUtils.isNotEmpty(values)) { + trackInfo.setParameters(name, Arrays.asList(values)); + log.debug("记录下parameter:{} -> {}", name, values); + } + } + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpReceiveGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpReceiveGrayTracker.java new file mode 100644 index 00000000..e3cfb1a5 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpReceiveGrayTracker.java @@ -0,0 +1,113 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import cn.springcloud.gray.request.GrayTrackInfo; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.function.Consumer; + +@Slf4j +public class HttpReceiveGrayTracker implements HttpGrayTracker { + + + private Map> loaders = new HashMap<>(); + + + public HttpReceiveGrayTracker() { + init(); + } + + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + Enumeration headerNames = request.getHeaderNames(); + if (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + if (headerName.startsWith(GrayTrackInfo.GRAY_TRACK_PREFIX)) { + String[] names = headerName.split(GrayTrackInfo.GRAY_TRACK_SEPARATE); + loadGrayTrackInfo(new LoadSpec(headerName, names, trackInfo, request)); + } + } + } + + + @Override + public int getOrder() { + return 100; + } + + private void loadGrayTrackInfo(LoadSpec loadSpec) { + String[] names = loadSpec.getNames(); + Optional.ofNullable(loaders.get(names[0])).ifPresent(loader -> { + loader.accept(loadSpec); + }); + } + + private void init() { + loaders.put(GrayTrackInfo.GRAY_TRACK_TRACE_IP, loadSpec -> { + String value = loadSpec.getHeaderValue(); + loadSpec.getTrackInfo().setTraceIp(value); + log.debug("接收到{} --> {}", GrayTrackInfo.GRAY_TRACK_TRACE_IP, value); + }); + + loaders.put(GrayTrackInfo.GRAY_TRACK_ATTRIBUTE_PREFIX, loadSpec -> { + String value = loadSpec.getHeaderValue(); + loadSpec.getTrackInfo().setAttribute(loadSpec.getNames()[1], loadSpec.getHeaderValue()); + log.debug("接收到{} --> {}", loadSpec.getHeaderName(), value); + }); + + loaders.put(GrayHttpTrackInfo.GRAY_TRACK_HEADER_PREFIX, loadSpec -> { + List value = loadSpec.getHeaderValues(); + loadSpec.getTrackInfo().setHeader(loadSpec.getNames()[1], value); + log.debug("接收到{} --> {}", loadSpec.getHeaderName(), value); + }); + + loaders.put(GrayHttpTrackInfo.GRAY_TRACK_METHOD, loadSpec -> { + String value = loadSpec.getHeaderValue(); + loadSpec.getTrackInfo().setMethod(value); + log.debug("接收到{} --> {}", loadSpec.getHeaderName(), value); + }); + + loaders.put(GrayHttpTrackInfo.GRAY_TRACK_URI, loadSpec -> { + String value = loadSpec.getHeaderValue(); + loadSpec.getTrackInfo().setUri(value); + log.debug("接收到{} --> {}", loadSpec.getHeaderName(), value); + }); + + loaders.put(GrayHttpTrackInfo.GRAY_TRACK_PARAMETER_PREFIX, loadSpec -> { + List value = loadSpec.getHeaderValues(); + loadSpec.getTrackInfo().setParameters(loadSpec.getNames()[1], value); + log.debug("接收到{} --> {}", loadSpec.getHeaderName(), value); + }); + } + + + @Getter + @AllArgsConstructor + private static class LoadSpec { + private String headerName; + private String[] names; + private GrayHttpTrackInfo trackInfo; + private HttpServletRequest request; + + + public String getHeaderValue() { + return request.getHeader(headerName); + } + + public List getHeaderValues() { + Enumeration ve = request.getHeaders(headerName); + List values = new ArrayList<>(); + while (ve.hasMoreElements()) { + values.add(ve.nextElement()); + } + return values; + } + + + } +} diff --git a/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpURIGrayTracker.java b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpURIGrayTracker.java new file mode 100644 index 00000000..50b3f545 --- /dev/null +++ b/spring-cloud-gray-client/src/main/java/cn/springcloud/gray/web/tracker/HttpURIGrayTracker.java @@ -0,0 +1,17 @@ +package cn.springcloud.gray.web.tracker; + +import cn.springcloud.gray.request.GrayHttpTrackInfo; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; + +@Slf4j +public class HttpURIGrayTracker implements HttpGrayTracker { + + + @Override + public void call(GrayHttpTrackInfo trackInfo, HttpServletRequest request) { + trackInfo.setUri(request.getRequestURI()); + log.debug("记录下uri:{}", trackInfo.getUri()); + } +} diff --git a/spring-cloud-gray-core/README.md b/spring-cloud-gray-core/README.md index 63072869..e69de29b 100644 --- a/spring-cloud-gray-core/README.md +++ b/spring-cloud-gray-core/README.md @@ -1,280 +0,0 @@ -## 灰度发布 -灰度发布是在多版本控制的基础上进一步扩展实现出来的项目 -> fm-cloud-graybunny,抽象出灰度服务、灰度服务实例、灰度策略、灰度决策等。支持A/B test, 金丝雀 test。 灰度策略可以从request ip, request patameter, request header等方面进行去创建,也可以根据bamboo的LoadBalanceRequestTrigger结合graybuanny的接口去扩展灰度策略和灰度决策。 - -##### 场景 - -有两个服务,共四个服务实例,分别是ServiceA-1, ServiceA-2, ServiceB-1。其中ServiceA-2是灰度实例。 -场景1:所有请求头usertype:old,ip:10.217.***.***的请求或者请求头usertype:test, url 参数action:create的请求,都会被转发到的灰度服务ServiceA-2 。 -场景2:ServiceA-2通过一段时间的观察,判定运行稳定,开始ServiceA-2删除灰度标记,开始和ServiceA-1一样会加入正常的负载均衡规则当中。 -场景3:服务ServiceB发布新版本,ServiceB-2需要灰度注册,注册成功后所有的请求不能转发到ServiceB-2, 在为ServiceB-2设置灰度策略后,符合策略的请求才会被转发到ServiceB-2上。 - - -##### 思路 - -从上面的场景分析,可以归纳出两个对象:服务实例和调用请求;服务实例的灰度管理是基础,调用请求时如何决策路由,都是根据服务实例的灰度策略去判断的。既然有灰度管理这个概念,那么从功能上分,就会有client-server之分,所以又可以从gray-client和gray-server去分析。接下来将一步一步去分析这四个方面。 - -* 灰度实例 - -![灰度实例](../doc/img/u-case-service-instance.png) - -实例注册:服务实例添加到灰度管理中。 -实例下线:服务实例下线,从灰度管理中删除。 -灰度开关:调整服务实例的灰度状态,有启用、禁用两个状态,禁用的实例不纳入灰度列表中。 -灰度策略:请求是否可以被转发到该服务实例的条件,只有通过,请求才有可能会被转发到该实例上。 - -* 调用请求 - -![调用请求](../doc/img/u-case-call-request.png) - -灰度决策:根据请求的信息去匹配灰度服务实例的灰度策略,如果匹配上,会将服务实例加入到通过列表中。如果都没有匹配上,就按bamboo的路由规则去筛选非灰度的服务实例进行转发。 - - -* 灰度客户端 - -调用请求的服务消费者,和提供服务的服务提供者都可以是灰度客户端,因为微服务中,大多服务实例既是服务提供者,同时也是服务消费者。 - -![灰度客户端](../doc/img/u-case-gray-client.png) - -灰度服务注册:服务实例在启动时,就会向灰度服务端发起请求,将实例自身的灰度开关打开。 -灰度服务下线:在服务实例下线前,会触发钩子,向灰度服务端发起请求将实例自身从灰度列表中删除。 -接收灰度实例调整消息:接收由灰度服务端推送过来的灰度列表更新消息比如新增灰度实例,删除灰度实例等,维护缓存在实例上的灰度列表。 -定时拉取灰度列表:定时从灰度服务端拉取最新的灰度列表,维护实例自身缓存的灰度列表。 - -* 灰度服务端 - -灰度服务端负表维护灰度列表,可以新增、删除、编辑灰度信息。 - -![灰度服务端](../doc/img/u-case-gray-server.png) - -编辑灰度实例:新增灰度实例,删除灰度实例,修改实例灰度状态。 -编辑灰度策略:新增实例灰度策略,删除实例灰度策略,修改灰度策略状态。 -推送灰度服务调整消息:向灰度客户端推送灰度列表变动消息,比如新增灰度实例,删除灰度实例,修改实例灰度状态等。 -定时检查服务实例是否下线:定时检查灰度服务实例是否下线,下线的的实例将从灰度列表中删除。 - - - -##### 代码设计 -根据上面的思路,设计以下对象和接口。共6个接口,4个模型对象。 - -![灰度代码设计](../doc/img/cd-gray.png) - -对象: -* GrayService: 灰度服务 -* GrayInstance: 灰度实例,有状态属性 -* GrayPolicyGroup: 灰度策略组,有状态属性 -* GrayPolicy: 灰度策略 - -接口: -* GrayManager: -灰度客户端管理器,维护灰度列表,维护自身灰度状态,创建灰度决策对象。抽象实现类AbstractGrayManager实现了基础的获取灰度列表, 创建灰度决策对象的能力。BaseGrayManger在期基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。 - -* InformationClient: -该接口主要是负责和灰度服务端进行通信,获取灰度列表,编辑灰度实例等能力。其实现类HttpInformationClient默认使用http方式访问灰度服务端。 -子类InformationClientDecorator是一个适配器类,RetryableInformationClient继承了InformationClientDecorator类,实现了重试的功能。 - -* GrayDecision: -该接口是灰度决策,用来判断请求是否匹配灰度策略。实现了ip匹配、request parameter匹配、request header匹配、BambooRequestContext中的参数匹配器以及合并匹配等多个匹配能力。 - -* GrayDecisionFactory: -灰度决策的工厂类,其默认实现类支持上述几种灰度决策的创建。 - -* GrayServiceManager: -灰度服务管理类,属于服务端的类。主要是编辑服务实例,编辑灰度策略,以及维护最新的灰度列表。 - -* GrayBunnyServerEvictor: -如果灰度服务实例下线后, 由于意外情况,没有向灰度服务端发送删除请求, 服务端会每隔一段时间调用该接口的方法,检查灰度列表中的实例是否下线,如果实例已下线,就将其从灰度列表中删除。 - - -##### 代码实现 -将模型抽象成接口和对象设计出来之后,实现思路就清晰了。 - -* 灰度路由: -灰度路由是客户端必须要实现的能力,gray是在bamboo的基础上扩展的,所以gray的路由规则对象GrayLoadBalanceRule继承了BambooZoneAvoidanceRule, -逻辑是这样的: -1、 判断目标服务是否有灰度实例。 -2.1、 如果没有, 执行父类逻辑。结束。 -2.2、 有灰度实例,先将灰度实例和非灰度实例筛选出来。 -3、 挑选灰度实例, 筛选调用请求匹配上灰度实例的策略。 -4.1、 如果没有匹配的灰度实例, 将非灰度实例列表传递过去执行父类的筛选逻辑。结束。 -4.2、 如果有匹配的灰度实例, 从其中按轮询的方式挑选出一个实例。结束。 -```java -/** - * 灰度发布的负载规则 - */ -public class GrayLoadBalanceRule extends BambooZoneAvoidanceRule { - - protected CompositePredicate grayCompositePredicate; - - public GrayLoadBalanceRule() { - super(); - GrayDecisionPredicate apiVersionPredicate = new GrayDecisionPredicate(this); - grayCompositePredicate = CompositePredicate.withPredicates(super.getPredicate(), - apiVersionPredicate).build(); - } - - - @Override - public Server choose(Object key) { - ILoadBalancer lb = getLoadBalancer(); - BambooRequestContext requestContext = BambooRequestContext.currentRequestCentxt(); - if (requestContext != null && getGrayManager().isOpen(requestContext.getServiceId())) { - GrayService grayService = getGrayManager().grayService(requestContext.getServiceId()); - List servers = lb.getAllServers(); - List grayServers = new ArrayList<>(grayService.getGrayInstances().size()); - List normalServers = new ArrayList<>(servers.size() - grayService.getGrayInstances().size()); - for (Server server : servers) { - DiscoveryEnabledServer disServer = (DiscoveryEnabledServer) server; - if (grayService.getGrayInstance(disServer.getInstanceInfo().getInstanceId()) != null) { - grayServers.add(server); - } else { - normalServers.add(server); - } - } - - Optional server = grayCompositePredicate.chooseRoundRobinAfterFiltering(grayServers, key); - if (server.isPresent()) { - return server.get(); - } else { - return choose(super.getPredicate(), normalServers, key); - } - } - return super.choose(key); - } - - - private Server choose(AbstractServerPredicate serverPredicate, List servers, Object key) { - Optional server = serverPredicate.chooseRoundRobinAfterFiltering(servers, key); - if (server.isPresent()) { - return server.get(); - } else { - return null; - } - } - - - public GrayManager getGrayManager() { - return GrayClientAppContext.getGrayManager(); - } -} - -``` - -灰度决策的执行代码在GrayDecisionPredicate中 - -```java -public class GrayDecisionPredicate extends AbstractServerPredicate { - - public GrayDecisionPredicate(GrayLoadBalanceRule rule) { - super(rule); - } - - @Override - public boolean apply(PredicateKey input) { - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); - if (bambooRequestContext == null || bambooRequestContext.getBambooRequest() == null) { - return false; - } - DiscoveryEnabledServer server = (DiscoveryEnabledServer) input.getServer(); - BambooRequest bambooRequest = bambooRequestContext.getBambooRequest(); - List grayDecisions = - getIRule().getGrayManager().grayDecision(bambooRequest.getServiceId(), server.getInstanceInfo().getInstanceId()); - for (GrayDecision grayDecision : grayDecisions) { - if (grayDecision.test(bambooRequest)) { - return true; - } - } - return false; - } - - - protected GrayLoadBalanceRule getIRule() { - return (GrayLoadBalanceRule) this.rule; - } -} -``` - -* 灰度管理: -灰度管理是灰度服务端的功能,主要是维护灰度列表。其实现类DefaultGrayServiceManager有一个Map, 用来维护GrayService,key是service id。并且每隔一段时间就调用EurekaGrayServerEvictor,检查列表中的实例是否下线,将下线的服务从灰度列表中删除。 - -```java -public class DefaultGrayServiceManager implements GrayServiceManager { - - - private Map grayServiceMap = new ConcurrentHashMap<>(); - private Timer evictionTimer = new Timer("Gray-EvictionTimer", true); - - //... - - @Override - public void openForWork() { - evictionTimer.schedule(new EvictionTask(), - serverConfig.getEvictionIntervalTimerInMs(), - serverConfig.getEvictionIntervalTimerInMs()); - } - - @Override - public void shutdown() { - evictionTimer.cancel(); - } - - - protected void evict() { - GrayServerContext.getGrayServerEvictor().evict(this); - } - - - class EvictionTask extends TimerTask { - - @Override - public void run() { - evict(); - } - } - -} - -``` - -EurekaGrayServerEvictor是依赖EurekaClient来检查服务实例是否下线。 - -```java -public class EurekaGrayServerEvictor implements GrayServerEvictor { - - private EurekaClient eurekaClient; - - - public EurekaGrayServerEvictor(EurekaClient eurekaClient) { - this.eurekaClient = eurekaClient; - } - - @Override - public void evict(GrayServiceManager serviceManager) { - Collection grayServices = serviceManager.allGrayService(); - grayServices.forEach(grayService -> { - grayService.getGrayInstances().forEach(grayInstance -> { - evict(serviceManager, grayInstance); - }); - }); - - } - - - private void evict(GrayServiceManager serviceManager, GrayInstance grayInstance) { - if (isDownline(grayInstance)) { - serviceManager.deleteGrayInstance(grayInstance.getServiceId(), grayInstance.getInstanceId()); - } - } - - - private boolean isDownline(GrayInstance grayInstance) { - Application app = eurekaClient.getApplication(grayInstance.getServiceId()); - return app == null || app.getByInstanceId(grayInstance.getInstanceId()) == null; - } - -} -``` - - - -##### 使用说明 -灰度发布 --> [spring-cloud-gray-samples](../spring-cloud-gray-samples/README.md) \ No newline at end of file diff --git a/spring-cloud-gray-core/pom.xml b/spring-cloud-gray-core/pom.xml index ceee4fc9..fe6e7846 100644 --- a/spring-cloud-gray-core/pom.xml +++ b/spring-cloud-gray-core/pom.xml @@ -5,19 +5,43 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 spring-cloud-gray-core - - cn.springcloud.gray - spring-cloud-bamboo + org.projectlombok + lombok + + spring-boot:run + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + \ No newline at end of file diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecisionFactory.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecisionFactory.java deleted file mode 100644 index bdacff92..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayDecisionFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.springcloud.gray.core; - -/** - * 灰度决策的工厂类 - */ -public interface GrayDecisionFactory { - - - GrayDecision getDecision(GrayPolicy grayPolicy); -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayInstance.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayInstance.java deleted file mode 100644 index 99152ee5..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayInstance.java +++ /dev/null @@ -1,136 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - - -/** - * 灰度实例,有状态属性 - */ -public class GrayInstance { - private String serviceId; - private String instanceId; - - /** - * 类度策略组 - */ - private List grayPolicyGroups = new ArrayList<>(); - private boolean openGray = true; - - - public String getServiceId() { - return serviceId; - } - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public String getInstanceId() { - return instanceId; - } - - public void setInstanceId(String instanceId) { - this.instanceId = instanceId; - } - - public List getGrayPolicyGroups() { - return grayPolicyGroups; - } - - public void setGrayPolicyGroups(List grayPolicyGroups) { - this.grayPolicyGroups = grayPolicyGroups; - } - - public void addGrayPolicyGroup(GrayPolicyGroup policyGroup) { - if (getGrayPolicyGroup(policyGroup.getPolicyGroupId()) == null) { - this.grayPolicyGroups.add(policyGroup); - } - } - - public GrayPolicyGroup removeGrayPolicyGroup(String policyGroupId) { - Iterator iter = grayPolicyGroups.iterator(); - while (iter.hasNext()) { - GrayPolicyGroup policyGroup = iter.next(); - if (policyGroup.getPolicyGroupId().equals(policyGroupId)) { - iter.remove(); - return policyGroup; - } - } - return null; - } - - public void addGrayPolicy(String policyGroupId, GrayPolicy policy) { - GrayPolicyGroup policyGroup = getGrayPolicyGroup(policyGroupId); - if (policyGroup != null) { - policyGroup.addGrayPolicy(policy); - } - } - - public void removeGrayPolicy(String policyGroupId, String policyId) { - GrayPolicyGroup policyGroup = getGrayPolicyGroup(policyGroupId); - if (policyGroup != null) { - policyGroup.removeGrayPolicy(policyId); - } - } - - public boolean containsPolicyGroup(String policyGroupId) { - return getGrayPolicyGroup(policyGroupId) != null; - } - - public GrayPolicyGroup getGrayPolicyGroup(String policyGroupId) { - for (GrayPolicyGroup grayPolicyGroup : grayPolicyGroups) { - if (grayPolicyGroup.getPolicyGroupId().equals(policyGroupId)) { - return grayPolicyGroup; - } - } - return null; - } - - - public boolean isOpenGray() { - return openGray; - } - - public void setOpenGray(boolean openGray) { - this.openGray = openGray; - } - - public boolean hasGrayPolicy() { - for (GrayPolicyGroup policyGroup : getGrayPolicyGroups()) { - if (policyGroup.getList() != null || policyGroup.getList().size() > 0) { - return true; - } - } - return false; - } - - - public int countGrayPolicy() { - int count = 0; - for (GrayPolicyGroup policyGroup : getGrayPolicyGroups()) { - count += policyGroup.getList().size(); - } - return count; - } - - public GrayInstance toNewGrayInstance() { - GrayInstance newInstance = new GrayInstance(); - newInstance.setInstanceId(instanceId); - newInstance.setServiceId(serviceId); - newInstance.setOpenGray(openGray); - return newInstance; - } - - - public GrayInstance takeNewOpenGrayInstance() { - GrayInstance instance = toNewGrayInstance(); - for (GrayPolicyGroup grayPolicyGroup : grayPolicyGroups) { - if (grayPolicyGroup.isEnable()) { - instance.addGrayPolicyGroup(grayPolicyGroup); - } - } - return instance; - } -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayManager.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayManager.java deleted file mode 100644 index 255a90e3..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayManager.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.Collection; -import java.util.List; - - -/** - * 灰度客户端管理器,维护灰度列表,维护自身灰度状态,创建灰度决策对象。 - * 抽象实现类AbstractGrayManager实现了基础的获取灰度列表, 创建灰度决策对象的能力。 - * BaseGrayManger在其基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。 - */ -public interface GrayManager { - - void openForWork(); - - boolean isOpen(String serviceId); - - List listGrayService(); - - GrayService grayService(String serviceId); - - GrayInstance grayInstance(String serviceId, String instanceId); - - List grayDecision(GrayInstance instance); - - List grayDecision(String serviceId, String instanceId); - - void updateGrayServices(Collection grayServices); - - void serviceDownline(); -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicy.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicy.java deleted file mode 100644 index 615a6778..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicy.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.HashMap; -import java.util.Map; - -/** - * 灰度策略 - */ -public class GrayPolicy { - - private String policyId; - private String policyType; - private Map infos = new HashMap<>(); - - - public String getPolicyId() { - return policyId; - } - - public void setPolicyId(String policyId) { - this.policyId = policyId; - } - - public String getPolicyType() { - return policyType; - } - - public void setPolicyType(String policyType) { - this.policyType = policyType; - } - - public void addInfo(String key, String value) { - infos.put(key, value); - } - - - public Map getInfos() { - return infos; - } - - public void setInfos(Map infos) { - this.infos = new HashMap<>(infos); - } -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicyGroup.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicyGroup.java deleted file mode 100644 index 2963aed7..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayPolicyGroup.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - - -/** - * 灰度策略组,有状态属性 - */ -public class GrayPolicyGroup { - - private String policyGroupId; - private String alias; - private List list = new ArrayList<>(); - private boolean enable = true; - - public String getPolicyGroupId() { - return policyGroupId; - } - - public void setPolicyGroupId(String policyGroupId) { - this.policyGroupId = policyGroupId; - } - - public void addGrayPolicy(GrayPolicy policy) { - removeGrayPolicy(policy.getPolicyId()); - list.add(policy); - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public List getList() { - return list; - } - - public void setList(List list) { - this.list = list; - } - - public GrayPolicy removeGrayPolicy(String policyId) { - Iterator iter = list.iterator(); - while (iter.hasNext()) { - GrayPolicy policy = iter.next(); - if (policy.getPolicyId().equals(policyId)) { - iter.remove(); - return policy; - } - } - return null; - } - - public GrayPolicy getGrayPolicy(String policyId) { - for (GrayPolicy grayPolicy : list) { - if (grayPolicy.getPolicyId().equals(policyId)) { - return grayPolicy; - } - } - return null; - } - - public boolean isEnable() { - return enable; - } - - public void setEnable(boolean enable) { - this.enable = enable; - } -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayService.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayService.java deleted file mode 100644 index f29b12ee..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayService.java +++ /dev/null @@ -1,124 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - - -/** - * 注册的服务, 维护一个实例列表。 - */ -public class GrayService { - private String serviceId; - private List grayInstances = new ArrayList<>(); - - - public GrayService() { - - } - - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public String getServiceId() { - return serviceId; - } - - public List getGrayInstances() { - return grayInstances; - } - - public void setGrayInstances(List grayInstances) { - this.grayInstances = grayInstances; - } - - - public boolean contains(String instanceId) { - for (GrayInstance grayInstance : grayInstances) { - if (grayInstance.getInstanceId().equals(instanceId)) { - return true; - } - } - return false; - } - - public void addGrayInstance(GrayInstance grayInstance) { - grayInstances.add(grayInstance); - } - - public GrayInstance removeGrayInstance(String instanceId) { - Iterator iter = grayInstances.iterator(); - while (iter.hasNext()) { - GrayInstance instance = iter.next(); - if (instance.getInstanceId().equals(instanceId)) { - iter.remove(); - return instance; - } - } - return null; - } - - - public GrayInstance getGrayInstance(String instanceId) { - Iterator iter = grayInstances.iterator(); - while (iter.hasNext()) { - GrayInstance instance = iter.next(); - if (instance.getInstanceId().equals(instanceId)) { - return instance; - } - } - return null; - } - - public int countGrayPolicy() { - int count = 0; - for (GrayInstance grayInstance : grayInstances) { - count += grayInstance.countGrayPolicy(); - } - return count; - } - - public boolean isOpenGray() { - return getGrayInstances() != null - && !getGrayInstances().isEmpty() - && hasGrayInstance(); - } - - public boolean hasGrayInstance() { - for (GrayInstance grayInstance : getGrayInstances()) { - if (grayInstance.isOpenGray()) { - return true; - } - } - return false; - } - - public boolean hasGrayPolicy() { - for (GrayInstance grayInstance : getGrayInstances()) { - if (grayInstance.hasGrayPolicy()) { - return true; - } - } - return false; - } - - - public GrayService toNewGrayService() { - GrayService newService = new GrayService(); - newService.setServiceId(serviceId); - return newService; - } - - - public GrayService takeNewOpenGrayService() { - GrayService service = toNewGrayService(); - for (GrayInstance grayInstance : grayInstances) { - if (grayInstance.isOpenGray()) { - service.addGrayInstance(grayInstance.takeNewOpenGrayInstance()); - } - } - return service; - } -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayServiceManager.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayServiceManager.java deleted file mode 100644 index dc168d65..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/GrayServiceManager.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.Collection; - - -/** - * 灰度服务管理类,属于服务端的类。主要是编辑服务实例,编辑灰度策略,以及维护最新的灰度列表。 - */ -public interface GrayServiceManager { - - void addGrayInstance(GrayInstance instance); - - void deleteGrayInstance(String serviceId, String instanceId); - - void addGrayPolicy(String serviceId, String instanceId, String policyGroupId, GrayPolicy policy); - - void deleteGrayPolicy(String serviceId, String instanceId, String policyGroupId, String policyId); - - void addGrayPolicyGroup(String serviceId, String instanceId, GrayPolicyGroup policyGroup); - - void deleteGrayPolicyGroup(String serviceId, String instanceId, String policyGroupId); - - Collection allGrayService(); - - GrayService getGrayService(String serviceId); - - GrayInstance getGrayInstane(String serviceId, String instanceId); - - - /** - * 更新实例实例灰度状态 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param status 0:关闭, 1:启用 - * @return boolean - */ - boolean updateInstanceStatus(String serviceId, String instanceId, int status); - - - /** - * 更新实例策略组启用状态 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param groupId 灰度策略组id - * @param enable 0:关闭, 1:启用 - */ - boolean updatePolicyGroupStatus(String serviceId, String instanceId, String groupId, int enable); - - /** - * 打开检查 - */ - void openForWork(); - - - void shutdown(); -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClient.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClient.java deleted file mode 100644 index f77eb88c..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClient.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.List; - - -/** - * 该接口主要是负责和灰度服务端进行通信,获取灰度列表,编辑灰度实例等能力。其实现类HttpInformationClient默认使用http方式访问灰度服务端。 - * 子类InformationClientDecorator是一个适配器类,RetryableInformationClient继承了InformationClientDecorator类,实现了重试的功能。 - */ -public interface InformationClient { - - - /** - * 返回在灰度中注册的所有实例(包括非灰度实例) - * - * @return 类度服务列表 - */ - List listGrayService(); - - /** - * 根据serviceId返回灰度服务对象 - * - * @param serviceId 服务id - * @return 灰度服务 - */ - GrayService grayService(String serviceId); - - - /** - * 返回注册的实例对象 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @return 灰度实例 - */ - GrayInstance grayInstance(String serviceId, String instanceId); - - - /** - * 注册灰度实例 - * - * @param serviceId 服务id - * @param instanceId 实例id - */ - void addGrayInstance(String serviceId, String instanceId); - - - /** - * 灰度实例下线 - */ - void serviceDownline(); - - - /** - * 灰度实例下线 - * - * @param serviceId 服务id - * @param instanceId 实例id - */ - void serviceDownline(String serviceId, String instanceId); - -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClientDecorator.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClientDecorator.java deleted file mode 100644 index 30b2c3b5..00000000 --- a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/core/InformationClientDecorator.java +++ /dev/null @@ -1,126 +0,0 @@ -package cn.springcloud.gray.core; - -import java.util.List; - - -/** - * InformationClientDecorator是一个适配器类 - */ -public abstract class InformationClientDecorator implements InformationClient { - - - public enum RequestType { - AddGrayInstance, - ServiceDownline, - GetGrayInstance, - GetGrayService, - ListGrayServices - } - - - public interface RequestExecutor { - R execute(InformationClient delegate); - - RequestType getRequestType(); - } - - - protected abstract R execute(RequestExecutor requestExecutor); - - - @Override - public List listGrayService() { - return execute(new RequestExecutor>() { - @Override - public List execute(InformationClient delegate) { - return delegate.listGrayService(); - } - - @Override - public RequestType getRequestType() { - return RequestType.ListGrayServices; - } - }); - } - - @Override - public GrayService grayService(String serviceId) { - - return execute(new RequestExecutor() { - @Override - public GrayService execute(InformationClient delegate) { - return delegate.grayService(serviceId); - } - - @Override - public RequestType getRequestType() { - return RequestType.GetGrayService; - } - }); - - } - - @Override - public GrayInstance grayInstance(String serviceId, String instanceId) { - return execute(new RequestExecutor() { - @Override - public GrayInstance execute(InformationClient delegate) { - return delegate.grayInstance(serviceId, instanceId); - } - - @Override - public RequestType getRequestType() { - return RequestType.GetGrayInstance; - } - }); - } - - @Override - public void addGrayInstance(String serviceId, String instanceId) { - - execute(new RequestExecutor() { - @Override - public Object execute(InformationClient delegate) { - delegate.addGrayInstance(serviceId, instanceId); - return null; - } - - @Override - public RequestType getRequestType() { - return RequestType.AddGrayInstance; - } - }); - } - - @Override - public void serviceDownline() { - execute(new RequestExecutor() { - @Override - public Object execute(InformationClient delegate) { - delegate.serviceDownline(); - return null; - } - - @Override - public RequestType getRequestType() { - return RequestType.ServiceDownline; - } - }); - } - - @Override - public void serviceDownline(String serviceId, String instanceId) { - execute(new RequestExecutor() { - @Override - public Object execute(InformationClient delegate) { - delegate.serviceDownline(serviceId, instanceId); - return null; - } - - @Override - public RequestType getRequestType() { - return RequestType.ServiceDownline; - } - }); - } -} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/EventType.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/EventType.java new file mode 100644 index 00000000..a20bf3a0 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/EventType.java @@ -0,0 +1,7 @@ +package cn.springcloud.gray.event; + +import java.io.Serializable; + +public enum EventType implements Serializable { + DOWN, UPDATE +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventListener.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventListener.java new file mode 100644 index 00000000..69756b84 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventListener.java @@ -0,0 +1,10 @@ +package cn.springcloud.gray.event; + +import cn.springcloud.gray.exceptions.EventException; + +public interface GrayEventListener { + + + void onEvent(GrayEventMsg msg) throws EventException; + +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventMsg.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventMsg.java new file mode 100644 index 00000000..6bce2c9b --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventMsg.java @@ -0,0 +1,18 @@ +package cn.springcloud.gray.event; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +@Setter +@Getter +@ToString +public class GrayEventMsg implements Serializable { + private static final long serialVersionUID = -8114806214567175543L; + private String serviceId; + private String instanceId; + private EventType eventType; + +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventPublisher.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventPublisher.java new file mode 100644 index 00000000..4fd08df8 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/event/GrayEventPublisher.java @@ -0,0 +1,8 @@ +package cn.springcloud.gray.event; + +import cn.springcloud.gray.exceptions.EventException; + +public interface GrayEventPublisher { + + void publishEvent(GrayEventMsg msg) throws EventException; +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/exceptions/EventException.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/exceptions/EventException.java new file mode 100644 index 00000000..c6ccd518 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/exceptions/EventException.java @@ -0,0 +1,25 @@ +package cn.springcloud.gray.exceptions; + +public class EventException extends RuntimeException { + + private static final long serialVersionUID = -2822193541909545645L; + + public EventException() { + } + + public EventException(String message) { + super(message); + } + + public EventException(String message, Throwable cause) { + super(message, cause); + } + + public EventException(Throwable cause) { + super(cause); + } + + public EventException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/DecisionDefinition.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/DecisionDefinition.java new file mode 100644 index 00000000..f20e5939 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/DecisionDefinition.java @@ -0,0 +1,23 @@ +package cn.springcloud.gray.model; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 灰度策略 + */ +@Setter +@Getter +public class DecisionDefinition implements Serializable { + + private String id; + private String name; + private Map infos = new HashMap<>(); + + +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayInstance.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayInstance.java new file mode 100644 index 00000000..c34e80c7 --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayInstance.java @@ -0,0 +1,35 @@ +package cn.springcloud.gray.model; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + + +/** + * 灰度实例,有状态属性 + */ +@Setter +@Getter +public class GrayInstance implements Serializable { + + private String serviceId; + private String instanceId; + private String host; + private Integer port; + + /** + * 类度策略组 + */ + private List policyDefinitions = new ArrayList<>(); + + private GrayStatus grayStatus; + + + public boolean isGray() { + return grayStatus == GrayStatus.OPEN; + } + +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayStatus.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayStatus.java new file mode 100644 index 00000000..616247ae --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/GrayStatus.java @@ -0,0 +1,6 @@ +package cn.springcloud.gray.model; + +public enum GrayStatus { + + OPEN, CLOSE +} diff --git a/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/PolicyDefinition.java b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/PolicyDefinition.java new file mode 100644 index 00000000..0c75cb1f --- /dev/null +++ b/spring-cloud-gray-core/src/main/java/cn/springcloud/gray/model/PolicyDefinition.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.model; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + + +/** + * 灰度策略,包含一组(0,n)决策元素 + */ +@Setter +@Getter +public class PolicyDefinition { + + private String policyId; + private String alias; + private List list = new ArrayList<>(); + +} diff --git a/spring-cloud-gray-dependencies/pom.xml b/spring-cloud-gray-dependencies/pom.xml index b1c86fba..3cd8ee9e 100644 --- a/spring-cloud-gray-dependencies/pom.xml +++ b/spring-cloud-gray-dependencies/pom.xml @@ -2,22 +2,22 @@ - - - - - - org.springframework.boot - spring-boot-starter-parent - 1.5.4.RELEASE + spring-cloud-gray + cn.springcloud.gray + 2.0.0 + + + + + 4.0.0 cn.springcloud.gray spring-cloud-gray-dependencies - 1.1.0 + 2.0.0 pom @@ -42,22 +42,15 @@ - Dalston.SR5 + - - org.springframework.cloud - spring-cloud-dependencies - ${spring-cloud.version} - pom - import - cn.springcloud.gray - spring-cloud-bamboo + spring-cloud-gray-utils ${project.version} @@ -72,25 +65,14 @@ cn.springcloud.gray - spring-cloud-gray-server - ${project.version} - - - cn.springcloud.gray - spring-cloud-starter-multi-version + spring-cloud-gray-client-netflix ${project.version} cn.springcloud.gray - spring-cloud-starter-gray - ${project.version} - - - cn.springcloud.gray - spring-cloud-starter-gray-server + spring-cloud-gray-server ${project.version} - diff --git a/spring-cloud-gray-samples/README.md b/spring-cloud-gray-samples/README.md index ac94cb5e..e69de29b 100644 --- a/spring-cloud-gray-samples/README.md +++ b/spring-cloud-gray-samples/README.md @@ -1,142 +0,0 @@ -## 灰度发布使用说明 -(以下说明都是假设浏览者对spring-cloud-netflix有过了解) -灰度管理的配置和bamboo的配置是一样的, 配置方式差别不大。不过由于gray-server需要注册到eureka,所以要先运行eureka-server。下面先说gray-server的配置。 - -##### gray-server -在项目的pom.xml加入spring-boot相关的依赖,再加入spring-cloud-starter-gray-server,然后启动就可以了。比如spring-cloud-gray-server-sample -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-eureka - - - - cn.springcloud.gray - spring-cloud-starter-gray-server - - - -``` - -在启动类中,需要启用服务发现,以及@EnableGrayServer。 - -```java -@SpringBootApplication -@EnableGrayServer -@EnableDiscoveryClient -public class GrayServerApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(GrayServerApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(GrayServerApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} -``` - -启动后,可以访问 http://localhost:20202/swagger-ui.html 查看接口列表,也可以调用其中的接口。 - -![灰度服务端swagger api list](../doc/img/web-api-gray-server.png) - -以上介绍完了gray-server的配置,下面再看gray-client的配置。 - - -##### gray-client - -1、在pom.xml中加入spring-cloud-starter-gray。比如spring-cloud-gray-service-b-sample -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-eureka - - - org.springframework.cloud - spring-cloud-starter-feign - - - org.apache.commons - commons-lang3 - 3.5 - - - - - cn.springcloud.gray - spring-cloud-starter-gray - - -``` - -2、在application.yaml中加入灰度配置。 -```yaml -gray: - client: - instance: - grayEnroll: false #是否在启动后自动注册成灰度实例 - serverUrl: http://localhost:20202 #灰度服务端的url -``` -更多gray-client配置请查看cn.springcloud.gray.client.GrayClientConfig接口 - -3、在启动类中加入灰度客户端的注解@EnableGrayClient -```java -@SpringBootApplication -@EnableDiscoveryClient -@EnableGrayClient -@EnableFeignClients -public class ServiceBApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(ServiceBApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(ServiceBApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} -``` - -这样灰略度的服务端和客户端都配置好了, 只要在灰度服务端开启灰度实例和灰度策,在灰度客户端就会自动进行灰度路由。 - - - diff --git a/spring-cloud-gray-samples/pom.xml b/spring-cloud-gray-samples/pom.xml index 031c7a4b..d08eb884 100644 --- a/spring-cloud-gray-samples/pom.xml +++ b/spring-cloud-gray-samples/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -17,6 +17,8 @@ spring-cloud-gray-service-a-sample spring-cloud-gray-service-b-sample spring-cloud-gray-server-sample + spring-cloud-gray-service-a1-sample + spring-cloud-gray-stream-sample diff --git a/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/pom.xml index 12881dd1..440854a0 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -17,6 +17,21 @@ org.springframework.cloud spring-cloud-starter-eureka-server + + spring-cloud-gray-utils + cn.springcloud.gray + 2.0.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + diff --git a/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/configuration/TestConfiguration.java b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/configuration/TestConfiguration.java new file mode 100644 index 00000000..d64b54b3 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/configuration/TestConfiguration.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.eureka.configuration; + +import cn.springcloud.gray.eureka.domain.ApiRes; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestConfiguration { + + @Bean + public ApiRes apiRes() { + return ApiRes.builder().code("0").data("data").build(); + } +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/domain/ApiRes.java b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/domain/ApiRes.java new file mode 100644 index 00000000..8c3f5950 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/main/java/cn/springcloud/gray/eureka/domain/ApiRes.java @@ -0,0 +1,13 @@ +package cn.springcloud.gray.eureka.domain; + +import lombok.*; + +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ApiRes { + private String code; + private Object data; +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/test/java/cn/springcloud/test/Test.java b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/test/java/cn/springcloud/test/Test.java new file mode 100644 index 00000000..7f5a8657 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-eureka-sample/src/test/java/cn/springcloud/test/Test.java @@ -0,0 +1,84 @@ +package cn.springcloud.test; + +import cn.springcloud.gray.eureka.GrayEruekaApplication; +import cn.springcloud.gray.eureka.domain.ApiRes; +import cn.springcloud.gray.utils.ConfigurationUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableBiMap; +import lombok.Data; +import org.junit.runner.RunWith; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.Expression; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ActiveProfiles({"dev"}) +@RunWith(SpringRunner.class) +@SpringBootTest(classes = GrayEruekaApplication.class) +public class Test { + + + @Autowired + private ApplicationContext cxt; + @Autowired + private ConversionService conversionService; + @Autowired + private Validator validator; + + @org.junit.Test + public void testBind() { + Map args = ImmutableBiMap.of("s", "10", "adf", "a,b,c,d", "apiRes", "#{@apiRes}"); + SpelExpressionParser parser = new SpelExpressionParser(); + + Map properties = ConfigurationUtils.normalize(args, parser, cxt); + + + Object configuration = new St(); + + ConfigurationUtils.bind(configuration, properties, + "", "", validator, + conversionService); + System.out.println(configuration); + + + } + + + @Data + public static class St { + private int s; + private List adf; + private ApiRes apiRes; + + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (Exception e) { + return null; + } + } + } + + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/pom.xml index 0b691402..44d03fe3 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -30,6 +30,11 @@ spring-cloud-starter-gray-server + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + + diff --git a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/app/GrayServerApplication.java similarity index 94% rename from spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java rename to spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/app/GrayServerApplication.java index e31c2a46..a896bd6f 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java +++ b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/java/cn/springcloud/gray/server/app/GrayServerApplication.java @@ -1,5 +1,6 @@ -package cn.springcloud.gray.server; +package cn.springcloud.gray.server.app; +import cn.springcloud.gray.server.EnableGrayServer; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; diff --git a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/resources/config/application.yml b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/resources/config/application.yml index d2362340..8b772327 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/resources/config/application.yml +++ b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/main/resources/config/application.yml @@ -1,11 +1,54 @@ +server: + port: 20202 spring: application: name: gray-server -server: - port: 20202 + #通用数据源配置 + datasource: + driver-class-name: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost:3306/gray_server01?charset=utf8mb4&useSSL=false + username: root + password: root + # Hikari 数据源专用配置 + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + # JPA 相关配置 + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + show-sql: true + generate-ddl: true + hibernate: + ddl-auto: none + rabbitmq: + addresses: 127.0.0.1:5672 + username: admin + password: admin + cloud: + stream: + bindings: + # GrayEventInput: + # group: service-a + # destination: test + # consumer: + # concurrency: 1 #并发数 + # max-attempts: 1 + GrayEventOutput: + destination: gray_event + # rabbit: + # bindings: + # GrayEventInput: + # consumer: + # maxConcurrency: 1 #并发数 + # prefetch: 1 #从mq一次获取消息的数量 + # requeueRejected: true #spring cloud stream 如果出现异常, 是否需要重新投递消息, false表示丢弃。 也有相应的Exception, true-MessageRejectedWhileStoppingException false-AmqpRejectAndDontRequeueException + # auto-bind-dlq: true + # acknowledgeMode: AUTO +# acknowledgeMode: MANUAL eureka: client: register-with-eureka: true fetch-registry: true serviceUrl: defaultZone: http://localhost:20001/eureka/ + diff --git a/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/test/java/cn/springcloud/gray/service/test/GrayInstanceServiceTest.java b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/test/java/cn/springcloud/gray/service/test/GrayInstanceServiceTest.java new file mode 100644 index 00000000..1720b753 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-server-sample/src/test/java/cn/springcloud/gray/service/test/GrayInstanceServiceTest.java @@ -0,0 +1,40 @@ +package cn.springcloud.gray.service.test; + + +import cn.springcloud.gray.model.GrayStatus; +import cn.springcloud.gray.server.app.GrayServerApplication; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.module.domain.InstanceStatus; +import cn.springcloud.gray.server.service.GrayInstanceService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.List; + +@ActiveProfiles({"dev"}) +@RunWith(SpringRunner.class) +@SpringBootTest(classes = GrayServerApplication.class) +@Slf4j +public class GrayInstanceServiceTest { + + + @Autowired + private GrayInstanceService grayInstanceService; + @Autowired + private ObjectMapper objectMapper; + + + @Test + public void test() throws JsonProcessingException { + List grayInstances = grayInstanceService.findAllByStatus(GrayStatus.OPEN, InstanceStatus.UP); + log.info("{}", objectMapper.writeValueAsString(grayInstances)); + + } +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/pom.xml index dc691100..f4bb4ae7 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -33,7 +33,7 @@ cn.springcloud.gray - spring-cloud-starter-gray + spring-cloud-starter-gray-client diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java b/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java new file mode 100644 index 00000000..ca770727 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java @@ -0,0 +1,25 @@ +package cn.springcloud.service.a.configuration; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.SimpleGrayManager; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +//@Configuration +public class GraylConfiguration { + + @Autowired + private GrayDecisionFactoryKeeper grayDecisionFactoryKeeper; + @Autowired(required = false) + private List requestInterceptors; + + @Bean + public SimpleGrayManager grayManager() { + return new SimpleGrayManager(grayDecisionFactoryKeeper, requestInterceptors); + } + + +} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/pom.xml similarity index 64% rename from spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/pom.xml rename to spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/pom.xml index 74416751..bc7ece69 100644 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/pom.xml @@ -3,14 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-mult-version-samples + spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 - spring-cloud-bamboo-service-a-samples - + spring-cloud-gray-service-a1-sample org.springframework.boot @@ -24,17 +23,16 @@ org.springframework.cloud spring-cloud-starter-eureka + + org.apache.commons + commons-lang3 + 3.5 + + + + cn.springcloud.gray + spring-cloud-starter-gray-client + - - - - - maven-deploy-plugin - - true - - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/java/cn/springcloud/service/a/ServiceAApplication.java b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/ServiceA1Application.java similarity index 89% rename from spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/java/cn/springcloud/service/a/ServiceAApplication.java rename to spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/ServiceA1Application.java index d392ee06..c23345ab 100644 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/java/cn/springcloud/service/a/ServiceAApplication.java +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/ServiceA1Application.java @@ -16,12 +16,12 @@ @SpringBootApplication @EnableDiscoveryClient @EnableGrayClient -public class ServiceAApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(ServiceAApplication.class); +public class ServiceA1Application { + private static final org.slf4j.Logger log = LoggerFactory.getLogger(ServiceA1Application.class); public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(ServiceAApplication.class).web(true).run(args).getEnvironment(); + Environment env = new SpringApplicationBuilder(ServiceA1Application.class).web(true).run(args).getEnvironment(); log.info( "\n----------------------------------------------------------\n\t" + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java new file mode 100644 index 00000000..b470fffb --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/configuration/GraylConfiguration.java @@ -0,0 +1,26 @@ +package cn.springcloud.service.a.configuration; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.SimpleGrayManager; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +//@Configuration +public class GraylConfiguration { + + @Autowired + private GrayDecisionFactoryKeeper grayDecisionFactoryKeeper; + @Autowired(required = false) + private List requestInterceptors; + + @Bean + public GrayManager grayManager() { + return new SimpleGrayManager(grayDecisionFactoryKeeper, requestInterceptors); + } + + +} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/java/cn/springcloud/service/a/rest/TestResource.java b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/rest/TestResource.java similarity index 100% rename from spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/java/cn/springcloud/service/a/rest/TestResource.java rename to spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/java/cn/springcloud/service/a/rest/TestResource.java diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/resources/config/application.yml b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/resources/config/application.yml new file mode 100644 index 00000000..6d703589 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-a1-sample/src/main/resources/config/application.yml @@ -0,0 +1,18 @@ +spring: + application: + name: service-a +server: + port: 20104 +eureka: + client: + register-with-eureka: true + fetch-registry: true + serviceUrl: + defaultZone: http://localhost:20001/eureka/ + instance: + instanceId: ${spring.application.name}:${server.port} +gray: + client: + instance: + grayEnroll: true #是否在启动后自动注册成灰度实例 + serverUrl: http://localhost:20202 #灰度服务端的url \ No newline at end of file diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/pom.xml index d209b29b..d2c238a1 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -38,7 +38,12 @@ cn.springcloud.gray - spring-cloud-starter-gray + spring-cloud-starter-gray-client + + + + org.springframework.cloud + spring-cloud-starter-stream-rabbit diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/java/cn/springcloud/gray/service/b/configuration/GraylConfiguration.java b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/java/cn/springcloud/gray/service/b/configuration/GraylConfiguration.java new file mode 100644 index 00000000..d1b3495a --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/java/cn/springcloud/gray/service/b/configuration/GraylConfiguration.java @@ -0,0 +1,25 @@ +package cn.springcloud.gray.service.b.configuration; + +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.SimpleGrayManager; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +//@Configuration +public class GraylConfiguration { + + @Autowired + private GrayDecisionFactoryKeeper grayDecisionFactoryKeeper; + @Autowired + private List requestInterceptors; + + @Bean + public SimpleGrayManager grayManager() { + return new SimpleGrayManager(grayDecisionFactoryKeeper, requestInterceptors); + } + + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/resources/config/application.yml b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/resources/config/application.yml index 11df4b65..58cdcbc1 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/resources/config/application.yml +++ b/spring-cloud-gray-samples/spring-cloud-gray-service-b-sample/src/main/resources/config/application.yml @@ -1,6 +1,31 @@ spring: application: name: service-b + rabbitmq: + addresses: 127.0.0.1:5672 + username: admin + password: admin + cloud: + stream: + bindings: + GrayEventInput: + group: service-a + destination: gray_event + consumer: + concurrency: 1 #并发数 + # max-attempts: 1 + # GrayEventOutput: + # destination: gray-event + rabbit: + bindings: + GrayEventInput: + consumer: + maxConcurrency: 1 #并发数 + # prefetch: 1 #从mq一次获取消息的数量 + # requeueRejected: true #spring cloud stream 如果出现异常, 是否需要重新投递消息, false表示丢弃。 也有相应的Exception, true-MessageRejectedWhileStoppingException false-AmqpRejectAndDontRequeueException + # auto-bind-dlq: true + acknowledgeMode: AUTO +# acknowledgeMode: MANUAL server: port: 20102 eureka: @@ -17,6 +42,33 @@ feign: gray: client: - instance: - grayEnroll: false #是否在启动后自动注册成灰度实例 - serverUrl: http://localhost:20202 #灰度服务端的url \ No newline at end of file + server-url: http://localhost:20202 + request: + track: + web: + need: + uri: enable + ip: enable + method: enable + headers: test,test-mm + parameters: version,test + path-patterns: /* + load: + enabled: false + gray-instances: + - serviceId: service-a + instance-id: service-a:20104 + policy-definitions: + - policyId: policy-1 + alias: policy-1 + list: + - id: decision-1 + name: HttpHeader + infos: + compare-mode: EQUAL + header: _graytrack_header__test + values: true +# client: +# instance: +# grayEnroll: false #是否在启动后自动注册成灰度实例 +# serverUrl: http://localhost:20202 #灰度服务端的url \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/pom.xml similarity index 72% rename from spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/pom.xml rename to spring-cloud-gray-samples/spring-cloud-gray-stream-sample/pom.xml index 91e4926d..11963e01 100644 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/pom.xml @@ -3,14 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-gray-zookeeper-samples + spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 - spring-cloud-gray-zookeeper-server-sample - + spring-cloud-gray-stream-sample @@ -21,13 +20,13 @@ org.springframework.boot spring-boot-starter-test + + + + org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - cn.springcloud.gray - spring-cloud-starter-gray-server + spring-cloud-starter-stream-rabbit diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/java/cn/springcloud/bamboo/erueka/BambooEruekaApplication.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/GrayStreamApplication.java old mode 100644 new mode 100755 similarity index 56% rename from spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/java/cn/springcloud/bamboo/erueka/BambooEruekaApplication.java rename to spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/GrayStreamApplication.java index 1f937078..a9d2a309 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/java/cn/springcloud/bamboo/erueka/BambooEruekaApplication.java +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/GrayStreamApplication.java @@ -1,34 +1,40 @@ -package cn.springcloud.bamboo.erueka; +package cn.springcloud.gray.stream; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; import org.springframework.core.env.Environment; import java.net.InetAddress; import java.net.UnknownHostException; @SpringBootApplication -@EnableEurekaServer -public class BambooEruekaApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooEruekaApplication.class); +public class GrayStreamApplication { + private static final org.slf4j.Logger log = LoggerFactory.getLogger(GrayStreamApplication.class); public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooEruekaApplication.class).web(true).run(args).getEnvironment(); + Environment env = + new SpringApplicationBuilder(GrayStreamApplication.class) + .web(true) + .run(args) + .getEnvironment(); log.info( "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\thttp://127.0.0.1:{}\n\t" + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); + env.getProperty("spring.application.name"), + env.getProperty("server.port"), + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port")); String configServerStatus = env.getProperty("configserver.status"); log.info( "\n----------------------------------------------------------\n\t" + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); + configServerStatus == null + ? "Not found or not setup for this application" + : configServerStatus); } } diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageAckListener.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageAckListener.java new file mode 100755 index 00000000..879fdf47 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageAckListener.java @@ -0,0 +1,30 @@ +package cn.springcloud.gray.stream.component; + +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@EnableBinding({TestSourceInput.class}) +public class MessageAckListener { + + private static final Logger log = LoggerFactory.getLogger(MessageAckListener.class); + + + @StreamListener(TestSourceInput.INPUT) + public void receive(Object msg, @Header(AmqpHeaders.CHANNEL) Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException { + log.info("Received3:" + msg); + boolean request = false;//true=重新发送 +// channel.basicReject(deliveryTag, request); + channel.basicAck(deliveryTag, false); + } + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageSender.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageSender.java new file mode 100755 index 00000000..36c2e1c6 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/MessageSender.java @@ -0,0 +1,22 @@ +package cn.springcloud.gray.stream.component; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + + +@Component +@EnableBinding({TestSourceOutput.class}) +public class MessageSender { + + @Autowired + private TestSourceOutput sender; + + + public boolean send(Object obj) { + return sender.output().send(MessageBuilder.withPayload(obj).build()); + } + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceInput.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceInput.java new file mode 100755 index 00000000..b808dbc9 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceInput.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.stream.component; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +/** + * + */ +public interface TestSourceInput { + + String INPUT = "TestInput"; + + @Input(INPUT) + SubscribableChannel input(); + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceOutput.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceOutput.java new file mode 100755 index 00000000..9f54c709 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/java/cn/springcloud/gray/stream/component/TestSourceOutput.java @@ -0,0 +1,19 @@ +package cn.springcloud.gray.stream.component; + +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; + + +/** + * topic see + */ +public interface TestSourceOutput { + + String OUTPUT = "TestOutput"; + + + @Output(OUTPUT) + MessageChannel output(); + + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/resources/config/application.yaml b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/resources/config/application.yaml new file mode 100644 index 00000000..a0526810 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/main/resources/config/application.yaml @@ -0,0 +1,30 @@ +server: + port: 8889 +spring: + application: + name: mq-rabbit + rabbitmq: + addresses: 127.0.0.1:5672 + username: admin + password: admin + cloud: + stream: + bindings: + TestInput: + group: service-a + destination: test + consumer: + concurrency: 1 #并发数 + # max-attempts: 1 + TestOutput: + destination: test + rabbit: + bindings: + TestInput: + consumer: + maxConcurrency: 1 #并发数 + # prefetch: 1 #从mq一次获取消息的数量 + # requeueRejected: true #spring cloud stream 如果出现异常, 是否需要重新投递消息, false表示丢弃。 也有相应的Exception, true-MessageRejectedWhileStoppingException false-AmqpRejectAndDontRequeueException + # auto-bind-dlq: true + # acknowledgeMode: AUTO +# acknowledgeMode: MANUAL \ No newline at end of file diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/InputStreamTest.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/InputStreamTest.java new file mode 100644 index 00000000..fae1a61c --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/InputStreamTest.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.stream.test; + + +import cn.springcloud.gray.stream.GrayStreamApplication; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = GrayStreamApplication.class) +public class InputStreamTest { + + + @Test + public void testInput() throws InterruptedException { + System.out.println("start testInput....."); + Thread.sleep(100000l); + System.out.println("end testInput....."); + } +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/OutputStreamTest.java b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/OutputStreamTest.java new file mode 100644 index 00000000..0370f8dd --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-stream-sample/src/test/java/cn/springcloud/gray/stream/test/OutputStreamTest.java @@ -0,0 +1,25 @@ +package cn.springcloud.gray.stream.test; + +import cn.springcloud.gray.stream.GrayStreamApplication; +import cn.springcloud.gray.stream.component.MessageSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = GrayStreamApplication.class) +public class OutputStreamTest { + + @Autowired + private MessageSender sender; + + + @Test + public void testOutput() { + boolean bool = sender.send("hello"); + System.out.println(bool); + } + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/pom.xml b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/pom.xml index 4fab81b0..c6d36062 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/pom.xml +++ b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray-samples cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -28,10 +28,14 @@ org.springframework.cloud spring-cloud-starter-eureka - + cn.springcloud.gray - spring-cloud-starter-gray + spring-cloud-starter-gray-client + + + org.springframework.cloud + spring-cloud-starter-stream-rabbit diff --git a/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/java/cn/springcloud/gray/zuul/configuration/GraylConfiguration.java b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/java/cn/springcloud/gray/zuul/configuration/GraylConfiguration.java new file mode 100644 index 00000000..5174bfd2 --- /dev/null +++ b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/java/cn/springcloud/gray/zuul/configuration/GraylConfiguration.java @@ -0,0 +1,26 @@ +package cn.springcloud.gray.zuul.configuration; + +import cn.springcloud.gray.GrayManager; +import cn.springcloud.gray.RequestInterceptor; +import cn.springcloud.gray.SimpleGrayManager; +import cn.springcloud.gray.decision.GrayDecisionFactoryKeeper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +//@Configuration +public class GraylConfiguration { + + @Autowired + private GrayDecisionFactoryKeeper grayDecisionFactoryKeeper; + @Autowired(required = false) + private List requestInterceptors; + + @Bean + public GrayManager grayManager() { + return new SimpleGrayManager(grayDecisionFactoryKeeper, requestInterceptors); + } + + +} diff --git a/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/resources/config/application.yml b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/resources/config/application.yml index 1bc44619..bb4725bb 100644 --- a/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/resources/config/application.yml +++ b/spring-cloud-gray-samples/spring-cloud-gray-zuul-sample/src/main/resources/config/application.yml @@ -1,6 +1,30 @@ spring: application: name: api-gateway + rabbitmq: + addresses: 127.0.0.1:5672 + username: admin + password: admin + cloud: + stream: + bindings: + GrayEventInput: + group: zuul + destination: gray_event + consumer: + concurrency: 1 #并发数 + # max-attempts: 1 + # GrayEventOutput: + # destination: gray-event + rabbit: + bindings: + GrayEventInput: + consumer: + maxConcurrency: 1 #并发数 + # prefetch: 1 #从mq一次获取消息的数量 + # requeueRejected: true #spring cloud stream 如果出现异常, 是否需要重新投递消息, false表示丢弃。 也有相应的Exception, true-MessageRejectedWhileStoppingException false-AmqpRejectAndDontRequeueException + # auto-bind-dlq: true + acknowledgeMode: AUTO server: port: 20301 eureka: @@ -9,18 +33,44 @@ eureka: defaultZone: http://localhost:20001/eureka/ ribbon: -# ReadTimeout: 30000 -# ConnectTimeout: 30000 - eureka: - enabled: true + # ReadTimeout: 30000 + # ConnectTimeout: 30000 + eureka: + enabled: true zuul: prefix: /gateway #为zuul设置一个公共的前缀 + ribbonIsolationStrategy: THREAD # routes: # eureka-client: #随便定义,当不存在serviceId时,默认该值为serviceId(就是注册服务的名称,属性spring.application.name) # path: /client/** #匹配/techouse/usersystem/** 均路由到cloud-client # serviceId: eureka-client #指定路由到的serviceId gray: client: - server-url: http://localhost:20202 #灰度服务端的url - instance: - use-multi-version: false \ No newline at end of file + server-url: http://localhost:20202 + request: + track: + web: + need: + uri: enable + ip: enable + method: enable + headers: test,test-mm + parameters: version,test + path-patterns: /* + load: + enabled: false + gray-instances: + - serviceId: service-a + instance-id: service-a:20104 + policy-definitions: + - policyId: policy-1 + alias: policy-1 + list: + - id: decision-1 + name: HttpHeader + infos: + compare-mode: EQUAL + header: _graytrack_header__test + values: true +# _graytrack_heander__test: true + diff --git a/spring-cloud-gray-server/pom.xml b/spring-cloud-gray-server/pom.xml index 1bef468a..18896264 100644 --- a/spring-cloud-gray-server/pom.xml +++ b/spring-cloud-gray-server/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -60,6 +60,41 @@ io.springfox springfox-bean-validators + + + + + + org.hibernate + hibernate-core + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + + + + + javax.transaction + javax.transaction-api + + + org.springframework.data + spring-data-jpa + + + mysql + mysql-connector-java + + + org.mapstruct + mapstruct-jdk8 + + + + org.springframework.cloud + spring-cloud-stream + diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/DefaultGrayServiceManager.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/DefaultGrayServiceManager.java deleted file mode 100644 index bff86b86..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/DefaultGrayServiceManager.java +++ /dev/null @@ -1,168 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.*; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - - -/** - * 维护一个Map, 用来管理GrayService,key是service id。 - * 并且每隔一段时间就调用EurekaGrayServerEvictor,检查列表中的实例是否下线,将下线的服务从灰度列表中删除。 - */ -public class DefaultGrayServiceManager implements GrayServiceManager { - - - private Map grayServiceMap = new ConcurrentHashMap<>(); - private Lock lock = new ReentrantLock(); - private GrayServerConfig serverConfig; - private Timer evictionTimer = new Timer("Gray-EvictionTimer", true); - - - public DefaultGrayServiceManager(GrayServerConfig config) { - this.serverConfig = config; - } - - @Override - public void addGrayInstance(GrayInstance instance) { - - GrayService grayService = grayServiceMap.get(instance.getServiceId()); - lock.lock(); - try { - if (grayService == null) { - grayService = new GrayService(); - grayService.setServiceId(instance.getServiceId()); - grayServiceMap.put(instance.getServiceId(), grayService); - } - if (!grayService.contains(instance.getInstanceId())) { - grayService.addGrayInstance(instance); - } - } finally { - lock.unlock(); - } - } - - @Override - public void deleteGrayInstance(String serviceId, String instanceId) { - GrayService grayService = grayServiceMap.get(serviceId); - if (grayService == null) { - return; - } - lock.lock(); - try { - if (grayService.removeGrayInstance(instanceId) != null && grayService.getGrayInstances().isEmpty()) { - grayServiceMap.remove(serviceId); - } - } finally { - lock.unlock(); - } - } - - @Override - public void addGrayPolicy(String serviceId, String instanceId, String policyGroupId, GrayPolicy policy) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - grayInstance.addGrayPolicy(policyGroupId, policy); - } - } - - @Override - public void deleteGrayPolicy(String serviceId, String instanceId, String policyGroupId, String policyId) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - grayInstance.removeGrayPolicy(policyGroupId, policyId); - } - } - - @Override - public void addGrayPolicyGroup(String serviceId, String instanceId, GrayPolicyGroup policyGroup) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - grayInstance.addGrayPolicyGroup(policyGroup); - } - } - - @Override - public void deleteGrayPolicyGroup(String serviceId, String instanceId, String policyGroupId) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - grayInstance.removeGrayPolicyGroup(policyGroupId); - } - } - - - @Override - public Collection allGrayService() { - return new ArrayList<>(grayServiceMap.values()); - } - - @Override - public GrayService getGrayService(String serviceId) { - return grayServiceMap.get(serviceId); - } - - - @Override - public GrayInstance getGrayInstane(String serviceId, String instanceId) { - GrayService grayService = getGrayService(serviceId); - if (grayService != null) { - return grayService.getGrayInstance(instanceId); - } - return null; - } - - @Override - public boolean updateInstanceStatus(String serviceId, String instanceId, int status) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance == null) { - grayInstance = new GrayInstance(); - grayInstance.setServiceId(serviceId); - grayInstance.setInstanceId(instanceId); - addGrayInstance(grayInstance); - } - grayInstance.setOpenGray(status == 1); - return true; - } - - @Override - public boolean updatePolicyGroupStatus(String serviceId, String instanceId, String groupId, int enable) { - GrayInstance grayInstance = getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - GrayPolicyGroup policyGroup = grayInstance.getGrayPolicyGroup(groupId); - if (policyGroup != null) { - policyGroup.setEnable(enable == 1); - return true; - } - } - return false; - } - - @Override - public void openForWork() { - evictionTimer.schedule(new EvictionTask(), - serverConfig.getEvictionIntervalTimerInMs(), - serverConfig.getEvictionIntervalTimerInMs()); - } - - @Override - public void shutdown() { - evictionTimer.cancel(); - } - - - protected void evict() { - GrayServerContext.getGrayServerEvictor().evict(this); - } - - - class EvictionTask extends TimerTask { - - @Override - public void run() { - evict(); - } - } - -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EnableGrayServer.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EnableGrayServer.java index 9ce5a4b7..cb6282e8 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EnableGrayServer.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EnableGrayServer.java @@ -1,7 +1,7 @@ package cn.springcloud.gray.server; -import cn.springcloud.gray.server.config.GrayServerMarkerConfiguration; +import cn.springcloud.gray.server.configuration.GrayServerMarkerConfiguration; import org.springframework.context.annotation.Import; import java.lang.annotation.*; diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EurekaGrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EurekaGrayServerEvictor.java deleted file mode 100644 index 4b15d6c7..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/EurekaGrayServerEvictor.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayService; -import cn.springcloud.gray.core.GrayServiceManager; -import com.netflix.discovery.EurekaClient; -import com.netflix.discovery.shared.Application; - -import java.util.Collection; - - -/** - * 依赖EurekaClient来检查服务实例是否下线 - */ -public class EurekaGrayServerEvictor implements GrayServerEvictor { - - private EurekaClient eurekaClient; - - - public EurekaGrayServerEvictor(EurekaClient eurekaClient) { - this.eurekaClient = eurekaClient; - } - - @Override - public void evict(GrayServiceManager serviceManager) { - Collection grayServices = serviceManager.allGrayService(); - grayServices.forEach(grayService -> { - grayService.getGrayInstances().forEach(grayInstance -> { - evict(serviceManager, grayInstance); - }); - }); - - } - - - private void evict(GrayServiceManager serviceManager, GrayInstance grayInstance) { - if (isDownline(grayInstance)) { - serviceManager.deleteGrayInstance(grayInstance.getServiceId(), grayInstance.getInstanceId()); - } - } - - - private boolean isDownline(GrayInstance grayInstance) { - Application app = eurekaClient.getApplication(grayInstance.getServiceId()); - return app == null || app.getByInstanceId(grayInstance.getInstanceId()) == null; - } - -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerConfig.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerConfig.java deleted file mode 100644 index 467c1441..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.springcloud.gray.server; - -public interface GrayServerConfig { - - - /** - * 检查服务实例是否下线的间隔时间(ms) - * - * @return 返回服务实例是否下线的间隔时间(ms) - */ - int getEvictionIntervalTimerInMs(); - - -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerContext.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerContext.java deleted file mode 100644 index 8a2db484..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerContext.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.GrayServiceManager; - -public class GrayServerContext { - - public static final String DEFAULT_PREFIX = "gray"; - - private static GrayServiceManager grayServiceManager; - private static GrayServerEvictor grayServerEvictor; - - - public static GrayServiceManager getGrayServiceManager() { - return grayServiceManager; - } - - static void setGrayServiceManager(GrayServiceManager grayServiceManager) { - GrayServerContext.grayServiceManager = grayServiceManager; - } - - public static GrayServerEvictor getGrayServerEvictor() { - return grayServerEvictor; - } - - static void setGrayServerEvictor(GrayServerEvictor grayServerEvictor) { - GrayServerContext.grayServerEvictor = grayServerEvictor; - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerEvictor.java deleted file mode 100644 index 24f10b87..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerEvictor.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.GrayServiceManager; - - -/** - * - */ -public interface GrayServerEvictor { - - void evict(GrayServiceManager serviceManager); - - -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingBean.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingBean.java deleted file mode 100644 index 80cfc047..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingBean.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.GrayServiceManager; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -import javax.annotation.PreDestroy; - -public class GrayServerInitializingBean implements InitializingBean, ApplicationContextAware { - private ApplicationContext cxt; - - @Override - public void afterPropertiesSet() throws Exception { - GrayServerContext.setGrayServiceManager(cxt.getBean(GrayServiceManager.class)); - GrayServerContext.setGrayServerEvictor(cxt.getBean(GrayServerEvictor.class)); - - initToWork(); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.cxt = applicationContext; - } - - private void initToWork() { - GrayServerContext.getGrayServiceManager().openForWork(); - } - - - @PreDestroy - public void shutdown() { - GrayServerContext.getGrayServiceManager().shutdown(); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingDestroyBean.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingDestroyBean.java new file mode 100644 index 00000000..635f1da9 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/GrayServerInitializingDestroyBean.java @@ -0,0 +1,30 @@ +package cn.springcloud.gray.server; + +import cn.springcloud.gray.server.manager.GrayServiceManager; +import org.springframework.beans.factory.InitializingBean; + +import javax.annotation.PreDestroy; + +public class GrayServerInitializingDestroyBean implements InitializingBean { + private GrayServiceManager grayServiceManager; + + public GrayServerInitializingDestroyBean(GrayServiceManager grayServiceManager) { + this.grayServiceManager = grayServiceManager; + } + + @Override + public void afterPropertiesSet() { + initToWork(); + } + + + private void initToWork() { + grayServiceManager.openForWork(); + } + + + @PreDestroy + public void shutdown() { + grayServiceManager.shutdown(); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/ZookeeperGrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/ZookeeperGrayServerEvictor.java deleted file mode 100644 index 4b4d4288..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/ZookeeperGrayServerEvictor.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.springcloud.gray.server; - -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayService; -import cn.springcloud.gray.core.GrayServiceManager; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.discovery.DiscoveryClient; - -import java.util.Collection; -import java.util.List; - - -/** - * 依赖EurekaClient来检查服务实例是否下线 - */ -public class ZookeeperGrayServerEvictor implements GrayServerEvictor { - - private DiscoveryClient discoveryClient; - - public ZookeeperGrayServerEvictor(DiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; - } - - @Override - public void evict(GrayServiceManager serviceManager) { - Collection grayServices = serviceManager.allGrayService(); - grayServices.forEach(grayService -> { - grayService.getGrayInstances().forEach(grayInstance -> { - evict(serviceManager, grayInstance); - }); - }); - - } - - - private void evict(GrayServiceManager serviceManager, GrayInstance grayInstance) { - if (isDownline(grayInstance)) { - serviceManager.deleteGrayInstance(grayInstance.getServiceId(), grayInstance.getInstanceId()); - } - } - - - private boolean isDownline(GrayInstance grayInstance) { - String serviceId = grayInstance.getServiceId(); - String instanceId = grayInstance.getInstanceId(); - List instances = discoveryClient.getInstances(serviceId); - if (null == instances || instances.isEmpty()) { - return true; - } - - return instances.stream() - .anyMatch(instance -> instanceId.equals(instance.getHost() + ":" + instance.getPort())); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/api/GrayServiceApi.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/api/GrayServiceApi.java deleted file mode 100644 index 132b9748..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/api/GrayServiceApi.java +++ /dev/null @@ -1,69 +0,0 @@ -package cn.springcloud.gray.server.api; - -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayPolicy; -import cn.springcloud.gray.core.GrayPolicyGroup; -import cn.springcloud.gray.core.GrayService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RequestMapping("/gray") -public interface GrayServiceApi { - - - @RequestMapping(value = "/services", method = RequestMethod.GET) - List services(); - - @RequestMapping(value = "/services/enable", method = RequestMethod.GET) - List enableServices(); - - - @RequestMapping(value = "/services/{serviceId}", method = RequestMethod.GET) - GrayService service(@PathVariable("serviceId") String serviceId); - - - @RequestMapping(value = "/services/{serviceId}/instances", method = RequestMethod.GET) - List instances(@PathVariable("serviceId") String serviceId); - - - @RequestMapping(value = "/services/{serviceId}/instance", method = RequestMethod.GET) - GrayInstance getInstance(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId); - - - @RequestMapping(value = "/services/{serviceId}/instance", method = RequestMethod.DELETE) - ResponseEntity delInstance(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId); - - @RequestMapping(value = "/services/{serviceId}/instance", method = RequestMethod.POST) - ResponseEntity instance(@PathVariable("serviceId") String serviceId, @RequestBody GrayInstance instance); - - - @RequestMapping(value = "/services/{serviceId}/instance/policyGroups", method = RequestMethod.GET) - List policyGroups(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId); - - - @RequestMapping(value = "/services/{serviceId}/instance/policyGroups/{groupId}", - method = RequestMethod.GET) - GrayPolicyGroup policyGroup(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @PathVariable("groupId") String groupId); - - - @RequestMapping(value = "/services/{serviceId}/instance/policyGroups/{groupId}/policies", - method = RequestMethod.GET) - List policies(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @PathVariable("groupId") String groupId); - - - @RequestMapping(value = "/services/{serviceId}/instance/policyGroups/{groupId}/policies/{policyId}", - method = RequestMethod.GET) - GrayPolicy policy(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @PathVariable("groupId") String groupId, - @PathVariable("policyId") String policyId); -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerAutoConfiguration.java deleted file mode 100644 index e6060c20..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerAutoConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.springcloud.gray.server.config; - -import cn.springcloud.bamboo.BambooConstants; -import cn.springcloud.gray.server.DefaultGrayServiceManager; -import cn.springcloud.gray.server.GrayServerConfig; -import cn.springcloud.gray.server.GrayServerInitializingBean; -import cn.springcloud.gray.server.config.properties.GrayServerConfigBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -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 org.springframework.context.annotation.Import; -import org.springframework.core.annotation.Order; - -@Configuration -@EnableConfigurationProperties({GrayServerConfigBean.class}) -@Import(value = {WebConfiguration.class}) -@ConditionalOnBean(GrayServerMarkerConfiguration.GrayServerMarker.class) -public class GrayServerAutoConfiguration { - - @Autowired - private GrayServerConfig grayServerConfig; - - @Bean - @ConditionalOnMissingBean - public DefaultGrayServiceManager defaultGrayServiceManager() { - return new DefaultGrayServiceManager(grayServerConfig); - } - - - @Bean - @Order(value = BambooConstants.INITIALIZING_ORDER + 1) - public GrayServerInitializingBean grayServerInitializingBean() { - return new GrayServerInitializingBean(); - } - - -// @Bean -// @ConditionalOnMissingBean -// public GrayServerEvictor grayServerEvictor(@Autowired(required = false) EurekaClient eurekaClient) { -// return eurekaClient == null ? NoActionGrayServerEvictor.INSTANCE : new EurekaGrayServerEvictor(eurekaClient); -// } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceEurekaAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceEurekaAutoConfiguration.java deleted file mode 100644 index f942d070..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceEurekaAutoConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.springcloud.gray.server.config; - -/** - * @Author: duozl - * @Date: 2018/6/4 18:59 - */ - -import cn.springcloud.gray.core.GrayServiceManager; -import cn.springcloud.gray.server.EurekaGrayServerEvictor; -import cn.springcloud.gray.server.GrayServerEvictor; -import cn.springcloud.gray.server.service.AbstractGrayService; -import cn.springcloud.gray.server.service.EurekaGrayService; -import com.netflix.discovery.EurekaClient; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConditionalOnBean(EurekaClient.class) -public class GrayServiceEurekaAutoConfiguration { - - private final EurekaClient eurekaClient; - private final DiscoveryClient discoveryClient; - private final GrayServiceManager grayServiceManager; - - @Autowired - public GrayServiceEurekaAutoConfiguration(EurekaClient eurekaClient, DiscoveryClient discoveryClient, - GrayServiceManager grayServiceManager) { - this.eurekaClient = eurekaClient; - this.discoveryClient = discoveryClient; - this.grayServiceManager = grayServiceManager; - } - - @Bean - public AbstractGrayService grayService() { - return new EurekaGrayService(eurekaClient, discoveryClient, grayServiceManager); - } - - @Bean - @ConditionalOnMissingBean - public GrayServerEvictor grayServerEvictor() { - return new EurekaGrayServerEvictor(eurekaClient); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceZookeeperAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceZookeeperAutoConfiguration.java deleted file mode 100644 index fc62dc79..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServiceZookeeperAutoConfiguration.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.springcloud.gray.server.config; - -import cn.springcloud.gray.core.GrayServiceManager; -import cn.springcloud.gray.server.GrayServerEvictor; -import cn.springcloud.gray.server.ZookeeperGrayServerEvictor; -import cn.springcloud.gray.server.service.AbstractGrayService; -import cn.springcloud.gray.server.service.ZookeeperGrayService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.zookeeper.serviceregistry.ZookeeperRegistration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @Author: duozl - * @Date: 2018/6/4 18:59 - */ -@Configuration -@ConditionalOnBean(ZookeeperRegistration.class) -public class GrayServiceZookeeperAutoConfiguration { - - private final DiscoveryClient discoveryClient; - private final GrayServiceManager grayServiceManager; - - @Autowired - public GrayServiceZookeeperAutoConfiguration(DiscoveryClient discoveryClient, - GrayServiceManager grayServiceManager) { - this.discoveryClient = discoveryClient; - this.grayServiceManager = grayServiceManager; - } - - @Bean - public AbstractGrayService grayService() { - return new ZookeeperGrayService(discoveryClient, grayServiceManager); - } - - @Bean - @ConditionalOnMissingBean - public GrayServerEvictor grayServerEvictor() { - return new ZookeeperGrayServerEvictor(discoveryClient); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/DBStorageConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/DBStorageConfiguration.java new file mode 100644 index 00000000..080d7159 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/DBStorageConfiguration.java @@ -0,0 +1,48 @@ +package cn.springcloud.gray.server.configuration; + +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.SimpleGrayServerModule; +import cn.springcloud.gray.server.service.GrayDecisionService; +import cn.springcloud.gray.server.service.GrayInstanceService; +import cn.springcloud.gray.server.service.GrayPolicyService; +import cn.springcloud.gray.server.service.GrayServiceService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; + +@Configuration +@EnableJpaRepositories(basePackages = {"cn.springcloud.gray.server.dao.repository"}) +@EntityScan(basePackages = {"cn.springcloud.gray.server.dao.model"}) +@EnableTransactionManagement +@ConditionalOnBean(GrayServerMarkerConfiguration.GrayServerMarker.class) +public class DBStorageConfiguration { + + + @ComponentScan(basePackages = {"cn.springcloud.gray.server.dao.mapper", "cn.springcloud.gray.server.service"}) + @Configuration + public class DBGrayServrConfiguration { + + @Bean + public GrayServerModule grayServerModule( + GrayEventPublisher grayEventPublisher, + GrayServiceService grayServiceService, GrayInstanceService grayInstanceService, + GrayDecisionService grayDecisionService, GrayPolicyService grayPolicyService) { + return new SimpleGrayServerModule(grayEventPublisher, grayServiceService, grayInstanceService, grayDecisionService, grayPolicyService); + } + } + + @Bean + public PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerAutoConfiguration.java new file mode 100644 index 00000000..84edfaeb --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerAutoConfiguration.java @@ -0,0 +1,74 @@ +package cn.springcloud.gray.server.configuration; + +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.server.*; +import cn.springcloud.gray.server.configuration.properties.GrayServerProperties; +import cn.springcloud.gray.server.event.DefaultGrayEventPublisher; +import cn.springcloud.gray.server.evictor.GrayServerEvictor; +import cn.springcloud.gray.server.evictor.NoActionGrayServerEvictor; +import cn.springcloud.gray.server.manager.DefaultGrayServiceManager; +import cn.springcloud.gray.server.manager.GrayServiceManager; +import cn.springcloud.gray.server.module.GrayModle; +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.SimpleGrayModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +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 org.springframework.context.annotation.Import; + +@Configuration +@EnableConfigurationProperties({GrayServerProperties.class}) +@Import(value = {WebConfiguration.class}) +@ConditionalOnBean(GrayServerMarkerConfiguration.GrayServerMarker.class) +public class GrayServerAutoConfiguration { + + @Autowired + private GrayServerProperties grayServerConfig; + + @Autowired + private GrayServerModule grayServerModule; + + + @Bean + @ConditionalOnMissingBean + public DefaultGrayServiceManager defaultGrayServiceManager(GrayServerEvictor grayServerEvictor) { + return new DefaultGrayServiceManager(grayServerConfig, grayServerModule, grayServerEvictor); + } + + + @Bean + public GrayServerInitializingDestroyBean grayServerInitializingBean(GrayServiceManager grayServiceManager) { + return new GrayServerInitializingDestroyBean(grayServiceManager); + } + + + @Configuration + public static class DefaultConfiguration { + @Bean + @ConditionalOnMissingBean + public GrayServerEvictor grayServerEvictor() { + return NoActionGrayServerEvictor.INSTANCE; + } + + + @Bean + @ConditionalOnMissingBean + public GrayModle grayModle(GrayServerModule grayServerModule, @Autowired(required = false) ObjectMapper objectMapper) { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + } + return new SimpleGrayModule(grayServerModule, objectMapper); + } + + @Bean + @ConditionalOnMissingBean + public GrayEventPublisher grayEventPublisher() { + return new DefaultGrayEventPublisher(); + } + } + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerEventAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerEventAutoConfiguration.java new file mode 100644 index 00000000..81803d0c --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerEventAutoConfiguration.java @@ -0,0 +1,29 @@ +package cn.springcloud.gray.server.configuration; + +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.server.event.stream.StreamGrayEventPublisher; +import cn.springcloud.gray.server.event.stream.StreamOutput; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GrayServerEventAutoConfiguration { + + + @Configuration + @ConditionalOnClass(EnableBinding.class) + @ConditionalOnProperty(value = "spring.cloud.stream.bindings.GrayEventOutput.destination") + @EnableBinding({StreamOutput.class}) + public static class StreamEventConfiguration { + + @Bean + public GrayEventPublisher grayEventPublisher(StreamOutput streamOutput) { + return new StreamGrayEventPublisher(streamOutput); + } + } + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerMarkerConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerMarkerConfiguration.java similarity index 86% rename from spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerMarkerConfiguration.java rename to spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerMarkerConfiguration.java index 28581c7d..c724a1af 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/GrayServerMarkerConfiguration.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/GrayServerMarkerConfiguration.java @@ -1,4 +1,4 @@ -package cn.springcloud.gray.server.config; +package cn.springcloud.gray.server.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/Swagger2Configuration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/Swagger2Configuration.java similarity index 91% rename from spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/Swagger2Configuration.java rename to spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/Swagger2Configuration.java index b5dc7427..d9450586 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/Swagger2Configuration.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/Swagger2Configuration.java @@ -1,6 +1,7 @@ -package cn.springcloud.gray.server.config; +package cn.springcloud.gray.server.configuration; -import org.springframework.boot.context.properties.ConfigurationProperties; +import cn.springcloud.gray.server.configuration.apidoc.PageableParameterAlternateTypeRuleConvention; +import com.fasterxml.classmate.TypeResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -27,12 +28,24 @@ */ @Configuration @EnableSwagger2 -@ConfigurationProperties //@Import(springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration.class) @ComponentScan({"cn.springcloud.gray.server.resources"}) public class Swagger2Configuration extends WebMvcConfigurerAdapter { +// @Bean +// @Order(Ordered.LOWEST_PRECEDENCE) +// public PageableParameterBuilderPlugin pageableParameterBuilderPlugin( +// TypeNameExtractor nameExtractor, TypeResolver resolver) { +// return new PageableParameterBuilderPlugin(nameExtractor, resolver); +// } + + @Bean + public PageableParameterAlternateTypeRuleConvention pageableParameterAlternateTypeRuleConvention(TypeResolver resolver) { + return new PageableParameterAlternateTypeRuleConvention(resolver); + } + + @Override public void addViewControllers(ViewControllerRegistry registry) { // registry.addViewController("/").setViewName("index.html"); diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/WebConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/WebConfiguration.java similarity index 96% rename from spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/WebConfiguration.java rename to spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/WebConfiguration.java index 4ff2f588..bfc756cb 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/WebConfiguration.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/WebConfiguration.java @@ -1,4 +1,4 @@ -package cn.springcloud.gray.server.config; +package cn.springcloud.gray.server.configuration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -71,7 +71,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo @Override public void destroy() { - // TODO Auto-generated method stub } } diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterAlternateTypeRuleConvention.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterAlternateTypeRuleConvention.java new file mode 100644 index 00000000..86e52801 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterAlternateTypeRuleConvention.java @@ -0,0 +1,48 @@ +package cn.springcloud.gray.server.configuration.apidoc; + +import com.fasterxml.classmate.TypeResolver; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.core.Ordered; +import org.springframework.data.domain.Pageable; +import springfox.documentation.schema.AlternateTypeRule; +import springfox.documentation.schema.AlternateTypeRuleConvention; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PageableParameterAlternateTypeRuleConvention implements AlternateTypeRuleConvention { + + private TypeResolver resolver; + + public PageableParameterAlternateTypeRuleConvention(TypeResolver resolver) { + this.resolver = resolver; + } + + @Override + public List rules() { + return new ArrayList(Arrays.asList(new AlternateTypeRule(resolver.resolve(Pageable.class), resolver.resolve(Page.class)))); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + + @ApiModel + @Data + public static class Page { + @ApiModelProperty(value = "第page页,从0开始计数", example = "0") + private Integer page = 0; + + @ApiModelProperty(value = "每页数据数量", example = "10") + private Integer size = 10; + + @ApiModelProperty("按属性排序,格式:属性(,asc|desc)") + private List sort; + } +} + diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterBuilderPlugin.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterBuilderPlugin.java new file mode 100755 index 00000000..cfbccfd0 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/apidoc/PageableParameterBuilderPlugin.java @@ -0,0 +1,94 @@ +package cn.springcloud.gray.server.configuration.apidoc; + +import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.classmate.TypeResolver; +import com.google.common.base.Function; +import org.springframework.data.domain.Pageable; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.schema.ModelReference; +import springfox.documentation.schema.ResolvedTypes; +import springfox.documentation.schema.TypeNameExtractor; +import springfox.documentation.service.Parameter; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.schema.contexts.ModelContext; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.spi.service.contexts.ParameterContext; + +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; +import static springfox.documentation.spi.schema.contexts.ModelContext.inputParam; + + +public class PageableParameterBuilderPlugin implements OperationBuilderPlugin { + private final TypeNameExtractor nameExtractor; + private final TypeResolver resolver; + private final ResolvedType pageableType; + + public PageableParameterBuilderPlugin(TypeNameExtractor nameExtractor, TypeResolver resolver) { + this.nameExtractor = nameExtractor; + this.resolver = resolver; + this.pageableType = resolver.resolve(Pageable.class); + } + + @Override + public void apply(OperationContext context) { + List methodParameters = context.getParameters(); + List parameters = newArrayList(); + + for (ResolvedMethodParameter methodParameter : methodParameters) { + ResolvedType resolvedType = methodParameter.getParameterType(); + + if (pageableType.equals(resolvedType)) { + ParameterContext parameterContext = new ParameterContext(methodParameter, + new ParameterBuilder(), + context.getDocumentationContext(), + context.getGenericsNamingStrategy(), + context); + Function factory = createModelRefFactory(parameterContext); + + ModelReference intModel = factory.apply(resolver.resolve(Integer.TYPE)); + ModelReference stringModel = factory.apply(resolver.resolve(List.class, String.class)); + + parameters.add(new ParameterBuilder() + .parameterType("query") + .name("page") + .modelRef(intModel) + .description("Results page you want to retrieve (0..N)").build()); + parameters.add(new ParameterBuilder() + .parameterType("query") + .name("size") + .modelRef(intModel) + .description("Number of records per page").build()); + parameters.add(new ParameterBuilder() + .parameterType("query") + .name("sort") + .modelRef(stringModel) + .allowMultiple(true) + .description("Sorting criteria in the format: property(,asc|desc). " + + "Default sort order is ascending. " + + "Multiple sort criteria are supported.") + .build()); + context.operationBuilder().parameters(parameters); + } + } + } + + @Override + public boolean supports(DocumentationType delimiter) { + return true; + } + + private Function createModelRefFactory(ParameterContext context) { + ModelContext modelContext = inputParam( + context.getGroupName(), + context.resolvedMethodParameter().getParameterType(), + context.getDocumentationType(), + context.getAlternateTypeProvider(), + context.getGenericNamingStrategy(), + context.getIgnorableParameterTypes()); + return ResolvedTypes.modelRefFactory(modelContext, nameExtractor); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/properties/GrayServerConfigBean.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/properties/GrayServerProperties.java similarity index 60% rename from spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/properties/GrayServerConfigBean.java rename to spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/properties/GrayServerProperties.java index 70877783..007cd5e7 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/config/properties/GrayServerConfigBean.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/configuration/properties/GrayServerProperties.java @@ -1,14 +1,12 @@ -package cn.springcloud.gray.server.config.properties; +package cn.springcloud.gray.server.configuration.properties; -import cn.springcloud.gray.server.GrayServerConfig; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "gray.server") -public class GrayServerConfigBean implements GrayServerConfig { +public class GrayServerProperties { private int evictionIntervalTimerInMs = 60000; - @Override public int getEvictionIntervalTimerInMs() { return evictionIntervalTimerInMs; } diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayDecisionMapper.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayDecisionMapper.java new file mode 100644 index 00000000..24108d0f --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayDecisionMapper.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.server.dao.mapper; + +import cn.springcloud.gray.server.dao.model.GrayDecisionDO; +import cn.springcloud.gray.server.dao.model.GrayPolicyDO; +import cn.springcloud.gray.server.module.domain.GrayDecision; +import cn.springcloud.gray.server.module.domain.GrayPolicy; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; + +@Mapper( + componentModel = "spring", + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS +) +public interface GrayDecisionMapper extends ModelMapper { + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayInstanceMapper.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayInstanceMapper.java new file mode 100644 index 00000000..88cc1036 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayInstanceMapper.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.server.dao.mapper; + +import cn.springcloud.gray.server.dao.model.GrayInstanceDO; +import cn.springcloud.gray.server.dao.model.GrayServiceDO; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.module.domain.GrayService; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; + +@Mapper( + componentModel = "spring", + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS +) +public interface GrayInstanceMapper extends ModelMapper { + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayPolicyMapper.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayPolicyMapper.java new file mode 100644 index 00000000..9a199082 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayPolicyMapper.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.server.dao.mapper; + +import cn.springcloud.gray.server.dao.model.GrayInstanceDO; +import cn.springcloud.gray.server.dao.model.GrayPolicyDO; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.module.domain.GrayPolicy; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; + +@Mapper( + componentModel = "spring", + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS +) +public interface GrayPolicyMapper extends ModelMapper { + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayServiceMapper.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayServiceMapper.java new file mode 100644 index 00000000..7a8ea6ce --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/GrayServiceMapper.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.server.dao.mapper; + +import cn.springcloud.gray.server.dao.model.GrayServiceDO; +import cn.springcloud.gray.server.module.domain.GrayService; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; + +@Mapper( + componentModel = "spring", + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS +) +public interface GrayServiceMapper extends ModelMapper { + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/ModelMapper.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/ModelMapper.java new file mode 100755 index 00000000..3a5a5104 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/mapper/ModelMapper.java @@ -0,0 +1,17 @@ +package cn.springcloud.gray.server.dao.mapper; + +import java.util.List; + +/** + * Created by saleson on 2017/7/20. + */ +public interface ModelMapper { + + DO model2do(MO d); + + List models2dos(Iterable d); + + MO do2model(DO d); + + List dos2models(List d); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayDecisionDO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayDecisionDO.java new file mode 100644 index 00000000..e3bf6238 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayDecisionDO.java @@ -0,0 +1,27 @@ +package cn.springcloud.gray.server.dao.model; + +import lombok.*; + +import javax.persistence.*; + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "gray_decision", indexes = {@Index(columnList = "policyId"), @Index(columnList = "instanceId")}) +public class GrayDecisionDO { + + @Id + @Column(length = 20) + private Long id; + @Column(length = 20) + private Long policyId; + @Column(length = 64) + private String instanceId; + @Column(length = 64) + private String name; + @Column(length = 256) + private String infos; +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayInstanceDO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayInstanceDO.java new file mode 100644 index 00000000..ca6682d1 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayInstanceDO.java @@ -0,0 +1,37 @@ +package cn.springcloud.gray.server.dao.model; + +import lombok.*; + +import javax.persistence.*; + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "gray_instance", indexes = {@Index(columnList = "serviceId")}) +public class GrayInstanceDO { + @Id + @Column(length = 64) + private String instanceId; + @Column(length = 32) + private String serviceId; + @Column(length = 32) + private String host; + @Column(length = 5) + private Integer port; + + /** + * 实例状态 + */ + @Column(length = 16) + private String instanceStatus; + /** + * 灰度状态 + */ + @Column(length = 16) + private String grayStatus; + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayPolicyDO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayPolicyDO.java new file mode 100644 index 00000000..1eab9cf0 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayPolicyDO.java @@ -0,0 +1,23 @@ +package cn.springcloud.gray.server.dao.model; + +import lombok.*; + +import javax.persistence.*; + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "gray_policy", indexes = {@Index(columnList = "instanceId")}) +public class GrayPolicyDO { + + @Id + @Column(length = 20) + private Long id; + @Column(length = 64) + private String instanceId; + @Column(length = 256, name = "alias_name") + private String alias; +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayServiceDO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayServiceDO.java new file mode 100644 index 00000000..9895cb13 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/model/GrayServiceDO.java @@ -0,0 +1,32 @@ +package cn.springcloud.gray.server.dao.model; + +import lombok.*; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "gray_service") +public class GrayServiceDO { + + @Id + @Column(length = 32) + private String serviceId; + @Column(length = 64) + private String serviceName; + @Column(length = 4) + private Integer instanceNumber; + @Column(length = 4) + private Integer grayInstanceNumber; + @Column(length = 256, name = "des") + private String describe; + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayDecisionRepository.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayDecisionRepository.java new file mode 100644 index 00000000..00266f43 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayDecisionRepository.java @@ -0,0 +1,19 @@ +package cn.springcloud.gray.server.dao.repository; + +import cn.springcloud.gray.server.dao.model.GrayDecisionDO; +import cn.springcloud.gray.server.module.domain.GrayDecision; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface GrayDecisionRepository extends JpaRepository { + List findByPolicyId(Long policyId); + + void deleteAllByPolicyId(Long id); + + Page findAllByPolicyId(Long policyId, Pageable pageable); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayInstanceRepository.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayInstanceRepository.java new file mode 100644 index 00000000..96508317 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayInstanceRepository.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.server.dao.repository; + +import cn.springcloud.gray.server.dao.model.GrayInstanceDO; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface GrayInstanceRepository extends JpaRepository { + List findByServiceId(String serviceId); + + List findAllByGrayStatus(String status); + + List findAllByGrayStatusAndInstanceStatus(String name, String name1); + + Page findAllByServiceId(String serviceId, Pageable pageable); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayPolicyRepository.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayPolicyRepository.java new file mode 100644 index 00000000..4f04727f --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayPolicyRepository.java @@ -0,0 +1,16 @@ +package cn.springcloud.gray.server.dao.repository; + +import cn.springcloud.gray.server.dao.model.GrayPolicyDO; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface GrayPolicyRepository extends JpaRepository { + List findByInstanceId(String instanceId); + + Page findAllByInstanceId(String instanceId, Pageable pageable); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayServiceRepository.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayServiceRepository.java new file mode 100644 index 00000000..dc43f18e --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/dao/repository/GrayServiceRepository.java @@ -0,0 +1,9 @@ +package cn.springcloud.gray.server.dao.repository; + +import cn.springcloud.gray.server.dao.model.GrayServiceDO; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface GrayServiceRepository extends JpaRepository { +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/InstanceInfo.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/InstanceInfo.java new file mode 100644 index 00000000..ee3be952 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/InstanceInfo.java @@ -0,0 +1,8 @@ +package cn.springcloud.gray.server.discovery; + +import lombok.*; + +@Setter +@Getter +public class InstanceInfo { +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceDiscover.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceDiscover.java new file mode 100644 index 00000000..995c3100 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceDiscover.java @@ -0,0 +1,19 @@ +package cn.springcloud.gray.server.discovery; + +import java.util.List; + + +/** + * 从注册中心获取服务信息 + */ +public interface ServiceDiscover { + + List listAllSerivceInfos(); + + ServiceInfo getServiceInfo(String serviceId); + + List listInstanceInfos(String serviceId); + + InstanceInfo getInstanceInfo(String serviceId, String instanceId); + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceInfo.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceInfo.java new file mode 100644 index 00000000..96645471 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/discovery/ServiceInfo.java @@ -0,0 +1,8 @@ +package cn.springcloud.gray.server.discovery; + +import lombok.*; + +@Setter +@Getter +public class ServiceInfo { +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/DefaultGrayEventPublisher.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/DefaultGrayEventPublisher.java new file mode 100644 index 00000000..cf4630e5 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/DefaultGrayEventPublisher.java @@ -0,0 +1,13 @@ +package cn.springcloud.gray.server.event; + +import cn.springcloud.gray.event.GrayEventMsg; +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.exceptions.EventException; + +public class DefaultGrayEventPublisher implements GrayEventPublisher { + + @Override + public void publishEvent(GrayEventMsg msg) throws EventException { + + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamGrayEventPublisher.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamGrayEventPublisher.java new file mode 100755 index 00000000..9282fc90 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamGrayEventPublisher.java @@ -0,0 +1,26 @@ +package cn.springcloud.gray.server.event.stream; + + +import cn.springcloud.gray.event.GrayEventMsg; +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.exceptions.EventException; +import org.springframework.messaging.support.MessageBuilder; + + +public class StreamGrayEventPublisher implements GrayEventPublisher { + + private StreamOutput sender; + + public StreamGrayEventPublisher(StreamOutput sender) { + this.sender = sender; + } + + public boolean send(Object obj) { + return sender.output().send(MessageBuilder.withPayload(obj).build()); + } + + @Override + public void publishEvent(GrayEventMsg msg) throws EventException { + send(msg); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamOutput.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamOutput.java new file mode 100644 index 00000000..9995f96b --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/event/stream/StreamOutput.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.server.event.stream; + +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; + +public interface StreamOutput { + + + String OUTPUT = "GrayEventOutput"; + + + @Output(OUTPUT) + MessageChannel output(); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/GrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/GrayServerEvictor.java new file mode 100644 index 00000000..60cb8dab --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/GrayServerEvictor.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.server.evictor; + + +import cn.springcloud.gray.server.module.GrayServerModule; + +/** + * + */ +public interface GrayServerEvictor { + + void evict(GrayServerModule grayServerModule); + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/NoActionGrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/NoActionGrayServerEvictor.java similarity index 59% rename from spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/NoActionGrayServerEvictor.java rename to spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/NoActionGrayServerEvictor.java index 67f3e2d4..902f93db 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/NoActionGrayServerEvictor.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/evictor/NoActionGrayServerEvictor.java @@ -1,6 +1,6 @@ -package cn.springcloud.gray.server; +package cn.springcloud.gray.server.evictor; -import cn.springcloud.gray.core.GrayServiceManager; +import cn.springcloud.gray.server.module.GrayServerModule; public class NoActionGrayServerEvictor implements GrayServerEvictor { @@ -13,7 +13,7 @@ private NoActionGrayServerEvictor() { } @Override - public void evict(GrayServiceManager serviceManager) { + public void evict(GrayServerModule grayServerModule) { } } diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/DefaultGrayServiceManager.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/DefaultGrayServiceManager.java new file mode 100644 index 00000000..3f550a07 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/DefaultGrayServiceManager.java @@ -0,0 +1,60 @@ +package cn.springcloud.gray.server.manager; + +import cn.springcloud.gray.server.configuration.properties.GrayServerProperties; +import cn.springcloud.gray.server.evictor.GrayServerEvictor; +import cn.springcloud.gray.server.module.GrayServerModule; + +import java.util.*; + + +/** + * 维护一个Map, 用来管理GrayService,key是service id。 + * 并且每隔一段时间就调用EurekaGrayServerEvictor,检查列表中的实例是否下线,将下线的服务从灰度列表中删除。 + */ +public class DefaultGrayServiceManager implements GrayServiceManager { + + + private GrayServerProperties grayServerProperties; + private Timer evictionTimer = new Timer("Gray-EvictionTimer", true); + private GrayServerModule grayServerModule; + private GrayServerEvictor grayServerEvictor; + + + public DefaultGrayServiceManager(GrayServerProperties grayServerProperties, GrayServerModule grayServerModule, GrayServerEvictor grayServerEvictor) { + this.grayServerProperties = grayServerProperties; + this.grayServerModule = grayServerModule; + this.grayServerEvictor = grayServerEvictor; + } + + @Override + public GrayServerModule getGrayServerModule() { + return grayServerModule; + } + + @Override + public void openForWork() { + evictionTimer.schedule(new EvictionTask(), + grayServerProperties.getEvictionIntervalTimerInMs(), + grayServerProperties.getEvictionIntervalTimerInMs()); + } + + @Override + public void shutdown() { + evictionTimer.cancel(); + } + + + protected void evict() { + grayServerEvictor.evict(getGrayServerModule()); + } + + + class EvictionTask extends TimerTask { + + @Override + public void run() { + evict(); + } + } + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/GrayServiceManager.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/GrayServiceManager.java new file mode 100644 index 00000000..c7ad817c --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/manager/GrayServiceManager.java @@ -0,0 +1,21 @@ +package cn.springcloud.gray.server.manager; + + +import cn.springcloud.gray.server.module.GrayServerModule; + +/** + * 灰度服务管理类,属于服务端的类。主要是编辑服务实例,编辑灰度策略,以及维护最新的灰度列表。 + */ +public interface GrayServiceManager { + + GrayServerModule getGrayServerModule(); + + + /** + * 打开检查 + */ + void openForWork(); + + + void shutdown(); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayModle.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayModle.java new file mode 100644 index 00000000..8b2e36b2 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayModle.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.server.module; + +import cn.springcloud.gray.model.GrayInstance; + +import java.util.List; + +public interface GrayModle { + + GrayServerModule getGrayServerModule(); + + List allOpenInstances(); + + GrayInstance getGrayInstance(String serviceId, String instanceId); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayServerModule.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayServerModule.java new file mode 100644 index 00000000..d0f5379c --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/GrayServerModule.java @@ -0,0 +1,72 @@ +package cn.springcloud.gray.server.module; + +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.model.GrayStatus; +import cn.springcloud.gray.server.module.domain.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface GrayServerModule { + + List allGrayServices(); + + List listGrayInstancesBySerivceId(String serviceId); + + List listGrayInstancesByStatus(GrayStatus grayStatus, InstanceStatus instanceStatus); + + default void closeGray(String instanceId) { + updateGrayStatus(instanceId, GrayStatus.CLOSE); + } + + void deleteGrayService(String serviceId); + + default void instanceShutdown(String instanceId) { + updateInstanceStatus(instanceId, InstanceStatus.DOWN); + } + + default void openGray(String instanceId) { + updateGrayStatus(instanceId, GrayStatus.OPEN); + } + + void updateGrayStatus(String instanceId, GrayStatus grayStatus); + + void saveGrayInstance(GrayInstance instance); + + void updateInstanceStatus(String instanceId, InstanceStatus instanceStatus); + + void deleteGrayInstance(String intanceId); + + void saveGrayPolicy(GrayPolicy grayPolicy); + + void deleteGrayPolicy(Long policyId); + + void saveGrayDecision(GrayDecision grayDecision); + + void deleteGrayDecision(Long decisionId); + + GrayDecision getGrayDecision(Long id); + + List listGrayDecisionsByPolicyId(Long policyId); + + List listGrayInstancesByServiceId(String serviceId); + + GrayInstance getGrayInstance(String id); + + List listAllGrayServices(); + + void saveGrayService(GrayService grayPolicy); + + GrayService getGrayService(String id); + + List listGrayPoliciesByInstanceId(String instanceId); + + Page listAllGrayServices(Pageable pageable); + + Page listGrayPoliciesByInstanceId(String instanceId, Pageable pageable); + + Page listGrayInstancesByServiceId(String serviceId, Pageable pageable); + + Page listGrayDecisionsByPolicyId(Long policyId, Pageable pageable); +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayModule.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayModule.java new file mode 100644 index 00000000..825e2cf6 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayModule.java @@ -0,0 +1,122 @@ +package cn.springcloud.gray.server.module; + +import cn.springcloud.gray.model.DecisionDefinition; +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.model.GrayStatus; +import cn.springcloud.gray.model.PolicyDefinition; +import cn.springcloud.gray.server.module.domain.GrayDecision; +import cn.springcloud.gray.server.module.domain.GrayPolicy; +import cn.springcloud.gray.server.module.domain.InstanceStatus; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Slf4j +public class SimpleGrayModule implements GrayModle { + + private GrayServerModule grayServerModule; + private ObjectMapper objectMapper; + + public SimpleGrayModule(GrayServerModule grayServerModule, ObjectMapper objectMapper) { + this.grayServerModule = grayServerModule; + this.objectMapper = objectMapper; + } + + @Override + public GrayServerModule getGrayServerModule() { + return grayServerModule; + } + + @Override + public GrayInstance getGrayInstance(String serviceId, String instanceId) { + cn.springcloud.gray.server.module.domain.GrayInstance grayInstance = grayServerModule.getGrayInstance(instanceId); + if (grayInstance == null) { + return null; + } + return ofGrayInstanceInfo(grayInstance); + } + + + @Override + public List allOpenInstances() { + List instances = grayServerModule.listGrayInstancesByStatus(GrayStatus.OPEN, InstanceStatus.UP); + + List grayInstances = new ArrayList<>(instances.size()); + instances.forEach(instance -> { + cn.springcloud.gray.model.GrayInstance grayInstance = ofGrayInstanceInfo(instance); + grayInstances.add(grayInstance); + }); + return grayInstances; + } + + private cn.springcloud.gray.model.GrayInstance ofGrayInstanceInfo(cn.springcloud.gray.server.module.domain.GrayInstance instance) { + cn.springcloud.gray.model.GrayInstance grayInstance = ofGrayInstance(instance); + grayInstance.setPolicyDefinitions(ofGrayPoliciesByInstanceId(instance.getInstanceId())); + return grayInstance; + } + + + private cn.springcloud.gray.model.GrayInstance ofGrayInstance(cn.springcloud.gray.server.module.domain.GrayInstance instance) { + cn.springcloud.gray.model.GrayInstance grayInstance = new cn.springcloud.gray.model.GrayInstance(); + grayInstance.setPort(instance.getPort()); + grayInstance.setServiceId(instance.getServiceId()); + grayInstance.setInstanceId(instance.getInstanceId()); + grayInstance.setGrayStatus(instance.getGrayStatus()); + grayInstance.setHost(instance.getHost()); + return grayInstance; + } + + private List ofGrayPoliciesByInstanceId(String instanceId) { + List grayPolicies = grayServerModule.listGrayPoliciesByInstanceId(instanceId); + List policyDefinitions = new ArrayList<>(grayPolicies.size()); + grayPolicies.forEach(grayPolicy -> { + PolicyDefinition policyDefinition = ofGrayPolicy(grayPolicy); + policyDefinition.setList(ofGrayDecisionByPolicyId(grayPolicy.getId())); + policyDefinitions.add(policyDefinition); + }); + + return policyDefinitions; + } + + private PolicyDefinition ofGrayPolicy(GrayPolicy grayPolicy) { + PolicyDefinition policyDefinition = new PolicyDefinition(); + policyDefinition.setAlias(grayPolicy.getAlias()); + policyDefinition.setPolicyId(String.valueOf(grayPolicy.getId())); + return policyDefinition; + } + + private List ofGrayDecisionByPolicyId(Long policyId) { + List grayDecisions = grayServerModule.listGrayDecisionsByPolicyId(policyId); + List decisionDefinitions = new ArrayList<>(grayDecisions.size()); + grayDecisions.forEach(grayDecision -> { + try { + DecisionDefinition definition = ofGrayDecision(grayDecision); + if (definition != null) { + decisionDefinitions.add(definition); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + }); + return decisionDefinitions; + } + + + private DecisionDefinition ofGrayDecision(GrayDecision grayDecision) throws IOException { + DecisionDefinition decisionDefinition = new DecisionDefinition(); + decisionDefinition.setId(String.valueOf(grayDecision.getId())); + decisionDefinition.setName(grayDecision.getName()); + if (StringUtils.isEmpty(grayDecision.getInfos())) { + return null; + } + decisionDefinition.setInfos(objectMapper.readValue(grayDecision.getInfos(), new TypeReference>() { + })); + return decisionDefinition; + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayServerModule.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayServerModule.java new file mode 100644 index 00000000..a16aaba4 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/SimpleGrayServerModule.java @@ -0,0 +1,235 @@ +package cn.springcloud.gray.server.module; + +import cn.springcloud.gray.event.EventType; +import cn.springcloud.gray.event.GrayEventMsg; +import cn.springcloud.gray.event.GrayEventPublisher; +import cn.springcloud.gray.model.GrayStatus; +import cn.springcloud.gray.server.module.domain.*; +import cn.springcloud.gray.server.service.GrayDecisionService; +import cn.springcloud.gray.server.service.GrayInstanceService; +import cn.springcloud.gray.server.service.GrayPolicyService; +import cn.springcloud.gray.server.service.GrayServiceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@Slf4j +public class SimpleGrayServerModule implements GrayServerModule { + + private GrayServiceService grayServiceService; + private GrayInstanceService grayInstanceService; + private GrayDecisionService grayDecisionService; + private GrayPolicyService grayPolicyService; + private GrayEventPublisher grayEventPublisher; + + public SimpleGrayServerModule(GrayEventPublisher grayEventPublisher, GrayServiceService grayServiceService, + GrayInstanceService grayInstanceService, + GrayDecisionService grayDecisionService, GrayPolicyService grayPolicyService) { + this.grayEventPublisher = grayEventPublisher; + this.grayServiceService = grayServiceService; + this.grayInstanceService = grayInstanceService; + this.grayDecisionService = grayDecisionService; + this.grayPolicyService = grayPolicyService; + } + + @Override + public List allGrayServices() { + return grayServiceService.findAllModel(); + } + + @Override + public List listGrayInstancesBySerivceId(String serviceId) { + return grayInstanceService.findByServiceId(serviceId); + } + + @Override + public List listGrayInstancesByStatus(GrayStatus grayStatus, InstanceStatus instanceStatus) { + return grayInstanceService.findAllByStatus(grayStatus, instanceStatus); + } + + @Override + public void deleteGrayService(String serviceId) { + List grayInstances = grayInstanceService.findByServiceId(serviceId); + grayServiceService.deleteReactById(serviceId); + grayInstances.forEach(this::publishDownIntanceEvent); + } + + + @Override + public void updateGrayStatus(String instanceId, GrayStatus grayStatus) { + GrayInstance instance = grayInstanceService.findOneModel(instanceId); + if (instance != null && instance.getGrayStatus() != grayStatus) { + instance.setGrayStatus(grayStatus); + grayInstanceService.saveModel(instance); + if (grayStatus == GrayStatus.OPEN) { + publishUpdateIntanceEvent(instance); + } else { + publishDownIntanceEvent(instance); + } + } + } + + + @Override + public void saveGrayInstance(GrayInstance instance) { + grayInstanceService.saveModel(instance); + } + + @Override + public void updateInstanceStatus(String instanceId, InstanceStatus instanceStatus) { + GrayInstance instance = grayInstanceService.findOneModel(instanceId); + if (instance != null && instance.getInstanceStatus() != instanceStatus) { + instance.setInstanceStatus(instanceStatus); + grayInstanceService.saveModel(instance); + if (instanceStatus == InstanceStatus.UP) { + publishUpdateIntanceEvent(instance); + } else { + publishDownIntanceEvent(instance); + } + } + } + + @Override + public void deleteGrayInstance(String intanceId) { + GrayInstance grayInstance = grayInstanceService.findOneModel(intanceId); + if (grayInstance != null) { + grayInstanceService.deleteReactById(intanceId); + publishDownIntanceEvent(grayInstance); + } + } + + @Override + public void saveGrayPolicy(GrayPolicy grayPolicy) { + grayPolicyService.saveModel(grayPolicy); + publishUpdateIntanceEvent(grayInstanceService.findOneModel(grayPolicy.getInstanceId())); + } + + @Override + public void deleteGrayPolicy(Long policyId) { + GrayPolicy policy = grayPolicyService.findOneModel(policyId); + if (policy != null) { + grayPolicyService.deleteReactById(policyId); + publishUpdateIntanceEvent(grayInstanceService.findOneModel(policy.getInstanceId())); + } + } + + @Override + public void saveGrayDecision(GrayDecision grayDecision) { + grayDecisionService.saveModel(grayDecision); + publishUpdateIntanceEvent(grayInstanceService.findOneModel(grayDecision.getInstanceId())); + } + + @Override + public void deleteGrayDecision(Long decisionId) { + GrayDecision decision = grayDecisionService.findOneModel(decisionId); + if (decision != null) { + grayDecisionService.delete(decisionId); + publishUpdateIntanceEvent(grayInstanceService.findOneModel(decision.getInstanceId())); + } + + } + + + @Override + public GrayDecision getGrayDecision(Long id) { + return grayDecisionService.findOneModel(id); + } + + @Override + public List listGrayDecisionsByPolicyId(Long policyId) { + return grayDecisionService.findByPolicyId(policyId); + } + + @Override + public List listGrayInstancesByServiceId(String serviceId) { + return grayInstanceService.findByServiceId(serviceId); + } + + @Override + public GrayInstance getGrayInstance(String id) { + return grayInstanceService.findOneModel(id); + } + + @Override + public List listAllGrayServices() { + return grayServiceService.findAllModel(); + } + + @Override + public void saveGrayService(GrayService grayPolicy) { + grayServiceService.saveModel(grayPolicy); + } + + @Override + public GrayService getGrayService(String id) { + return null; + } + + @Override + public List listGrayPoliciesByInstanceId(String instanceId) { + return grayPolicyService.findByInstanceId(instanceId); + } + + @Override + public Page listAllGrayServices(Pageable pageable) { + return grayServiceService.listAllGrayServices(pageable); + } + + @Override + public Page listGrayPoliciesByInstanceId(String instanceId, Pageable pageable) { + return grayPolicyService.listGrayPoliciesByInstanceId(instanceId, pageable); + } + + @Override + public Page listGrayInstancesByServiceId(String serviceId, Pageable pageable) { + return grayInstanceService.listGrayInstancesByServiceId(serviceId, pageable); + } + + @Override + public Page listGrayDecisionsByPolicyId(Long policyId, Pageable pageable) { + return grayDecisionService.listGrayDecisionsByPolicyId(policyId, pageable); + } + + protected GrayEventPublisher getGrayEventPublisher() { + return grayEventPublisher; + } + + + private void publishUpdateIntanceEvent(String serviceId, String instanceId) { + publishGrayEvent(serviceId, instanceId, EventType.UPDATE); + } + + + private void publishDownIntanceEvent(String serviceId, String instanceId) { + publishGrayEvent(serviceId, instanceId, EventType.DOWN); + } + + private void publishUpdateIntanceEvent(GrayInstance grayInstance) { + this.publishGrayEvent(grayInstance, EventType.UPDATE); + } + + + private void publishDownIntanceEvent(GrayInstance grayInstance) { + publishGrayEvent(grayInstance, EventType.DOWN); + } + + private void publishGrayEvent(GrayInstance grayInstance, EventType eventType) { + publishGrayEvent(grayInstance.getServiceId(), grayInstance.getInstanceId(), eventType); + } + + + private void publishGrayEvent(String serviceId, String instanceId, EventType eventType) { + GrayEventMsg eventMsg = new GrayEventMsg(); + eventMsg.setInstanceId(instanceId); + eventMsg.setServiceId(serviceId); + eventMsg.setEventType(eventType); + publishGrayEvent(eventMsg); + } + + private void publishGrayEvent(GrayEventMsg eventMsg) { + getGrayEventPublisher().publishEvent(eventMsg); + + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayDecision.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayDecision.java new file mode 100644 index 00000000..e0b036f3 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayDecision.java @@ -0,0 +1,23 @@ +package cn.springcloud.gray.server.module.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + + +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GrayDecision { + + private Long id; + @ApiModelProperty("实例id") + private String instanceId; + @ApiModelProperty("策略id") + private Long policyId; + @ApiModelProperty("灰度决策名称") + private String name; + @ApiModelProperty("决策参数") + private String infos; +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayInstance.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayInstance.java new file mode 100644 index 00000000..75a173b1 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayInstance.java @@ -0,0 +1,38 @@ +package cn.springcloud.gray.server.module.domain; + +import cn.springcloud.gray.model.GrayStatus; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + + +@ApiModel("实例的灰度信息") +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GrayInstance { + + + @ApiModelProperty("服务实例id") + private String instanceId; + @ApiModelProperty("服务id") + private String serviceId; + private String host; + @ApiModelProperty("服务实例端口") + private Integer port; + + /** + * 实例状态 + */ + @ApiModelProperty("服务实例状态") + private InstanceStatus instanceStatus; + /** + * 灰度状态 + */ + @ApiModelProperty("灰度状态") + private GrayStatus grayStatus; + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayPolicy.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayPolicy.java new file mode 100644 index 00000000..cf7085d4 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayPolicy.java @@ -0,0 +1,19 @@ +package cn.springcloud.gray.server.module.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + + +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GrayPolicy { + + private Long id; + @ApiModelProperty("服务实例id") + private String instanceId; + @ApiModelProperty("策略别名") + private String alias; +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayService.java new file mode 100644 index 00000000..4edb65d5 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/GrayService.java @@ -0,0 +1,27 @@ +package cn.springcloud.gray.server.module.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + + +@Setter +@Getter +@Builder +@ApiModel +@AllArgsConstructor +@NoArgsConstructor +public class GrayService { + + @ApiModelProperty("服务id") + private String serviceId; + @ApiModelProperty("服务名称") + private String serviceName; + @ApiModelProperty("服务实例个数") + private Integer instanceNumber; + @ApiModelProperty("灰度实例个数") + private Integer grayInstanceNumber; + @ApiModelProperty("服务描述") + private String describe; + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/InstanceStatus.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/InstanceStatus.java new file mode 100644 index 00000000..07c0079d --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/module/domain/InstanceStatus.java @@ -0,0 +1,6 @@ +package cn.springcloud.gray.server.module.domain; + +public enum InstanceStatus { + + UP, DOWN, UNKNOWN +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/EurekaGrayServerEvictor.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/EurekaGrayServerEvictor.java new file mode 100644 index 00000000..8df42b03 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/EurekaGrayServerEvictor.java @@ -0,0 +1,55 @@ +package cn.springcloud.gray.server.netflix.eureka; + +import cn.springcloud.gray.server.evictor.GrayServerEvictor; +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.module.domain.InstanceStatus; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; + + +/** + * 依赖EurekaClient来检查服务实例是否下线 + */ +public class EurekaGrayServerEvictor implements GrayServerEvictor { + + private EurekaClient eurekaClient; + + + public EurekaGrayServerEvictor(EurekaClient eurekaClient) { + this.eurekaClient = eurekaClient; + } + + private void evict(GrayServerModule grayServerModule, InstanceInfo instanceInfo, GrayInstance grayInstance) { + InstanceStatus instanceStatus = getInstanceStatus(instanceInfo); + if (grayInstance.getInstanceStatus() != instanceStatus) { + grayServerModule.updateInstanceStatus(grayInstance.getInstanceId(), instanceStatus); + } + } + + + private InstanceStatus getInstanceStatus(InstanceInfo instanceInfo) { + if (instanceInfo == null) { + return InstanceStatus.DOWN; + } + InstanceInfo.InstanceStatus status = instanceInfo.getStatus(); + if (status == InstanceInfo.InstanceStatus.UP) { + return InstanceStatus.UP; + } + return InstanceStatus.UNKNOWN; + } + + + @Override + public void evict(GrayServerModule grayServerModule) { + grayServerModule.allGrayServices().forEach(grayService -> { + Application app = eurekaClient.getApplication(grayService.getServiceId()); + if (app != null) { + grayServerModule.listGrayInstancesBySerivceId(grayService.getServiceId()).forEach(instance -> { + evict(grayServerModule, app.getByInstanceId(instance.getInstanceId()), instance); + }); + } + }); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/configuration/GrayServiceEurekaAutoConfiguration.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/configuration/GrayServiceEurekaAutoConfiguration.java new file mode 100644 index 00000000..42b1a40b --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/netflix/eureka/configuration/GrayServiceEurekaAutoConfiguration.java @@ -0,0 +1,32 @@ +package cn.springcloud.gray.server.netflix.eureka.configuration; + +/** + * @Author: duozl + * @Date: 2018/6/4 18:59 + */ + +import cn.springcloud.gray.server.evictor.GrayServerEvictor; +import cn.springcloud.gray.server.netflix.eureka.EurekaGrayServerEvictor; +import com.netflix.discovery.EurekaClient; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +//import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnBean(EurekaClient.class) +public class GrayServiceEurekaAutoConfiguration { + +// @Autowired +// private EurekaClient eurekaClient; +// @Autowired +// private DiscoveryClient discoveryClient; + + + @Bean + public GrayServerEvictor grayServerEvictor(EurekaClient eurekaClient) { + return new EurekaGrayServerEvictor(eurekaClient); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/Res.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/Res.java new file mode 100644 index 00000000..3680ca02 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/Res.java @@ -0,0 +1,14 @@ +package cn.springcloud.gray.server.resources.domain; + +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Res { + + private String code; + private String msg; + private T result; +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/fo/GrayPolicyGroupFO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/fo/GrayPolicyGroupFO.java deleted file mode 100644 index b1d5e3d2..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/fo/GrayPolicyGroupFO.java +++ /dev/null @@ -1,67 +0,0 @@ -package cn.springcloud.gray.server.resources.domain.fo; - -import cn.springcloud.gray.core.GrayPolicy; -import cn.springcloud.gray.core.GrayPolicyGroup; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; - -import java.util.List; - -@ApiModel -public class GrayPolicyGroupFO { - private String instanceId; - private String policyGroupId; - private String alias; - private List policies; - @ApiModelProperty("0:关闭, 1:启用") - private boolean enable; - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getInstanceId() { - return instanceId; - } - - public void setInstanceId(String instanceId) { - this.instanceId = instanceId; - } - - public String getPolicyGroupId() { - return policyGroupId; - } - - public void setPolicyGroupId(String policyGroupId) { - this.policyGroupId = policyGroupId; - } - - public List getPolicies() { - return policies; - } - - public void setPolicies(List policies) { - this.policies = policies; - } - - public boolean isEnable() { - return enable; - } - - public void setEnable(boolean enable) { - this.enable = enable; - } - - public GrayPolicyGroup toGrayPolicyGroup() { - GrayPolicyGroup policyGroup = new GrayPolicyGroup(); - policyGroup.setAlias(this.getAlias()); - policyGroup.setList(this.getPolicies()); - policyGroup.setEnable(this.isEnable()); - policyGroup.setPolicyGroupId(this.getPolicyGroupId()); - return policyGroup; - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayInstanceVO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayInstanceVO.java deleted file mode 100644 index fd4567bc..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayInstanceVO.java +++ /dev/null @@ -1,71 +0,0 @@ -package cn.springcloud.gray.server.resources.domain.vo; - -import java.util.Map; - -public class GrayInstanceVO { - - private String serviceId; - private String instanceId; - private String appName; - private String url; - private Map metadata; - private boolean hasGrayPolicies; - private boolean openGray; - - - public String getServiceId() { - return serviceId; - } - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public String getInstanceId() { - return instanceId; - } - - public void setInstanceId(String instanceId) { - this.instanceId = instanceId; - } - - public String getAppName() { - return appName; - } - - public void setAppName(String appName) { - this.appName = appName; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Map getMetadata() { - return metadata; - } - - public void setMetadata(Map metadata) { - this.metadata = metadata; - } - - public boolean isHasGrayPolicies() { - return hasGrayPolicies; - } - - public void setHasGrayPolicies(boolean hasGrayPolicies) { - this.hasGrayPolicies = hasGrayPolicies; - } - - public boolean isOpenGray() { - return openGray; - } - - public void setOpenGray(boolean openGray) { - this.openGray = openGray; - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayPolicyGroupVO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayPolicyGroupVO.java deleted file mode 100644 index 00da7d4a..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayPolicyGroupVO.java +++ /dev/null @@ -1,81 +0,0 @@ -package cn.springcloud.gray.server.resources.domain.vo; - -import cn.springcloud.gray.core.GrayPolicy; - -import java.util.List; - -public class GrayPolicyGroupVO { - private String serviceId; - private String instanceId; - private String appName; - private String url; - private String policyGroupId; - private String alias; - private List policies; - private boolean enable; - - - public String getServiceId() { - return serviceId; - } - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public String getInstanceId() { - return instanceId; - } - - public void setInstanceId(String instanceId) { - this.instanceId = instanceId; - } - - public String getAppName() { - return appName; - } - - public void setAppName(String appName) { - this.appName = appName; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public List getPolicies() { - return policies; - } - - public void setPolicies(List policies) { - this.policies = policies; - } - - public String getPolicyGroupId() { - return policyGroupId; - } - - public void setPolicyGroupId(String policyGroupId) { - this.policyGroupId = policyGroupId; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public boolean isEnable() { - return enable; - } - - public void setEnable(boolean enable) { - this.enable = enable; - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayServiceVO.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayServiceVO.java deleted file mode 100644 index 2c26987d..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/domain/vo/GrayServiceVO.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.springcloud.gray.server.resources.domain.vo; - -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; - -@ApiModel -public class GrayServiceVO { - - @ApiModelProperty("服务名") - private String appName; - - @ApiModelProperty("服务id") - private String serviceId; - - @ApiModelProperty("服务实例数") - private int instanceSize; - - - @ApiModelProperty("是否拥有灰度实例") - private boolean hasGrayInstances; - - @ApiModelProperty("是否拥有灰度策略") - private boolean hasGrayPolicies; - - public String getServiceId() { - return serviceId; - } - - public void setServiceId(String serviceId) { - this.serviceId = serviceId; - } - - public int getInstanceSize() { - return instanceSize; - } - - public void setInstanceSize(int instanceSize) { - this.instanceSize = instanceSize; - } - - public boolean isHasGrayInstances() { - return hasGrayInstances; - } - - public void setHasGrayInstances(boolean hasGrayInstances) { - this.hasGrayInstances = hasGrayInstances; - } - - - public String getAppName() { - return appName; - } - - public void setAppName(String appName) { - this.appName = appName; - } - - public boolean isHasGrayPolicies() { - return hasGrayPolicies; - } - - public void setHasGrayPolicies(boolean hasGrayPolicies) { - this.hasGrayPolicies = hasGrayPolicies; - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayDecisionResource.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayDecisionResource.java new file mode 100644 index 00000000..28d6ff83 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayDecisionResource.java @@ -0,0 +1,59 @@ +package cn.springcloud.gray.server.resources.rest; + +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.domain.GrayDecision; +import cn.springcloud.gray.server.utils.PaginationUtils; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/gray/decision") +public class GrayDecisionResource { + + + @Autowired + private GrayServerModule grayServerModule; + + @RequestMapping(value = "/list", method = RequestMethod.GET, params = {"policyId"}) + public List list(@RequestParam("policyId") Long policyId) { + return grayServerModule.listGrayDecisionsByPolicyId(policyId); + } + + @GetMapping(value = "/page") + public ResponseEntity> page( + @RequestParam("policyId") Long policyId, + @ApiParam @PageableDefault(direction = Sort.Direction.DESC) Pageable pageable) { + Page page = grayServerModule.listGrayDecisionsByPolicyId(policyId, pageable); + HttpHeaders headers = PaginationUtils.generatePaginationHttpHeaders(page); + return new ResponseEntity>( + page.getContent(), + headers, + HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public GrayDecision info(@PathVariable("id") Long id) { + return grayServerModule.getGrayDecision(id); + } + + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") Long id) { + grayServerModule.deleteGrayDecision(id); + } + + @RequestMapping(value = "/", method = RequestMethod.POST) + public void save(@RequestBody GrayDecision grayDecision) { + grayServerModule.saveGrayDecision(grayDecision); + } + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayInstanceResource.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayInstanceResource.java new file mode 100644 index 00000000..142019b4 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayInstanceResource.java @@ -0,0 +1,78 @@ +package cn.springcloud.gray.server.resources.rest; + +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.utils.PaginationUtils; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/gray/instance") +public class GrayInstanceResource { + + + @Autowired + private GrayServerModule grayServerModule; + + @RequestMapping(value = "/list", method = RequestMethod.GET, params = {"serviceId"}) + public List listByServiceId(@RequestParam("serviceId") String serviceId) { + return grayServerModule.listGrayInstancesByServiceId(serviceId); + } + + @GetMapping(value = "/page") + public ResponseEntity> page( + @RequestParam("serviceId") String serviceId, + @ApiParam @PageableDefault(direction = Sort.Direction.DESC) Pageable pageable) { + Page page = grayServerModule.listGrayInstancesByServiceId(serviceId, pageable); + HttpHeaders headers = PaginationUtils.generatePaginationHttpHeaders(page); + return new ResponseEntity>( + page.getContent(), + headers, + HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public GrayInstance info(@PathVariable("id") String id) { + return grayServerModule.getGrayInstance(id); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") String id) { + grayServerModule.deleteGrayInstance(id); + } + + @RequestMapping(value = "/", method = RequestMethod.POST) + public void save(@RequestBody GrayInstance grayInstance) { + grayServerModule.saveGrayInstance(grayInstance); + } + + + @RequestMapping(value = "{id}/switchStatus", method = RequestMethod.PUT) + public void switchGrayStatus(@PathVariable("id") String instanceId, + @ApiParam(value = "灰度开关{0: close, 1: open}", defaultValue = "0") @RequestParam("switch") int onoff) { + switch (onoff) { + case 1: + grayServerModule.openGray(instanceId); + return; + case 0: + grayServerModule.closeGray(instanceId); + return; + default: + throw new UnsupportedOperationException("不支持的开关类型"); + } + + + } + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayPolicyResource.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayPolicyResource.java new file mode 100644 index 00000000..5ad0e15c --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayPolicyResource.java @@ -0,0 +1,53 @@ +package cn.springcloud.gray.server.resources.rest; + +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.domain.GrayPolicy; +import cn.springcloud.gray.server.utils.PaginationUtils; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/gray/policy") +public class GrayPolicyResource { + + @Autowired + private GrayServerModule grayServerModule; + + @RequestMapping(value = "list", method = RequestMethod.GET, params = "instanceId") + public List listByInstanceId(@RequestParam("instanceId") String instanceId) { + return grayServerModule.listGrayPoliciesByInstanceId(instanceId); + } + + + @GetMapping(value = "/page") + public ResponseEntity> page( + @RequestParam("instanceId") String instanceId, + @ApiParam @PageableDefault(direction = Sort.Direction.DESC) Pageable pageable) { + Page page = grayServerModule.listGrayPoliciesByInstanceId(instanceId, pageable); + HttpHeaders headers = PaginationUtils.generatePaginationHttpHeaders(page); + return new ResponseEntity>( + page.getContent(), + headers, + HttpStatus.OK); + } + + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") Long id) { + grayServerModule.deleteGrayPolicy(id); + } + + @RequestMapping(value = "/", method = RequestMethod.POST) + public void save(@RequestBody GrayPolicy grayPolicy) { + grayServerModule.saveGrayPolicy(grayPolicy); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayResource.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayResource.java new file mode 100644 index 00000000..7665ed2e --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayResource.java @@ -0,0 +1,40 @@ +package cn.springcloud.gray.server.resources.rest; + +import cn.springcloud.gray.model.GrayInstance; +import cn.springcloud.gray.server.discovery.ServiceDiscover; +import cn.springcloud.gray.server.module.GrayModle; +import cn.springcloud.gray.server.module.GrayServerModule; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Api("gray-client调用的接口") +@RestController +@RequestMapping("/gray") +public class GrayResource { + + + @Autowired + private GrayModle grayModle; + + + @ApiOperation("返回所有已经打开灰度状态的实例信息(包含决策信息)") + @RequestMapping(value = "/instances/enable", method = RequestMethod.GET) + public List allOpens() { + return grayModle.allOpenInstances(); + } + + + @ApiOperation("返回指定实例的信息(包含决策信息)") + @RequestMapping(value = "/instance", method = RequestMethod.GET) + public GrayInstance instance(@RequestParam("serviceId") String serviceId, @RequestParam("instanceId") String instanceId) { + return grayModle.getGrayInstance(serviceId, instanceId); + } + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayServiceResource.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayServiceResource.java index 9488cd9a..6d44a12f 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayServiceResource.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/GrayServiceResource.java @@ -1,90 +1,61 @@ package cn.springcloud.gray.server.resources.rest; -import cn.springcloud.gray.core.*; -import cn.springcloud.gray.server.api.GrayServiceApi; +import cn.springcloud.gray.server.module.GrayServerModule; +import cn.springcloud.gray.server.module.domain.GrayService; +import cn.springcloud.gray.server.utils.PaginationUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; +@Api @RestController -public class GrayServiceResource implements GrayServiceApi { - @Autowired - private GrayServiceManager grayServiceManager; - - - @Override - public List services() { - return new ArrayList<>(grayServiceManager.allGrayService()); - } +@RequestMapping("/gray/service") +public class GrayServiceResource { - @Override - public List enableServices() { - Collection grayServices = grayServiceManager.allGrayService(); - List serviceList = new ArrayList<>(grayServices.size()); - for (GrayService grayService : grayServices) { - if (grayService.isOpenGray()) { - serviceList.add(grayService.takeNewOpenGrayService()); - } - } - return serviceList; - } - - @Override - public GrayService service(@PathVariable("serviceId") String serviceId) { - return grayServiceManager.getGrayService(serviceId); - } - - @Override - public List instances(@PathVariable("serviceId") String serviceId) { - return grayServiceManager.getGrayService(serviceId).getGrayInstances(); - } + @Autowired + private GrayServerModule grayServerModule; - @Override - public GrayInstance getInstance(@PathVariable("serviceId") String serviceId, String instanceId) { - return grayServiceManager.getGrayInstane(serviceId, instanceId); + @RequestMapping(value = "/list", method = RequestMethod.GET) + public List list() { + return grayServerModule.listAllGrayServices(); } - @Override - public ResponseEntity delInstance(@PathVariable("serviceId") String serviceId, String instanceId) { - grayServiceManager.deleteGrayInstance(serviceId, instanceId); - return ResponseEntity.ok().build(); - } - @Override - public ResponseEntity instance(@PathVariable("serviceId") String serviceId, @RequestBody GrayInstance instance) { - instance.setServiceId(serviceId); - grayServiceManager.addGrayInstance(instance); - return ResponseEntity.ok().build(); + @GetMapping(value = "/page") + public ResponseEntity> list( + @ApiParam @PageableDefault(direction = Sort.Direction.DESC) Pageable pageable) { + Page page = grayServerModule.listAllGrayServices(pageable); + HttpHeaders headers = PaginationUtils.generatePaginationHttpHeaders(page); + return new ResponseEntity>( + page.getContent(), + headers, + HttpStatus.OK); } - @Override - public List policyGroups(@PathVariable("serviceId") String serviceId, String instanceId) { - return grayServiceManager.getGrayInstane(serviceId, instanceId).getGrayPolicyGroups(); - } - - @Override - public GrayPolicyGroup policyGroup(@PathVariable("serviceId") String serviceId, String instanceId, - @PathVariable("groupId") String groupId) { - return grayServiceManager.getGrayInstane(serviceId, instanceId).getGrayPolicyGroup(groupId); + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public GrayService info(@PathVariable("id") String id) { + return grayServerModule.getGrayService(id); } - @Override - public List policies(@PathVariable("serviceId") String serviceId, String instanceId, - @PathVariable("groupId") String groupId) { - return grayServiceManager.getGrayInstane(serviceId, instanceId).getGrayPolicyGroup(groupId).getList(); + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") String id) { + grayServerModule.deleteGrayService(id); } - @Override - public GrayPolicy policy(@PathVariable("serviceId") String serviceId, String instanceId, - @PathVariable("groupId") String groupId, @PathVariable("policyId") String policyId) { - return grayServiceManager.getGrayInstane(serviceId, instanceId).getGrayPolicyGroup(groupId).getGrayPolicy(policyId); + @RequestMapping(value = "/", method = RequestMethod.POST) + public void save(@RequestBody GrayService grayPolicy) { + grayServerModule.saveGrayService(grayPolicy); } } diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/ServiceGrayResouce.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/ServiceGrayResouce.java deleted file mode 100644 index 5d64ae0c..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/resources/rest/ServiceGrayResouce.java +++ /dev/null @@ -1,128 +0,0 @@ -package cn.springcloud.gray.server.resources.rest; - -import cn.springcloud.gray.server.resources.domain.fo.GrayPolicyGroupFO; -import cn.springcloud.gray.server.resources.domain.vo.GrayInstanceVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayPolicyGroupVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayServiceVO; -import cn.springcloud.gray.server.service.AbstractGrayService; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/gray/manager") -public class ServiceGrayResouce { - - @Autowired - private AbstractGrayService grayService; - - - /** - * 返回所有服务 - * - * @return 灰度服务VO集合 - */ - @RequestMapping(value = "/services", method = RequestMethod.GET) - public ResponseEntity> services() { - return grayService.services(); - } - - - /** - * 返回服务实例列表 - * - * @param serviceId 服务id - * @return 灰度服务实例VO列表 - */ - @RequestMapping(value = "/services/{serviceId}/instances", method = RequestMethod.GET) - public ResponseEntity> instances(@PathVariable("serviceId") String serviceId) { - return grayService.instances(serviceId); - } - - - @ApiOperation(value = "更新实例灰度状态") - @RequestMapping(value = "/services/{serviceId}/instance/status/{status}", method = RequestMethod.PUT) - public ResponseEntity editInstanceStatus( - @PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @ApiParam("0:关闭, 1:启用") @PathVariable("status") int status) { - return grayService.editInstanceStatus(serviceId, instanceId, status); - } - - - /** - * 服务实例的所有灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @return 灰策略组VO列表 - */ - @RequestMapping(value = "/services/{serviceId}/instance/policyGroups", method = RequestMethod.GET) - public ResponseEntity> policyGroups(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId) { - return grayService.policyGroups(serviceId, instanceId); - } - - - /** - * 灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param groupId 灰度策略组id - * @return 灰度策略组VO - */ - @RequestMapping(value = "/services/{serviceId}/instance/policyGroup/{groupId}", method = RequestMethod.GET) - public ResponseEntity policyGroup(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @PathVariable("groupId") String groupId) { - return grayService.policyGroup(serviceId, instanceId, groupId); - } - - - @ApiOperation(value = "更新实例策略组启用状态") - @RequestMapping(value = "/services/{serviceId}/instance/policyGroup/{groupId}/status/{status}", method = RequestMethod.PUT) - public ResponseEntity editPolicyGroupStatus(@PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @PathVariable("groupId") String groupId, - @ApiParam("0:关闭, 1:启用") @PathVariable("status") int enable) { - return grayService.editPolicyGroupStatus(serviceId, instanceId, groupId, enable); - } - - - /** - * 添加策略组 - * - * @param serviceId 服务id - * @param policyGroupFO 灰度策略组FO - * @return Void - */ - @RequestMapping(value = "/services/{serviceId}/instance/policyGroup", method = RequestMethod.POST) - public ResponseEntity policyGroup( - @PathVariable("serviceId") String serviceId, - @RequestBody GrayPolicyGroupFO policyGroupFO) { - return grayService.policyGroup(serviceId, policyGroupFO); - } - - - /** - * 删除策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param policyGroupId 灰度策略组id - * @return Void - */ - @ApiOperation("删除策略组") - @RequestMapping(value = "/services/{serviceId}/instance/policyGroup", method = RequestMethod.DELETE) - public ResponseEntity delPolicyGroup( - @PathVariable("serviceId") String serviceId, - @RequestParam("instanceId") String instanceId, - @RequestParam("groupId") String policyGroupId) { - return grayService.delPolicyGroup(serviceId, instanceId, policyGroupId); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstraceCRUDService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstraceCRUDService.java new file mode 100644 index 00000000..956b7f2f --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstraceCRUDService.java @@ -0,0 +1,84 @@ +package cn.springcloud.gray.server.service; + +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.io.Serializable; +import java.util.List; + +public abstract class AbstraceCRUDService, T, ID extends Serializable> { + + + protected abstract REPOSITORY getRepository(); + + protected abstract ModelMapper getModelMapper(); + + protected void save(T entity) { + getRepository().save(entity); + } + + public void saveModel(MODEL entity) { + save(getModelMapper().model2do(entity)); + } + + protected List save(Iterable entities) { + return getRepository().save(entities); + } + + public List saveModels(Iterable entities) { + return getModelMapper().dos2models(save(getModelMapper().models2dos(entities))); + } + + protected T findOne(ID id) { + return getRepository().findOne(id); + } + + public MODEL findOneModel(ID id) { + return getModelMapper().do2model(findOne(id)); + } + + + public boolean exists(ID id) { + return getRepository().exists(id); + } + + protected List findAll() { + return getRepository().findAll(); + } + + protected List findAll(Iterable ids) { + return getRepository().findAll(ids); + } + + public List findAllModel() { + return getModelMapper().dos2models(findAll()); + } + + public List findAllModel(Iterable ids) { + return getModelMapper().dos2models(findAll(ids)); + } + + + public long count() { + return getRepository().count(); + } + + public void delete(ID id) { + getRepository().delete(id); + } + + + protected void delete(Iterable entities) { + getRepository().delete(entities); + } + + public void deleteModel(Iterable models) { + delete(getModelMapper().models2dos(models)); + } + + public void deleteAll() { + getRepository().deleteAll(); + } + + +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstractGrayService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstractGrayService.java deleted file mode 100644 index cb1999e5..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/AbstractGrayService.java +++ /dev/null @@ -1,157 +0,0 @@ -package cn.springcloud.gray.server.service; - -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayPolicyGroup; -import cn.springcloud.gray.core.GrayService; -import cn.springcloud.gray.core.GrayServiceManager; -import cn.springcloud.gray.server.resources.domain.fo.GrayPolicyGroupFO; -import cn.springcloud.gray.server.resources.domain.vo.GrayInstanceVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayPolicyGroupVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayServiceVO; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.http.ResponseEntity; - -import java.util.ArrayList; -import java.util.List; - -public abstract class AbstractGrayService { - - private GrayServiceManager grayServiceManager; - private DiscoveryClient discoveryClient; - - public AbstractGrayService(GrayServiceManager grayServiceManager, DiscoveryClient discoveryClient) { - this.grayServiceManager = grayServiceManager; - this.discoveryClient = discoveryClient; - } - - /** - * 返回所有服务 - * - * @return 灰度服务VO集合 - */ - public ResponseEntity> services() { - List serviceIds = discoveryClient.getServices(); - List services = new ArrayList<>(serviceIds.size()); - for (String serviceId : serviceIds) { - List instances = discoveryClient.getInstances(serviceId); - if (null == instances || instances.isEmpty()) { - continue; - } - GrayServiceVO vo = new GrayServiceVO(); - vo.setServiceId(serviceId); - vo.setAppName(serviceId); - vo.setInstanceSize(instances.size()); - GrayService grayService = grayServiceManager.getGrayService(serviceId); - if (grayService != null) { - vo.setHasGrayInstances(grayService.isOpenGray()); - vo.setHasGrayPolicies(grayService.hasGrayPolicy()); - } - services.add(vo); - } - return ResponseEntity.ok(services); - } - - /** - * 返回服务实例列表 - * - * @param serviceId 服务id - * @return 灰度服务实例VO列表 - */ - public abstract ResponseEntity> instances(String serviceId); - - public ResponseEntity editInstanceStatus(String serviceId, String instanceId, int status) { - - return grayServiceManager.updateInstanceStatus(serviceId, instanceId, status) - ? ResponseEntity.ok().build() : ResponseEntity.badRequest().build(); - } - - - /** - * 服务实例的所有灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @return 灰策略组VO列表 - */ - public abstract ResponseEntity> policyGroups(String serviceId, String instanceId); - - - /** - * 灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param groupId 灰度策略组id - * @return 灰度策略组VO - */ - public abstract ResponseEntity policyGroup(String serviceId, String instanceId, String groupId); - - - public ResponseEntity editPolicyGroupStatus(String serviceId, String instanceId, String groupId, int enable) { - - return grayServiceManager.updatePolicyGroupStatus(serviceId, instanceId, groupId, enable) - ? ResponseEntity.ok().build() : - ResponseEntity.badRequest().build(); - } - - - /** - * 添加策略组 - * - * @param serviceId 服务id - * @param policyGroupFO 灰度策略组FO - * @return Void - */ - public ResponseEntity policyGroup(String serviceId, GrayPolicyGroupFO policyGroupFO) { - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, policyGroupFO.getInstanceId()); - if (grayInstance == null) { - grayInstance = new GrayInstance(); - grayInstance.setServiceId(serviceId); - grayInstance.setInstanceId(policyGroupFO.getInstanceId()); - grayServiceManager.addGrayInstance(grayInstance); - grayInstance = grayServiceManager.getGrayInstane(serviceId, policyGroupFO.getInstanceId()); - } - - grayInstance.addGrayPolicyGroup(policyGroupFO.toGrayPolicyGroup()); - - return ResponseEntity.ok().build(); - } - - - /** - * 删除策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param policyGroupId 灰度策略组id - * @return Void - */ - public ResponseEntity delPolicyGroup(String serviceId, String instanceId, String policyGroupId) { - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - if (grayInstance.removeGrayPolicyGroup(policyGroupId) != null && grayInstance.getGrayPolicyGroups() - .isEmpty()) { - grayServiceManager.deleteGrayInstance(serviceId, instanceId); - } - } - - return ResponseEntity.ok().build(); - } - - - protected GrayPolicyGroupVO getPolicyGroup(String serviceId, String appName, String instanceId, String homePageUrl, - GrayPolicyGroup policyGroup) { - GrayPolicyGroupVO vo = new GrayPolicyGroupVO(); - vo.setAppName(appName); - vo.setInstanceId(instanceId); - vo.setServiceId(serviceId); - vo.setUrl(homePageUrl); - vo.setAlias(policyGroup.getAlias()); - vo.setPolicyGroupId(policyGroup.getPolicyGroupId()); - vo.setPolicies(policyGroup.getList()); - vo.setEnable(policyGroup.isEnable()); - return vo; - } - -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/EurekaGrayService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/EurekaGrayService.java index e36ecfad..3aa4fb8a 100644 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/EurekaGrayService.java +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/EurekaGrayService.java @@ -1,10 +1,5 @@ package cn.springcloud.gray.server.service; -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayPolicyGroup; -import cn.springcloud.gray.core.GrayServiceManager; -import cn.springcloud.gray.server.resources.domain.vo.GrayInstanceVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayPolicyGroupVO; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; @@ -20,98 +15,48 @@ @Service @ConditionalOnBean(EurekaClient.class) -public class EurekaGrayService extends AbstractGrayService { +public class EurekaGrayService { + + private EurekaClient eurekaClient; + private DiscoveryClient discoveryClient; +// private final GrayServiceManager grayServiceManager; + +// @Autowired +// public EurekaGrayService(EurekaClient eurekaClient, DiscoveryClient discoveryClient, GrayServiceManager +// grayServiceManager) { +// super(grayServiceManager, discoveryClient); +// this.eurekaClient = eurekaClient; +// this.discoveryClient = discoveryClient; +// this.grayServiceManager = grayServiceManager; +// } +// +// /** +// * 返回服务实例列表 +// * +// * @param serviceId 服务id +// * @return 灰度服务实例VO列表 +// */ +// @Override +// public ResponseEntity> instances(String serviceId) { +// List list = new ArrayList<>(); +// Application app = eurekaClient.getApplication(serviceId); +// List instanceInfos = app.getInstances(); +// for (InstanceInfo instanceInfo : instanceInfos) { +// GrayInstanceVO vo = new GrayInstanceVO(); +// vo.setAppName(app.getName()); +// vo.setServiceId(serviceId); +// vo.setInstanceId(instanceInfo.getInstanceId()); +// vo.setMetadata(instanceInfo.getMetadata()); +// vo.setUrl(instanceInfo.getHomePageUrl()); +// GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceInfo.getInstanceId()); +// if (grayInstance != null) { +// vo.setOpenGray(grayInstance.isOpenGray()); +// vo.setHasGrayPolicies(grayInstance.hasGrayPolicy()); +// } +// list.add(vo); +// } +// return ResponseEntity.ok(list); +// } - private final EurekaClient eurekaClient; - private final DiscoveryClient discoveryClient; - private final GrayServiceManager grayServiceManager; - @Autowired - public EurekaGrayService(EurekaClient eurekaClient, DiscoveryClient discoveryClient, GrayServiceManager - grayServiceManager) { - super(grayServiceManager, discoveryClient); - this.eurekaClient = eurekaClient; - this.discoveryClient = discoveryClient; - this.grayServiceManager = grayServiceManager; - } - - /** - * 返回服务实例列表 - * - * @param serviceId 服务id - * @return 灰度服务实例VO列表 - */ - @Override - public ResponseEntity> instances(String serviceId) { - List list = new ArrayList<>(); - Application app = eurekaClient.getApplication(serviceId); - List instanceInfos = app.getInstances(); - for (InstanceInfo instanceInfo : instanceInfos) { - GrayInstanceVO vo = new GrayInstanceVO(); - vo.setAppName(app.getName()); - vo.setServiceId(serviceId); - vo.setInstanceId(instanceInfo.getInstanceId()); - vo.setMetadata(instanceInfo.getMetadata()); - vo.setUrl(instanceInfo.getHomePageUrl()); - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceInfo.getInstanceId()); - if (grayInstance != null) { - vo.setOpenGray(grayInstance.isOpenGray()); - vo.setHasGrayPolicies(grayInstance.hasGrayPolicy()); - } - list.add(vo); - } - return ResponseEntity.ok(list); - } - - - /** - * 服务实例的所有灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @return 灰策略组VO列表 - */ - @Override - public ResponseEntity> policyGroups(String serviceId, String instanceId) { - - Application app = eurekaClient.getApplication(serviceId); - InstanceInfo instanceInfo = app.getByInstanceId(instanceId); - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - String appName = instanceInfo.getAppName(); - String homePageUrl = instanceInfo.getHomePageUrl(); - if (grayInstance != null && grayInstance.getGrayPolicyGroups() != null) { - List policyGroups = grayInstance.getGrayPolicyGroups(); - List vos = new ArrayList<>(policyGroups.size()); - for (GrayPolicyGroup policyGroup : policyGroups) { - vos.add(getPolicyGroup(serviceId, appName, instanceId, homePageUrl, policyGroup)); - } - return ResponseEntity.ok(vos); - } - return ResponseEntity.ok(Collections.emptyList()); - } - - - /** - * 灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param groupId 灰度策略组id - * @return 灰度策略组VO - */ - @Override - public ResponseEntity policyGroup(String serviceId, String instanceId, String groupId) { - Application app = eurekaClient.getApplication(serviceId); - InstanceInfo instanceInfo = app.getByInstanceId(instanceId); - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - String appName = instanceInfo.getAppName(); - String homePageUrl = instanceInfo.getHomePageUrl(); - if (grayInstance != null) { - GrayPolicyGroup policyGroup = grayInstance.getGrayPolicyGroup(groupId); - if (policyGroup != null) { - return ResponseEntity.ok(getPolicyGroup(serviceId, appName, instanceId, homePageUrl, policyGroup)); - } - } - return ResponseEntity.ok().build(); - } } diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayDecisionService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayDecisionService.java new file mode 100644 index 00000000..d6965e30 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayDecisionService.java @@ -0,0 +1,47 @@ +package cn.springcloud.gray.server.service; + +import cn.springcloud.gray.server.dao.mapper.GrayDecisionMapper; +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import cn.springcloud.gray.server.dao.model.GrayDecisionDO; +import cn.springcloud.gray.server.dao.repository.GrayDecisionRepository; +import cn.springcloud.gray.server.module.domain.GrayDecision; +import cn.springcloud.gray.server.utils.PaginationUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GrayDecisionService extends AbstraceCRUDService { + + @Autowired + private GrayDecisionRepository repository; + @Autowired + private GrayDecisionMapper grayDecisionMapper; + + @Override + protected GrayDecisionRepository getRepository() { + return repository; + } + + @Override + protected ModelMapper getModelMapper() { + return grayDecisionMapper; + } + + public List findByPolicyId(Long policyId) { + return grayDecisionMapper.dos2models(repository.findByPolicyId(policyId)); + + } + + public void deleteAllByPolicyId(Long policyId) { + repository.deleteAllByPolicyId(policyId); + } + + public Page listGrayDecisionsByPolicyId(Long policyId, Pageable pageable) { + Page entities = repository.findAllByPolicyId(policyId, pageable); + return PaginationUtils.convert(pageable, entities, grayDecisionMapper); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayInstanceService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayInstanceService.java new file mode 100644 index 00000000..c57482ca --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayInstanceService.java @@ -0,0 +1,70 @@ +package cn.springcloud.gray.server.service; + +import cn.springcloud.gray.model.GrayStatus; +import cn.springcloud.gray.server.dao.mapper.GrayInstanceMapper; +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import cn.springcloud.gray.server.dao.model.GrayInstanceDO; +import cn.springcloud.gray.server.dao.repository.GrayInstanceRepository; +import cn.springcloud.gray.server.module.domain.GrayInstance; +import cn.springcloud.gray.server.module.domain.InstanceStatus; +import cn.springcloud.gray.server.utils.PaginationUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +public class GrayInstanceService extends AbstraceCRUDService { + + @Autowired + private GrayInstanceRepository repository; + @Autowired + private GrayPolicyService grayPolicyService; + @Autowired + private GrayInstanceMapper grayInstanceMapper; + + + @Override + protected GrayInstanceRepository getRepository() { + return repository; + } + + @Override + protected ModelMapper getModelMapper() { + return grayInstanceMapper; + } + + public List findByServiceId(String serviceId) { + return grayInstanceMapper.dos2models(repository.findByServiceId(serviceId)); + } + + @Transactional + public void deleteByServiceId(String serviceId) { + findByServiceId(serviceId).forEach(entity -> { + delete(entity.getInstanceId()); + grayPolicyService.deleteByInstanceId(entity.getInstanceId()); + }); + } + + + @Transactional + public void deleteReactById(String id) { + delete(id); + grayPolicyService.findByInstanceId(id).forEach(entity -> { + grayPolicyService.deleteReactById(entity.getId()); + }); + } + + public List findAllByStatus(GrayStatus grayStatus, InstanceStatus instanceStatus) { + return grayInstanceMapper.dos2models( + repository.findAllByGrayStatusAndInstanceStatus(grayStatus.name(), instanceStatus.name())); + } + + public Page listGrayInstancesByServiceId(String serviceId, Pageable pageable) { + Page entities = repository.findAllByServiceId(serviceId, pageable); + return PaginationUtils.convert(pageable, entities, grayInstanceMapper); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayPolicyService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayPolicyService.java new file mode 100644 index 00000000..1ec07b6d --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayPolicyService.java @@ -0,0 +1,57 @@ +package cn.springcloud.gray.server.service; + +import cn.springcloud.gray.server.dao.mapper.GrayPolicyMapper; +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import cn.springcloud.gray.server.dao.model.GrayPolicyDO; +import cn.springcloud.gray.server.dao.repository.GrayPolicyRepository; +import cn.springcloud.gray.server.module.domain.GrayPolicy; +import cn.springcloud.gray.server.utils.PaginationUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +public class GrayPolicyService extends AbstraceCRUDService { + @Autowired + private GrayPolicyRepository repository; + @Autowired + private GrayDecisionService grayDecisionService; + @Autowired + private GrayPolicyMapper grayPolicyMapper; + + @Override + protected GrayPolicyRepository getRepository() { + return repository; + } + + @Override + protected ModelMapper getModelMapper() { + return grayPolicyMapper; + } + + public List findByInstanceId(String instanceId) { + return grayPolicyMapper.dos2models(repository.findByInstanceId(instanceId)); + } + + public void deleteByInstanceId(String instanceId) { + findByInstanceId(instanceId).forEach(entity -> { + delete(entity.getId()); + grayDecisionService.deleteAllByPolicyId(entity.getId()); + }); + } + + @Transactional + public void deleteReactById(Long id) { + delete(id); + grayDecisionService.deleteAllByPolicyId(id); + } + + public Page listGrayPoliciesByInstanceId(String instanceId, Pageable pageable) { + Page entities = repository.findAllByInstanceId(instanceId, pageable); + return PaginationUtils.convert(pageable, entities, grayPolicyMapper); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayServiceService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayServiceService.java new file mode 100644 index 00000000..1edfbc6a --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/GrayServiceService.java @@ -0,0 +1,52 @@ +package cn.springcloud.gray.server.service; + +import cn.springcloud.gray.server.dao.mapper.GrayServiceMapper; +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import cn.springcloud.gray.server.dao.model.GrayServiceDO; +import cn.springcloud.gray.server.dao.repository.GrayServiceRepository; +import cn.springcloud.gray.server.module.domain.GrayService; +import cn.springcloud.gray.server.utils.PaginationUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +public class GrayServiceService extends AbstraceCRUDService { + + @Autowired + private GrayServiceRepository repository; + @Autowired + private GrayInstanceService grayInstanceService; + @Autowired + private GrayServiceMapper grayServiceMapper; + + + @Override + protected GrayServiceRepository getRepository() { + return repository; + } + + @Override + protected ModelMapper getModelMapper() { + return grayServiceMapper; + } + + public void deleteById(String id) { + delete(id); + grayInstanceService.findByServiceId(id); + } + + public void deleteReactById(String id) { + delete(id); + grayInstanceService.findByServiceId(id).forEach(entity -> { + grayInstanceService.deleteReactById(entity.getInstanceId()); + }); + } + + + public Page listAllGrayServices(Pageable pageable) { + Page entities = repository.findAll(pageable); + return PaginationUtils.convert(pageable, entities, grayServiceMapper); + } +} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/ZookeeperGrayService.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/ZookeeperGrayService.java deleted file mode 100644 index 493a8394..00000000 --- a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/service/ZookeeperGrayService.java +++ /dev/null @@ -1,133 +0,0 @@ -package cn.springcloud.gray.server.service; - -import cn.springcloud.gray.core.GrayInstance; -import cn.springcloud.gray.core.GrayPolicyGroup; -import cn.springcloud.gray.core.GrayServiceManager; -import cn.springcloud.gray.server.resources.domain.vo.GrayInstanceVO; -import cn.springcloud.gray.server.resources.domain.vo.GrayPolicyGroupVO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.zookeeper.serviceregistry.ZookeeperRegistration; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@Service -@ConditionalOnBean(ZookeeperRegistration.class) -public class ZookeeperGrayService extends AbstractGrayService { - private static final String METADATA_KEY_INSTANCE_ID = "instanceId"; - - private final DiscoveryClient discoveryClient; - private final GrayServiceManager grayServiceManager; - - @Autowired - public ZookeeperGrayService(DiscoveryClient discoveryClient, GrayServiceManager grayServiceManager) { - super(grayServiceManager, discoveryClient); - this.discoveryClient = discoveryClient; - this.grayServiceManager = grayServiceManager; - } - - /** - * 返回服务实例列表 - * - * @param serviceId 服务id - * @return 灰度服务实例VO列表 - */ - @Override - public ResponseEntity> instances(String serviceId) { - List list = new ArrayList<>(); - List instances = discoveryClient.getInstances(serviceId); - if (null != instances && !instances.isEmpty()) { - instances.forEach(instance -> { - String instanceId = getInstanceId(instance); - GrayInstanceVO vo = new GrayInstanceVO(); - vo.setAppName(serviceId); - vo.setServiceId(serviceId); - vo.setInstanceId(instanceId); - vo.setMetadata(instance.getMetadata()); - vo.setUrl(instance.getUri().toString()); - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - if (grayInstance != null) { - vo.setOpenGray(grayInstance.isOpenGray()); - vo.setHasGrayPolicies(grayInstance.hasGrayPolicy()); - } - list.add(vo); - }); - } - return ResponseEntity.ok(list); - } - - /** - * 服务实例的所有灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @return 灰策略组VO列表 - */ - @Override - public ResponseEntity> policyGroups(String serviceId, String instanceId) { - List instances = discoveryClient.getInstances(serviceId); - if (null == instances || instances.isEmpty()) { - return ResponseEntity.ok(Collections.emptyList()); - } - - ServiceInstance serviceInstance = instances.stream() - .filter(instance -> instanceId.equals(getInstanceId(instance))) - .findFirst().orElse(null); - - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - if (null != serviceInstance && grayInstance != null && grayInstance.getGrayPolicyGroups() != null) { - String homePageUrl = serviceInstance.getUri().toString(); - List policyGroups = grayInstance.getGrayPolicyGroups(); - List vos = new ArrayList<>(policyGroups.size()); - for (GrayPolicyGroup policyGroup : policyGroups) { - vos.add(getPolicyGroup(serviceId, serviceId, instanceId, homePageUrl, policyGroup)); - } - return ResponseEntity.ok(vos); - } - return ResponseEntity.ok(Collections.emptyList()); - } - - - /** - * 灰度策略组 - * - * @param serviceId 服务id - * @param instanceId 实例id - * @param groupId 灰度策略组id - * @return 灰度策略组VO - */ - @Override - public ResponseEntity policyGroup(String serviceId, String instanceId, String groupId) { - List instances = discoveryClient.getInstances(serviceId); - if (null == instances || instances.isEmpty()) { - return ResponseEntity.ok().build(); - } - - ServiceInstance serviceInstance = instances.stream() - .filter(instance -> instanceId.equals(getInstanceId(instance))) - .findFirst().orElse(null); - - GrayInstance grayInstance = grayServiceManager.getGrayInstane(serviceId, instanceId); - if (null != serviceInstance && grayInstance != null) { - GrayPolicyGroup policyGroup = grayInstance.getGrayPolicyGroup(groupId); - if (policyGroup != null) { - String homePageUrl = serviceInstance.getUri().toString(); - return ResponseEntity.ok(getPolicyGroup(serviceId, serviceId, instanceId, homePageUrl, policyGroup)); - } - } - return ResponseEntity.ok().build(); - } - - private String getInstanceId(ServiceInstance instance) { - if (instance.getMetadata().containsKey(METADATA_KEY_INSTANCE_ID)) { - return instance.getMetadata().get(METADATA_KEY_INSTANCE_ID); - } - throw new IllegalStateException("Unable to find config spring.cloud.zookeeper.discovery.metadata.instanceId!"); - } -} diff --git a/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/utils/PaginationUtils.java b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/utils/PaginationUtils.java new file mode 100755 index 00000000..1d611905 --- /dev/null +++ b/spring-cloud-gray-server/src/main/java/cn/springcloud/gray/server/utils/PaginationUtils.java @@ -0,0 +1,105 @@ +package cn.springcloud.gray.server.utils; + +import cn.springcloud.gray.server.dao.mapper.ModelMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; + +/** + * Created by admin on 2017/2/28. + */ +public class PaginationUtils { + private static final Logger log = LoggerFactory.getLogger(PaginationUtils.class); + + public static final ObjectMapper PAGINATION_OBJECT_MAPPER = new ObjectMapper(); + + + public static Page convert(Pageable pageable, Page p, ModelMapper modelMapper) { + List list = modelMapper.dos2models(p.getContent()); + return new PageImpl(list, pageable, p.getTotalElements()); + } + + + public static HttpHeaders generatePaginationHttpHeaders(Page page) { + return generatePaginationHttpHeaders(page, null); + } + + public static HttpHeaders generatePaginationHttpHeaders(Page page, String baseUrl) { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Total-Count", "" + page.getTotalElements()); + + if (!StringUtils.isEmpty(baseUrl)) { + Pagination pagination = new Pagination(); + int lastPage = 0; + if (page.getTotalPages() > 0) { + lastPage = page.getTotalPages() - 1; + } + if (page.getNumber() + 1 < page.getTotalPages()) { + pagination.setNext(generateUri(baseUrl, page.getNumber() + 1, page.getSize())); + } + + if (page.getNumber() > 0) { + pagination.setPrev(generateUri(baseUrl, page.getNumber() - 1, page.getSize())); + } + pagination.setLast(generateUri(baseUrl, lastPage, page.getSize())); + pagination.setFirst(generateUri(baseUrl, 0, page.getSize())); + try { + headers.add("X-Pagination", PAGINATION_OBJECT_MAPPER.writeValueAsString(pagination)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return headers; + } + + public static String generateUri(String baseUrl, int page, int size) { + return UriComponentsBuilder.fromUriString(baseUrl).queryParam("page", new Object[]{Integer.valueOf(page)}).queryParam("size", new Object[]{Integer.valueOf(size)}).toUriString(); + } + + static class Pagination { + private String next; + private String prev; + private String last; + private String first; + + public String getNext() { + return next; + } + + public void setNext(String next) { + this.next = next; + } + + public String getPrev() { + return prev; + } + + public void setPrev(String prev) { + this.prev = prev; + } + + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + } +} diff --git a/spring-cloud-gray-starter-dependencies/pom.xml b/spring-cloud-gray-starter-dependencies/pom.xml new file mode 100644 index 00000000..0387814e --- /dev/null +++ b/spring-cloud-gray-starter-dependencies/pom.xml @@ -0,0 +1,132 @@ + + + + spring-cloud-gray + cn.springcloud.gray + 2.0.0 + + 4.0.0 + + spring-cloud-gray-starter-dependencies + pom + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + master + https://github.com/SpringCloud/spring-cloud-gray + git@github.com:SpringCloud/spring-cloud-gray.git + scm:git:git@github.com:SpringCloud/spring-cloud-gray.git + + + + saleson + qlichunyu@163.com + Spring Cloud中国社区 + + + + + + + + + + + + cn.springcloud.gray + spring-cloud-gray-dependencies + ${project.version} + pom + import + + + cn.springcloud.gray + spring-cloud-starter-gray-client + ${project.version} + + + cn.springcloud.gray + spring-cloud-starter-gray-server + ${project.version} + + + + + + + + sonatype-oss-release + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + package + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + + + + sonatype-nexus-snapshots + + https://oss.sonatype.org/content/repositories/snapshots + + + + sonatype-nexus-staging + + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-gray-utils/pom.xml b/spring-cloud-gray-utils/pom.xml new file mode 100644 index 00000000..09fe2e4b --- /dev/null +++ b/spring-cloud-gray-utils/pom.xml @@ -0,0 +1,57 @@ + + + + spring-cloud-gray + cn.springcloud.gray + 2.0.0 + + 4.0.0 + + spring-cloud-gray-utils + + + + + org.springframework.boot + spring-boot + provided + + + org.springframework + spring-jcl + 5.1.6.RELEASE + + + javax.validation + validation-api + + + + commons-collections + commons-collections + + + org.apache.commons + commons-lang3 + + + javax.servlet + javax.servlet-api + provided + + + org.slf4j + slf4j-api + provided + + + + junit + junit + + + + + \ No newline at end of file diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ApplicationConversionService.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ApplicationConversionService.java new file mode 100644 index 00000000..6cf2ba3d --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ApplicationConversionService.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StringValueResolver; + +/** + * A specialization of {@link FormattingConversionService} configured by default with + * converters and formatters appropriate for most Spring Boot applications. + *

+ * Designed for direct instantiation but also exposes the static + * {@link #addApplicationConverters} and + * {@link #addApplicationFormatters(FormatterRegistry)} utility methods for ad-hoc use + * against registry instance. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class ApplicationConversionService extends FormattingConversionService { + + private static volatile ApplicationConversionService sharedInstance; + + public ApplicationConversionService() { + this(null); + } + + public ApplicationConversionService(StringValueResolver embeddedValueResolver) { + if (embeddedValueResolver != null) { + setEmbeddedValueResolver(embeddedValueResolver); + } + configure(this); + } + + /** + * Return a shared default application {@code ConversionService} instance, lazily + * building it once needed. + *

+ * Note: This method actually returns an {@link ApplicationConversionService} + * instance. However, the {@code ConversionService} signature has been preserved for + * binary compatibility. + * + * @return the shared {@code ApplicationConversionService} instance (never + * {@code null}) + */ + public static ConversionService getSharedInstance() { + ApplicationConversionService sharedInstance = ApplicationConversionService.sharedInstance; + if (sharedInstance == null) { + synchronized (ApplicationConversionService.class) { + sharedInstance = ApplicationConversionService.sharedInstance; + if (sharedInstance == null) { + sharedInstance = new ApplicationConversionService(); + ApplicationConversionService.sharedInstance = sharedInstance; + } + } + } + return sharedInstance; + } + + /** + * Configure the given {@link FormatterRegistry} with formatters and converters + * appropriate for most Spring Boot applications. + * + * @param registry the registry of converters to add to (must also be castable to + * ConversionService, e.g. being a {@link ConfigurableConversionService}) + * @throws ClassCastException if the given FormatterRegistry could not be cast to a + * ConversionService + */ + public static void configure(FormatterRegistry registry) { + DefaultConversionService.addDefaultConverters(registry); + DefaultFormattingConversionService.addDefaultFormatters(registry); + addApplicationFormatters(registry); + addApplicationConverters(registry); + } + + /** + * Add converters useful for most Spring Boot applications. + * + * @param registry the registry of converters to add to (must also be castable to + * ConversionService, e.g. being a {@link ConfigurableConversionService}) + * @throws ClassCastException if the given ConverterRegistry could not be cast to a + * ConversionService + */ + public static void addApplicationConverters(ConverterRegistry registry) { + addDelimitedStringConverters(registry); + registry.addConverter(new StringToDurationConverter()); + registry.addConverter(new DurationToStringConverter()); + registry.addConverter(new NumberToDurationConverter()); + registry.addConverter(new DurationToNumberConverter()); + registry.addConverter(new StringToDataSizeConverter()); + registry.addConverter(new NumberToDataSizeConverter()); + registry.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory()); + } + + /** + * Add converters to support delimited strings. + * + * @param registry the registry of converters to add to (must also be castable to + * ConversionService, e.g. being a {@link ConfigurableConversionService}) + * @throws ClassCastException if the given ConverterRegistry could not be cast to a + * ConversionService + */ + public static void addDelimitedStringConverters(ConverterRegistry registry) { + ConversionService service = (ConversionService) registry; + registry.addConverter(new ArrayToDelimitedStringConverter(service)); + registry.addConverter(new CollectionToDelimitedStringConverter(service)); + registry.addConverter(new DelimitedStringToArrayConverter(service)); + registry.addConverter(new DelimitedStringToCollectionConverter(service)); + } + + /** + * Add formatters useful for most Spring Boot applications. + * + * @param registry the service to register default formatters with + */ + public static void addApplicationFormatters(FormatterRegistry registry) { + registry.addFormatter(new CharArrayFormatter()); + registry.addFormatter(new InetAddressFormatter()); + registry.addFormatter(new IsoOffsetFormatter()); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ArrayToDelimitedStringConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ArrayToDelimitedStringConverter.java new file mode 100644 index 00000000..d79f776a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/ArrayToDelimitedStringConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * Converts an array to a delimited String. + * + * @author Phillip Webb + */ +final class ArrayToDelimitedStringConverter implements ConditionalGenericConverter { + + private final CollectionToDelimitedStringConverter delegate; + + ArrayToDelimitedStringConverter(ConversionService conversionService) { + this.delegate = new CollectionToDelimitedStringConverter(conversionService); + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.delegate.matches(sourceType, targetType); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + List list = Arrays.asList(ObjectUtils.toObjectArray(source)); + return this.delegate.convert(list, sourceType, targetType); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CharArrayFormatter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CharArrayFormatter.java new file mode 100644 index 00000000..90ac37b8 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CharArrayFormatter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.text.ParseException; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} for {@code char[]}. + * + * @author Phillip Webb + */ +final class CharArrayFormatter implements Formatter { + + @Override + public String print(char[] object, Locale locale) { + return new String(object); + } + + @Override + public char[] parse(String text, Locale locale) throws ParseException { + return text.toCharArray(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CollectionToDelimitedStringConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CollectionToDelimitedStringConverter.java new file mode 100644 index 00000000..203022fe --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/CollectionToDelimitedStringConverter.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Collection to a delimited String. + * + * @author Phillip Webb + */ +final class CollectionToDelimitedStringConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + CollectionToDelimitedStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + TypeDescriptor sourceElementType = sourceType.getElementTypeDescriptor(); + if (targetType == null || sourceElementType == null) { + return true; + } + if (this.conversionService.canConvert(sourceElementType, targetType) + || sourceElementType.getType().isAssignableFrom(targetType.getType())) { + return true; + } + return false; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection sourceCollection = (Collection) source; + return convert(sourceCollection, sourceType, targetType); + } + + private Object convert(Collection source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source.isEmpty()) { + return ""; + } + return source.stream() + .map((element) -> convertElement(element, sourceType, targetType)) + .collect(Collectors.joining(getDelimiter(sourceType))); + } + + private CharSequence getDelimiter(TypeDescriptor sourceType) { + Delimiter annotation = sourceType.getAnnotation(Delimiter.class); + return (annotation != null) ? annotation.value() : ","; + } + + private String convertElement(Object element, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return String.valueOf(this.conversionService.convert(element, + sourceType.elementTypeDescriptor(element), targetType)); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DataSizeUnit.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DataSizeUnit.java new file mode 100644 index 00000000..187bebbe --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DataSizeUnit.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import cn.springcloud.gray.bean.domain.DataUnit; + +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; + + +/** + * Annotation that can be used to change the default unit used when converting a + * {@link cn.springcloud.gray.bean.domain.DataSize}. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataSizeUnit { + + /** + * The {@link DataUnit} to use if one is not specified. + * + * @return the data unit + */ + DataUnit value(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToArrayConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToArrayConverter.java new file mode 100644 index 00000000..80342cff --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToArrayConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.convert; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts a {@link Delimiter delimited} String to an Array. + * + * @author Phillip Webb + */ +final class DelimitedStringToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + DelimitedStringToArrayConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.getElementTypeDescriptor() == null || this.conversionService + .canConvert(sourceType, targetType.getElementTypeDescriptor()); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((String) source, sourceType, targetType); + } + + private Object convert(String source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + Delimiter delimiter = targetType.getAnnotation(Delimiter.class); + String[] elements = getElements(source, + (delimiter != null) ? delimiter.value() : ","); + TypeDescriptor elementDescriptor = targetType.getElementTypeDescriptor(); + Object target = Array.newInstance(elementDescriptor.getType(), elements.length); + for (int i = 0; i < elements.length; i++) { + String sourceElement = elements[i]; + Object targetElement = this.conversionService.convert(sourceElement.trim(), + sourceType, elementDescriptor); + Array.set(target, i, targetElement); + } + return target; + } + + private String[] getElements(String source, String delimiter) { + return StringUtils.delimitedListToStringArray(source, + Delimiter.NONE.equals(delimiter) ? null : delimiter); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToCollectionConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToCollectionConverter.java new file mode 100644 index 00000000..55ce6ba0 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DelimitedStringToCollectionConverter.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.convert; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts a {@link Delimiter delimited} String to a Collection. + * + * @author Phillip Webb + */ +final class DelimitedStringToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + DelimitedStringToCollectionConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.getElementTypeDescriptor() == null || this.conversionService + .canConvert(sourceType, targetType.getElementTypeDescriptor()); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((String) source, sourceType, targetType); + } + + private Object convert(String source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + Delimiter delimiter = targetType.getAnnotation(Delimiter.class); + String[] elements = getElements(source, + (delimiter != null) ? delimiter.value() : ","); + TypeDescriptor elementDescriptor = targetType.getElementTypeDescriptor(); + Collection target = createCollection(targetType, elementDescriptor, + elements.length); + Stream stream = Arrays.stream(elements).map(String::trim); + if (elementDescriptor != null) { + stream = stream.map((element) -> this.conversionService.convert(element, + sourceType, elementDescriptor)); + } + stream.forEach(target::add); + return target; + } + + private Collection createCollection(TypeDescriptor targetType, + TypeDescriptor elementDescriptor, int length) { + return CollectionFactory.createCollection(targetType.getType(), + (elementDescriptor != null) ? elementDescriptor.getType() : null, length); + } + + private String[] getElements(String source, String delimiter) { + return StringUtils.delimitedListToStringArray(source, + Delimiter.NONE.equals(delimiter) ? null : delimiter); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/Delimiter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/Delimiter.java new file mode 100644 index 00000000..d991fe1a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/Delimiter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +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; + +/** + * Declares a field or method parameter should be converted to collection using the + * specified delimiter. + * + * @author Phillip Webb + * @since 2.0.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, + ElementType.ANNOTATION_TYPE}) +public @interface Delimiter { + + /** + * A delimiter value used to indicate that no delimiter is required and the result + * should be a single element containing the entire string. + */ + String NONE = ""; + + /** + * The delimiter to use or {@code NONE} if the entire contents should be treated as a + * single element. + * + * @return the delimiter + */ + String value(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationFormat.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationFormat.java new file mode 100644 index 00000000..99b24a51 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationFormat.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +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 java.time.Duration; + +/** + * Annotation that can be used to indicate the format to use when converting a + * {@link Duration}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DurationFormat { + + /** + * The duration format style. + * + * @return the duration format style. + */ + DurationStyle value(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationStyle.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationStyle.java new file mode 100644 index 00000000..cf478782 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationStyle.java @@ -0,0 +1,263 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Duration format styles. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public enum DurationStyle { + + /** + * Simple formatting, for example '1s'. + */ + SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") { + @Override + public Duration parse(String value, ChronoUnit unit) { + try { + Matcher matcher = matcher(value); + Assert.state(matcher.matches(), "Does not match simple duration pattern"); + String suffix = matcher.group(2); + return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) + : Unit.fromChronoUnit(unit)).parse(matcher.group(1)); + } catch (Exception ex) { + throw new IllegalArgumentException( + "'" + value + "' is not a valid simple duration", ex); + } + } + + @Override + public String print(Duration value, ChronoUnit unit) { + return Unit.fromChronoUnit(unit).print(value); + } + + }, + + /** + * ISO-8601 formatting. + */ + ISO8601("^[\\+\\-]?P.*$") { + @Override + public Duration parse(String value, ChronoUnit unit) { + try { + return Duration.parse(value); + } catch (Exception ex) { + throw new IllegalArgumentException( + "'" + value + "' is not a valid ISO-8601 duration", ex); + } + } + + @Override + public String print(Duration value, ChronoUnit unit) { + return value.toString(); + } + + }; + + private final Pattern pattern; + + DurationStyle(String pattern) { + this.pattern = Pattern.compile(pattern); + } + + protected final boolean matches(String value) { + return this.pattern.matcher(value).matches(); + } + + protected final Matcher matcher(String value) { + return this.pattern.matcher(value); + } + + /** + * Parse the given value to a duration. + * + * @param value the value to parse + * @return a duration + */ + public Duration parse(String value) { + return parse(value, null); + } + + /** + * Parse the given value to a duration. + * + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return a duration + */ + public abstract Duration parse(String value, ChronoUnit unit); + + /** + * Print the specified duration. + * + * @param value the value to print + * @return the printed result + */ + public String print(Duration value) { + return print(value, null); + } + + /** + * Print the specified duration using the given unit. + * + * @param value the value to print + * @param unit the value to use for printing + * @return the printed result + */ + public abstract String print(Duration value, ChronoUnit unit); + + /** + * Detect the style then parse the value to return a duration. + * + * @param value the value to parse + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value) { + return detectAndParse(value, null); + } + + /** + * Detect the style then parse the value to return a duration. + * + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value, ChronoUnit unit) { + return detect(value).parse(value, unit); + } + + /** + * Detect the style from the given source value. + * + * @param value the source value + * @return the duration style + * @throws IllegalStateException if the value is not a known style + */ + public static DurationStyle detect(String value) { + Assert.notNull(value, "Value must not be null"); + for (DurationStyle candidate : values()) { + if (candidate.matches(value)) { + return candidate; + } + } + throw new IllegalArgumentException("'" + value + "' is not a valid duration"); + } + + /** + * Units that we support. + */ + enum Unit { + + /** + * Nanoseconds. + */ + NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos), + + /** + * Microseconds. + */ + MICROS(ChronoUnit.MICROS, "us", (duration) -> duration.toMillis() * 1000L), + + /** + * Milliseconds. + */ + MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis), + + /** + * Seconds. + */ + SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds), + + /** + * Minutes. + */ + MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes), + + /** + * Hours. + */ + HOURS(ChronoUnit.HOURS, "h", Duration::toHours), + + /** + * Days. + */ + DAYS(ChronoUnit.DAYS, "d", Duration::toDays); + + private final ChronoUnit chronoUnit; + + private final String suffix; + + private Function longValue; + + Unit(ChronoUnit chronoUnit, String suffix, Function toUnit) { + this.chronoUnit = chronoUnit; + this.suffix = suffix; + this.longValue = toUnit; + } + + public Duration parse(String value) { + return Duration.of(Long.valueOf(value), this.chronoUnit); + } + + public String print(Duration value) { + return longValue(value) + this.suffix; + } + + public long longValue(Duration value) { + return this.longValue.apply(value); + } + + public static Unit fromChronoUnit(ChronoUnit chronoUnit) { + if (chronoUnit == null) { + return Unit.MILLIS; + } + for (Unit candidate : values()) { + if (candidate.chronoUnit == chronoUnit) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit " + chronoUnit); + } + + public static Unit fromSuffix(String suffix) { + for (Unit candidate : values()) { + if (candidate.suffix.equalsIgnoreCase(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToNumberConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToNumberConverter.java new file mode 100644 index 00000000..3e5f864b --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToNumberConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Converter} to convert from a {@link Duration} to a {@link Number}. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class DurationToNumberConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Duration.class, Number.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((Duration) source, getDurationUnit(sourceType), + targetType.getObjectType()); + } + + private ChronoUnit getDurationUnit(TypeDescriptor sourceType) { + DurationUnit annotation = sourceType.getAnnotation(DurationUnit.class); + return (annotation != null) ? annotation.value() : null; + } + + private Object convert(Duration source, ChronoUnit unit, Class type) { + try { + return type.getConstructor(String.class).newInstance(String + .valueOf(DurationStyle.Unit.fromChronoUnit(unit).longValue(source))); + } catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToStringConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToStringConverter.java new file mode 100644 index 00000000..02c942ae --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationToStringConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link Converter} to convert from a {@link Duration} to a {@link String}. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class DurationToStringConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Duration.class, String.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((Duration) source, getDurationStyle(sourceType), + getDurationUnit(sourceType)); + } + + private ChronoUnit getDurationUnit(TypeDescriptor sourceType) { + DurationUnit annotation = sourceType.getAnnotation(DurationUnit.class); + return (annotation != null) ? annotation.value() : null; + } + + private DurationStyle getDurationStyle(TypeDescriptor sourceType) { + DurationFormat annotation = sourceType.getAnnotation(DurationFormat.class); + return (annotation != null) ? annotation.value() : null; + } + + private String convert(Duration source, DurationStyle style, ChronoUnit unit) { + style = (style != null) ? style : DurationStyle.ISO8601; + return style.print(source, unit); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationUnit.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationUnit.java new file mode 100644 index 00000000..78b798fd --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/DurationUnit.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +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 java.time.Duration; +import java.time.temporal.ChronoUnit; + +/** + * Annotation that can be used to change the default unit used when converting a + * {@link Duration}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DurationUnit { + + /** + * The duration unit to use if one is not specified. + * + * @return the duration unit + */ + ChronoUnit value(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/InetAddressFormatter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/InetAddressFormatter.java new file mode 100644 index 00000000..8f8625bd --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/InetAddressFormatter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.ParseException; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} for {@link InetAddress}. + * + * @author Phillip Webb + */ +final class InetAddressFormatter implements Formatter { + + @Override + public String print(InetAddress object, Locale locale) { + return object.getHostAddress(); + } + + @Override + public InetAddress parse(String text, Locale locale) throws ParseException { + try { + return InetAddress.getByName(text); + } catch (UnknownHostException ex) { + throw new IllegalStateException("Unknown host " + text, ex); + } + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/IsoOffsetFormatter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/IsoOffsetFormatter.java new file mode 100644 index 00000000..f0c51ec0 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/IsoOffsetFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.text.ParseException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * A {@link Formatter} for {@link OffsetDateTime} that uses + * {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME ISO offset formatting}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Phillip Webb + */ +class IsoOffsetFormatter implements Formatter { + + @Override + public String print(OffsetDateTime object, Locale locale) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(object); + } + + @Override + public OffsetDateTime parse(String text, Locale locale) throws ParseException { + return OffsetDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDataSizeConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDataSizeConverter.java new file mode 100644 index 00000000..651adfc4 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDataSizeConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.util.Collections; +import java.util.Set; + +import cn.springcloud.gray.bean.domain.DataSize; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link Converter} to convert from a {@link Number} to a {@link DataSize}. + * + * @author Stephane Nicoll + * @see DataSizeUnit + */ +final class NumberToDataSizeConverter implements GenericConverter { + + private final StringToDataSizeConverter delegate = new StringToDataSizeConverter(); + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Number.class, DataSize.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return this.delegate.convert((source != null) ? source.toString() : null, + TypeDescriptor.valueOf(String.class), targetType); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDurationConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDurationConverter.java new file mode 100644 index 00000000..ad99ea09 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/NumberToDurationConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.time.Duration; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link Converter} to convert from a {@link Number} to a {@link Duration}. Supports + * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class NumberToDurationConverter implements GenericConverter { + + private final StringToDurationConverter delegate = new StringToDurationConverter(); + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Number.class, Duration.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return this.delegate.convert((source != null) ? source.toString() : null, + TypeDescriptor.valueOf(String.class), targetType); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDataSizeConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDataSizeConverter.java new file mode 100644 index 00000000..306fdaa6 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDataSizeConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.util.Collections; +import java.util.Set; + +import cn.springcloud.gray.bean.domain.DataSize; +import cn.springcloud.gray.bean.domain.DataUnit; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * {@link Converter} to convert from a {@link String} to a {@link DataSize}. Supports + * {@link DataSize#parse(CharSequence)}. + * + * @author Stephane Nicoll + * @see DataSizeUnit + */ +final class StringToDataSizeConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, DataSize.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (ObjectUtils.isEmpty(source)) { + return null; + } + return convert(source.toString(), getDataUnit(targetType)); + } + + private DataUnit getDataUnit(TypeDescriptor targetType) { + DataSizeUnit annotation = targetType.getAnnotation(DataSizeUnit.class); + return (annotation != null) ? annotation.value() : null; + } + + private DataSize convert(String source, DataUnit unit) { + return DataSize.parse(source, unit); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDurationConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDurationConverter.java new file mode 100644 index 00000000..c7a91664 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToDurationConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * {@link Converter} to convert from a {@link String} to a {@link Duration}. Supports + * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class StringToDurationConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Duration.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (ObjectUtils.isEmpty(source)) { + return null; + } + return convert(source.toString(), getStyle(targetType), + getDurationUnit(targetType)); + } + + private DurationStyle getStyle(TypeDescriptor targetType) { + DurationFormat annotation = targetType.getAnnotation(DurationFormat.class); + return (annotation != null) ? annotation.value() : null; + } + + private ChronoUnit getDurationUnit(TypeDescriptor targetType) { + DurationUnit annotation = targetType.getAnnotation(DurationUnit.class); + return (annotation != null) ? annotation.value() : null; + } + + private Duration convert(String source, DurationStyle style, ChronoUnit unit) { + style = (style != null) ? style : DurationStyle.detect(source); + return style.parse(source, unit); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToEnumIgnoringCaseConverterFactory.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToEnumIgnoringCaseConverterFactory.java new file mode 100644 index 00000000..f53ce151 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/StringToEnumIgnoringCaseConverterFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.convert; + +import java.util.EnumSet; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; + +/** + * Converts from a String to a {@link java.lang.Enum} by calling searching matching enum + * names (ignoring case). + * + * @author Phillip Webb + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +final class StringToEnumIgnoringCaseConverterFactory + implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + Assert.notNull(enumType, "The target type " + targetType.getName() + + " does not refer to an enum"); + return new StringToEnum(enumType); + } + + private class StringToEnum implements Converter { + + private final Class enumType; + + StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (source.isEmpty()) { + return null; + } + source = source.trim(); + try { + return (T) Enum.valueOf(this.enumType, source); + } catch (Exception ex) { + return findEnum(source); + } + } + + private T findEnum(String source) { + String name = getLettersAndDigits(source); + for (T candidate : (Set) EnumSet.allOf(this.enumType)) { + if (getLettersAndDigits(candidate.name()).equals(name)) { + return candidate; + } + } + throw new IllegalArgumentException("No enum constant " + + this.enumType.getCanonicalName() + "." + source); + } + + private String getLettersAndDigits(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().map((c) -> (char) c).filter(Character::isLetterOrDigit) + .map(Character::toLowerCase).forEach(canonicalName::append); + return canonicalName.toString(); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/package-info.java new file mode 100644 index 00000000..cc11c752 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/convert/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-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. + */ + +/** + * Support for type conversion. + */ +package cn.springcloud.gray.bean.convert; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataSize.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataSize.java new file mode 100644 index 00000000..4e58571b --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataSize.java @@ -0,0 +1,251 @@ +package cn.springcloud.gray.bean.domain; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A data size, such as '12MB'. + * + *

This class models a size in terms of bytes and is immutable and thread-safe. + * + * @author Stephane Nicoll + * @since 5.1 + */ +public final class DataSize implements Comparable { + + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$"); + + /** + * Bytes per Kilobyte. + */ + private static long BYTES_PER_KB = 1024; + + /** + * Bytes per Megabyte. + */ + private static long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per Gigabyte. + */ + private static long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per Terabyte. + */ + private static long BYTES_PER_TB = BYTES_PER_GB * 1024; + + + private final long bytes; + + + private DataSize(long bytes) { + this.bytes = bytes; + } + + + /** + * Obtain a {@link DataSize} representing the specified number of bytes. + * + * @param bytes the number of bytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * Obtain a {@link DataSize} representing the specified number of kilobytes. + * + * @param kilobytes the number of kilobytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofKilobytes(long kilobytes) { + return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of megabytes. + * + * @param megabytes the number of megabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofMegabytes(long megabytes) { + return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of gigabytes. + * + * @param gigabytes the number of gigabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofGigabytes(long gigabytes) { + return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of terabytes. + * + * @param terabytes the number of terabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofTerabytes(long terabytes) { + return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); + } + + /** + * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. + * + * @param amount the amount of the size, measured in terms of the unit, + * positive or negative + * @return a corresponding {@link DataSize} + */ + public static DataSize of(long amount, DataUnit unit) { + Assert.notNull(unit, "Unit must not be null"); + return new DataSize(Math.multiplyExact(amount, unit.size().toBytes())); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * {@link DataUnit#BYTES} if no unit is specified. + *

+ * Examples: + *

+     * "12KB" -- parses as "12 kilobytes"
+     * "5MB"  -- parses as "5 megabytes"
+     * "20"   -- parses as "20 bytes"
+     * 
+ * + * @param text the text to parse + * @return the parsed {@link DataSize} + * @see #parse(CharSequence, DataUnit) + */ + public static DataSize parse(CharSequence text) { + return parse(text, null); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

+ * The string starts with a number followed optionally by a unit matching one of the + * supported {@link DataUnit suffixes}. + *

+ * Examples: + *

+     * "12KB" -- parses as "12 kilobytes"
+     * "5MB"  -- parses as "5 megabytes"
+     * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
+     * 
+ * + * @param text the text to parse + * @return the parsed {@link DataSize} + */ + public static DataSize parse(CharSequence text, DataUnit defaultUnit) { + Assert.notNull(text, "Text must not be null"); + try { + Matcher matcher = PATTERN.matcher(text); + Assert.state(matcher.matches(), "Does not match data size pattern"); + DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit); + long amount = Long.parseLong(matcher.group(1)); + return DataSize.of(amount, unit); + } catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); + } + } + + private static DataUnit determineDataUnit(String suffix, DataUnit defaultUnit) { + DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES); + return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse); + } + + /** + * Checks if this size is negative, excluding zero. + * + * @return true if this size has a size less than zero bytes + */ + public boolean isNegative() { + return this.bytes < 0; + } + + /** + * Return the number of bytes in this instance. + * + * @return the number of bytes + */ + public long toBytes() { + return this.bytes; + } + + /** + * Return the number of kilobytes in this instance. + * + * @return the number of kilobytes + */ + public long toKilobytes() { + return this.bytes / BYTES_PER_KB; + } + + /** + * Return the number of megabytes in this instance. + * + * @return the number of megabytes + */ + public long toMegabytes() { + return this.bytes / BYTES_PER_MB; + } + + /** + * Return the number of gigabytes in this instance. + * + * @return the number of gigabytes + */ + public long toGigabytes() { + return this.bytes / BYTES_PER_GB; + } + + /** + * Return the number of terabytes in this instance. + * + * @return the number of terabytes + */ + public long toTerabytes() { + return this.bytes / BYTES_PER_TB; + } + + @Override + public int compareTo(DataSize other) { + return Long.compare(this.bytes, other.bytes); + } + + @Override + public String toString() { + return String.format("%dB", this.bytes); + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + DataSize otherSize = (DataSize) other; + return (this.bytes == otherSize.bytes); + } + + @Override + public int hashCode() { + return Long.hashCode(this.bytes); + } + +} \ No newline at end of file diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataUnit.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataUnit.java new file mode 100644 index 00000000..914a0136 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/domain/DataUnit.java @@ -0,0 +1,70 @@ +package cn.springcloud.gray.bean.domain; + +import java.util.Objects; + +/** + * A standard set of data size units. + * + * @author Stephane Nicoll + * @since 5.1 + */ +public enum DataUnit { + + /** + * Bytes. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * Kilobytes. + */ + KILOBYTES("KB", DataSize.ofKilobytes(1)), + + /** + * Megabytes. + */ + MEGABYTES("MB", DataSize.ofMegabytes(1)), + + /** + * Gigabytes. + */ + GIGABYTES("GB", DataSize.ofGigabytes(1)), + + /** + * Terabytes. + */ + TERABYTES("TB", DataSize.ofTerabytes(1)); + + + private final String suffix; + + private final DataSize size; + + + DataUnit(String suffix, DataSize size) { + this.suffix = suffix; + this.size = size; + } + + DataSize size() { + return this.size; + } + + /** + * Return the {@link DataUnit} matching the specified {@code suffix}. + * + * @param suffix one of the standard suffix + * @return the {@link DataUnit} matching the specified {@code suffix} + * @throws IllegalArgumentException if the suffix does not match any + * of this enum's constants + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + if (Objects.equals(candidate.suffix, suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + } + +} \ No newline at end of file diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/Origin.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/Origin.java new file mode 100644 index 00000000..eb775017 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/Origin.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +import java.io.File; + +/** + * Interface that uniquely represents the origin of an item. For example, an item loaded + * from a {@link File} may have an origin made up of the file name along with line/column + * numbers. + *

+ * Implementations must provide sensible {@code hashCode()}, {@code equals(...)} and + * {@code #toString()} implementations. + * + * @author Madhura Bhave + * @author Phillip Webb + * @see OriginProvider + * @since 2.0.0 + */ +public interface Origin { + + /** + * Find the {@link Origin} that an object originated from. Checks if the source object + * is an {@link OriginProvider} and also searches exception stacks. + * + * @param source the source object or {@code null} + * @return an optional {@link Origin} + */ + static Origin from(Object source) { + if (source instanceof Origin) { + return (Origin) source; + } + Origin origin = null; + if (source != null && source instanceof OriginProvider) { + origin = ((OriginProvider) source).getOrigin(); + } + if (origin == null && source != null && source instanceof Throwable) { + return from(((Throwable) source).getCause()); + } + return origin; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginLookup.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginLookup.java new file mode 100644 index 00000000..5248dd3e --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginLookup.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +/** + * An interface that may be implemented by an object that can lookup {@link Origin} + * information from a given key. Can be used to add origin support to existing classes. + * + * @param the lookup key type + * @author Phillip Webb + * @since 2.0.0 + */ +@FunctionalInterface +public interface OriginLookup { + + /** + * Return the origin of the given key or {@code null} if the origin cannot be + * determined. + * + * @param key the key to lookup + * @return the origin of the key or {@code null} + */ + Origin getOrigin(K key); + + /** + * Attempt to lookup the origin from the given source. If the source is not a + * {@link OriginLookup} or if an exception occurs during lookup then {@code null} is + * returned. + * + * @param source the source object + * @param key the key to lookup + * @param the key type + * @return an {@link Origin} or {@code null} + */ + @SuppressWarnings("unchecked") + static Origin getOrigin(Object source, K key) { + if (!(source instanceof OriginLookup)) { + return null; + } + try { + return ((OriginLookup) source).getOrigin(key); + } catch (Throwable ex) { + return null; + } + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginProvider.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginProvider.java new file mode 100644 index 00000000..2a79fb82 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.origin; + +/** + * Interface to provide access to the origin of an item. + * + * @author Phillip Webb + * @see Origin + * @since 2.0.0 + */ +@FunctionalInterface +public interface OriginProvider { + + /** + * Return the source origin or {@code null} if the origin is not known. + * + * @return the origin or {@code null} + */ + Origin getOrigin(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginTrackedValue.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginTrackedValue.java new file mode 100644 index 00000000..8599a17c --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/OriginTrackedValue.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +import org.springframework.util.ObjectUtils; + +/** + * A wrapper for an {@link Object} value and {@link Origin}. + * + * @author Madhura Bhave + * @author Phillip Webb + * @see #of(Object) + * @see #of(Object, Origin) + * @since 2.0.0 + */ +public class OriginTrackedValue implements OriginProvider { + + private final Object value; + + private final Origin origin; + + private OriginTrackedValue(Object value, Origin origin) { + this.value = value; + this.origin = origin; + } + + /** + * Return the tracked value. + * + * @return the tracked value + */ + public Object getValue() { + return this.value; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.value, ((OriginTrackedValue) obj).value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public String toString() { + return (this.value != null) ? this.value.toString() : null; + } + + public static OriginTrackedValue of(Object value) { + return of(value, null); + } + + /** + * Create an {@link OriginTrackedValue} containing the specified {@code value} and + * {@code origin}. If the source value implements {@link CharSequence} then so will + * the resulting {@link OriginTrackedValue}. + * + * @param value the source value + * @param origin the origin + * @return an {@link OriginTrackedValue} or {@code null} if the source value was + * {@code null}. + */ + public static OriginTrackedValue of(Object value, Origin origin) { + if (value == null) { + return null; + } + if (value instanceof CharSequence) { + return new OriginTrackedCharSequence((CharSequence) value, origin); + } + return new OriginTrackedValue(value, origin); + } + + /** + * {@link OriginTrackedValue} for a {@link CharSequence}. + */ + private static class OriginTrackedCharSequence extends OriginTrackedValue + implements CharSequence { + + OriginTrackedCharSequence(CharSequence value, Origin origin) { + super(value, origin); + } + + @Override + public int length() { + return getValue().length(); + } + + @Override + public char charAt(int index) { + return getValue().charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return getValue().subSequence(start, end); + } + + @Override + public CharSequence getValue() { + return (CharSequence) super.getValue(); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/PropertySourceOrigin.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/PropertySourceOrigin.java new file mode 100644 index 00000000..a13018b6 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/PropertySourceOrigin.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; + +/** + * {@link Origin} from a {@link PropertySource}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class PropertySourceOrigin implements Origin { + + private final PropertySource propertySource; + + private final String propertyName; + + /** + * Create a new {@link PropertySourceOrigin} instance. + * + * @param propertySource the property source + * @param propertyName the name from the property source + */ + public PropertySourceOrigin(PropertySource propertySource, String propertyName) { + Assert.notNull(propertySource, "PropertySource must not be null"); + Assert.hasLength(propertyName, "PropertyName must not be empty"); + this.propertySource = propertySource; + this.propertyName = propertyName; + } + + /** + * Return the origin {@link PropertySource}. + * + * @return the origin property source + */ + public PropertySource getPropertySource() { + return this.propertySource; + } + + /** + * Return the property name that was used when obtaining the original value from the + * {@link #getPropertySource() property source}. + * + * @return the origin property name + */ + public String getPropertyName() { + return this.propertyName; + } + + @Override + public String toString() { + return "\"" + this.propertyName + "\" from property source \"" + + this.propertySource.getName() + "\""; + } + + /** + * Get an {@link Origin} for the given {@link PropertySource} and + * {@code propertyName}. Will either return an {@link OriginLookup} result or a + * {@link PropertySourceOrigin}. + * + * @param propertySource the origin property source + * @param name the property name + * @return the property origin + */ + public static Origin get(PropertySource propertySource, String name) { + Origin origin = OriginLookup.getOrigin(propertySource, name); + return (origin != null) ? origin : new PropertySourceOrigin(propertySource, name); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/SystemEnvironmentOrigin.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/SystemEnvironmentOrigin.java new file mode 100644 index 00000000..10548900 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/SystemEnvironmentOrigin.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link Origin} for an item loaded from the system environment. Provides access to the + * original property name. + * + * @author Madhura Bhave + * @since 2.0.0 + */ +public class SystemEnvironmentOrigin implements Origin { + + private final String property; + + public SystemEnvironmentOrigin(String property) { + Assert.notNull(property, "Property name must not be null"); + Assert.hasText(property, "Property name must not be empty"); + this.property = property; + } + + public String getProperty() { + return this.property; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SystemEnvironmentOrigin other = (SystemEnvironmentOrigin) obj; + return ObjectUtils.nullSafeEquals(this.property, other.property); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.property); + } + + @Override + public String toString() { + return "System Environment Property \"" + this.property + "\""; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/TextResourceOrigin.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/TextResourceOrigin.java new file mode 100644 index 00000000..1989d800 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/TextResourceOrigin.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.origin; + +import org.springframework.core.io.Resource; +import org.springframework.util.ObjectUtils; + +/** + * {@link Origin} for an item loaded from a text resource. Provides access to the original + * {@link Resource} that loaded the text and a {@link Location} within it. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.0.0 + */ +public class TextResourceOrigin implements Origin { + + private final Resource resource; + + private final Location location; + + public TextResourceOrigin(Resource resource, Location location) { + this.resource = resource; + this.location = location; + } + + /** + * Return the resource where the property originated. + * + * @return the text resource or {@code null} + */ + public Resource getResource() { + return this.resource; + } + + /** + * Return the location of the property within the source (if known). + * + * @return the location or {@code null} + */ + public Location getLocation() { + return this.location; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof TextResourceOrigin) { + TextResourceOrigin other = (TextResourceOrigin) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.resource, other.resource); + result = result && ObjectUtils.nullSafeEquals(this.location, other.location); + return result; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + ObjectUtils.nullSafeHashCode(this.resource); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.location); + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append((this.resource != null) ? this.resource.getDescription() + : "unknown resource [?]"); + if (this.location != null) { + result.append(":").append(this.location); + } + return result.toString(); + } + + /** + * A location (line and column number) within the resource. + */ + public static final class Location { + + private final int line; + + private final int column; + + /** + * Create a new {@link Location} instance. + * + * @param line the line number (zero indexed) + * @param column the column number (zero indexed) + */ + public Location(int line, int column) { + this.line = line; + this.column = column; + } + + /** + * Return the line of the text resource where the property originated. + * + * @return the line number (zero indexed) + */ + public int getLine() { + return this.line; + } + + /** + * Return the column of the text resource where the property originated. + * + * @return the column number (zero indexed) + */ + public int getColumn() { + return this.column; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Location other = (Location) obj; + boolean result = true; + result = result && this.line == other.line; + result = result && this.column == other.column; + return result; + } + + @Override + public int hashCode() { + return (31 * this.line) + this.column; + } + + @Override + public String toString() { + return (this.line + 1) + ":" + (this.column + 1); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/package-info.java new file mode 100644 index 00000000..6750ba2f --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/origin/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * Support for item origin tracking. + * + * @see org.springframework.boot.origin.Origin + */ +package cn.springcloud.gray.bean.origin; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationBeanFactoryMetadata.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationBeanFactoryMetadata.java new file mode 100644 index 00000000..6518f24a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationBeanFactoryMetadata.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility class to memorize {@code @Bean} definition meta data during initialization of + * the bean factory. + * + * @author Dave Syer + * @since 1.1.0 + */ +public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor { + + /** + * The bean name that this class is registered with. + */ + public static final String BEAN_NAME = ConfigurationBeanFactoryMetadata.class + .getName(); + + private ConfigurableListableBeanFactory beanFactory; + + private final Map beansFactoryMetadata = new HashMap<>(); + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + this.beanFactory = beanFactory; + for (String name : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + String method = definition.getFactoryMethodName(); + String bean = definition.getFactoryBeanName(); + if (method != null && bean != null) { + this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method)); + } + } + } + + public Map getBeansWithFactoryAnnotation( + Class type) { + Map result = new HashMap<>(); + for (String name : this.beansFactoryMetadata.keySet()) { + if (findFactoryAnnotation(name, type) != null) { + result.put(name, this.beanFactory.getBean(name)); + } + } + return result; + } + + public A findFactoryAnnotation(String beanName, + Class type) { + Method method = findFactoryMethod(beanName); + return (method != null) ? AnnotationUtils.findAnnotation(method, type) : null; + } + + public Method findFactoryMethod(String beanName) { + if (!this.beansFactoryMetadata.containsKey(beanName)) { + return null; + } + AtomicReference found = new AtomicReference<>(null); + FactoryMetadata metadata = this.beansFactoryMetadata.get(beanName); + Class factoryType = this.beanFactory.getType(metadata.getBean()); + String factoryMethod = metadata.getMethod(); + if (ClassUtils.isCglibProxyClass(factoryType)) { + factoryType = factoryType.getSuperclass(); + } + ReflectionUtils.doWithMethods(factoryType, (method) -> { + if (method.getName().equals(factoryMethod)) { + found.compareAndSet(null, method); + } + }); + return found.get(); + } + + private static class FactoryMetadata { + + private final String bean; + + private final String method; + + FactoryMetadata(String bean, String method) { + this.bean = bean; + this.method = method; + } + + public String getBean() { + return this.bean; + } + + public String getMethod() { + return this.method; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationProperties.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationProperties.java new file mode 100644 index 00000000..743e9e71 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationProperties.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +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 org.springframework.core.annotation.AliasFor; + +/** + * Annotation for externalized configuration. Add this to a class definition or a + * {@code @Bean} method in a {@code @Configuration} class if you want to bind and validate + * some external Properties (e.g. from a .properties file). + *

+ * Note that contrary to {@code @Value}, SpEL expressions are not evaluated since property + * values are externalized. + * + * @author Dave Syer + * @see ConfigurationPropertiesBindingPostProcessor + * @see EnableConfigurationProperties + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationProperties { + + /** + * The name prefix of the properties that are valid to bind to this object. Synonym + * for {@link #prefix()}. A valid prefix is defined by one or more words separated + * with dots (e.g. {@code "acme.system.feature"}). + * + * @return the name prefix of the properties to bind + */ + @AliasFor("prefix") + String value() default ""; + + /** + * The name prefix of the properties that are valid to bind to this object. Synonym + * for {@link #value()}. A valid prefix is defined by one or more words separated with + * dots (e.g. {@code "acme.system.feature"}). + * + * @return the name prefix of the properties to bind + */ + @AliasFor("value") + String prefix() default ""; + + /** + * Flag to indicate that when binding to this object invalid fields should be ignored. + * Invalid means invalid according to the binder that is used, and usually this means + * fields of the wrong type (or that cannot be coerced into the correct type). + * + * @return the flag value (default false) + */ + boolean ignoreInvalidFields() default false; + + /** + * Flag to indicate that when binding to this object unknown fields should be ignored. + * An unknown field could be a sign of a mistake in the Properties. + * + * @return the flag value (default true) + */ + boolean ignoreUnknownFields() default true; + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindException.java new file mode 100644 index 00000000..db3c1d77 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindException.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.util.ClassUtils; + +/** + * Exception thrown when {@link ConfigurationProperties @ConfigurationProperties} binding + * fails. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class ConfigurationPropertiesBindException extends BeanCreationException { + + private final Class beanType; + + private final ConfigurationProperties annotation; + + ConfigurationPropertiesBindException(String beanName, Object bean, + ConfigurationProperties annotation, Exception cause) { + super(beanName, getMessage(bean, annotation), cause); + this.beanType = bean.getClass(); + this.annotation = annotation; + } + + /** + * Return the bean type that was being bound. + * + * @return the bean type + */ + public Class getBeanType() { + return this.beanType; + } + + /** + * Return the configuration properties annotation that triggered the binding. + * + * @return the configuration properties annotation + */ + public ConfigurationProperties getAnnotation() { + return this.annotation; + } + + private static String getMessage(Object bean, ConfigurationProperties annotation) { + StringBuilder message = new StringBuilder(); + message.append("Could not bind properties to '" + + ClassUtils.getShortName(bean.getClass()) + "' : "); + message.append("prefix=").append(annotation.prefix()); + message.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); + message.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields()); + return message.toString(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindHandlerAdvisor.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindHandlerAdvisor.java new file mode 100644 index 00000000..57f3cc60 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindHandlerAdvisor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import cn.springcloud.gray.bean.properties.bind.AbstractBindHandler; +import cn.springcloud.gray.bean.properties.bind.BindHandler; + +/** + * Allows additional functionality to be applied to the {@link BindHandler} used by the + * {@link ConfigurationPropertiesBindingPostProcessor}. + * + * @author Phillip Webb + * @see AbstractBindHandler + * @since 2.1.0 + */ +@FunctionalInterface +public interface ConfigurationPropertiesBindHandlerAdvisor { + + /** + * Apply additional functionality to the source bind handler. + * + * @param bindHandler the source bind handler + * @return a replacement bind handler that delegates to the source and provides + * additional functionality + */ + BindHandler apply(BindHandler bindHandler); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinder.java new file mode 100644 index 00000000..f91286e8 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinder.java @@ -0,0 +1,175 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.springframework.beans.PropertyEditorRegistry; +import cn.springcloud.gray.bean.properties.bind.BindHandler; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.bind.Binder; +import cn.springcloud.gray.bean.properties.bind.PropertySourcesPlaceholdersResolver; +import cn.springcloud.gray.bean.properties.bind.handler.IgnoreErrorsBindHandler; +import cn.springcloud.gray.bean.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler; +import cn.springcloud.gray.bean.properties.bind.handler.NoUnboundElementsBindHandler; +import cn.springcloud.gray.bean.properties.bind.validation.ValidationBindHandler; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySources; +import cn.springcloud.gray.bean.properties.source.UnboundElementsSourceFilter; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * Internal class by the {@link ConfigurationPropertiesBindingPostProcessor} to handle the + * actual {@link ConfigurationProperties} binding. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ConfigurationPropertiesBinder { + + private final ApplicationContext applicationContext; + + private final PropertySources propertySources; + + private final Validator configurationPropertiesValidator; + + private final boolean jsr303Present; + + private volatile Validator jsr303Validator; + + private volatile Binder binder; + + ConfigurationPropertiesBinder(ApplicationContext applicationContext, + String validatorBeanName) { + this.applicationContext = applicationContext; + this.propertySources = new PropertySourcesDeducer(applicationContext) + .getPropertySources(); + this.configurationPropertiesValidator = getConfigurationPropertiesValidator( + applicationContext, validatorBeanName); + this.jsr303Present = ConfigurationPropertiesJsr303Validator + .isJsr303Present(applicationContext); + } + + public void bind(Bindable target) { + ConfigurationProperties annotation = target + .getAnnotation(ConfigurationProperties.class); + Assert.state(annotation != null, + "Missing @ConfigurationProperties on " + target); + List validators = getValidators(target); + BindHandler bindHandler = getBindHandler(annotation, validators); + getBinder().bind(annotation.prefix(), target, bindHandler); + } + + private Validator getConfigurationPropertiesValidator( + ApplicationContext applicationContext, String validatorBeanName) { + if (applicationContext.containsBean(validatorBeanName)) { + return applicationContext.getBean(validatorBeanName, Validator.class); + } + return null; + } + + private List getValidators(Bindable target) { + List validators = new ArrayList<>(3); + if (this.configurationPropertiesValidator != null) { + validators.add(this.configurationPropertiesValidator); + } + if (this.jsr303Present && target.getAnnotation(Validated.class) != null) { + validators.add(getJsr303Validator()); + } + if (target.getValue() != null && target.getValue().get() instanceof Validator) { + validators.add((Validator) target.getValue().get()); + } + return validators; + } + + private Validator getJsr303Validator() { + if (this.jsr303Validator == null) { + this.jsr303Validator = new ConfigurationPropertiesJsr303Validator( + this.applicationContext); + } + return this.jsr303Validator; + } + + private BindHandler getBindHandler(ConfigurationProperties annotation, + List validators) { + BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); + if (annotation.ignoreInvalidFields()) { + handler = new IgnoreErrorsBindHandler(handler); + } + if (!annotation.ignoreUnknownFields()) { + UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); + handler = new NoUnboundElementsBindHandler(handler, filter); + } + if (!validators.isEmpty()) { + handler = new ValidationBindHandler(handler, + validators.toArray(new Validator[0])); + } + for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) { + handler = advisor.apply(handler); + } + return handler; + } + + private List getBindHandlerAdvisors() { +// return this.applicationContext +// .getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class) +// .orderedStream().collect(Collectors.toList()); + + return this.applicationContext.getBeansOfType(ConfigurationPropertiesBindHandlerAdvisor.class) + .values().stream().collect(Collectors.toList()); + } + + private Binder getBinder() { + if (this.binder == null) { + this.binder = new Binder(getConfigurationPropertySources(), + getPropertySourcesPlaceholdersResolver(), getConversionService(), + getPropertyEditorInitializer()); + } + return this.binder; + } + + private Iterable getConfigurationPropertySources() { + return ConfigurationPropertySources.from(this.propertySources); + } + + private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() { + return new PropertySourcesPlaceholdersResolver(this.propertySources); + } + + private ConversionService getConversionService() { + return new ConversionServiceDeducer(this.applicationContext) + .getConversionService(); + } + + private Consumer getPropertyEditorInitializer() { + if (this.applicationContext instanceof ConfigurableApplicationContext) { + return ((ConfigurableApplicationContext) this.applicationContext) + .getBeanFactory()::copyRegisteredEditorsTo; + } + return null; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinding.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinding.java new file mode 100644 index 00000000..efd329f9 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBinding.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +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 org.springframework.beans.factory.annotation.Qualifier; + +/** + * Qualifier for beans that are needed to configure the binding of + * {@link ConfigurationProperties} (e.g. Converters). + * + * @author Dave Syer + */ +@Qualifier(ConfigurationPropertiesBinding.VALUE) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationPropertiesBinding { + + /** + * Concrete value for the {@link Qualifier @Qualifier}. + */ + String VALUE = "org.springframework.boot.context.properties.ConfigurationPropertiesBinding"; + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessor.java new file mode 100644 index 00000000..5964bae9 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.PropertySources; +import org.springframework.validation.annotation.Validated; + +/** + * {@link BeanPostProcessor} to bind {@link PropertySources} to beans annotated with + * {@link ConfigurationProperties}. + * + * @author Dave Syer + * @author Phillip Webb + * @author Christian Dupuis + * @author Stephane Nicoll + * @author Madhura Bhave + */ +public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, + PriorityOrdered, ApplicationContextAware, InitializingBean { + + /** + * The bean name that this post-processor is registered with. + */ + public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class + .getName(); + + /** + * The bean name of the configuration properties validator. + */ + public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; + + private ConfigurationBeanFactoryMetadata beanFactoryMetadata; + + private ApplicationContext applicationContext; + + private ConfigurationPropertiesBinder configurationPropertiesBinder; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + // We can't use constructor injection of the application context because + // it causes eager factory bean initialization + this.beanFactoryMetadata = this.applicationContext.getBean( + ConfigurationBeanFactoryMetadata.BEAN_NAME, + ConfigurationBeanFactoryMetadata.class); + this.configurationPropertiesBinder = new ConfigurationPropertiesBinder( + this.applicationContext, VALIDATOR_BEAN_NAME); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + ConfigurationProperties annotation = getAnnotation(bean, beanName, + ConfigurationProperties.class); + if (annotation != null) { + bind(bean, beanName, annotation); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + private void bind(Object bean, String beanName, ConfigurationProperties annotation) { + ResolvableType type = getBeanType(bean, beanName); + Validated validated = getAnnotation(bean, beanName, Validated.class); + Annotation[] annotations = (validated != null) + ? new Annotation[]{annotation, validated} + : new Annotation[]{annotation}; + Bindable target = Bindable.of(type).withExistingValue(bean) + .withAnnotations(annotations); + try { + this.configurationPropertiesBinder.bind(target); + } catch (Exception ex) { + throw new ConfigurationPropertiesBindException(beanName, bean, annotation, + ex); + } + } + + private ResolvableType getBeanType(Object bean, String beanName) { + Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName); + if (factoryMethod != null) { + return ResolvableType.forMethodReturnType(factoryMethod); + } + return ResolvableType.forClass(bean.getClass()); + } + + private A getAnnotation(Object bean, String beanName, + Class type) { + A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type); + if (annotation == null) { + annotation = AnnotationUtils.findAnnotation(bean.getClass(), type); + } + return annotation; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java new file mode 100644 index 00000000..79ceac6d --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +/** + * {@link ImportBeanDefinitionRegistrar} for binding externalized application properties + * to {@link ConfigurationProperties} beans. + * + * @author Dave Syer + * @author Phillip Webb + */ +public class ConfigurationPropertiesBindingPostProcessorRegistrar + implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition( + ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) { + registerConfigurationPropertiesBindingPostProcessor(registry); + registerConfigurationBeanFactoryMetadata(registry); + } + } + + private void registerConfigurationPropertiesBindingPostProcessor( + BeanDefinitionRegistry registry) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition( + ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition); + + } + + private void registerConfigurationBeanFactoryMetadata( + BeanDefinitionRegistry registry) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationBeanFactoryMetadata.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, + definition); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesJsr303Validator.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesJsr303Validator.java new file mode 100644 index 00000000..f60db3f7 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConfigurationPropertiesJsr303Validator.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import org.springframework.boot.validation.MessageInterpolatorFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ClassUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +/** + * Validator that supports configuration classes annotated with + * {@link Validated @Validated}. + * + * @author Phillip Webb + */ +final class ConfigurationPropertiesJsr303Validator implements Validator { + + private static final String[] VALIDATOR_CLASSES = {"javax.validation.Validator", + "javax.validation.ValidatorFactory", + "javax.validation.bootstrap.GenericBootstrap"}; + + private final Delegate delegate; + + ConfigurationPropertiesJsr303Validator(ApplicationContext applicationContext) { + this.delegate = new Delegate(applicationContext); + } + + @Override + public boolean supports(Class type) { + return this.delegate.supports(type); + } + + @Override + public void validate(Object target, Errors errors) { + this.delegate.validate(target, errors); + } + + public static boolean isJsr303Present(ApplicationContext applicationContext) { + ClassLoader classLoader = applicationContext.getClassLoader(); + for (String validatorClass : VALIDATOR_CLASSES) { + if (!ClassUtils.isPresent(validatorClass, classLoader)) { + return false; + } + } + return true; + } + + private static class Delegate extends LocalValidatorFactoryBean { + + Delegate(ApplicationContext applicationContext) { + setApplicationContext(applicationContext); + setMessageInterpolator(new MessageInterpolatorFactory().getObject()); + afterPropertiesSet(); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConversionServiceDeducer.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConversionServiceDeducer.java new file mode 100644 index 00000000..1360e645 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/ConversionServiceDeducer.java @@ -0,0 +1,177 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Predicate; + +import cn.springcloud.gray.bean.convert.ApplicationConversionService; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * Utility to deduce the {@link ConversionService} to use for configuration properties + * binding. + * + * @author Phillip Webb + */ +class ConversionServiceDeducer { + + private final ApplicationContext applicationContext; + + ConversionServiceDeducer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public ConversionService getConversionService() { + try { + return this.applicationContext.getBean( + ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, + ConversionService.class); + } catch (NoSuchBeanDefinitionException ex) { + return new Factory(this.applicationContext.getAutowireCapableBeanFactory()) + .create(); + } + } + + private static class Factory { + + @SuppressWarnings("rawtypes") + private final List converters; + + private final List genericConverters; + + Factory(BeanFactory beanFactory) { + this.converters = beans(beanFactory, Converter.class, + ConfigurationPropertiesBinding.VALUE); + this.genericConverters = beans(beanFactory, GenericConverter.class, + ConfigurationPropertiesBinding.VALUE); + } + + private List beans(BeanFactory beanFactory, Class type, + String qualifier) { + if (beanFactory instanceof ListableBeanFactory) { + return beans(type, qualifier, (ListableBeanFactory) beanFactory); + } + return Collections.emptyList(); + } + + private List beans(Class type, String qualifier, + ListableBeanFactory beanFactory) { + return new ArrayList(qualifiedBeansOfType(beanFactory, type, qualifier).values()); + } + + public ConversionService create() { + if (this.converters.isEmpty() && this.genericConverters.isEmpty()) { + return ApplicationConversionService.getSharedInstance(); + } + ApplicationConversionService conversionService = new ApplicationConversionService(); + for (Converter converter : this.converters) { + conversionService.addConverter(converter); + } + for (GenericConverter genericConverter : this.genericConverters) { + conversionService.addConverter(genericConverter); + } + return conversionService; + } + + } + + + public static Map qualifiedBeansOfType( + ListableBeanFactory beanFactory, Class beanType, String qualifier) throws BeansException { + + String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType); + Map result = new LinkedHashMap<>(4); + for (String beanName : candidateBeans) { + if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) { + result.put(beanName, beanFactory.getBean(beanName, beanType)); + } + } + return result; + } + + + public static boolean isQualifierMatch( + Predicate qualifier, String beanName, BeanFactory beanFactory) { + + // Try quick bean name or alias match first... + if (qualifier.test(beanName)) { + return true; + } + if (beanFactory != null) { + for (String alias : beanFactory.getAliases(beanName)) { + if (qualifier.test(alias)) { + return true; + } + } + try { + Class beanType = beanFactory.getType(beanName); + if (beanFactory instanceof ConfigurableBeanFactory) { + BeanDefinition bd = ((ConfigurableBeanFactory) beanFactory).getMergedBeanDefinition(beanName); + // Explicit qualifier metadata on bean definition? (typically in XML definition) + if (bd instanceof AbstractBeanDefinition) { + AbstractBeanDefinition abd = (AbstractBeanDefinition) bd; + AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName()); + if (candidate != null) { + Object value = candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY); + if (value != null && qualifier.test(value.toString())) { + return true; + } + } + } + // Corresponding qualifier on factory method? (typically in configuration class) + if (bd instanceof RootBeanDefinition) { + Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod(); + if (factoryMethod != null) { + Qualifier targetAnnotation = AnnotationUtils.getAnnotation(factoryMethod, Qualifier.class); + if (targetAnnotation != null) { + return qualifier.test(targetAnnotation.value()); + } + } + } + } + // Corresponding qualifier on bean implementation class? (for custom user types) + if (beanType != null) { + Qualifier targetAnnotation = AnnotationUtils.getAnnotation(beanType, Qualifier.class); + if (targetAnnotation != null) { + return qualifier.test(targetAnnotation.value()); + } + } + } catch (NoSuchBeanDefinitionException ex) { + // Ignore - can't compare qualifiers for a manually registered singleton object + } + } + return false; + } +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/DeprecatedConfigurationProperty.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/DeprecatedConfigurationProperty.java new file mode 100644 index 00000000..3bb012d2 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/DeprecatedConfigurationProperty.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties; + +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; + +/** + * Indicates that a getter in a {@link ConfigurationProperties} object is deprecated. This + * annotation has no bearing on the actual binding processes, but it is used by the + * {@code spring-boot-configuration-processor} to add deprecation meta-data. + *

+ * This annotation must be used on the getter of the deprecated element. + * + * @author Phillip Webb + * @since 1.3.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DeprecatedConfigurationProperty { + + /** + * The reason for the deprecation. + * + * @return the deprecation reason + */ + String reason() default ""; + + /** + * The field that should be used instead (if any). + * + * @return the replacement field + */ + String replacement() default ""; + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationProperties.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationProperties.java new file mode 100644 index 00000000..ff69e1f5 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationProperties.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties; + +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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * Enable support for {@link ConfigurationProperties} annotated beans. + * {@link ConfigurationProperties} beans can be registered in the standard way (for + * example using {@link Bean @Bean} methods) or, for convenience, can be specified + * directly on this annotation. + * + * @author Dave Syer + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(EnableConfigurationPropertiesImportSelector.class) +public @interface EnableConfigurationProperties { + + /** + * Convenient way to quickly register {@link ConfigurationProperties} annotated beans + * with Spring. Standard Spring Beans will also be scanned regardless of this value. + * + * @return {@link ConfigurationProperties} annotated beans to register + */ + Class[] value() default {}; + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationPropertiesImportSelector.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationPropertiesImportSelector.java new file mode 100644 index 00000000..a3a82230 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/EnableConfigurationPropertiesImportSelector.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Import selector that sets up binding of external properties to configuration classes + * (see {@link ConfigurationProperties}). It either registers a + * {@link ConfigurationProperties} bean or not, depending on whether the enclosing + * {@link EnableConfigurationProperties} explicitly declares one. If none is declared then + * a bean post processor will still kick in for any beans annotated as external + * configuration. If one is declared, then a bean definition is registered with id equal + * to the class name (thus an application context usually only contains one + * {@link ConfigurationProperties} bean of each unique type). + * + * @author Dave Syer + * @author Christian Dupuis + * @author Stephane Nicoll + */ +class EnableConfigurationPropertiesImportSelector implements ImportSelector { + + private static final String[] IMPORTS = { + ConfigurationPropertiesBeanRegistrar.class.getName(), + ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName()}; + + @Override + public String[] selectImports(AnnotationMetadata metadata) { + return IMPORTS; + } + + /** + * {@link ImportBeanDefinitionRegistrar} for configuration properties support. + */ + public static class ConfigurationPropertiesBeanRegistrar + implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + getTypes(metadata).forEach((type) -> register(registry, + (ConfigurableListableBeanFactory) registry, type)); + } + + private List> getTypes(AnnotationMetadata metadata) { + MultiValueMap attributes = metadata + .getAllAnnotationAttributes( + EnableConfigurationProperties.class.getName(), false); + return collectClasses((attributes != null) ? attributes.get("value") + : Collections.emptyList()); + } + + private List> collectClasses(List values) { + return values.stream().flatMap((value) -> Arrays.stream((Object[]) value)) + .map((o) -> (Class) o).filter((type) -> void.class != type) + .collect(Collectors.toList()); + } + + private void register(BeanDefinitionRegistry registry, + ConfigurableListableBeanFactory beanFactory, Class type) { + String name = getName(type); + if (!containsBeanDefinition(beanFactory, name)) { + registerBeanDefinition(registry, name, type); + } + } + + private String getName(Class type) { + ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, + ConfigurationProperties.class); + String prefix = (annotation != null) ? annotation.prefix() : ""; + return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() + : type.getName()); + } + + private boolean containsBeanDefinition( + ConfigurableListableBeanFactory beanFactory, String name) { + if (beanFactory.containsBeanDefinition(name)) { + return true; + } + BeanFactory parent = beanFactory.getParentBeanFactory(); + if (parent instanceof ConfigurableListableBeanFactory) { + return containsBeanDefinition((ConfigurableListableBeanFactory) parent, + name); + } + return false; + } + + private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, + Class type) { + assertHasAnnotation(type); + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(type); + registry.registerBeanDefinition(name, definition); + } + + private void assertHasAnnotation(Class type) { + Assert.notNull( + AnnotationUtils.findAnnotation(type, ConfigurationProperties.class), + "No " + ConfigurationProperties.class.getSimpleName() + + " annotation found on '" + type.getName() + "'."); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/NestedConfigurationProperty.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/NestedConfigurationProperty.java new file mode 100644 index 00000000..52f16677 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/NestedConfigurationProperty.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties; + +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; + +/** + * Indicates that a field in a {@link ConfigurationProperties} object should be treated as + * if it were a nested type. This annotation has no bearing on the actual binding + * processes, but it is used by the {@code spring-boot-configuration-processor} as a hint + * that a field is not bound as a single value. When this is specified, a nested group is + * created for the field and its type is harvested. + *

+ * This has no effect on collections and maps as these types are automatically identified. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NestedConfigurationProperty { + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertyMapper.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertyMapper.java new file mode 100644 index 00000000..cb429fc0 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertyMapper.java @@ -0,0 +1,392 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Utility that can be used to map values from a supplied source to a destination. + * Primarily intended to be help when mapping from + * {@link ConfigurationProperties @ConfigurationProperties} to third-party classes. + *

+ * Can filter values based on predicates and adapt values if needed. For example: + *

+ * PropertyMapper map = PropertyMapper.get();
+ * map.from(source::getName)
+ *   .to(destination::setName);
+ * map.from(source::getTimeout)
+ *   .whenNonNull()
+ *   .asInt(Duration::getSeconds)
+ *   .to(destination::setTimeoutSecs);
+ * map.from(source::isEnabled)
+ *   .whenFalse().
+ *   .toCall(destination::disable);
+ * 
+ *

+ * Mappings can ultimately be applied to a {@link Source#to(Consumer) setter}, trigger a + * {@link Source#toCall(Runnable) method call} or create a + * {@link Source#toInstance(Function) new instance}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public final class PropertyMapper { + + private static final Predicate ALWAYS = (t) -> true; + + private static final PropertyMapper INSTANCE = new PropertyMapper(null, null); + + private final PropertyMapper parent; + + private final SourceOperator sourceOperator; + + private PropertyMapper(PropertyMapper parent, SourceOperator sourceOperator) { + this.parent = parent; + this.sourceOperator = sourceOperator; + } + + /** + * Return a new {@link PropertyMapper} instance that applies + * {@link Source#whenNonNull() whenNonNull} to every source. + * + * @return a new property mapper instance + */ + public PropertyMapper alwaysApplyingWhenNonNull() { + return alwaysApplying(this::whenNonNull); + } + + private Source whenNonNull(Source source) { + return source.whenNonNull(); + } + + /** + * Return a new {@link PropertyMapper} instance that applies the given + * {@link SourceOperator} to every source. + * + * @param operator the source operator to apply + * @return a new property mapper instance + */ + public PropertyMapper alwaysApplying(SourceOperator operator) { + Assert.notNull(operator, "Operator must not be null"); + return new PropertyMapper(this, operator); + } + + /** + * Return a new {@link Source} from the specified value supplier that can be used to + * perform the mapping. + * + * @param the source type + * @param supplier the value supplier + * @return a {@link Source} that can be used to complete the mapping + * @see #from(Object) + */ + public Source from(Supplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + Source source = getSource(supplier); + if (this.sourceOperator != null) { + source = this.sourceOperator.apply(source); + } + return source; + } + + /** + * Return a new {@link Source} from the specified value that can be used to perform + * the mapping. + * + * @param the source type + * @param value the value + * @return a {@link Source} that can be used to complete the mapping + */ + public Source from(T value) { + return from(() -> value); + } + + @SuppressWarnings("unchecked") + private Source getSource(Supplier supplier) { + if (this.parent != null) { + return this.parent.from(supplier); + } + return new Source<>(new CachingSupplier<>(supplier), (Predicate) ALWAYS); + } + + /** + * Return the property mapper. + * + * @return the property mapper + */ + public static PropertyMapper get() { + return INSTANCE; + } + + /** + * Supplier that caches the value to prevent multiple calls. + */ + private static class CachingSupplier implements Supplier { + + private final Supplier supplier; + + private boolean hasResult; + + private T result; + + CachingSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + if (!this.hasResult) { + this.result = this.supplier.get(); + this.hasResult = true; + } + return this.result; + } + + } + + /** + * An operation that can be applied to a {@link Source}. + */ + @FunctionalInterface + public interface SourceOperator { + + /** + * Apply the operation to the given source. + * + * @param the source type + * @param source the source to operate on + * @return the updated source + */ + Source apply(Source source); + + } + + /** + * A source that is in the process of being mapped. + * + * @param the source type + */ + public static final class Source { + + private final Supplier supplier; + + private final Predicate predicate; + + private Source(Supplier supplier, Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + this.supplier = supplier; + this.predicate = predicate; + } + + /** + * Return an adapted version of the source with {@link Integer} type. + * + * @param the resulting type + * @param adapter an adapter to convert the current value to a number. + * @return a new adapted source instance + */ + public Source asInt(Function adapter) { + return as(adapter).as(Number::intValue); + } + + /** + * Return an adapted version of the source changed via the given adapter function. + * + * @param the resulting type + * @param adapter the adapter to apply + * @return a new adapted source instance + */ + public Source as(Function adapter) { + Assert.notNull(adapter, "Adapter must not be null"); + Supplier test = () -> this.predicate.test(this.supplier.get()); + Predicate predicate = (t) -> test.get(); + Supplier supplier = () -> { + if (test.get()) { + return adapter.apply(this.supplier.get()); + } + return null; + }; + return new Source<>(supplier, predicate); + } + + /** + * Return a filtered version of the source that won't map non-null values or + * suppliers that throw a {@link NullPointerException}. + * + * @return a new filtered source instance + */ + public Source whenNonNull() { + return new Source<>(new NullPointerExceptionSafeSupplier<>(this.supplier), + Objects::nonNull); + } + + /** + * Return a filtered version of the source that will only map values that are + * {@code true}. + * + * @return a new filtered source instance + */ + public Source whenTrue() { + return when(Boolean.TRUE::equals); + } + + /** + * Return a filtered version of the source that will only map values that are + * {@code false}. + * + * @return a new filtered source instance + */ + public Source whenFalse() { + return when(Boolean.FALSE::equals); + } + + /** + * Return a filtered version of the source that will only map values that have a + * {@code toString()} containing actual text. + * + * @return a new filtered source instance + */ + public Source whenHasText() { + return when((value) -> StringUtils.hasText(Objects.toString(value, null))); + } + + /** + * Return a filtered version of the source that will only map values equal to the + * specified {@code object}. + * + * @param object the object to match + * @return a new filtered source instance + */ + public Source whenEqualTo(Object object) { + return when(object::equals); + } + + /** + * Return a filtered version of the source that will only map values that are an + * instance of the given type. + * + * @param the target type + * @param target the target type to match + * @return a new filtered source instance + */ + public Source whenInstanceOf(Class target) { + return when(target::isInstance).as(target::cast); + } + + /** + * Return a filtered version of the source that won't map values that match the + * given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source whenNot(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + return new Source<>(this.supplier, predicate.negate()); + } + + /** + * Return a filtered version of the source that won't map values that don't match + * the given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source when(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + return new Source<>(this.supplier, predicate); + } + + /** + * Complete the mapping by passing any non-filtered value to the specified + * consumer. + * + * @param consumer the consumer that should accept the value if it's not been + * filtered + */ + public void to(Consumer consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + T value = this.supplier.get(); + if (this.predicate.test(value)) { + consumer.accept(value); + } + } + + /** + * Complete the mapping by creating a new instance from the non-filtered value. + * + * @param the resulting type + * @param factory the factory used to create the instance + * @return the instance + * @throws NoSuchElementException if the value has been filtered + */ + public R toInstance(Function factory) { + Assert.notNull(factory, "Factory must not be null"); + T value = this.supplier.get(); + if (!this.predicate.test(value)) { + throw new NoSuchElementException("No value present"); + } + return factory.apply(value); + } + + /** + * Complete the mapping by calling the specified method when the value has not + * been filtered. + * + * @param runnable the method to call if the value has not been filtered + */ + public void toCall(Runnable runnable) { + Assert.notNull(runnable, "Runnable must not be null"); + T value = this.supplier.get(); + if (this.predicate.test(value)) { + runnable.run(); + } + } + + } + + /** + * Supplier that will catch and ignore any {@link NullPointerException}. + */ + private static class NullPointerExceptionSafeSupplier implements Supplier { + + private final Supplier supplier; + + NullPointerExceptionSafeSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + try { + return this.supplier.get(); + } catch (NullPointerException ex) { + return null; + } + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertySourcesDeducer.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertySourcesDeducer.java new file mode 100644 index 00000000..cfcaf48d --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/PropertySourcesDeducer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; + +/** + * Utility to deduce the {@link PropertySources} to use for configuration binding. + * + * @author Phillip Webb + */ +class PropertySourcesDeducer { + + private static final Log logger = LogFactory.getLog(PropertySourcesDeducer.class); + + private final ApplicationContext applicationContext; + + PropertySourcesDeducer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public PropertySources getPropertySources() { + PropertySourcesPlaceholderConfigurer configurer = getSinglePropertySourcesPlaceholderConfigurer(); + if (configurer != null) { + return configurer.getAppliedPropertySources(); + } + MutablePropertySources sources = extractEnvironmentPropertySources(); + if (sources != null) { + return sources; + } + throw new IllegalStateException("Unable to obtain PropertySources from " + + "PropertySourcesPlaceholderConfigurer or Environment"); + } + + private MutablePropertySources extractEnvironmentPropertySources() { + Environment environment = this.applicationContext.getEnvironment(); + if (environment instanceof ConfigurableEnvironment) { + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + return null; + } + + private PropertySourcesPlaceholderConfigurer getSinglePropertySourcesPlaceholderConfigurer() { + // Take care not to cause early instantiation of all FactoryBeans + Map beans = this.applicationContext + .getBeansOfType(PropertySourcesPlaceholderConfigurer.class, false, false); + if (beans.size() == 1) { + return beans.values().iterator().next(); + } + if (beans.size() > 1 && logger.isWarnEnabled()) { + logger.warn( + "Multiple PropertySourcesPlaceholderConfigurer " + "beans registered " + + beans.keySet() + ", falling back to Environment"); + } + return null; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AbstractBindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AbstractBindHandler.java new file mode 100644 index 00000000..13f572bb --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AbstractBindHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link BindHandler} implementations. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public abstract class AbstractBindHandler implements BindHandler { + + private final BindHandler parent; + + /** + * Create a new binding handler instance. + */ + public AbstractBindHandler() { + this(BindHandler.DEFAULT); + } + + /** + * Create a new binding handler instance with a specific parent. + * + * @param parent the parent handler + */ + public AbstractBindHandler(BindHandler parent) { + Assert.notNull(parent, "Parent must not be null"); + this.parent = parent; + } + + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return this.parent.onStart(name, target, context); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return this.parent.onSuccess(name, target, context, result); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return this.parent.onFailure(name, target, context, error); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + this.parent.onFinish(name, target, context, result); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateBinder.java new file mode 100644 index 00000000..9a670d9c --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateBinder.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.util.function.Supplier; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; + +/** + * Internal strategy used by {@link Binder} to bind aggregates (Maps, Lists, Arrays). + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class AggregateBinder { + + private final Context context; + + AggregateBinder(Context context) { + this.context = context; + } + + /** + * Determine if recursive binding is supported. + * + * @param source the configuration property source or {@code null} for all sources. + * @return if recursive binding is supported + */ + protected abstract boolean isAllowRecursiveBinding( + ConfigurationPropertySource source); + + /** + * Perform binding for the aggregate. + * + * @param name the configuration property name to bind + * @param target the target to bind + * @param elementBinder an element binder + * @return the bound aggregate or null + */ + @SuppressWarnings("unchecked") + public final Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder) { + Object result = bindAggregate(name, target, elementBinder); + Supplier value = target.getValue(); + if (result == null || value == null) { + return result; + } + return merge((Supplier) value, (T) result); + } + + /** + * Perform the actual aggregate binding. + * + * @param name the configuration property name to bind + * @param target the target to bind + * @param elementBinder an element binder + * @return the bound result + */ + protected abstract Object bindAggregate(ConfigurationPropertyName name, + Bindable target, AggregateElementBinder elementBinder); + + /** + * Merge any additional elements into the existing aggregate. + * + * @param existing the supplier for the existing value + * @param additional the additional elements to merge + * @return the merged result + */ + protected abstract T merge(Supplier existing, T additional); + + /** + * Return the context being used by this binder. + * + * @return the context + */ + protected final Context getContext() { + return this.context; + } + + /** + * Internal class used to supply the aggregate and cache the value. + * + * @param the aggregate type + */ + protected static class AggregateSupplier { + + private final Supplier supplier; + + private T supplied; + + public AggregateSupplier(Supplier supplier) { + this.supplier = supplier; + } + + public T get() { + if (this.supplied == null) { + this.supplied = this.supplier.get(); + } + return this.supplied; + } + + public boolean wasSupplied() { + return this.supplied != null; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateElementBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateElementBinder.java new file mode 100644 index 00000000..6f7f41af --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/AggregateElementBinder.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; + +/** + * Binder that can be used by {@link AggregateBinder} implementations to recursively bind + * elements. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +@FunctionalInterface +interface AggregateElementBinder { + + /** + * Bind the given name to a target bindable. + * + * @param name the name to bind + * @param target the target bindable + * @return a bound object or {@code null} + */ + default Object bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the given name to a target bindable using optionally limited to a single + * source. + * + * @param name the name to bind + * @param target the target bindable + * @param source the source of the elements or {@code null} to use all sources + * @return a bound object or {@code null} + */ + Object bind(ConfigurationPropertyName name, Bindable target, + ConfigurationPropertySource source); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/ArrayBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/ArrayBinder.java new file mode 100644 index 00000000..975fb404 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/ArrayBinder.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for arrays. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ArrayBinder extends IndexedElementsBinder { + + ArrayBinder(Context context) { + super(context); + } + + @Override + protected Object bindAggregate(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder) { + IndexedCollectionSupplier result = new IndexedCollectionSupplier(ArrayList::new); + ResolvableType aggregateType = target.getType(); + ResolvableType elementType = target.getType().getComponentType(); + bindIndexed(name, target, elementBinder, aggregateType, elementType, result); + if (result.wasSupplied()) { + List list = (List) result.get(); + Object array = Array.newInstance(elementType.resolve(), list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(array, i, list.get(i)); + } + return array; + } + return null; + } + + @Override + protected Object merge(Supplier existing, Object additional) { + return additional; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanBinder.java new file mode 100644 index 00000000..e2d47909 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanBinder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; + +/** + * Internal strategy used by {@link Binder} to bind beans. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +interface BeanBinder { + + /** + * Return a bound bean instance or {@code null} if the {@link BeanBinder} does not + * support the specified {@link Bindable}. + * + * @param name the name being bound + * @param target the bindable to bind + * @param context the bind context + * @param propertyBinder property binder + * @param the source type + * @return a bound instance or {@code null} + */ + T bind(ConfigurationPropertyName name, Bindable target, Context context, + BeanPropertyBinder propertyBinder); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyBinder.java new file mode 100644 index 00000000..a910620f --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyBinder.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.bind; + +/** + * Binder that can be used by {@link BeanBinder} implementations to recursively bind bean + * properties. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +interface BeanPropertyBinder { + + /** + * Bind the given property. + * + * @param propertyName the property name (in lowercase dashed form, e.g. + * {@code first-name}) + * @param target the target bindable + * @return the bound value or {@code null} + */ + Object bindProperty(String propertyName, Bindable target); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyName.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyName.java new file mode 100644 index 00000000..f78fc0fa --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BeanPropertyName.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.bind; + +/** + * Internal utility to help when dealing with Java Bean property names. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class BeanPropertyName { + + private BeanPropertyName() { + } + + /** + * Return the specified Java Bean property name in dashed form. + * + * @param name the source name + * @return the dashed from + */ + public static String toDashedForm(String name) { + return toDashedForm(name, 0); + } + + /** + * Return the specified Java Bean property name in dashed form. + * + * @param name the source name + * @param start the starting char + * @return the dashed from + */ + public static String toDashedForm(String name, int start) { + StringBuilder result = new StringBuilder(); + char[] chars = name.replace("_", "-").toCharArray(); + for (int i = start; i < chars.length; i++) { + char ch = chars[i]; + if (Character.isUpperCase(ch) && result.length() > 0 + && result.charAt(result.length() - 1) != '-') { + result.append("-"); + } + result.append(Character.toLowerCase(ch)); + } + return result.toString(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindContext.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindContext.java new file mode 100644 index 00000000..bc8c5880 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; + +/** + * Context information for use by {@link BindHandler BindHandlers}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindContext { + + /** + * Return the source binder that is performing the bind operation. + * + * @return the source binder + */ + Binder getBinder(); + + /** + * Return the current depth of the binding. Root binding starts with a depth of + * {@code 0}. Each subsequent property binding increases the depth by {@code 1}. + * + * @return the depth of the current binding + */ + int getDepth(); + + /** + * Return an {@link Iterable} of the {@link ConfigurationPropertySource sources} being + * used by the {@link Binder}. + * + * @return the sources + */ + Iterable getSources(); + + /** + * Return the {@link ConfigurationProperty} actually being bound or {@code null} if + * the property has not yet been determined. + * + * @return the configuration property (may be {@code null}). + */ + ConfigurationProperty getConfigurationProperty(); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindConverter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindConverter.java new file mode 100644 index 00000000..fda6221a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindConverter.java @@ -0,0 +1,266 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.beans.PropertyEditor; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.propertyeditors.FileEditor; +import cn.springcloud.gray.bean.convert.ApplicationConversionService; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.util.Assert; + +/** + * Utility to handle any conversion needed during binding. This class is not thread-safe + * and so a new instance is created for each top-level bind. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class BindConverter { + + private static final Set> EXCLUDED_EDITORS; + + static { + Set> excluded = new HashSet<>(); + excluded.add(FileEditor.class); // gh-12163 + EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded); + } + + private static BindConverter sharedInstance; + + private final ConversionService conversionService; + + private BindConverter(ConversionService conversionService, + Consumer propertyEditorInitializer) { + Assert.notNull(conversionService, "ConversionService must not be null"); + List conversionServices = getConversionServices( + conversionService, propertyEditorInitializer); + this.conversionService = new CompositeConversionService(conversionServices); + } + + private List getConversionServices( + ConversionService conversionService, + Consumer propertyEditorInitializer) { + List services = new ArrayList<>(); + services.add(new TypeConverterConversionService(propertyEditorInitializer)); + services.add(conversionService); + if (!(conversionService instanceof ApplicationConversionService)) { + services.add(ApplicationConversionService.getSharedInstance()); + } + return services; + } + + public boolean canConvert(Object value, ResolvableType type, + Annotation... annotations) { + return this.conversionService.canConvert(TypeDescriptor.forObject(value), + new ResolvableTypeDescriptor(type, annotations)); + } + + public T convert(Object result, Bindable target) { + return convert(result, target.getType(), target.getAnnotations()); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, ResolvableType type, Annotation... annotations) { + if (value == null) { + return null; + } + return (T) this.conversionService.convert(value, TypeDescriptor.forObject(value), + new ResolvableTypeDescriptor(type, annotations)); + } + + static BindConverter get(ConversionService conversionService, + Consumer propertyEditorInitializer) { + if (conversionService == ApplicationConversionService.getSharedInstance() + && propertyEditorInitializer == null) { + if (sharedInstance == null) { + sharedInstance = new BindConverter(conversionService, + propertyEditorInitializer); + } + return sharedInstance; + } + return new BindConverter(conversionService, propertyEditorInitializer); + } + + /** + * A {@link TypeDescriptor} backed by a {@link ResolvableType}. + */ + private static class ResolvableTypeDescriptor extends TypeDescriptor { + + ResolvableTypeDescriptor(ResolvableType resolvableType, + Annotation[] annotations) { + super(resolvableType, null, annotations); + } + + } + + /** + * Composite {@link ConversionService} used to call multiple services. + */ + static class CompositeConversionService implements ConversionService { + + private final List delegates; + + CompositeConversionService(List delegates) { + this.delegates = delegates; + } + + @Override + public boolean canConvert(Class sourceType, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return canConvert( + (sourceType != null) ? TypeDescriptor.valueOf(sourceType) : null, + TypeDescriptor.valueOf(targetType)); + } + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (ConversionService service : this.delegates) { + if (service.canConvert(sourceType, targetType)) { + return true; + } + } + return false; + } + + @Override + @SuppressWarnings("unchecked") + public T convert(Object source, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return (T) convert(source, TypeDescriptor.forObject(source), + TypeDescriptor.valueOf(targetType)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + for (int i = 0; i < this.delegates.size() - 1; i++) { + try { + ConversionService delegate = this.delegates.get(i); + if (delegate.canConvert(sourceType, targetType)) { + return delegate.convert(source, sourceType, targetType); + } + } catch (ConversionException ex) { + } + } + return this.delegates.get(this.delegates.size() - 1).convert(source, + sourceType, targetType); + } + + } + + /** + * A {@link ConversionService} implementation that delegates to a + * {@link SimpleTypeConverter}. Allows {@link PropertyEditor} based conversion for + * simple types, arrays and collections. + */ + private static class TypeConverterConversionService extends GenericConversionService { + + TypeConverterConversionService(Consumer initializer) { + addConverter(new TypeConverterConverter(createTypeConverter(initializer))); + ApplicationConversionService.addDelimitedStringConverters(this); + } + + private SimpleTypeConverter createTypeConverter( + Consumer initializer) { + SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + if (initializer != null) { + initializer.accept(typeConverter); + } + return typeConverter; + } + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Prefer conversion service to handle things like String to char[]. + if (targetType.isArray() + && targetType.getElementTypeDescriptor().isPrimitive()) { + return false; + } + return super.canConvert(sourceType, targetType); + } + + } + + /** + * {@link ConditionalGenericConverter} that delegates to {@link SimpleTypeConverter}. + */ + private static class TypeConverterConverter implements ConditionalGenericConverter { + + private final SimpleTypeConverter typeConverter; + + TypeConverterConverter(SimpleTypeConverter typeConverter) { + this.typeConverter = typeConverter; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return getPropertyEditor(targetType.getType()) != null; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + SimpleTypeConverter typeConverter = this.typeConverter; + return typeConverter.convertIfNecessary(source, targetType.getType()); + } + + private PropertyEditor getPropertyEditor(Class type) { + SimpleTypeConverter typeConverter = this.typeConverter; + if (type == null || type == Object.class + || Collection.class.isAssignableFrom(type) + || Map.class.isAssignableFrom(type)) { + return null; + } + PropertyEditor editor = typeConverter.getDefaultEditor(type); + if (editor == null) { + editor = typeConverter.findCustomEditor(type, null); + } + if (editor == null && String.class != type) { + editor = BeanUtils.findEditorByConvention(type); + } + if (editor == null || EXCLUDED_EDITORS.contains(editor.getClass())) { + return null; + } + return editor; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindException.java new file mode 100644 index 00000000..d7a27fdc --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindException.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; + +/** + * Exception thrown when binding fails. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class BindException extends RuntimeException { + + private final Bindable target; + + private final ConfigurationProperty property; + + private final ConfigurationPropertyName name; + + BindException(ConfigurationPropertyName name, Bindable target, + ConfigurationProperty property, Throwable cause) { + super(buildMessage(name, target), cause); + this.name = name; + this.target = target; + this.property = property; + } + + /** + * Return the name of the configuration property being bound. + * + * @return the configuration property name + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the target being bound. + * + * @return the bind target + */ + public Bindable getTarget() { + return this.target; + } + + /** + * Return the configuration property name of the item that was being bound. + * + * @return the configuration property name + */ + public ConfigurationProperty getProperty() { + return this.property; + } + + private static String buildMessage(ConfigurationPropertyName name, + Bindable target) { + StringBuilder message = new StringBuilder(); + message.append("Failed to bind properties"); + message.append((name != null) ? " under '" + name + "'" : ""); + message.append(" to ").append(target.getType()); + return message.toString(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindHandler.java new file mode 100644 index 00000000..f8a19703 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; + +/** + * Callback interface that can be used to handle additional logic during element + * {@link Binder binding}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindHandler { + + /** + * Default no-op bind handler. + */ + BindHandler DEFAULT = new BindHandler() { + + }; + + /** + * Called when binding of an element starts but before any result has been determined. + * + * @param the bindable source type + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @return the actual item that should be used for binding (may be {@code null}) + */ + default Bindable onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return target; + } + + /** + * Called when binding of an element ends with a successful result. Implementations + * may change the ultimately returned result or perform addition validation. + * + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (never {@code null}) + * @return the actual result that should be used (may be {@code null}) + */ + default Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return result; + } + + /** + * Called when binding fails for any reason (including failures from + * {@link #onSuccess} calls). Implementations may choose to swallow exceptions and + * return an alternative result. + * + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param error the cause of the error (if the exception stands it may be re-thrown) + * @return the actual result that should be used (may be {@code null}). + * @throws Exception if the binding isn't valid + */ + default Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + throw error; + } + + /** + * Called when binding finishes, regardless of whether the property was bound or not. + * + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (may be {@code null}) + * @throws Exception if the binding isn't valid + */ + default void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindResult.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindResult.java new file mode 100644 index 00000000..912b2bb8 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/BindResult.java @@ -0,0 +1,175 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A container object to return the result of a {@link Binder} bind operation. May contain + * either a successfully bound object or an empty result. + * + * @param the result type + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class BindResult { + + private static final BindResult UNBOUND = new BindResult<>(null); + + private final T value; + + private BindResult(T value) { + this.value = value; + } + + /** + * Return the object that was bound or throw a {@link NoSuchElementException} if no + * value was bound. + * + * @return the bound value (never {@code null}) + * @throws NoSuchElementException if no value was bound + * @see #isBound() + */ + public T get() throws NoSuchElementException { + if (this.value == null) { + throw new NoSuchElementException("No value bound"); + } + return this.value; + } + + /** + * Returns {@code true} if a result was bound. + * + * @return if a result was bound + */ + public boolean isBound() { + return (this.value != null); + } + + /** + * Invoke the specified consumer with the bound value, or do nothing if no value has + * been bound. + * + * @param consumer block to execute if a value has been bound + */ + public void ifBound(Consumer consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + if (this.value != null) { + consumer.accept(this.value); + } + } + + /** + * Apply the provided mapping function to the bound value, or return an updated + * unbound result if no value has been bound. + * + * @param the type of the result of the mapping function + * @param mapper a mapping function to apply to the bound value. The mapper will not + * be invoked if no value has been bound. + * @return an {@code BindResult} describing the result of applying a mapping function + * to the value of this {@code BindResult}. + */ + public BindResult map(Function mapper) { + Assert.notNull(mapper, "Mapper must not be null"); + return of((this.value != null) ? mapper.apply(this.value) : null); + } + + /** + * Return the object that was bound, or {@code other} if no value has been bound. + * + * @param other the value to be returned if there is no bound value (may be + * {@code null}) + * @return the value, if bound, otherwise {@code other} + */ + public T orElse(T other) { + return (this.value != null) ? this.value : other; + } + + /** + * Return the object that was bound, or the result of invoking {@code other} if no + * value has been bound. + * + * @param other a {@link Supplier} of the value to be returned if there is no bound + * value + * @return the value, if bound, otherwise the supplied {@code other} + */ + public T orElseGet(Supplier other) { + return (this.value != null) ? this.value : other.get(); + } + + /** + * Return the object that was bound, or a new instance of the specified class if no + * value has been bound. + * + * @param type the type to create if no value was bound + * @return the value, if bound, otherwise a new instance of {@code type} + */ + public T orElseCreate(Class type) { + Assert.notNull(type, "Type must not be null"); + return (this.value != null) ? this.value : BeanUtils.instantiateClass(type); + } + + /** + * Return the object that was bound, or throw an exception to be created by the + * provided supplier if no value has been bound. + * + * @param the type of the exception to be thrown + * @param exceptionSupplier the supplier which will return the exception to be thrown + * @return the present value + * @throws X if there is no value present + */ + public T orElseThrow(Supplier exceptionSupplier) + throws X { + if (this.value == null) { + throw exceptionSupplier.get(); + } + return this.value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.value, ((BindResult) obj).value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.value); + } + + @SuppressWarnings("unchecked") + static BindResult of(T value) { + if (value == null) { + return (BindResult) UNBOUND; + } + return new BindResult<>(value); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Bindable.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Bindable.java new file mode 100644 index 00000000..abc2e49a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Bindable.java @@ -0,0 +1,279 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Source that can be bound by a {@link Binder}. + * + * @param the source type + * @author Phillip Webb + * @author Madhura Bhave + * @see Bindable#of(Class) + * @see Bindable#of(ResolvableType) + * @since 2.0.0 + */ +public final class Bindable { + + private static final Annotation[] NO_ANNOTATIONS = {}; + + private final ResolvableType type; + + private final ResolvableType boxedType; + + private final Supplier value; + + private final Annotation[] annotations; + + private Bindable(ResolvableType type, ResolvableType boxedType, Supplier value, + Annotation[] annotations) { + this.type = type; + this.boxedType = boxedType; + this.value = value; + this.annotations = annotations; + } + + /** + * Return the type of the item to bind. + * + * @return the type being bound + */ + public ResolvableType getType() { + return this.type; + } + + /** + * Return the boxed type of the item to bind. + * + * @return the boxed type for the item being bound + */ + public ResolvableType getBoxedType() { + return this.boxedType; + } + + /** + * Return a supplier that provides the object value or {@code null}. + * + * @return the value or {@code null} + */ + public Supplier getValue() { + return this.value; + } + + /** + * Return any associated annotations that could affect binding. + * + * @return the associated annotations + */ + public Annotation[] getAnnotations() { + return this.annotations; + } + + /** + * Return a single associated annotations that could affect binding. + * + * @param the annotation type + * @param type annotation type + * @return the associated annotation or {@code null} + */ + @SuppressWarnings("unchecked") + public A getAnnotation(Class type) { + for (Annotation annotation : this.annotations) { + if (type.isInstance(annotation)) { + return (A) annotation; + } + } + return null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bindable other = (Bindable) obj; + boolean result = true; + result = result && nullSafeEquals(this.type.resolve(), other.type.resolve()); + result = result && nullSafeEquals(this.annotations, other.annotations); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.nullSafeHashCode(this.type); + result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations); + return result; + } + + @Override + public String toString() { + ToStringCreator creator = new ToStringCreator(this); + creator.append("type", this.type); + creator.append("value", (this.value != null) ? "provided" : "none"); + creator.append("annotations", this.annotations); + return creator.toString(); + } + + private boolean nullSafeEquals(Object o1, Object o2) { + return ObjectUtils.nullSafeEquals(o1, o2); + } + + /** + * Create an updated {@link Bindable} instance with the specified annotations. + * + * @param annotations the annotations + * @return an updated {@link Bindable} + */ + public Bindable withAnnotations(Annotation... annotations) { + return new Bindable<>(this.type, this.boxedType, this.value, + (annotations != null) ? annotations : NO_ANNOTATIONS); + } + + /** + * Create an updated {@link Bindable} instance with an existing value. + * + * @param existingValue the existing value + * @return an updated {@link Bindable} + */ + public Bindable withExistingValue(T existingValue) { + Assert.isTrue( + existingValue == null || this.type.isArray() + || this.boxedType.resolve().isInstance(existingValue), + "ExistingValue must be an instance of " + this.type); + Supplier value = (existingValue != null) ? () -> existingValue : null; + return new Bindable<>(this.type, this.boxedType, value, NO_ANNOTATIONS); + } + + /** + * Create an updated {@link Bindable} instance with a value supplier. + * + * @param suppliedValue the supplier for the value + * @return an updated {@link Bindable} + */ + public Bindable withSuppliedValue(Supplier suppliedValue) { + return new Bindable<>(this.type, this.boxedType, suppliedValue, NO_ANNOTATIONS); + } + + /** + * Create a new {@link Bindable} of the type of the specified instance with an + * existing value equal to the instance. + * + * @param the source type + * @param instance the instance (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + * @see #withExistingValue(Object) + */ + @SuppressWarnings("unchecked") + public static Bindable ofInstance(T instance) { + Assert.notNull(instance, "Instance must not be null"); + Class type = (Class) instance.getClass(); + return of(type).withExistingValue(instance); + } + + /** + * Create a new {@link Bindable} of the specified type. + * + * @param the source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + */ + public static Bindable of(Class type) { + Assert.notNull(type, "Type must not be null"); + return of(ResolvableType.forClass(type)); + } + + /** + * Create a new {@link Bindable} {@link List} of the specified element type. + * + * @param the element type + * @param elementType the list element type + * @return a {@link Bindable} instance + */ + public static Bindable> listOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(List.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Set} of the specified element type. + * + * @param the element type + * @param elementType the set element type + * @return a {@link Bindable} instance + */ + public static Bindable> setOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(Set.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Map} of the specified key and value type. + * + * @param the key type + * @param the value type + * @param keyType the map key type + * @param valueType the map value type + * @return a {@link Bindable} instance + */ + public static Bindable> mapOf(Class keyType, Class valueType) { + return of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType)); + } + + /** + * Create a new {@link Bindable} of the specified type. + * + * @param the source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(Class) + */ + public static Bindable of(ResolvableType type) { + Assert.notNull(type, "Type must not be null"); + ResolvableType boxedType = box(type); + return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS); + } + + private static ResolvableType box(ResolvableType type) { + Class resolved = type.resolve(); + if (resolved != null && resolved.isPrimitive()) { + Object array = Array.newInstance(resolved, 1); + Class wrapperType = Array.get(array, 0).getClass(); + return ResolvableType.forClass(wrapperType); + } + if (resolved != null && resolved.isArray()) { + return ResolvableType.forArrayComponent(box(type.getComponentType())); + } + return type; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Binder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Binder.java new file mode 100644 index 00000000..5aaa1035 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/Binder.java @@ -0,0 +1,502 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySources; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyState; +import cn.springcloud.gray.bean.convert.ApplicationConversionService; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.env.Environment; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.Assert; + +/** + * A container object which Binds objects from one or more + * {@link ConfigurationPropertySource ConfigurationPropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class Binder { + + private static final Set> NON_BEAN_CLASSES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class))); + + private static final List BEAN_BINDERS; + + static { + List binders = new ArrayList<>(); + binders.add(new JavaBeanBinder()); + BEAN_BINDERS = Collections.unmodifiableList(binders); + } + + private final Iterable sources; + + private final PlaceholdersResolver placeholdersResolver; + + private final ConversionService conversionService; + + private final Consumer propertyEditorInitializer; + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * + * @param sources the sources used for binding + */ + public Binder(ConfigurationPropertySource... sources) { + this(Arrays.asList(sources), null, null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * + * @param sources the sources used for binding + */ + public Binder(Iterable sources) { + this(sources, null, null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property placeholders + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver) { + this(sources, placeholdersResolver, null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property placeholders + * @param conversionService the conversion service to convert values (or {@code null} + * to use {@link ApplicationConversionService}) + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver, + ConversionService conversionService) { + this(sources, placeholdersResolver, conversionService, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property placeholders + * @param conversionService the conversion service to convert values (or {@code null} + * to use {@link ApplicationConversionService}) + * @param propertyEditorInitializer initializer used to configure the property editors + * that can convert values (or {@code null} if no initialization is required). Often + * used to call {@link ConfigurableListableBeanFactory#copyRegisteredEditorsTo}. + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver, + ConversionService conversionService, + Consumer propertyEditorInitializer) { + Assert.notNull(sources, "Sources must not be null"); + this.sources = sources; + this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver + : PlaceholdersResolver.NONE; + this.conversionService = (conversionService != null) ? conversionService + : ApplicationConversionService.getSharedInstance(); + this.propertyEditorInitializer = propertyEditorInitializer; + } + + /** + * Bind the specified target {@link Class} using this binder's + * {@link ConfigurationPropertySource property sources}. + * + * @param name the configuration property name to bind + * @param target the target class + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Class target) { + return bind(name, Bindable.of(target)); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources}. + * + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Bindable target) { + return bind(ConfigurationPropertyName.of(name), target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources}. + * + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources}. + * + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(String name, Bindable target, BindHandler handler) { + return bind(ConfigurationPropertyName.of(name), target, handler); + } + + /** + * Bind the specified target {@link Bindable} using this binder's + * {@link ConfigurationPropertySource property sources}. + * + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(target, "Target must not be null"); + handler = (handler != null) ? handler : BindHandler.DEFAULT; + Context context = new Context(); + T bound = bind(name, target, handler, context, false); + return BindResult.of(bound); + } + + protected final T bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, boolean allowRecursiveBinding) { + context.clearConfigurationProperty(); + try { + target = handler.onStart(name, target, context); + if (target == null) { + return null; + } + Object bound = bindObject(name, target, handler, context, + allowRecursiveBinding); + return handleBindResult(name, target, handler, context, bound); + } catch (Exception ex) { + return handleBindError(name, target, handler, context, ex); + } + } + + private T handleBindResult(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Object result) throws Exception { + if (result != null) { + result = handler.onSuccess(name, target, context, result); + result = context.getConverter().convert(result, target); + } + handler.onFinish(name, target, context, result); + return context.getConverter().convert(result, target); + } + + private T handleBindError(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Exception error) { + try { + Object result = handler.onFailure(name, target, context, error); + return context.getConverter().convert(result, target); + } catch (Exception ex) { + if (ex instanceof BindException) { + throw (BindException) ex; + } + throw new BindException(name, target, context.getConfigurationProperty(), ex); + } + } + + private Object bindObject(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, boolean allowRecursiveBinding) { + ConfigurationProperty property = findProperty(name, context); + if (property == null && containsNoDescendantOf(context.getSources(), name)) { + return null; + } + AggregateBinder aggregateBinder = getAggregateBinder(target, context); + if (aggregateBinder != null) { + return bindAggregate(name, target, handler, context, aggregateBinder); + } + if (property != null) { + try { + return bindProperty(target, context, property); + } catch (ConverterNotFoundException ex) { + // We might still be able to bind it as a bean + Object bean = bindBean(name, target, handler, context, + allowRecursiveBinding); + if (bean != null) { + return bean; + } + throw ex; + } + } + return bindBean(name, target, handler, context, allowRecursiveBinding); + } + + private AggregateBinder getAggregateBinder(Bindable target, Context context) { + Class resolvedType = target.getType().resolve(Object.class); + if (Map.class.isAssignableFrom(resolvedType)) { + return new MapBinder(context); + } + if (Collection.class.isAssignableFrom(resolvedType)) { + return new CollectionBinder(context); + } + if (target.getType().isArray()) { + return new ArrayBinder(context); + } + return null; + } + + private Object bindAggregate(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, AggregateBinder aggregateBinder) { + AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> { + boolean allowRecursiveBinding = aggregateBinder + .isAllowRecursiveBinding(source); + Supplier supplier = () -> bind(itemName, itemTarget, handler, context, + allowRecursiveBinding); + return context.withSource(source, supplier); + }; + return context.withIncreasedDepth( + () -> aggregateBinder.bind(name, target, elementBinder)); + } + + private ConfigurationProperty findProperty(ConfigurationPropertyName name, + Context context) { + if (name.isEmpty()) { + return null; + } + for (ConfigurationPropertySource source : context.getSources()) { + ConfigurationProperty property = source.getConfigurationProperty(name); + if (property != null) { + return property; + } + } + return null; + } + + private Object bindProperty(Bindable target, Context context, + ConfigurationProperty property) { + context.setConfigurationProperty(property); + Object result = property.getValue(); + result = this.placeholdersResolver.resolvePlaceholders(result); + result = context.getConverter().convert(result, target); + return result; + } + + private Object bindBean(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, boolean allowRecursiveBinding) { + if (containsNoDescendantOf(context.getSources(), name) + || isUnbindableBean(name, target, context)) { + return null; + } + BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind( + name.append(propertyName), propertyTarget, handler, context, false); + Class type = target.getType().resolve(Object.class); + if (!allowRecursiveBinding && context.hasBoundBean(type)) { + return null; + } + return context.withBean(type, () -> { + Stream boundBeans = BEAN_BINDERS.stream() + .map((b) -> b.bind(name, target, context, propertyBinder)); + return boundBeans.filter(Objects::nonNull).findFirst().orElse(null); + }); + } + + private boolean isUnbindableBean(ConfigurationPropertyName name, Bindable target, + Context context) { + for (ConfigurationPropertySource source : context.getSources()) { + if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { + // We know there are properties to bind so we can't bypass anything + return false; + } + } + Class resolved = target.getType().resolve(Object.class); + if (resolved.isPrimitive() || NON_BEAN_CLASSES.contains(resolved)) { + return true; + } + return resolved.getName().startsWith("java."); + } + + private boolean containsNoDescendantOf(Iterable sources, + ConfigurationPropertyName name) { + for (ConfigurationPropertySource source : sources) { + if (source.containsDescendantOf(name) != ConfigurationPropertyState.ABSENT) { + return false; + } + } + return true; + } + + /** + * Create a new {@link Binder} instance from the specified environment. + * + * @param environment the environment source (must have attached + * {@link ConfigurationPropertySources}) + * @return a {@link Binder} instance + */ + public static Binder get(Environment environment) { + return new Binder(ConfigurationPropertySources.get(environment), + new PropertySourcesPlaceholdersResolver(environment)); + } + + /** + * Context used when binding and the {@link BindContext} implementation. + */ + final class Context implements BindContext { + + private final BindConverter converter; + + private int depth; + + private final List source = Arrays + .asList((ConfigurationPropertySource) null); + + private int sourcePushCount; + + private final Deque> beans = new ArrayDeque<>(); + + private ConfigurationProperty configurationProperty; + + Context() { + this.converter = BindConverter.get(Binder.this.conversionService, + Binder.this.propertyEditorInitializer); + } + + private void increaseDepth() { + this.depth++; + } + + private void decreaseDepth() { + this.depth--; + } + + private T withSource(ConfigurationPropertySource source, + Supplier supplier) { + if (source == null) { + return supplier.get(); + } + this.source.set(0, source); + this.sourcePushCount++; + try { + return supplier.get(); + } finally { + this.sourcePushCount--; + } + } + + private T withBean(Class bean, Supplier supplier) { + this.beans.push(bean); + try { + return withIncreasedDepth(supplier); + } finally { + this.beans.pop(); + } + } + + private boolean hasBoundBean(Class bean) { + return this.beans.contains(bean); + } + + private T withIncreasedDepth(Supplier supplier) { + increaseDepth(); + try { + return supplier.get(); + } finally { + decreaseDepth(); + } + } + + private void setConfigurationProperty( + ConfigurationProperty configurationProperty) { + this.configurationProperty = configurationProperty; + } + + private void clearConfigurationProperty() { + this.configurationProperty = null; + } + + public PlaceholdersResolver getPlaceholdersResolver() { + return Binder.this.placeholdersResolver; + } + + public BindConverter getConverter() { + return this.converter; + } + + @Override + public Binder getBinder() { + return Binder.this; + } + + @Override + public int getDepth() { + return this.depth; + } + + @Override + public Iterable getSources() { + if (this.sourcePushCount > 0) { + return this.source; + } + return Binder.this.sources; + } + + @Override + public ConfigurationProperty getConfigurationProperty() { + return this.configurationProperty; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/CollectionBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/CollectionBinder.java new file mode 100644 index 00000000..5129bb8c --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/CollectionBinder.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.properties.bind; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for collections. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class CollectionBinder extends IndexedElementsBinder> { + + CollectionBinder(Context context) { + super(context); + } + + @Override + protected Object bindAggregate(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder) { + Class collectionType = (target.getValue() != null) ? List.class + : target.getType().resolve(Object.class); + ResolvableType aggregateType = ResolvableType.forClassWithGenerics(List.class, + target.getType().asCollection().getGenerics()); + ResolvableType elementType = target.getType().asCollection().getGeneric(); + IndexedCollectionSupplier result = new IndexedCollectionSupplier( + () -> CollectionFactory.createCollection(collectionType, + elementType.resolve(), 0)); + bindIndexed(name, target, elementBinder, aggregateType, elementType, result); + if (result.wasSupplied()) { + return result.get(); + } + return null; + } + + @Override + protected Collection merge(Supplier> existing, + Collection additional) { + Collection existingCollection = getExistingIfPossible(existing); + if (existingCollection == null) { + return additional; + } + try { + existingCollection.clear(); + existingCollection.addAll(additional); + return copyIfPossible(existingCollection); + } catch (UnsupportedOperationException ex) { + return createNewCollection(additional); + } + } + + private Collection getExistingIfPossible( + Supplier> existing) { + try { + return existing.get(); + } catch (Exception ex) { + return null; + } + } + + private Collection copyIfPossible(Collection collection) { + try { + return createNewCollection(collection); + } catch (Exception ex) { + return collection; + } + } + + private Collection createNewCollection(Collection collection) { + Collection result = CollectionFactory + .createCollection(collection.getClass(), collection.size()); + result.addAll(collection); + return result; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/IndexedElementsBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/IndexedElementsBinder.java new file mode 100644 index 00000000..e80b9fe6 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/IndexedElementsBinder.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName.Form; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.IterableConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Base class for {@link AggregateBinder AggregateBinders} that read a sequential run of + * indexed items. + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class IndexedElementsBinder extends AggregateBinder { + + private static final String INDEX_ZERO = "[0]"; + + IndexedElementsBinder(Context context) { + super(context); + } + + @Override + protected boolean isAllowRecursiveBinding(ConfigurationPropertySource source) { + return source == null || source instanceof IterableConfigurationPropertySource; + } + + /** + * Bind indexed elements to the supplied collection. + * + * @param name the name of the property to bind + * @param target the target bindable + * @param elementBinder the binder to use for elements + * @param aggregateType the aggregate type, may be a collection or an array + * @param elementType the element type + * @param result the destination for results + */ + protected final void bindIndexed(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, ResolvableType aggregateType, + ResolvableType elementType, IndexedCollectionSupplier result) { + for (ConfigurationPropertySource source : getContext().getSources()) { + bindIndexed(source, name, target, elementBinder, result, aggregateType, + elementType); + if (result.wasSupplied() && result.get() != null) { + return; + } + } + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, Bindable target, + AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, + ResolvableType aggregateType, ResolvableType elementType) { + ConfigurationProperty property = source.getConfigurationProperty(root); + if (property != null) { + bindValue(target, collection.get(), aggregateType, elementType, + property.getValue()); + } else { + bindIndexed(source, root, elementBinder, collection, elementType); + } + } + + private void bindValue(Bindable target, Collection collection, + ResolvableType aggregateType, ResolvableType elementType, Object value) { + if (value instanceof String && !StringUtils.hasText((String) value)) { + return; + } + Object aggregate = convert(value, aggregateType, target.getAnnotations()); + ResolvableType collectionType = ResolvableType + .forClassWithGenerics(collection.getClass(), elementType); + Collection elements = convert(aggregate, collectionType); + collection.addAll(elements); + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, AggregateElementBinder elementBinder, + IndexedCollectionSupplier collection, ResolvableType elementType) { + MultiValueMap knownIndexedChildren = getKnownIndexedChildren( + source, root); + for (int i = 0; i < Integer.MAX_VALUE; i++) { + ConfigurationPropertyName name = root + .append((i != 0) ? "[" + i + "]" : INDEX_ZERO); + Object value = elementBinder.bind(name, Bindable.of(elementType), source); + if (value == null) { + break; + } + knownIndexedChildren.remove(name.getLastElement(Form.UNIFORM)); + collection.get().add(value); + } + assertNoUnboundChildren(knownIndexedChildren); + } + + private MultiValueMap getKnownIndexedChildren( + ConfigurationPropertySource source, ConfigurationPropertyName root) { + MultiValueMap children = new LinkedMultiValueMap<>(); + if (!(source instanceof IterableConfigurationPropertySource)) { + return children; + } + for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source + .filter(root::isAncestorOf)) { + ConfigurationPropertyName choppedName = name + .chop(root.getNumberOfElements() + 1); + if (choppedName.isLastElementIndexed()) { + String key = choppedName.getLastElement(Form.UNIFORM); + ConfigurationProperty value = source.getConfigurationProperty(name); + children.add(key, value); + } + } + return children; + } + + private void assertNoUnboundChildren( + MultiValueMap children) { + if (!children.isEmpty()) { + throw new UnboundConfigurationPropertiesException( + children.values().stream().flatMap(List::stream) + .collect(Collectors.toCollection(TreeSet::new))); + } + } + + private C convert(Object value, ResolvableType type, Annotation... annotations) { + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + return getContext().getConverter().convert(value, type, annotations); + } + + /** + * {@link AggregateBinder.AggregateSupplier AggregateSupplier} for an indexed + * collection. + */ + protected static class IndexedCollectionSupplier + extends AggregateSupplier> { + + public IndexedCollectionSupplier(Supplier> supplier) { + super(supplier); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/JavaBeanBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/JavaBeanBinder.java new file mode 100644 index 00000000..b582106e --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/JavaBeanBinder.java @@ -0,0 +1,335 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.beans.Introspector; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyState; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; + +/** + * {@link BeanBinder} for mutable Java Beans. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class JavaBeanBinder implements BeanBinder { + + @Override + public T bind(ConfigurationPropertyName name, Bindable target, Context context, + BeanPropertyBinder propertyBinder) { + boolean hasKnownBindableProperties = hasKnownBindableProperties(name, context); + Bean bean = Bean.get(target, hasKnownBindableProperties); + if (bean == null) { + return null; + } + BeanSupplier beanSupplier = bean.getSupplier(target); + boolean bound = bind(propertyBinder, bean, beanSupplier); + return (bound ? beanSupplier.get() : null); + } + + private boolean hasKnownBindableProperties(ConfigurationPropertyName name, + Context context) { + for (ConfigurationPropertySource source : context.getSources()) { + if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { + return true; + } + } + return false; + } + + private boolean bind(BeanPropertyBinder propertyBinder, Bean bean, + BeanSupplier beanSupplier) { + boolean bound = false; + for (BeanProperty beanProperty : bean.getProperties().values()) { + bound |= bind(beanSupplier, propertyBinder, beanProperty); + } + return bound; + } + + private boolean bind(BeanSupplier beanSupplier, + BeanPropertyBinder propertyBinder, BeanProperty property) { + String propertyName = property.getName(); + ResolvableType type = property.getType(); + Supplier value = property.getValue(beanSupplier); + Annotation[] annotations = property.getAnnotations(); + Object bound = propertyBinder.bindProperty(propertyName, + Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); + if (bound == null) { + return false; + } + if (property.isSettable()) { + property.setValue(beanSupplier, bound); + } else if (value == null || !bound.equals(value.get())) { + throw new IllegalStateException( + "No setter found for property: " + property.getName()); + } + return true; + } + + /** + * The bean being bound. + */ + private static class Bean { + + private static Bean cached; + + private final Class type; + + private final ResolvableType resolvableType; + + private final Map properties = new LinkedHashMap<>(); + + Bean(ResolvableType resolvableType, Class type) { + this.resolvableType = resolvableType; + this.type = type; + putProperties(type); + } + + private void putProperties(Class type) { + while (type != null && !Object.class.equals(type)) { + for (Method method : type.getDeclaredMethods()) { + if (isCandidate(method)) { + addMethod(method); + } + } + for (Field field : type.getDeclaredFields()) { + addField(field); + } + type = type.getSuperclass(); + } + } + + private boolean isCandidate(Method method) { + int modifiers = method.getModifiers(); + return Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers) + && !Modifier.isStatic(modifiers) + && !Object.class.equals(method.getDeclaringClass()) + && !Class.class.equals(method.getDeclaringClass()); + } + + private void addMethod(Method method) { + addMethodIfPossible(method, "get", 0, BeanProperty::addGetter); + addMethodIfPossible(method, "is", 0, BeanProperty::addGetter); + addMethodIfPossible(method, "set", 1, BeanProperty::addSetter); + } + + private void addMethodIfPossible(Method method, String prefix, int parameterCount, + BiConsumer consumer) { + if (method.getParameterCount() == parameterCount + && method.getName().startsWith(prefix) + && method.getName().length() > prefix.length()) { + String propertyName = Introspector + .decapitalize(method.getName().substring(prefix.length())); + consumer.accept(this.properties.computeIfAbsent(propertyName, + this::getBeanProperty), method); + } + } + + private BeanProperty getBeanProperty(String name) { + return new BeanProperty(name, this.resolvableType); + } + + private void addField(Field field) { + BeanProperty property = this.properties.get(field.getName()); + if (property != null) { + property.addField(field); + } + } + + public Class getType() { + return this.type; + } + + public Map getProperties() { + return this.properties; + } + + @SuppressWarnings("unchecked") + public BeanSupplier getSupplier(Bindable target) { + return new BeanSupplier<>(() -> { + T instance = null; + if (target.getValue() != null) { + instance = target.getValue().get(); + } + if (instance == null) { + instance = (T) BeanUtils.instantiateClass(this.type); + } + return instance; + }); + } + + @SuppressWarnings("unchecked") + public static Bean get(Bindable bindable, boolean canCallGetValue) { + Class type = bindable.getType().resolve(Object.class); + Supplier value = bindable.getValue(); + T instance = null; + if (canCallGetValue && value != null) { + instance = value.get(); + type = (instance != null) ? instance.getClass() : type; + } + if (instance == null && !isInstantiable(type)) { + return null; + } + Bean bean = Bean.cached; + if (bean == null || !type.equals(bean.getType())) { + bean = new Bean<>(bindable.getType(), type); + cached = bean; + } + return (Bean) bean; + } + + private static boolean isInstantiable(Class type) { + if (type.isInterface()) { + return false; + } + try { + type.getDeclaredConstructor(); + return true; + } catch (Exception ex) { + return false; + } + } + + } + + private static class BeanSupplier implements Supplier { + + private final Supplier factory; + + private T instance; + + BeanSupplier(Supplier factory) { + this.factory = factory; + } + + @Override + public T get() { + if (this.instance == null) { + this.instance = this.factory.get(); + } + return this.instance; + } + + } + + /** + * A bean property being bound. + */ + private static class BeanProperty { + + private final String name; + + private final ResolvableType declaringClassType; + + private Method getter; + + private Method setter; + + private Field field; + + BeanProperty(String name, ResolvableType declaringClassType) { + this.name = BeanPropertyName.toDashedForm(name); + this.declaringClassType = declaringClassType; + } + + public void addGetter(Method getter) { + if (this.getter == null) { + this.getter = getter; + } + } + + public void addSetter(Method setter) { + if (this.setter == null) { + this.setter = setter; + } + } + + public void addField(Field field) { + if (this.field == null) { + this.field = field; + } + } + + public String getName() { + return this.name; + } + + public ResolvableType getType() { + if (this.setter != null) { + MethodParameter methodParameter = new MethodParameter(this.setter, 0); + return ResolvableType.forMethodParameter(methodParameter, + this.declaringClassType); + } + MethodParameter methodParameter = new MethodParameter(this.getter, -1); + return ResolvableType.forMethodParameter(methodParameter, + this.declaringClassType); + } + + public Annotation[] getAnnotations() { + try { + return (this.field != null) ? this.field.getDeclaredAnnotations() : null; + } catch (Exception ex) { + return null; + } + } + + public Supplier getValue(Supplier instance) { + if (this.getter == null) { + return null; + } + return () -> { + try { + this.getter.setAccessible(true); + return this.getter.invoke(instance.get()); + } catch (Exception ex) { + throw new IllegalStateException( + "Unable to get value for property " + this.name, ex); + } + }; + } + + public boolean isSettable() { + return this.setter != null; + } + + public void setValue(Supplier instance, Object value) { + try { + this.setter.setAccessible(true); + this.setter.invoke(instance.get(), value); + } catch (Exception ex) { + throw new IllegalStateException( + "Unable to set value for property " + this.name, ex); + } + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/MapBinder.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/MapBinder.java new file mode 100644 index 00000000..d48c9d7a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/MapBinder.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import java.util.Collection; +import java.util.Map; +import java.util.Properties; +import java.util.function.Supplier; + +import cn.springcloud.gray.bean.properties.bind.Binder.Context; +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName.Form; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyState; +import cn.springcloud.gray.bean.properties.source.IterableConfigurationPropertySource; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for Maps. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class MapBinder extends AggregateBinder> { + + private static final Bindable> STRING_STRING_MAP = Bindable + .mapOf(String.class, String.class); + + MapBinder(Context context) { + super(context); + } + + @Override + protected boolean isAllowRecursiveBinding(ConfigurationPropertySource source) { + return true; + } + + @Override + protected Object bindAggregate(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder) { + Map map = CollectionFactory.createMap((target.getValue() != null) + ? Map.class : target.getType().resolve(Object.class), 0); + Bindable resolvedTarget = resolveTarget(target); + boolean hasDescendants = hasDescendants(name); + for (ConfigurationPropertySource source : getContext().getSources()) { + if (!ConfigurationPropertyName.EMPTY.equals(name)) { + ConfigurationProperty property = source.getConfigurationProperty(name); + if (property != null && !hasDescendants) { + return getContext().getConverter().convert(property.getValue(), + target); + } + source = source.filter(name::isAncestorOf); + } + new EntryBinder(name, resolvedTarget, elementBinder).bindEntries(source, map); + } + return map.isEmpty() ? null : map; + } + + private boolean hasDescendants(ConfigurationPropertyName name) { + for (ConfigurationPropertySource source : getContext().getSources()) { + if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { + return true; + } + } + return false; + } + + private Bindable resolveTarget(Bindable target) { + Class type = target.getType().resolve(Object.class); + if (Properties.class.isAssignableFrom(type)) { + return STRING_STRING_MAP; + } + return target; + } + + @Override + protected Map merge(Supplier> existing, + Map additional) { + Map existingMap = getExistingIfPossible(existing); + if (existingMap == null) { + return additional; + } + try { + existingMap.putAll(additional); + return copyIfPossible(existingMap); + } catch (UnsupportedOperationException ex) { + Map result = createNewMap(additional.getClass(), existingMap); + result.putAll(additional); + return result; + } + } + + private Map getExistingIfPossible( + Supplier> existing) { + try { + return existing.get(); + } catch (Exception ex) { + return null; + } + } + + private Map copyIfPossible(Map map) { + try { + return createNewMap(map.getClass(), map); + } catch (Exception ex) { + return map; + } + } + + private Map createNewMap(Class mapClass, Map map) { + Map result = CollectionFactory.createMap(mapClass, map.size()); + result.putAll(map); + return result; + } + + private class EntryBinder { + + private final ConfigurationPropertyName root; + + private final AggregateElementBinder elementBinder; + + private final ResolvableType mapType; + + private final ResolvableType keyType; + + private final ResolvableType valueType; + + EntryBinder(ConfigurationPropertyName root, Bindable target, + AggregateElementBinder elementBinder) { + this.root = root; + this.elementBinder = elementBinder; + this.mapType = target.getType().asMap(); + this.keyType = this.mapType.getGeneric(0); + this.valueType = this.mapType.getGeneric(1); + } + + public void bindEntries(ConfigurationPropertySource source, + Map map) { + if (source instanceof IterableConfigurationPropertySource) { + for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source) { + Bindable valueBindable = getValueBindable(name); + ConfigurationPropertyName entryName = getEntryName(source, name); + Object key = getContext().getConverter() + .convert(getKeyName(entryName), this.keyType); + map.computeIfAbsent(key, + (k) -> this.elementBinder.bind(entryName, valueBindable)); + } + } + } + + private Bindable getValueBindable(ConfigurationPropertyName name) { + if (!this.root.isParentOf(name) && isValueTreatedAsNestedMap()) { + return Bindable.of(this.mapType); + } + return Bindable.of(this.valueType); + } + + private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + Class resolved = this.valueType.resolve(Object.class); + if (Collection.class.isAssignableFrom(resolved) || this.valueType.isArray()) { + return chopNameAtNumericIndex(name); + } + if (!this.root.isParentOf(name) + && (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) { + return name.chop(this.root.getNumberOfElements() + 1); + } + return name; + } + + private ConfigurationPropertyName chopNameAtNumericIndex( + ConfigurationPropertyName name) { + int start = this.root.getNumberOfElements() + 1; + int size = name.getNumberOfElements(); + for (int i = start; i < size; i++) { + if (name.isNumericIndex(i)) { + return name.chop(i); + } + } + return name; + } + + private boolean isValueTreatedAsNestedMap() { + return Object.class.equals(this.valueType.resolve(Object.class)); + } + + private boolean isScalarValue(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + Class resolved = this.valueType.resolve(Object.class); + if (!resolved.getName().startsWith("java.lang") && !resolved.isEnum()) { + return false; + } + ConfigurationProperty property = source.getConfigurationProperty(name); + if (property == null) { + return false; + } + Object value = property.getValue(); + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + return getContext().getConverter().canConvert(value, this.valueType); + } + + private String getKeyName(ConfigurationPropertyName name) { + StringBuilder result = new StringBuilder(); + for (int i = this.root.getNumberOfElements(); i < name + .getNumberOfElements(); i++) { + if (result.length() != 0) { + result.append('.'); + } + result.append(name.getElement(i, Form.ORIGINAL)); + } + return result.toString(); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PlaceholdersResolver.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PlaceholdersResolver.java new file mode 100644 index 00000000..180459e9 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PlaceholdersResolver.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import org.springframework.core.env.PropertyResolver; + +/** + * Optional strategy that used by a {@link Binder} to resolve property placeholders. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertySourcesPlaceholdersResolver + * @since 2.0.0 + */ +@FunctionalInterface +public interface PlaceholdersResolver { + + /** + * No-op {@link PropertyResolver}. + */ + PlaceholdersResolver NONE = (value) -> value; + + /** + * Called to resolve any placeholders in the given value. + * + * @param value the source value + * @return a value with placeholders resolved + */ + Object resolvePlaceholders(Object value); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PropertySourcesPlaceholdersResolver.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PropertySourcesPlaceholdersResolver.java new file mode 100644 index 00000000..0f209ded --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/PropertySourcesPlaceholdersResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * {@link PlaceholdersResolver} to resolve placeholders from {@link PropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class PropertySourcesPlaceholdersResolver implements PlaceholdersResolver { + + private final Iterable> sources; + + private final PropertyPlaceholderHelper helper; + + public PropertySourcesPlaceholdersResolver(Environment environment) { + this(getSources(environment), null); + } + + public PropertySourcesPlaceholdersResolver(Iterable> sources) { + this(sources, null); + } + + public PropertySourcesPlaceholdersResolver(Iterable> sources, + PropertyPlaceholderHelper helper) { + this.sources = sources; + this.helper = (helper != null) ? helper + : new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, + SystemPropertyUtils.PLACEHOLDER_SUFFIX, + SystemPropertyUtils.VALUE_SEPARATOR, true); + } + + @Override + public Object resolvePlaceholders(Object value) { + if (value != null && value instanceof String) { + return this.helper.replacePlaceholders((String) value, + this::resolvePlaceholder); + } + return value; + } + + protected String resolvePlaceholder(String placeholder) { + if (this.sources != null) { + for (PropertySource source : this.sources) { + Object value = source.getProperty(placeholder); + if (value != null) { + return String.valueOf(value); + } + } + } + return null; + } + + private static PropertySources getSources(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + Assert.isInstanceOf(ConfigurableEnvironment.class, environment, + "Environment must be a ConfigurableEnvironment"); + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/UnboundConfigurationPropertiesException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/UnboundConfigurationPropertiesException.java new file mode 100644 index 00000000..81219079 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/UnboundConfigurationPropertiesException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.bind; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; + +/** + * {@link BindException} thrown when {@link ConfigurationPropertySource} elements were + * left unbound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class UnboundConfigurationPropertiesException extends RuntimeException { + + private final Set unboundProperties; + + public UnboundConfigurationPropertiesException( + Set unboundProperties) { + super(buildMessage(unboundProperties)); + this.unboundProperties = Collections.unmodifiableSet(unboundProperties); + } + + public Set getUnboundProperties() { + return this.unboundProperties; + } + + private static String buildMessage(Set unboundProperties) { + StringBuilder builder = new StringBuilder(); + builder.append("The elements ["); + String message = unboundProperties.stream().map((p) -> p.getName().toString()) + .collect(Collectors.joining(",")); + builder.append(message).append("] were left unbound."); + return builder.toString(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreErrorsBindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreErrorsBindHandler.java new file mode 100644 index 00000000..6f1b4480 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreErrorsBindHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind.handler; + +import cn.springcloud.gray.bean.properties.bind.AbstractBindHandler; +import cn.springcloud.gray.bean.properties.bind.BindContext; +import cn.springcloud.gray.bean.properties.bind.BindHandler; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; + +/** + * {@link BindHandler} that can be used to ignore binding errors. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class IgnoreErrorsBindHandler extends AbstractBindHandler { + + public IgnoreErrorsBindHandler() { + } + + public IgnoreErrorsBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return (target.getValue() != null) ? target.getValue().get() : null; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreTopLevelConverterNotFoundBindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreTopLevelConverterNotFoundBindHandler.java new file mode 100644 index 00000000..f9e67ce7 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/IgnoreTopLevelConverterNotFoundBindHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind.handler; + +import cn.springcloud.gray.bean.properties.bind.AbstractBindHandler; +import cn.springcloud.gray.bean.properties.bind.BindContext; +import cn.springcloud.gray.bean.properties.bind.BindHandler; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import org.springframework.core.convert.ConverterNotFoundException; + +/** + * {@link BindHandler} that can be used to ignore top-level + * {@link ConverterNotFoundException}s. + * + * @author Madhura Bhave + * @since 2.0.1 + */ +public class IgnoreTopLevelConverterNotFoundBindHandler extends AbstractBindHandler { + + /** + * Create a new {@link IgnoreTopLevelConverterNotFoundBindHandler} instance. + */ + public IgnoreTopLevelConverterNotFoundBindHandler() { + } + + /** + * Create a new {@link IgnoreTopLevelConverterNotFoundBindHandler} instance with a + * specific parent. + * + * @param parent the parent handler + */ + public IgnoreTopLevelConverterNotFoundBindHandler(BindHandler parent) { + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + if (context.getDepth() == 0 && error instanceof ConverterNotFoundException) { + return null; + } + throw error; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/NoUnboundElementsBindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/NoUnboundElementsBindHandler.java new file mode 100644 index 00000000..f83d988e --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/NoUnboundElementsBindHandler.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.properties.bind.handler; + +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; + +import cn.springcloud.gray.bean.properties.bind.AbstractBindHandler; +import cn.springcloud.gray.bean.properties.bind.BindContext; +import cn.springcloud.gray.bean.properties.bind.BindHandler; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.bind.UnboundConfigurationPropertiesException; +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertySource; +import cn.springcloud.gray.bean.properties.source.IterableConfigurationPropertySource; + +/** + * {@link BindHandler} to enforce that all configuration properties under the root name + * have been bound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class NoUnboundElementsBindHandler extends AbstractBindHandler { + + private final Set boundNames = new HashSet<>(); + + private final Function filter; + + NoUnboundElementsBindHandler() { + this(BindHandler.DEFAULT, (configurationPropertySource) -> true); + } + + public NoUnboundElementsBindHandler(BindHandler parent) { + this(parent, (configurationPropertySource) -> true); + } + + public NoUnboundElementsBindHandler(BindHandler parent, + Function filter) { + super(parent); + this.filter = filter; + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + this.boundNames.add(name); + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + if (context.getDepth() == 0) { + checkNoUnboundElements(name, context); + } + } + + private void checkNoUnboundElements(ConfigurationPropertyName name, + BindContext context) { + Set unbound = new TreeSet<>(); + for (ConfigurationPropertySource source : context.getSources()) { + if (source instanceof IterableConfigurationPropertySource + && this.filter.apply(source)) { + collectUnbound(name, unbound, + (IterableConfigurationPropertySource) source); + } + } + if (!unbound.isEmpty()) { + throw new UnboundConfigurationPropertiesException(unbound); + } + } + + private void collectUnbound(ConfigurationPropertyName name, + Set unbound, + IterableConfigurationPropertySource source) { + IterableConfigurationPropertySource filtered = source + .filter((candidate) -> isUnbound(name, candidate)); + for (ConfigurationPropertyName unboundName : filtered) { + try { + unbound.add(source.filter((candidate) -> isUnbound(name, candidate)) + .getConfigurationProperty(unboundName)); + } catch (Exception ex) { + } + } + } + + private boolean isUnbound(ConfigurationPropertyName name, + ConfigurationPropertyName candidate) { + if (name.isAncestorOf(candidate)) { + if (!this.boundNames.contains(candidate) + && !isOverriddenCollectionElement(candidate)) { + return true; + } + } + return false; + } + + private boolean isOverriddenCollectionElement(ConfigurationPropertyName candidate) { + int length = candidate.getNumberOfElements(); + if (candidate.isNumericIndex(length - 1)) { + ConfigurationPropertyName propertyName = candidate + .chop(candidate.getNumberOfElements() - 1); + return this.boundNames.contains(propertyName); + } + return false; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/package-info.java new file mode 100644 index 00000000..81bfaae6 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/handler/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * General {@link org.springframework.boot.context.properties.bind.BindHandler + * BindHandler} implementations. + */ +package cn.springcloud.gray.bean.properties.bind.handler; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/package-info.java new file mode 100644 index 00000000..a6de7923 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * Support for {@code @ConfigurationProperties} binding. + */ +package cn.springcloud.gray.bean.properties.bind; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/BindValidationException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/BindValidationException.java new file mode 100644 index 00000000..68daddd7 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/BindValidationException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind.validation; + +import org.springframework.util.Assert; + +/** + * Error thrown when validation fails during a bind operation. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see ValidationErrors + * @see ValidationBindHandler + * @since 2.0.0 + */ +public class BindValidationException extends RuntimeException { + + private final ValidationErrors validationErrors; + + BindValidationException(ValidationErrors validationErrors) { + super(getMessage(validationErrors)); + Assert.notNull(validationErrors, "ValidationErrors must not be null"); + this.validationErrors = validationErrors; + } + + /** + * Return the validation errors that caused the exception. + * + * @return the validationErrors the validation errors + */ + public ValidationErrors getValidationErrors() { + return this.validationErrors; + } + + private static String getMessage(ValidationErrors errors) { + StringBuilder message = new StringBuilder("Binding validation errors"); + if (errors != null) { + message.append(" on " + errors.getName()); + errors.getAllErrors().forEach( + (error) -> message.append(String.format("%n - %s", error))); + } + return message.toString(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/OriginTrackedFieldError.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/OriginTrackedFieldError.java new file mode 100644 index 00000000..6ce56446 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/OriginTrackedFieldError.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind.validation; + +import org.springframework.validation.FieldError; + +/** + * {@link FieldError} implementation that tracks the source . + * + * @author Phillip Webb + * @author Madhura Bhave + */ +final class OriginTrackedFieldError extends FieldError { + + + private OriginTrackedFieldError(FieldError fieldError) { + super(fieldError.getObjectName(), fieldError.getField(), + fieldError.getRejectedValue(), fieldError.isBindingFailure(), + fieldError.getCodes(), fieldError.getArguments(), + fieldError.getDefaultMessage()); + } + + + @Override + public String toString() { + return super.toString(); + } + + public static FieldError of(FieldError fieldError) { + if (fieldError == null) { + return fieldError; + } + return new OriginTrackedFieldError(fieldError); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationBindHandler.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationBindHandler.java new file mode 100644 index 00000000..aa10eb53 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationBindHandler.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.properties.bind.validation; + +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Set; +import java.util.stream.Collectors; + +import cn.springcloud.gray.bean.properties.bind.AbstractBindHandler; +import cn.springcloud.gray.bean.properties.bind.BindContext; +import cn.springcloud.gray.bean.properties.bind.BindHandler; +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; + +/** + * {@link BindHandler} to apply {@link Validator Validators} to bound results. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationBindHandler extends AbstractBindHandler { + + private final Validator[] validators; + + private final Set boundProperties = new LinkedHashSet<>(); + + private final Deque exceptions = new LinkedList<>(); + + public ValidationBindHandler(Validator... validators) { + this.validators = validators; + } + + public ValidationBindHandler(BindHandler parent, Validator... validators) { + super(parent); + this.validators = validators; + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + if (context.getConfigurationProperty() != null) { + this.boundProperties.add(context.getConfigurationProperty()); + } + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + validate(name, target, context, result); + super.onFinish(name, target, context, result); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + Object result = super.onFailure(name, target, context, error); + validate(name, target, context, null); + return result; + } + + private void validate(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + Object validationTarget = getValidationTarget(target, context, result); + Class validationType = target.getBoxedType().resolve(); + if (validationTarget != null) { + validateAndPush(name, validationTarget, validationType); + } + if (context.getDepth() == 0 && !this.exceptions.isEmpty()) { + throw this.exceptions.pop(); + } + } + + private Object getValidationTarget(Bindable target, BindContext context, + Object result) { + if (result != null) { + return result; + } + if (context.getDepth() == 0 && target.getValue() != null) { + return target.getValue().get(); + } + return null; + } + + private void validateAndPush(ConfigurationPropertyName name, Object target, + Class type) { + BindingResult errors = new BeanPropertyBindingResult(target, name.toString()); + Arrays.stream(this.validators).filter((validator) -> validator.supports(type)) + .forEach((validator) -> validator.validate(target, errors)); + if (errors.hasErrors()) { + this.exceptions.push(getBindValidationException(name, errors)); + } + } + + private BindValidationException getBindValidationException( + ConfigurationPropertyName name, BindingResult errors) { + Set boundProperties = this.boundProperties.stream() + .filter((property) -> name.isAncestorOf(property.getName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + ValidationErrors validationErrors = new ValidationErrors(name, boundProperties, + errors.getAllErrors()); + return new BindValidationException(validationErrors); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationErrors.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationErrors.java new file mode 100644 index 00000000..de0e0c6a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/ValidationErrors.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.bind.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import cn.springcloud.gray.bean.properties.source.ConfigurationProperty; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName; +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName.Form; +import org.springframework.util.Assert; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * A collection of {@link ObjectError ObjectErrors} caused by bind validation failures. + * Where possible, included {@link FieldError FieldErrors} will be OriginProvider. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationErrors implements Iterable { + + private final ConfigurationPropertyName name; + + private final Set boundProperties; + + private final List errors; + + ValidationErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(boundProperties, "BoundProperties must not be null"); + Assert.notNull(errors, "Errors must not be null"); + this.name = name; + this.boundProperties = Collections.unmodifiableSet(boundProperties); + this.errors = convertErrors(name, boundProperties, errors); + } + + private List convertErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + List converted = new ArrayList<>(errors.size()); + for (ObjectError error : errors) { + converted.add(convertError(name, boundProperties, error)); + } + return Collections.unmodifiableList(converted); + } + + private ObjectError convertError(ConfigurationPropertyName name, + Set boundProperties, ObjectError error) { + if (error instanceof FieldError) { + return convertFieldError(name, boundProperties, (FieldError) error); + } + return error; + } + + private FieldError convertFieldError(ConfigurationPropertyName name, + Set boundProperties, FieldError error) { + return OriginTrackedFieldError.of(error); + } + + + private boolean isForError(ConfigurationPropertyName name, + ConfigurationPropertyName boundPropertyName, FieldError error) { + return name.isParentOf(boundPropertyName) && boundPropertyName + .getLastElement(Form.UNIFORM).equalsIgnoreCase(error.getField()); + } + + /** + * Return the name of the item that was being validated. + * + * @return the name of the item + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the properties that were bound before validation failed. + * + * @return the boundProperties + */ + public Set getBoundProperties() { + return this.boundProperties; + } + + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** + * Return the list of all validation errors. + * + * @return the errors + */ + public List getAllErrors() { + return this.errors; + } + + @Override + public Iterator iterator() { + return this.errors.iterator(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/package-info.java new file mode 100644 index 00000000..05aa34d0 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/bind/validation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * Binding validation support. + */ +package cn.springcloud.gray.bean.properties.bind.validation; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/package-info.java new file mode 100644 index 00000000..f8b444ee --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * Support for external configuration properties. + * + * @see org.springframework.boot.context.properties.ConfigurationProperties + * @see org.springframework.boot.context.properties.EnableConfigurationProperties + */ +package cn.springcloud.gray.bean.properties; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedConfigurationPropertySource.java new file mode 100644 index 00000000..ef45f1d5 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedConfigurationPropertySource.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import org.springframework.util.Assert; + +/** + * A {@link ConfigurationPropertySource} supporting name aliases. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class AliasedConfigurationPropertySource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final ConfigurationPropertyNameAliases aliases; + + AliasedConfigurationPropertySource(ConfigurationPropertySource source, + ConfigurationPropertyNameAliases aliases) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + this.source = source; + this.aliases = aliases; + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + ConfigurationProperty result = getSource().getConfigurationProperty(name); + if (result == null) { + ConfigurationPropertyName aliasedName = getAliases().getNameForAlias(name); + result = getSource().getConfigurationProperty(aliasedName); + } + return result; + } + + @Override + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + ConfigurationPropertyState result = this.source.containsDescendantOf(name); + if (result != ConfigurationPropertyState.ABSENT) { + return result; + } + for (ConfigurationPropertyName alias : getAliases().getAliases(name)) { + ConfigurationPropertyState aliasResult = this.source + .containsDescendantOf(alias); + if (aliasResult != ConfigurationPropertyState.ABSENT) { + return aliasResult; + } + } + for (ConfigurationPropertyName from : getAliases()) { + for (ConfigurationPropertyName alias : getAliases().getAliases(from)) { + if (name.isAncestorOf(alias)) { + if (this.source.getConfigurationProperty(from) != null) { + return ConfigurationPropertyState.PRESENT; + } + } + } + } + return ConfigurationPropertyState.ABSENT; + } + + @Override + public Object getUnderlyingSource() { + return this.source.getUnderlyingSource(); + } + + protected ConfigurationPropertySource getSource() { + return this.source; + } + + protected ConfigurationPropertyNameAliases getAliases() { + return this.aliases; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedIterableConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedIterableConfigurationPropertySource.java new file mode 100644 index 00000000..f0d4381d --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/AliasedIterableConfigurationPropertySource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.util.CollectionUtils; + +/** + * A {@link IterableConfigurationPropertySource} supporting name aliases. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class AliasedIterableConfigurationPropertySource + extends AliasedConfigurationPropertySource + implements IterableConfigurationPropertySource { + + AliasedIterableConfigurationPropertySource(IterableConfigurationPropertySource source, + ConfigurationPropertyNameAliases aliases) { + super(source, aliases); + } + + @Override + public Stream stream() { + return getSource().stream().flatMap(this::addAliases); + } + + private Stream addAliases(ConfigurationPropertyName name) { + Stream names = Stream.of(name); + List aliases = getAliases().getAliases(name); + if (CollectionUtils.isEmpty(aliases)) { + return names; + } + return Stream.concat(names, aliases.stream()); + } + + @Override + protected IterableConfigurationPropertySource getSource() { + return (IterableConfigurationPropertySource) super.getSource(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationProperty.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationProperty.java new file mode 100644 index 00000000..4cf3989a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationProperty.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import cn.springcloud.gray.bean.origin.Origin; +import cn.springcloud.gray.bean.origin.OriginProvider; +import cn.springcloud.gray.bean.origin.OriginTrackedValue; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A single configuration property obtained from a {@link ConfigurationPropertySource} + * consisting of a {@link #getName() name}, {@link #getValue() value} and optional + * {@link #getOrigin() origin}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class ConfigurationProperty + implements OriginProvider, Comparable { + + private final ConfigurationPropertyName name; + + private final Object value; + private final Origin origin; + + + public ConfigurationProperty(ConfigurationPropertyName name, Object value, + Origin origin) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(value, "Value must not be null"); + this.name = name; + this.value = value; + this.origin = origin; + } + + public ConfigurationPropertyName getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigurationProperty other = (ConfigurationProperty) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.name, other.name); + result = result && ObjectUtils.nullSafeEquals(this.value, other.value); + return result; + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.name); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.value); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name) + .append("value", this.value).append("origin", this.origin).toString(); + } + + @Override + public int compareTo(ConfigurationProperty other) { + return this.name.compareTo(other.name); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, + OriginTrackedValue value) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value.getValue(), value.getOrigin()); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, Object value, + Origin origin) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value, origin); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyName.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyName.java new file mode 100644 index 00000000..a26ddc72 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyName.java @@ -0,0 +1,922 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.util.Assert; + +/** + * A configuration property name composed of elements separated by dots. User created + * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they + * must be lower-case and must start with an alpha-numeric character. The "{@code -}" is + * used purely for formatting, i.e. "{@code foo-bar}" and "{@code foobar}" are considered + * equivalent. + *

+ * The "{@code [}" and "{@code ]}" characters may be used to indicate an associative + * index(i.e. a {@link Map} key or a {@link Collection} index. Indexes names are not + * restricted and are considered case-sensitive. + *

+ * Here are some typical examples: + *

    + *
  • {@code spring.main.banner-mode}
  • + *
  • {@code server.hosts[0].name}
  • + *
  • {@code log[org.springboot].level}
  • + *
+ * + * @author Phillip Webb + * @author Madhura Bhave + * @see #of(CharSequence) + * @see ConfigurationPropertySource + * @since 2.0.0 + */ +public final class ConfigurationPropertyName + implements Comparable { + + private static final String EMPTY_STRING = ""; + + /** + * An empty {@link ConfigurationPropertyName}. + */ + public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( + Elements.EMPTY); + + private Elements elements; + + private final CharSequence[] uniformElements; + + private String string; + + private ConfigurationPropertyName(Elements elements) { + this.elements = elements; + this.uniformElements = new CharSequence[elements.getSize()]; + } + + /** + * Returns {@code true} if this {@link ConfigurationPropertyName} is empty. + * + * @return {@code true} if the name is empty + */ + public boolean isEmpty() { + return this.elements.getSize() == 0; + } + + /** + * Return if the last element in the name is indexed. + * + * @return {@code true} if the last element is indexed + */ + public boolean isLastElementIndexed() { + int size = getNumberOfElements(); + return (size > 0 && isIndexed(size - 1)); + } + + /** + * Return if the element in the name is indexed. + * + * @param elementIndex the index of the element + * @return {@code true} if the element is indexed + */ + boolean isIndexed(int elementIndex) { + return this.elements.getType(elementIndex).isIndexed(); + } + + /** + * Return if the element in the name is indexed and numeric. + * + * @param elementIndex the index of the element + * @return {@code true} if the element is indexed and numeric + */ + public boolean isNumericIndex(int elementIndex) { + return this.elements.getType(elementIndex) == ElementType.NUMERICALLY_INDEXED; + } + + /** + * Return the last element in the name in the given form. + * + * @param form the form to return + * @return the last element + */ + public String getLastElement(Form form) { + int size = getNumberOfElements(); + return (size != 0) ? getElement(size - 1, form) : EMPTY_STRING; + } + + /** + * Return an element in the name in the given form. + * + * @param elementIndex the element index + * @param form the form to return + * @return the last element + */ + public String getElement(int elementIndex, Form form) { + CharSequence element = this.elements.get(elementIndex); + ElementType type = this.elements.getType(elementIndex); + if (type.isIndexed()) { + return element.toString(); + } + if (form == Form.ORIGINAL) { + if (type != ElementType.NON_UNIFORM) { + return element.toString(); + } + return convertToOriginalForm(element).toString(); + } + if (form == Form.DASHED) { + if (type == ElementType.UNIFORM || type == ElementType.DASHED) { + return element.toString(); + } + return convertToDashedElement(element).toString(); + } + CharSequence uniformElement = this.uniformElements[elementIndex]; + if (uniformElement == null) { + uniformElement = (type != ElementType.UNIFORM) + ? convertToUniformElement(element) : element; + this.uniformElements[elementIndex] = uniformElement.toString(); + } + return uniformElement.toString(); + } + + private CharSequence convertToOriginalForm(CharSequence element) { + return convertElement(element, false, (ch, i) -> ch == '_' + || ElementsParser.isValidChar(Character.toLowerCase(ch), i)); + } + + private CharSequence convertToDashedElement(CharSequence element) { + return convertElement(element, true, ElementsParser::isValidChar); + } + + private CharSequence convertToUniformElement(CharSequence element) { + return convertElement(element, true, + (ch, i) -> ElementsParser.isAlphaNumeric(ch)); + } + + private CharSequence convertElement(CharSequence element, boolean lowercase, + ElementCharPredicate filter) { + StringBuilder result = new StringBuilder(element.length()); + for (int i = 0; i < element.length(); i++) { + char ch = lowercase ? Character.toLowerCase(element.charAt(i)) + : element.charAt(i); + if (filter.test(ch, i)) { + result.append(ch); + } + } + return result; + } + + /** + * Return the total number of elements in the name. + * + * @return the number of elements + */ + public int getNumberOfElements() { + return this.elements.getSize(); + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given element + * value. + * + * @param elementValue the single element value to append + * @return a new {@link ConfigurationPropertyName} + * @throws InvalidConfigurationPropertyNameException if elementValue is not valid + */ + public ConfigurationPropertyName append(String elementValue) { + if (elementValue == null) { + return this; + } + Elements additionalElements = of(elementValue).elements; + return new ConfigurationPropertyName(this.elements.append(additionalElements)); + } + + /** + * Return a new {@link ConfigurationPropertyName} by chopping this name to the given + * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return + * {@code foo}. + * + * @param size the size to chop + * @return the chopped name + */ + public ConfigurationPropertyName chop(int size) { + if (size >= getNumberOfElements()) { + return this; + } + return new ConfigurationPropertyName(this.elements.chop(size)); + } + + /** + * Returns {@code true} if this element is an immediate parent of the specified name. + * + * @param name the name to check + * @return {@code true} if this name is an ancestor + */ + public boolean isParentOf(ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + if (this.getNumberOfElements() != name.getNumberOfElements() - 1) { + return false; + } + return isAncestorOf(name); + } + + /** + * Returns {@code true} if this element is an ancestor (immediate or nested parent) of + * the specified name. + * + * @param name the name to check + * @return {@code true} if this name is an ancestor + */ + public boolean isAncestorOf(ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + if (this.getNumberOfElements() >= name.getNumberOfElements()) { + return false; + } + for (int i = 0; i < this.elements.getSize(); i++) { + if (!elementEquals(this.elements, name.elements, i)) { + return false; + } + } + return true; + } + + @Override + public int compareTo(ConfigurationPropertyName other) { + return compare(this, other); + } + + private int compare(ConfigurationPropertyName n1, ConfigurationPropertyName n2) { + int l1 = n1.getNumberOfElements(); + int l2 = n2.getNumberOfElements(); + int i1 = 0; + int i2 = 0; + while (i1 < l1 || i2 < l2) { + try { + ElementType type1 = (i1 < l1) ? n1.elements.getType(i1) : null; + ElementType type2 = (i2 < l2) ? n2.elements.getType(i2) : null; + String e1 = (i1 < l1) ? n1.getElement(i1++, Form.UNIFORM) : null; + String e2 = (i2 < l2) ? n2.getElement(i2++, Form.UNIFORM) : null; + int result = compare(e1, type1, e2, type2); + if (result != 0) { + return result; + } + } catch (ArrayIndexOutOfBoundsException ex) { + throw new RuntimeException(ex); + } + } + return 0; + } + + private int compare(String e1, ElementType type1, String e2, ElementType type2) { + if (e1 == null) { + return -1; + } + if (e2 == null) { + return 1; + } + int result = Boolean.compare(type2.isIndexed(), type1.isIndexed()); + if (result != 0) { + return result; + } + if (type1 == ElementType.NUMERICALLY_INDEXED + && type2 == ElementType.NUMERICALLY_INDEXED) { + long v1 = Long.parseLong(e1); + long v2 = Long.parseLong(e2); + return Long.compare(v1, v2); + } + return e1.compareTo(e2); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != getClass()) { + return false; + } + ConfigurationPropertyName other = (ConfigurationPropertyName) obj; + if (getNumberOfElements() != other.getNumberOfElements()) { + return false; + } + if (this.elements.canShortcutWithSource(ElementType.UNIFORM) + && other.elements.canShortcutWithSource(ElementType.UNIFORM)) { + return toString().equals(other.toString()); + } + for (int i = 0; i < this.elements.getSize(); i++) { + if (!elementEquals(this.elements, other.elements, i)) { + return false; + } + } + return true; + } + + private boolean elementEquals(Elements e1, Elements e2, int i) { + int l1 = e1.getLength(i); + int l2 = e2.getLength(i); + boolean indexed1 = e1.getType(i).isIndexed(); + boolean indexed2 = e2.getType(i).isIndexed(); + int i1 = 0; + int i2 = 0; + while (i1 < l1) { + if (i2 >= l2) { + return false; + } + char ch1 = indexed1 ? e1.charAt(i, i1) + : Character.toLowerCase(e1.charAt(i, i1)); + char ch2 = indexed2 ? e2.charAt(i, i2) + : Character.toLowerCase(e2.charAt(i, i2)); + if (!indexed1 && !ElementsParser.isAlphaNumeric(ch1)) { + i1++; + } else if (!indexed2 && !ElementsParser.isAlphaNumeric(ch2)) { + i2++; + } else if (ch1 != ch2) { + return false; + } else { + i1++; + i2++; + } + } + while (i2 < l2) { + char ch2 = Character.toLowerCase(e2.charAt(i, i2++)); + if (indexed2 || ElementsParser.isAlphaNumeric(ch2)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + if (this.string == null) { + this.string = buildToString(); + } + return this.string; + } + + private String buildToString() { + if (this.elements.canShortcutWithSource(ElementType.UNIFORM, + ElementType.DASHED)) { + return this.elements.getSource().toString(); + } + StringBuilder result = new StringBuilder(); + for (int i = 0; i < getNumberOfElements(); i++) { + boolean indexed = isIndexed(i); + if (result.length() > 0 && !indexed) { + result.append('.'); + } + if (indexed) { + result.append("["); + result.append(getElement(i, Form.ORIGINAL)); + result.append("]"); + } else { + result.append(getElement(i, Form.DASHED)); + } + } + return result.toString(); + } + + /** + * Returns if the given name is valid. If this method returns {@code true} then the + * name may be used with {@link #of(CharSequence)} without throwing an exception. + * + * @param name the name to test + * @return {@code true} if the name is valid + */ + public static boolean isValid(CharSequence name) { + return of(name, true) != null; + } + + /** + * Return a {@link ConfigurationPropertyName} for the specified string. + * + * @param name the source name + * @return a {@link ConfigurationPropertyName} instance + * @throws InvalidConfigurationPropertyNameException if the name is not valid + */ + public static ConfigurationPropertyName of(CharSequence name) { + return of(name, false); + } + + /** + * Return a {@link ConfigurationPropertyName} for the specified string. + * + * @param name the source name + * @param returnNullIfInvalid if null should be returned if the name is not valid + * @return a {@link ConfigurationPropertyName} instance + * @throws InvalidConfigurationPropertyNameException if the name is not valid and + * {@code returnNullIfInvalid} is {@code false} + */ + static ConfigurationPropertyName of(CharSequence name, boolean returnNullIfInvalid) { + if (name == null) { + Assert.isTrue(returnNullIfInvalid, "Name must not be null"); + return null; + } + if (name.length() == 0) { + return EMPTY; + } + if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') { + if (returnNullIfInvalid) { + return null; + } + throw new InvalidConfigurationPropertyNameException(name, + Collections.singletonList('.')); + } + Elements elements = new ElementsParser(name, '.').parse(); + for (int i = 0; i < elements.getSize(); i++) { + if (elements.getType(i) == ElementType.NON_UNIFORM) { + if (returnNullIfInvalid) { + return null; + } + throw new InvalidConfigurationPropertyNameException(name, + getInvalidChars(elements, i)); + } + } + return new ConfigurationPropertyName(elements); + } + + private static List getInvalidChars(Elements elements, int index) { + List invalidChars = new ArrayList<>(); + for (int charIndex = 0; charIndex < elements.getLength(index); charIndex++) { + char ch = elements.charAt(index, charIndex); + if (!ElementsParser.isValidChar(ch, charIndex)) { + invalidChars.add(ch); + } + } + return invalidChars; + } + + /** + * Create a {@link ConfigurationPropertyName} by adapting the given source. See + * {@link #adapt(CharSequence, char, Function)} for details. + * + * @param name the name to parse + * @param separator the separator used to split the name + * @return a {@link ConfigurationPropertyName} + */ + static ConfigurationPropertyName adapt(CharSequence name, char separator) { + return adapt(name, separator, null); + } + + /** + * Create a {@link ConfigurationPropertyName} by adapting the given source. The name + * is split into elements around the given {@code separator}. This method is more + * lenient than {@link #of} in that it allows mixed case names and '{@code _}' + * characters. Other invalid characters are stripped out during parsing. + *

+ * The {@code elementValueProcessor} function may be used if additional processing is + * required on the extracted element values. + * + * @param name the name to parse + * @param separator the separator used to split the name + * @param elementValueProcessor a function to process element values + * @return a {@link ConfigurationPropertyName} + */ + static ConfigurationPropertyName adapt(CharSequence name, char separator, + Function elementValueProcessor) { + Assert.notNull(name, "Name must not be null"); + if (name.length() == 0) { + return EMPTY; + } + Elements elements = new ElementsParser(name, separator) + .parse(elementValueProcessor); + if (elements.getSize() == 0) { + return EMPTY; + } + return new ConfigurationPropertyName(elements); + } + + /** + * The various forms that a non-indexed element value can take. + */ + public enum Form { + + /** + * The original form as specified when the name was created or adapted. For + * example: + *

    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code fooBar}"
  • + *
  • "{@code foo_bar}" = "{@code foo_bar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + ORIGINAL, + + /** + * The dashed configuration form (used for toString; lower-case with only + * alphanumeric characters and dashes). + *
    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + DASHED, + + /** + * The uniform configuration form (used for equals/hashCode; lower-case with only + * alphanumeric characters). + *
    + *
  • "{@code foo-bar}" = "{@code foobar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + UNIFORM + + } + + /** + * Allows access to the individual elements that make up the name. We store the + * indexes in arrays rather than a list of object in order to conserve memory. + */ + private static class Elements { + + private static final int[] NO_POSITION = {}; + + private static final ElementType[] NO_TYPE = {}; + + public static final Elements EMPTY = new Elements("", 0, NO_POSITION, NO_POSITION, + NO_TYPE, null); + + private final CharSequence source; + + private final int size; + + private final int[] start; + + private final int[] end; + + private final ElementType[] type; + + /** + * Contains any resolved elements or can be {@code null} if there aren't any. + * Resolved elements allow us to modify the element values in some way (or example + * when adapting with a mapping function, or when append has been called). Note + * that this array is not used as a cache, in fact, when it's not null then + * {@link #canShortcutWithSource} will always return false which may hurt + * performance. + */ + private final CharSequence[] resolved; + + Elements(CharSequence source, int size, int[] start, int[] end, + ElementType[] type, CharSequence[] resolved) { + super(); + this.source = source; + this.size = size; + this.start = start; + this.end = end; + this.type = type; + this.resolved = resolved; + } + + public Elements append(Elements additional) { + Assert.isTrue(additional.getSize() == 1, "Element value '" + + additional.getSource() + "' must be a single item"); + ElementType[] type = new ElementType[this.size + 1]; + System.arraycopy(this.type, 0, type, 0, this.size); + type[this.size] = additional.type[0]; + CharSequence[] resolved = newResolved(this.size + 1); + resolved[this.size] = additional.get(0); + return new Elements(this.source, this.size + 1, this.start, this.end, type, + resolved); + } + + public Elements chop(int size) { + CharSequence[] resolved = newResolved(size); + return new Elements(this.source, size, this.start, this.end, this.type, + resolved); + } + + private CharSequence[] newResolved(int size) { + CharSequence[] resolved = new CharSequence[size]; + if (this.resolved != null) { + System.arraycopy(this.resolved, 0, resolved, 0, + Math.min(size, this.size)); + } + return resolved; + } + + public int getSize() { + return this.size; + } + + public CharSequence get(int index) { + if (this.resolved != null && this.resolved[index] != null) { + return this.resolved[index]; + } + int start = this.start[index]; + int end = this.end[index]; + return this.source.subSequence(start, end); + } + + public int getLength(int index) { + if (this.resolved != null && this.resolved[index] != null) { + return this.resolved[index].length(); + } + int start = this.start[index]; + int end = this.end[index]; + return end - start; + } + + public char charAt(int index, int charIndex) { + if (this.resolved != null && this.resolved[index] != null) { + return this.resolved[index].charAt(charIndex); + } + int start = this.start[index]; + return this.source.charAt(start + charIndex); + } + + public ElementType getType(int index) { + return this.type[index]; + } + + public CharSequence getSource() { + return this.source; + } + + /** + * Returns if the element source can be used as a shortcut for an operation such + * as {@code equals} or {@code toString}. + * + * @param requiredType the required type + * @return {@code true} if all elements match at least one of the types + */ + public boolean canShortcutWithSource(ElementType requiredType) { + return canShortcutWithSource(requiredType, requiredType); + } + + /** + * Returns if the element source can be used as a shortcut for an operation such + * as {@code equals} or {@code toString}. + * + * @param requiredType the required type + * @param alternativeType and alternative required type + * @return {@code true} if all elements match at least one of the types + */ + public boolean canShortcutWithSource(ElementType requiredType, + ElementType alternativeType) { + if (this.resolved != null) { + return false; + } + for (int i = 0; i < this.size; i++) { + ElementType type = this.type[i]; + if (type != requiredType && type != alternativeType) { + return false; + } + if (i > 0 && this.end[i - 1] + 1 != this.start[i]) { + return false; + } + } + return true; + } + + } + + /** + * Main parsing logic used to convert a {@link CharSequence} to {@link Elements}. + */ + private static class ElementsParser { + + private static final int DEFAULT_CAPACITY = 6; + + private final CharSequence source; + + private final char separator; + + private int size; + + private int[] start; + + private int[] end; + + private ElementType[] type; + + private CharSequence[] resolved; + + ElementsParser(CharSequence source, char separator) { + this(source, separator, DEFAULT_CAPACITY); + } + + ElementsParser(CharSequence source, char separator, int capacity) { + this.source = source; + this.separator = separator; + this.start = new int[capacity]; + this.end = new int[capacity]; + this.type = new ElementType[capacity]; + } + + public Elements parse() { + return parse(null); + } + + public Elements parse(Function valueProcessor) { + int length = this.source.length(); + int openBracketCount = 0; + int start = 0; + ElementType type = ElementType.EMPTY; + for (int i = 0; i < length; i++) { + char ch = this.source.charAt(i); + if (ch == '[') { + if (openBracketCount == 0) { + add(start, i, type, valueProcessor); + start = i + 1; + type = ElementType.NUMERICALLY_INDEXED; + } + openBracketCount++; + } else if (ch == ']') { + openBracketCount--; + if (openBracketCount == 0) { + add(start, i, type, valueProcessor); + start = i + 1; + type = ElementType.EMPTY; + } + } else if (!type.isIndexed() && ch == this.separator) { + add(start, i, type, valueProcessor); + start = i + 1; + type = ElementType.EMPTY; + } else { + type = updateType(type, ch, i - start); + } + } + if (openBracketCount != 0) { + type = ElementType.NON_UNIFORM; + } + add(start, length, type, valueProcessor); + return new Elements(this.source, this.size, this.start, this.end, this.type, + this.resolved); + } + + private ElementType updateType(ElementType existingType, char ch, int index) { + if (existingType.isIndexed()) { + if (existingType == ElementType.NUMERICALLY_INDEXED && !isNumeric(ch)) { + return ElementType.INDEXED; + } + return existingType; + } + if (existingType == ElementType.EMPTY && isValidChar(ch, index)) { + return (index == 0) ? ElementType.UNIFORM : ElementType.NON_UNIFORM; + } + if (existingType == ElementType.UNIFORM && ch == '-') { + return ElementType.DASHED; + } + if (!isValidChar(ch, index)) { + if (existingType == ElementType.EMPTY + && !isValidChar(Character.toLowerCase(ch), index)) { + return ElementType.EMPTY; + } + return ElementType.NON_UNIFORM; + } + return existingType; + } + + private void add(int start, int end, ElementType type, + Function valueProcessor) { + if ((end - start) < 1 || type == ElementType.EMPTY) { + return; + } + if (this.start.length <= end) { + this.start = expand(this.start); + this.end = expand(this.end); + this.type = expand(this.type); + this.resolved = expand(this.resolved); + } + if (valueProcessor != null) { + if (this.resolved == null) { + this.resolved = new CharSequence[this.start.length]; + } + CharSequence resolved = valueProcessor + .apply(this.source.subSequence(start, end)); + Elements resolvedElements = new ElementsParser(resolved, '.').parse(); + Assert.state(resolvedElements.getSize() == 1, + "Resolved element must not contain multiple elements"); + this.resolved[this.size] = resolvedElements.get(0); + type = resolvedElements.getType(0); + } + this.start[this.size] = start; + this.end[this.size] = end; + this.type[this.size] = type; + this.size++; + } + + private int[] expand(int[] src) { + int[] dest = new int[src.length + DEFAULT_CAPACITY]; + System.arraycopy(src, 0, dest, 0, src.length); + return dest; + } + + private ElementType[] expand(ElementType[] src) { + ElementType[] dest = new ElementType[src.length + DEFAULT_CAPACITY]; + System.arraycopy(src, 0, dest, 0, src.length); + return dest; + } + + private CharSequence[] expand(CharSequence[] src) { + if (src == null) { + return null; + } + CharSequence[] dest = new CharSequence[src.length + DEFAULT_CAPACITY]; + System.arraycopy(src, 0, dest, 0, src.length); + return dest; + } + + public static boolean isValidChar(char ch, int index) { + return isAlpha(ch) || isNumeric(ch) || (index != 0 && ch == '-'); + } + + public static boolean isAlphaNumeric(char ch) { + return isAlpha(ch) || isNumeric(ch); + } + + private static boolean isAlpha(char ch) { + return ch >= 'a' && ch <= 'z'; + } + + private static boolean isNumeric(char ch) { + return ch >= '0' && ch <= '9'; + } + + } + + /** + * The various types of element that we can detect. + */ + private enum ElementType { + + /** + * The element is logically empty (contains no valid chars). + */ + EMPTY(false), + + /** + * The element is a uniform name (a-z, 0-9, no dashes, lowercase). + */ + UNIFORM(false), + + /** + * The element is almost uniform, but it contains (but does not start with) at + * least one dash. + */ + DASHED(false), + + /** + * The element contains non uniform characters and will need to be converted. + */ + NON_UNIFORM(false), + + /** + * The element is non-numerically indexed. + */ + INDEXED(true), + + /** + * The element is numerically indexed. + */ + NUMERICALLY_INDEXED(true); + + private final boolean indexed; + + ElementType(boolean indexed) { + this.indexed = indexed; + } + + public boolean isIndexed() { + return this.indexed; + } + + } + + /** + * Predicate used to filter element chars. + */ + private interface ElementCharPredicate { + + boolean test(char ch, int index); + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyNameAliases.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyNameAliases.java new file mode 100644 index 00000000..dbcc9b77 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyNameAliases.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Maintains a mapping of {@link ConfigurationPropertyName} aliases. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see ConfigurationPropertySource#withAliases(ConfigurationPropertyNameAliases) + * @since 2.0.0 + */ +public final class ConfigurationPropertyNameAliases + implements Iterable { + + private final MultiValueMap aliases = new LinkedMultiValueMap<>(); + + public ConfigurationPropertyNameAliases() { + } + + public ConfigurationPropertyNameAliases(String name, String... aliases) { + addAliases(name, aliases); + } + + public ConfigurationPropertyNameAliases(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + addAliases(name, aliases); + } + + public void addAliases(String name, String... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + addAliases(ConfigurationPropertyName.of(name), + Arrays.stream(aliases).map(ConfigurationPropertyName::of) + .toArray(ConfigurationPropertyName[]::new)); + } + + public void addAliases(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + Arrays.asList(aliases).forEach(v -> this.aliases.add(name, v)); +// this.aliases.addAll(name, Arrays.asList(aliases)); + } + + public List getAliases(ConfigurationPropertyName name) { + return this.aliases.getOrDefault(name, Collections.emptyList()); + } + + public ConfigurationPropertyName getNameForAlias(ConfigurationPropertyName alias) { + return this.aliases.entrySet().stream() + .filter((e) -> e.getValue().contains(alias)).map(Map.Entry::getKey) + .findFirst().orElse(null); + } + + @Override + public Iterator iterator() { + return this.aliases.keySet().iterator(); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySource.java new file mode 100644 index 00000000..3ac3f7d1 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySource.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.function.Predicate; + + +/** + * A source of {@link ConfigurationProperty ConfigurationProperties}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see ConfigurationPropertyName + * @see #getConfigurationProperty(ConfigurationPropertyName) + * @since 2.0.0 + */ +@FunctionalInterface +public interface ConfigurationPropertySource { + + /** + * Return a single {@link ConfigurationProperty} from the source or {@code null} if no + * property can be found. + * + * @param name the name of the property (must not be {@code null}) + * @return the associated object or {@code null}. + */ + ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name); + + /** + * Returns if the source contains any descendants of the specified name. May return + * {@link ConfigurationPropertyState#PRESENT} or + * {@link ConfigurationPropertyState#ABSENT} if an answer can be determined or + * {@link ConfigurationPropertyState#UNKNOWN} if it's not possible to determine a + * definitive answer. + * + * @param name the name to check + * @return if the source contains any descendants + */ + default ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + return ConfigurationPropertyState.UNKNOWN; + } + + /** + * Return a filtered variant of this source, containing only names that match the + * given {@link Predicate}. + * + * @param filter the filter to match + * @return a filtered {@link ConfigurationPropertySource} instance + */ + default ConfigurationPropertySource filter( + Predicate filter) { + return new FilteredConfigurationPropertiesSource(this, filter); + } + + /** + * Return a variant of this source that supports name aliases. + * + * @param aliases a function that returns a stream of aliases for any given name + * @return a {@link ConfigurationPropertySource} instance supporting name aliases + */ + default ConfigurationPropertySource withAliases( + ConfigurationPropertyNameAliases aliases) { + return new AliasedConfigurationPropertySource(this, aliases); + } + + /** + * Return the underlying source that is actually providing the properties. + * + * @return the underlying property source or {@code null}. + */ + default Object getUnderlyingSource() { + return null; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySources.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySources.java new file mode 100644 index 00000000..128a2e5a --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySources.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Collections; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySource.StubPropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.util.Assert; + +/** + * Provides access to {@link ConfigurationPropertySource ConfigurationPropertySources}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public final class ConfigurationPropertySources { + + /** + * The name of the {@link PropertySource} . + */ + private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties"; + + private ConfigurationPropertySources() { + } + + /** + * Determines if the specific {@link PropertySource} is the + * {@link ConfigurationPropertySource} that was {@link #attach(Environment) attached} + * to the {@link Environment}. + * + * @param propertySource the property source to test + * @return {@code true} if this is the attached {@link ConfigurationPropertySource} + */ + public static boolean isAttachedConfigurationPropertySource( + PropertySource propertySource) { + return ATTACHED_PROPERTY_SOURCE_NAME.equals(propertySource.getName()); + } + + /** + * Attach a {@link ConfigurationPropertySource} support to the specified + * {@link Environment}. Adapts each {@link PropertySource} managed by the environment + * to a {@link ConfigurationPropertySource} and allows classic + * {@link PropertySourcesPropertyResolver} calls to resolve using + * {@link ConfigurationPropertyName configuration property names}. + *

+ * The attached resolver will dynamically track any additions or removals from the + * underlying {@link Environment} property sources. + * + * @param environment the source environment (must be an instance of + * {@link ConfigurableEnvironment}) + * @see #get(Environment) + */ + public static void attach(Environment environment) { + Assert.isInstanceOf(ConfigurableEnvironment.class, environment); + MutablePropertySources sources = ((ConfigurableEnvironment) environment) + .getPropertySources(); + PropertySource attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME); + if (attached != null && attached.getSource() != sources) { + sources.remove(ATTACHED_PROPERTY_SOURCE_NAME); + attached = null; + } + if (attached == null) { + sources.addFirst(new ConfigurationPropertySourcesPropertySource( + ATTACHED_PROPERTY_SOURCE_NAME, + new SpringConfigurationPropertySources(sources))); + } + } + + /** + * Return a set of {@link ConfigurationPropertySource} instances that have previously + * been {@link #attach(Environment) attached} to the {@link Environment}. + * + * @param environment the source environment (must be an instance of + * {@link ConfigurableEnvironment}) + * @return an iterable set of configuration property sources + * @throws IllegalStateException if not configuration property sources have been + * attached + */ + public static Iterable get(Environment environment) { + Assert.isInstanceOf(ConfigurableEnvironment.class, environment); + MutablePropertySources sources = ((ConfigurableEnvironment) environment) + .getPropertySources(); + ConfigurationPropertySourcesPropertySource attached = (ConfigurationPropertySourcesPropertySource) sources + .get(ATTACHED_PROPERTY_SOURCE_NAME); + if (attached == null) { + return from(sources); + } + return attached.getSource(); + } + + /** + * Return {@link Iterable} containing a single new {@link ConfigurationPropertySource} + * adapted from the given Spring {@link PropertySource}. + * + * @param source the Spring property source to adapt + * @return an {@link Iterable} containing a single newly adapted + * {@link SpringConfigurationPropertySource} + */ + public static Iterable from(PropertySource source) { + return Collections.singleton(SpringConfigurationPropertySource.from(source)); + } + + /** + * Return {@link Iterable} containing new {@link ConfigurationPropertySource} + * instances adapted from the given Spring {@link PropertySource PropertySources}. + *

+ * This method will flatten any nested property sources and will filter all + * {@link StubPropertySource stub property sources}. Updates to the underlying source, + * identified by changes in the sources returned by its iterator, will be + * automatically tracked. The underlying source should be thread safe, for example a + * {@link MutablePropertySources} + * + * @param sources the Spring property sources to adapt + * @return an {@link Iterable} containing newly adapted + * {@link SpringConfigurationPropertySource} instances + */ + public static Iterable from( + Iterable> sources) { + return new SpringConfigurationPropertySources(sources); + } + + private static Stream> streamPropertySources( + PropertySources sources) { + return StreamSupport.stream(sources.spliterator(), false).flatMap(ConfigurationPropertySources::flatten) + .filter(ConfigurationPropertySources::isIncluded); + } + + private static Stream> flatten(PropertySource source) { + if (source.getSource() instanceof ConfigurableEnvironment) { + return streamPropertySources( + ((ConfigurableEnvironment) source.getSource()).getPropertySources()); + } + return Stream.of(source); + } + + private static boolean isIncluded(PropertySource source) { + return !(source instanceof StubPropertySource) + && !(source instanceof ConfigurationPropertySourcesPropertySource); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySourcesPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySourcesPropertySource.java new file mode 100644 index 00000000..efdd4e39 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertySourcesPropertySource.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import cn.springcloud.gray.bean.origin.Origin; +import cn.springcloud.gray.bean.origin.OriginLookup; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySource; + +/** + * {@link PropertySource} that exposes {@link ConfigurationPropertySource} instances so + * that they can be used with a {@link PropertyResolver} or added to the + * {@link Environment}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigurationPropertySourcesPropertySource + extends PropertySource> + implements OriginLookup { + + ConfigurationPropertySourcesPropertySource(String name, + Iterable source) { + super(name, source); + } + + @Override + public Object getProperty(String name) { + ConfigurationProperty configurationProperty = findConfigurationProperty(name); + return (configurationProperty != null) ? configurationProperty.getValue() : null; + } + + @Override + public Origin getOrigin(String name) { + return Origin.from(findConfigurationProperty(name)); + } + + private ConfigurationProperty findConfigurationProperty(String name) { + try { + return findConfigurationProperty(ConfigurationPropertyName.of(name, true)); + } catch (Exception ex) { + return null; + } + } + + private ConfigurationProperty findConfigurationProperty( + ConfigurationPropertyName name) { + if (name == null) { + return null; + } + for (ConfigurationPropertySource configurationPropertySource : getSource()) { + ConfigurationProperty configurationProperty = configurationPropertySource + .getConfigurationProperty(name); + if (configurationProperty != null) { + return configurationProperty; + } + } + return null; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyState.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyState.java new file mode 100644 index 00000000..cb7f770e --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/ConfigurationPropertyState.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * The state of content from a {@link ConfigurationPropertySource}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public enum ConfigurationPropertyState { + + /** + * The {@link ConfigurationPropertySource} has at least one matching + * {@link ConfigurationProperty}. + */ + PRESENT, + + /** + * The {@link ConfigurationPropertySource} has no matching + * {@link ConfigurationProperty ConfigurationProperties}. + */ + ABSENT, + + /** + * It's not possible to determine if {@link ConfigurationPropertySource} has matching + * {@link ConfigurationProperty ConfigurationProperties} or not. + */ + UNKNOWN; + + /** + * Search the given iterable using a predicate to determine if content is + * {@link #PRESENT} or {@link #ABSENT}. + * + * @param the data type + * @param source the source iterable to search + * @param predicate the predicate used to test for presence + * @return {@link #PRESENT} if the iterable contains a matching item, otherwise + * {@link #ABSENT}. + */ + static ConfigurationPropertyState search(Iterable source, + Predicate predicate) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(predicate, "Predicate must not be null"); + for (T item : source) { + if (predicate.test(item)) { + return PRESENT; + } + } + return ABSENT; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/DefaultPropertyMapper.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/DefaultPropertyMapper.java new file mode 100644 index 00000000..32048c53 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/DefaultPropertyMapper.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import org.springframework.util.ObjectUtils; + +/** + * Default {@link PropertyMapper} implementation. Names are mapped by removing invalid + * characters and converting to lower case. For example "{@code my.server_name.PORT}" is + * mapped to "{@code my.servername.port}". + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see SpringConfigurationPropertySource + */ +final class DefaultPropertyMapper implements PropertyMapper { + + public static final PropertyMapper INSTANCE = new DefaultPropertyMapper(); + + private LastMapping lastMappedConfigurationPropertyName; + + private LastMapping lastMappedPropertyName; + + private DefaultPropertyMapper() { + } + + @Override + public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { + // Use a local copy in case another thread changes things + LastMapping last = this.lastMappedConfigurationPropertyName; + if (last != null && last.isFrom(configurationPropertyName)) { + return last.getMapping(); + } + String convertedName = configurationPropertyName.toString(); + PropertyMapping[] mapping = { + new PropertyMapping(convertedName, configurationPropertyName)}; + this.lastMappedConfigurationPropertyName = new LastMapping<>( + configurationPropertyName, mapping); + return mapping; + } + + @Override + public PropertyMapping[] map(String propertySourceName) { + // Use a local copy in case another thread changes things + LastMapping last = this.lastMappedPropertyName; + if (last != null && last.isFrom(propertySourceName)) { + return last.getMapping(); + } + PropertyMapping[] mapping = tryMap(propertySourceName); + this.lastMappedPropertyName = new LastMapping<>(propertySourceName, mapping); + return mapping; + } + + private PropertyMapping[] tryMap(String propertySourceName) { + try { + ConfigurationPropertyName convertedName = ConfigurationPropertyName + .adapt(propertySourceName, '.'); + if (!convertedName.isEmpty()) { + return new PropertyMapping[]{ + new PropertyMapping(propertySourceName, convertedName)}; + } + } catch (Exception ex) { + } + return NO_MAPPINGS; + } + + private static class LastMapping { + + private final T from; + + private final PropertyMapping[] mapping; + + LastMapping(T from, PropertyMapping[] mapping) { + this.from = from; + this.mapping = mapping; + } + + public boolean isFrom(T from) { + return ObjectUtils.nullSafeEquals(from, this.from); + } + + public PropertyMapping[] getMapping() { + return this.mapping; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredConfigurationPropertiesSource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredConfigurationPropertiesSource.java new file mode 100644 index 00000000..8ae6a646 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredConfigurationPropertiesSource.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * A filtered {@link ConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class FilteredConfigurationPropertiesSource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final Predicate filter; + + FilteredConfigurationPropertiesSource(ConfigurationPropertySource source, + Predicate filter) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(filter, "Filter must not be null"); + this.source = source; + this.filter = filter; + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + boolean filtered = getFilter().test(name); + return filtered ? getSource().getConfigurationProperty(name) : null; + } + + @Override + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + ConfigurationPropertyState result = this.source.containsDescendantOf(name); + if (result == ConfigurationPropertyState.PRESENT) { + // We can't be sure a contained descendant won't be filtered + return ConfigurationPropertyState.UNKNOWN; + } + return result; + } + + @Override + public Object getUnderlyingSource() { + return this.source.getUnderlyingSource(); + } + + protected ConfigurationPropertySource getSource() { + return this.source; + } + + protected Predicate getFilter() { + return this.filter; + } + + @Override + public String toString() { + return this.source.toString() + " (filtered)"; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredIterableConfigurationPropertiesSource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredIterableConfigurationPropertiesSource.java new file mode 100644 index 00000000..c36444ca --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/FilteredIterableConfigurationPropertiesSource.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * A filtered {@link IterableConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class FilteredIterableConfigurationPropertiesSource + extends FilteredConfigurationPropertiesSource + implements IterableConfigurationPropertySource { + + FilteredIterableConfigurationPropertiesSource( + IterableConfigurationPropertySource source, + Predicate filter) { + super(source, filter); + } + + @Override + public Stream stream() { + return getSource().stream().filter(getFilter()); + } + + @Override + protected IterableConfigurationPropertySource getSource() { + return (IterableConfigurationPropertySource) super.getSource(); + } + + @Override + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + return ConfigurationPropertyState.search(this, name::isAncestorOf); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyNameException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyNameException.java new file mode 100644 index 00000000..1dba27ab --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyNameException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import java.util.List; + +/** + * Exception thrown when {@link ConfigurationPropertyName} has invalid characters. + * + * @author Madhura Bhave + * @since 2.0.0 + */ +public class InvalidConfigurationPropertyNameException extends RuntimeException { + + private final CharSequence name; + + private final List invalidCharacters; + + public InvalidConfigurationPropertyNameException(CharSequence name, + List invalidCharacters) { + super("Configuration property name '" + name + "' is not valid"); + this.name = name; + this.invalidCharacters = invalidCharacters; + } + + public List getInvalidCharacters() { + return this.invalidCharacters; + } + + public CharSequence getName() { + return this.name; + } + + public static void throwIfHasInvalidChars(CharSequence name, + List invalidCharacters) { + if (!invalidCharacters.isEmpty()) { + throw new InvalidConfigurationPropertyNameException(name, invalidCharacters); + } + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyValueException.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyValueException.java new file mode 100644 index 00000000..d79092cf --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/InvalidConfigurationPropertyValueException.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.springcloud.gray.bean.properties.source; + +import org.springframework.util.Assert; + +/** + * Exception thrown when a configuration property value is invalid. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +@SuppressWarnings("serial") +public class InvalidConfigurationPropertyValueException extends RuntimeException { + + private final String name; + + private final Object value; + + private final String reason; + + /** + * Creates a new instance for the specified property {@code name} and {@code value}, + * including a {@code reason} why the value is invalid. + * + * @param name the name of the property in canonical format + * @param value the value of the property, can be {@code null} + * @param reason a human-readable text that describes why the reason is invalid. + * Starts with an upper-case and ends with a dot. Several sentences and carriage + * returns are allowed. + */ + public InvalidConfigurationPropertyValueException(String name, Object value, + String reason) { + super("Property " + name + " with value '" + value + "' is invalid: " + reason); + Assert.notNull(name, "Name must not be null"); + this.name = name; + this.value = value; + this.reason = reason; + } + + /** + * Return the name of the property. + * + * @return the property name + */ + public String getName() { + return this.name; + } + + /** + * Return the invalid value, can be {@code null}. + * + * @return the invalid value + */ + public Object getValue() { + return this.value; + } + + /** + * Return the reason why the value is invalid. + * + * @return the reason + */ + public String getReason() { + return this.reason; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/IterableConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/IterableConfigurationPropertySource.java new file mode 100644 index 00000000..9c07eb43 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/IterableConfigurationPropertySource.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import cn.springcloud.gray.bean.origin.OriginTrackedValue; + +/** + * A {@link ConfigurationPropertySource} with a fully {@link Iterable} set of entries. + * Implementations of this interface must be able to iterate over all + * contained configuration properties. Any {@code non-null} result from + * {@link #getConfigurationProperty(ConfigurationPropertyName)} must also have an + * equivalent entry in the {@link #iterator() iterator}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see ConfigurationPropertyName + * @see OriginTrackedValue + * @see #getConfigurationProperty(ConfigurationPropertyName) + * @see #iterator() + * @see #stream() + * @since 2.0.0 + */ +public interface IterableConfigurationPropertySource + extends ConfigurationPropertySource, Iterable { + + /** + * Return an iterator for the {@link ConfigurationPropertyName names} managed by this + * source. + * + * @return an iterator (never {@code null}) + */ + @Override + default Iterator iterator() { + return stream().iterator(); + } + + /** + * Returns a sequential {@code Stream} for the {@link ConfigurationPropertyName names} + * managed by this source. + * + * @return a stream of names (never {@code null}) + */ + Stream stream(); + + @Override + default ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + return ConfigurationPropertyState.search(this, name::isAncestorOf); + } + + @Override + default IterableConfigurationPropertySource filter( + Predicate filter) { + return new FilteredIterableConfigurationPropertiesSource(this, filter); + } + + @Override + default IterableConfigurationPropertySource withAliases( + ConfigurationPropertyNameAliases aliases) { + return new AliasedIterableConfigurationPropertySource(this, aliases); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/MapConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/MapConfigurationPropertySource.java new file mode 100644 index 00000000..78662df9 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/MapConfigurationPropertySource.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.Assert; + +/** + * An {@link ConfigurationPropertySource} backed by a {@link Map} and using standard name + * mapping rules. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MapConfigurationPropertySource + implements IterableConfigurationPropertySource { + + private final Map source; + + private final IterableConfigurationPropertySource delegate; + + /** + * Create a new empty {@link MapConfigurationPropertySource} instance. + */ + public MapConfigurationPropertySource() { + this(Collections.emptyMap()); + } + + /** + * Create a new {@link MapConfigurationPropertySource} instance with entries copies + * from the specified map. + * + * @param map the source map + */ + public MapConfigurationPropertySource(Map map) { + this.source = new LinkedHashMap<>(); + this.delegate = new SpringIterableConfigurationPropertySource( + new MapPropertySource("source", this.source), + DefaultPropertyMapper.INSTANCE); + putAll(map); + } + + /** + * Add all entries from the specified map. + * + * @param map the source map + */ + public void putAll(Map map) { + Assert.notNull(map, "Map must not be null"); + assertNotReadOnlySystemAttributesMap(map); + map.forEach(this::put); + } + + /** + * Add an individual entry. + * + * @param name the name + * @param value the value + */ + public void put(Object name, Object value) { + this.source.put((name != null) ? name.toString() : null, value); + } + + @Override + public Object getUnderlyingSource() { + return this.source; + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + return this.delegate.getConfigurationProperty(name); + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + @Override + public Stream stream() { + return this.delegate.stream(); + } + + private void assertNotReadOnlySystemAttributesMap(Map map) { + try { + map.size(); + } catch (UnsupportedOperationException ex) { + throw new IllegalArgumentException( + "Security restricted maps are not supported", ex); + } + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapper.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapper.java new file mode 100644 index 00000000..ab5735cf --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Strategy used to provide a mapping between a {@link PropertySource} and a + * {@link ConfigurationPropertySource}. + *

+ * Mappings should be provided for both {@link ConfigurationPropertyName + * ConfigurationPropertyName} types and {@code String} based names. This allows the + * {@link SpringConfigurationPropertySource} to first attempt any direct mappings (i.e. + * map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} name) + * before falling back to {@link EnumerablePropertySource enumerating} property names, + * mapping them to a {@link ConfigurationPropertyName} and checking for + * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See + * {@link SpringConfigurationPropertySource} for more details. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see SpringConfigurationPropertySource + */ +interface PropertyMapper { + + PropertyMapping[] NO_MAPPINGS = {}; + + /** + * Provide mappings from a {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * + * @param configurationPropertyName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName); + + /** + * Provide mappings from a {@link PropertySource} property name. + * + * @param propertySourceName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + PropertyMapping[] map(String propertySourceName); + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapping.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapping.java new file mode 100644 index 00000000..5fe7a29d --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/PropertyMapping.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import org.springframework.core.env.PropertySource; + +/** + * Details a mapping between a {@link PropertySource} item and a + * {@link ConfigurationPropertySource} item. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see SpringConfigurationPropertySource + */ +class PropertyMapping { + + private final String propertySourceName; + + private final ConfigurationPropertyName configurationPropertyName; + + /** + * Create a new {@link PropertyMapper} instance. + * + * @param propertySourceName the {@link PropertySource} name + * @param configurationPropertyName the {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName} + */ + PropertyMapping(String propertySourceName, + ConfigurationPropertyName configurationPropertyName) { + this.propertySourceName = propertySourceName; + this.configurationPropertyName = configurationPropertyName; + } + + /** + * Return the mapped {@link PropertySource} name. + * + * @return the property source name (never {@code null}) + */ + public String getPropertySourceName() { + return this.propertySourceName; + + } + + /** + * Return the mapped {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * + * @return the configuration property source name (never {@code null}) + */ + public ConfigurationPropertyName getConfigurationPropertyName() { + return this.configurationPropertyName; + + } + + /** + * Return if this mapping is applicable for the given + * {@link ConfigurationPropertyName}. + * + * @param name the name to check + * @return if the mapping is applicable + */ + public boolean isApplicable(ConfigurationPropertyName name) { + return this.configurationPropertyName.equals(name); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySource.java new file mode 100644 index 00000000..00a8dfae --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySource.java @@ -0,0 +1,282 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +import cn.springcloud.gray.bean.origin.Origin; +import cn.springcloud.gray.bean.origin.PropertySourceOrigin; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link ConfigurationPropertySource} backed by a non-enumerable Spring + * {@link PropertySource} or a restricted {@link EnumerablePropertySource} implementation + * (such as a security restricted {@code systemEnvironment} source). A + * {@link PropertySource} is adapted with the help of a {@link PropertyMapper} which + * provides the mapping rules for individual properties. + *

+ * Each {@link ConfigurationPropertySource#getConfigurationProperty + * getConfigurationProperty} call attempts to + * {@link PropertyMapper#map(ConfigurationPropertyName) map} the + * {@link ConfigurationPropertyName} to one or more {@code String} based names. This + * allows fast property resolution for well formed property sources. + *

+ * When possible the {@link SpringIterableConfigurationPropertySource} will be used in + * preference to this implementation since it supports full "relaxed" style resolution. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see #from(PropertySource) + * @see PropertyMapper + * @see SpringIterableConfigurationPropertySource + */ +class SpringConfigurationPropertySource implements ConfigurationPropertySource { + + private static final ConfigurationPropertyName RANDOM = ConfigurationPropertyName + .of("random"); + + private final PropertySource propertySource; + + private final PropertyMapper mapper; + + private final Function containsDescendantOf; + + /** + * Create a new {@link SpringConfigurationPropertySource} implementation. + * + * @param propertySource the source property source + * @param mapper the property mapper + * @param containsDescendantOf function used to implement + * {@link #containsDescendantOf(ConfigurationPropertyName)} (may be {@code null}) + */ + SpringConfigurationPropertySource(PropertySource propertySource, + PropertyMapper mapper, + Function containsDescendantOf) { + Assert.notNull(propertySource, "PropertySource must not be null"); + Assert.notNull(mapper, "Mapper must not be null"); + this.propertySource = propertySource; + this.mapper = (mapper instanceof DelegatingPropertyMapper) ? mapper + : new DelegatingPropertyMapper(mapper); + this.containsDescendantOf = (containsDescendantOf != null) ? containsDescendantOf + : (n) -> ConfigurationPropertyState.UNKNOWN; + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + PropertyMapping[] mappings = getMapper().map(name); + return find(mappings, name); + } + + @Override + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + return this.containsDescendantOf.apply(name); + } + + @Override + public Object getUnderlyingSource() { + return this.propertySource; + } + + protected final ConfigurationProperty find(PropertyMapping[] mappings, + ConfigurationPropertyName name) { + for (PropertyMapping candidate : mappings) { + if (candidate.isApplicable(name)) { + ConfigurationProperty result = find(candidate); + if (result != null) { + return result; + } + } + } + return null; + } + + private ConfigurationProperty find(PropertyMapping mapping) { + String propertySourceName = mapping.getPropertySourceName(); + Object value = getPropertySource().getProperty(propertySourceName); + if (value == null) { + return null; + } + ConfigurationPropertyName configurationPropertyName = mapping + .getConfigurationPropertyName(); + Origin origin = PropertySourceOrigin.get(this.propertySource, propertySourceName); + return ConfigurationProperty.of(configurationPropertyName, value, origin); + } + + protected PropertySource getPropertySource() { + return this.propertySource; + } + + protected final PropertyMapper getMapper() { + return this.mapper; + } + + @Override + public String toString() { + return this.propertySource.toString(); + } + + /** + * Create a new {@link SpringConfigurationPropertySource} for the specified + * {@link PropertySource}. + * + * @param source the source Spring {@link PropertySource} + * @return a {@link SpringConfigurationPropertySource} or + * {@link SpringIterableConfigurationPropertySource} instance + */ + public static SpringConfigurationPropertySource from(PropertySource source) { + Assert.notNull(source, "Source must not be null"); + PropertyMapper mapper = getPropertyMapper(source); + if (isFullEnumerable(source)) { + return new SpringIterableConfigurationPropertySource( + (EnumerablePropertySource) source, mapper); + } + return new SpringConfigurationPropertySource(source, mapper, + getContainsDescendantOfForSource(source)); + } + + private static PropertyMapper getPropertyMapper(PropertySource source) { + if (source instanceof SystemEnvironmentPropertySource + && hasSystemEnvironmentName(source)) { + return new DelegatingPropertyMapper(SystemEnvironmentPropertyMapper.INSTANCE, + DefaultPropertyMapper.INSTANCE); + } + return new DelegatingPropertyMapper(DefaultPropertyMapper.INSTANCE); + } + + private static boolean hasSystemEnvironmentName(PropertySource source) { + String name = source.getName(); + return StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME.equals(name) + || name.endsWith("-" + + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + } + + private static boolean isFullEnumerable(PropertySource source) { + PropertySource rootSource = getRootSource(source); + if (rootSource.getSource() instanceof Map) { + // Check we're not security restricted + try { + ((Map) rootSource.getSource()).size(); + } catch (UnsupportedOperationException ex) { + return false; + } + } + return (source instanceof EnumerablePropertySource); + } + + private static PropertySource getRootSource(PropertySource source) { + while (source.getSource() != null + && source.getSource() instanceof PropertySource) { + source = (PropertySource) source.getSource(); + } + return source; + } + + private static Function getContainsDescendantOfForSource( + PropertySource source) { + if (source.getSource() instanceof Random) { + return SpringConfigurationPropertySource::containsDescendantOfForRandom; + } + return null; + } + + private static ConfigurationPropertyState containsDescendantOfForRandom( + ConfigurationPropertyName name) { + if (name.isAncestorOf(RANDOM) || name.equals(RANDOM)) { + return ConfigurationPropertyState.PRESENT; + } + return ConfigurationPropertyState.ABSENT; + } + + /** + * {@link PropertyMapper} that delegates to other {@link PropertyMapper}s and also + * swallows exceptions when the mapping fails. + */ + private static class DelegatingPropertyMapper implements PropertyMapper { + + private static final PropertyMapping[] NONE = {}; + + private final PropertyMapper first; + + private final PropertyMapper second; + + DelegatingPropertyMapper(PropertyMapper first) { + this(first, null); + } + + DelegatingPropertyMapper(PropertyMapper first, PropertyMapper second) { + this.first = first; + this.second = second; + } + + @Override + public PropertyMapping[] map( + ConfigurationPropertyName configurationPropertyName) { + PropertyMapping[] first = map(this.first, configurationPropertyName); + PropertyMapping[] second = map(this.second, configurationPropertyName); + return merge(first, second); + } + + private PropertyMapping[] map(PropertyMapper mapper, + ConfigurationPropertyName configurationPropertyName) { + try { + return (mapper != null) ? mapper.map(configurationPropertyName) : NONE; + } catch (Exception ex) { + return NONE; + } + } + + @Override + public PropertyMapping[] map(String propertySourceName) { + PropertyMapping[] first = map(this.first, propertySourceName); + PropertyMapping[] second = map(this.second, propertySourceName); + return merge(first, second); + } + + private PropertyMapping[] map(PropertyMapper mapper, String propertySourceName) { + try { + return (mapper != null) ? mapper.map(propertySourceName) : NONE; + } catch (Exception ex) { + return NONE; + } + } + + private PropertyMapping[] merge(PropertyMapping[] first, + PropertyMapping[] second) { + if (ObjectUtils.isEmpty(second)) { + return first; + } + if (ObjectUtils.isEmpty(first)) { + return second; + } + PropertyMapping[] merged = new PropertyMapping[first.length + second.length]; + System.arraycopy(first, 0, merged, 0, first.length); + System.arraycopy(second, 0, merged, first.length, second.length); + return merged; + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySources.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySources.java new file mode 100644 index 00000000..a29f48fe --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringConfigurationPropertySources.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySource.StubPropertySource; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; + +/** + * Adapter to convert Spring's {@link MutablePropertySources} to + * {@link ConfigurationPropertySource ConfigurationPropertySources}. + * + * @author Phillip Webb + */ +class SpringConfigurationPropertySources + implements Iterable { + + private final Iterable> sources; + + private final Map, ConfigurationPropertySource> cache = new ConcurrentReferenceHashMap<>( + 16, ReferenceType.SOFT); + + SpringConfigurationPropertySources(Iterable> sources) { + Assert.notNull(sources, "Sources must not be null"); + this.sources = sources; + } + + @Override + public Iterator iterator() { + return new SourcesIterator(this.sources.iterator(), this::adapt); + } + + private ConfigurationPropertySource adapt(PropertySource source) { + ConfigurationPropertySource result = this.cache.get(source); + // Most PropertySources test equality only using the source name, so we need to + // check the actual source hasn't also changed. + if (result != null && result.getUnderlyingSource() == source) { + return result; + } + result = SpringConfigurationPropertySource.from(source); + this.cache.put(source, result); + return result; + } + + private static class SourcesIterator + implements Iterator { + + private final Deque>> iterators; + + private ConfigurationPropertySource next; + + private final Function, ConfigurationPropertySource> adapter; + + SourcesIterator(Iterator> iterator, + Function, ConfigurationPropertySource> adapter) { + this.iterators = new ArrayDeque<>(4); + this.iterators.push(iterator); + this.adapter = adapter; + } + + @Override + public boolean hasNext() { + return fetchNext() != null; + } + + @Override + public ConfigurationPropertySource next() { + ConfigurationPropertySource next = fetchNext(); + if (next == null) { + throw new NoSuchElementException(); + } + this.next = null; + return next; + } + + private ConfigurationPropertySource fetchNext() { + if (this.next == null) { + if (this.iterators.isEmpty()) { + return null; + } + if (!this.iterators.peek().hasNext()) { + this.iterators.pop(); + return fetchNext(); + } + PropertySource candidate = this.iterators.peek().next(); + if (candidate.getSource() instanceof ConfigurableEnvironment) { + push((ConfigurableEnvironment) candidate.getSource()); + return fetchNext(); + } + if (isIgnored(candidate)) { + return fetchNext(); + } + this.next = this.adapter.apply(candidate); + } + return this.next; + } + + private void push(ConfigurableEnvironment environment) { + this.iterators.push(environment.getPropertySources().iterator()); + } + + private boolean isIgnored(PropertySource candidate) { + return (candidate instanceof StubPropertySource + || candidate instanceof ConfigurationPropertySourcesPropertySource); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringIterableConfigurationPropertySource.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringIterableConfigurationPropertySource.java new file mode 100644 index 00000000..e2391363 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SpringIterableConfigurationPropertySource.java @@ -0,0 +1,218 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.util.ObjectUtils; + +/** + * {@link ConfigurationPropertySource} backed by an {@link EnumerablePropertySource}. + * Extends {@link SpringConfigurationPropertySource} with full "relaxed" mapping support. + * In order to use this adapter the underlying {@link PropertySource} must be fully + * enumerable. A security restricted {@link SystemEnvironmentPropertySource} cannot be + * adapted. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + */ +class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource + implements IterableConfigurationPropertySource { + + private volatile Object cacheKey; + + private volatile Cache cache; + + SpringIterableConfigurationPropertySource(EnumerablePropertySource propertySource, + PropertyMapper mapper) { + super(propertySource, mapper, null); + assertEnumerablePropertySource(); + } + + private void assertEnumerablePropertySource() { + if (getPropertySource() instanceof MapPropertySource) { + try { + ((MapPropertySource) getPropertySource()).getSource().size(); + } catch (UnsupportedOperationException ex) { + throw new IllegalArgumentException( + "PropertySource must be fully enumerable"); + } + } + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + ConfigurationProperty configurationProperty = super.getConfigurationProperty( + name); + if (configurationProperty == null) { + configurationProperty = find(getPropertyMappings(getCache()), name); + } + return configurationProperty; + } + + @Override + public Stream stream() { + return getConfigurationPropertyNames().stream(); + } + + @Override + public Iterator iterator() { + return getConfigurationPropertyNames().iterator(); + } + + @Override + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { + return ConfigurationPropertyState.search(this, name::isAncestorOf); + } + + private List getConfigurationPropertyNames() { + Cache cache = getCache(); + List names = (cache != null) ? cache.getNames() : null; + if (names != null) { + return names; + } + PropertyMapping[] mappings = getPropertyMappings(cache); + names = new ArrayList<>(mappings.length); + for (PropertyMapping mapping : mappings) { + names.add(mapping.getConfigurationPropertyName()); + } + names = Collections.unmodifiableList(names); + if (cache != null) { + cache.setNames(names); + } + return names; + } + + private PropertyMapping[] getPropertyMappings(Cache cache) { + PropertyMapping[] result = (cache != null) ? cache.getMappings() : null; + if (result != null) { + return result; + } + String[] names = getPropertySource().getPropertyNames(); + List mappings = new ArrayList<>(names.length * 2); + for (String name : names) { + for (PropertyMapping mapping : getMapper().map(name)) { + mappings.add(mapping); + } + } + result = mappings.toArray(new PropertyMapping[0]); + if (cache != null) { + cache.setMappings(result); + } + return result; + } + + private Cache getCache() { + CacheKey cacheKey = CacheKey.get(getPropertySource()); + if (cacheKey == null) { + return null; + } + if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey)) { + return this.cache; + } + this.cache = new Cache(); + this.cacheKey = cacheKey.copy(); + return this.cache; + } + + @Override + protected EnumerablePropertySource getPropertySource() { + return (EnumerablePropertySource) super.getPropertySource(); + } + + private static class Cache { + + private List names; + + private PropertyMapping[] mappings; + + public List getNames() { + return this.names; + } + + public void setNames(List names) { + this.names = names; + } + + public PropertyMapping[] getMappings() { + return this.mappings; + } + + public void setMappings(PropertyMapping[] mappings) { + this.mappings = mappings; + } + + } + + private static final class CacheKey { + + private final Object key; + + private CacheKey(Object key) { + this.key = key; + } + + public CacheKey copy() { + return new CacheKey(copyKey(this.key)); + } + + private Object copyKey(Object key) { + if (key instanceof Set) { + return new HashSet((Set) key); + } + return ((String[]) key).clone(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.key, ((CacheKey) obj).key); + } + + @Override + public int hashCode() { + return this.key.hashCode(); + } + + public static CacheKey get(EnumerablePropertySource source) { + if (source instanceof MapPropertySource) { + return new CacheKey(((MapPropertySource) source).getSource().keySet()); + } + return new CacheKey(source.getPropertyNames()); + } + + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SystemEnvironmentPropertyMapper.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SystemEnvironmentPropertyMapper.java new file mode 100644 index 00000000..f47e2bf7 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/SystemEnvironmentPropertyMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-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 cn.springcloud.gray.bean.properties.source; + +import java.util.Locale; + +import cn.springcloud.gray.bean.properties.source.ConfigurationPropertyName.Form; + +/** + * {@link PropertyMapper} for system environment variables. Names are mapped by removing + * invalid characters, converting to lower case and replacing "{@code _}" with + * "{@code .}". For example, "{@code SERVER_PORT}" is mapped to "{@code server.port}". In + * addition, numeric elements are mapped to indexes (e.g. "{@code HOST_0}" is mapped to + * "{@code host[0]}"). + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see SpringConfigurationPropertySource + */ +final class SystemEnvironmentPropertyMapper implements PropertyMapper { + + public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); + + @Override + public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { + String name = convertName(configurationPropertyName); + String legacyName = convertLegacyName(configurationPropertyName); + if (name.equals(legacyName)) { + return new PropertyMapping[]{ + new PropertyMapping(name, configurationPropertyName)}; + } + return new PropertyMapping[]{ + new PropertyMapping(name, configurationPropertyName), + new PropertyMapping(legacyName, configurationPropertyName)}; + } + + @Override + public PropertyMapping[] map(String propertySourceName) { + ConfigurationPropertyName name = convertName(propertySourceName); + if (name == null || name.isEmpty()) { + return NO_MAPPINGS; + } + return new PropertyMapping[]{new PropertyMapping(propertySourceName, name)}; + } + + private ConfigurationPropertyName convertName(String propertySourceName) { + try { + return ConfigurationPropertyName.adapt(propertySourceName, '_', + this::processElementValue); + } catch (Exception ex) { + return null; + } + } + + private String convertName(ConfigurationPropertyName name) { + return convertName(name, name.getNumberOfElements()); + } + + private String convertName(ConfigurationPropertyName name, int numberOfElements) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < numberOfElements; i++) { + if (result.length() > 0) { + result.append("_"); + } + result.append(name.getElement(i, Form.UNIFORM).toUpperCase(Locale.ENGLISH)); + } + return result.toString(); + } + + private String convertLegacyName(ConfigurationPropertyName name) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < name.getNumberOfElements(); i++) { + if (result.length() > 0) { + result.append("_"); + } + result.append(convertLegacyNameElement(name.getElement(i, Form.ORIGINAL))); + } + return result.toString(); + } + + private Object convertLegacyNameElement(String element) { + return element.replace('-', '_').toUpperCase(Locale.ENGLISH); + } + + private CharSequence processElementValue(CharSequence value) { + String result = value.toString().toLowerCase(Locale.ENGLISH); + return isNumber(result) ? "[" + result + "]" : result; + } + + private static boolean isNumber(String string) { + return string.chars().allMatch(Character::isDigit); + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/UnboundElementsSourceFilter.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/UnboundElementsSourceFilter.java new file mode 100644 index 00000000..45bbd1d7 --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/UnboundElementsSourceFilter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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 cn.springcloud.gray.bean.properties.source; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; + +/** + * Function used to determine if a {@link ConfigurationPropertySource} should be included + * when determining unbound elements. If the underlying {@link PropertySource} is a + * systemEnvironment or systemProperties property source, it will not be considered for + * unbound element failures. + * + * @author Madhura Bhave + * @since 2.0.0 + */ +public class UnboundElementsSourceFilter + implements Function { + + private static final Set BENIGN_PROPERTY_SOURCE_NAMES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME))); + + @Override + public Boolean apply(ConfigurationPropertySource configurationPropertySource) { + Object underlyingSource = configurationPropertySource.getUnderlyingSource(); + if (underlyingSource instanceof PropertySource) { + String name = ((PropertySource) underlyingSource).getName(); + return !BENIGN_PROPERTY_SOURCE_NAMES.contains(name); + + } + return true; + } + +} diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/package-info.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/package-info.java new file mode 100644 index 00000000..8655e20c --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/bean/properties/source/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.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. + */ + +/** + * Sources for external configuration properties. + * + * @see org.springframework.boot.context.properties.source.ConfigurationPropertySource + */ +package cn.springcloud.gray.bean.properties.source; diff --git a/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/ConfigurationUtils.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/ConfigurationUtils.java new file mode 100644 index 00000000..8ce5ea3f --- /dev/null +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/ConfigurationUtils.java @@ -0,0 +1,93 @@ +package cn.springcloud.gray.utils; + +import cn.springcloud.gray.bean.properties.bind.Bindable; +import cn.springcloud.gray.bean.properties.bind.Binder; +import cn.springcloud.gray.bean.properties.source.MapConfigurationPropertySource; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.Expression; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ConfigurationUtils { + + + public static Map normalize(Map args, + SpelExpressionParser parser, + BeanFactory beanFactory) { + Map map = new HashMap<>(); + for (Map.Entry entry : args.entrySet()) { + String key = entry.getKey(); + Object value = getValue(parser, beanFactory, entry.getValue()); + + map.put(key, value); + } + return map; + } + + + public static void bind(Object o, Map properties, + String configurationPropertyName, String bindingName, Validator validator, + ConversionService conversionService) { + Object toBind = getTargetObject(o); + + new Binder( + Collections.singletonList(new MapConfigurationPropertySource(properties)), + null, conversionService).bind(configurationPropertyName, + Bindable.ofInstance(toBind)); + + if (validator != null) { + BindingResult errors = new BeanPropertyBindingResult(toBind, bindingName); + validator.validate(toBind, errors); + if (errors.hasErrors()) { + throw new RuntimeException(new BindException(errors)); + } + } + } + + + private static T getTargetObject(Object candidate) { + try { + if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) { + return (T) ((Advised) candidate).getTargetSource().getTarget(); + } + } catch (Exception ex) { + throw new IllegalStateException("Failed to unwrap proxied object", ex); + } + return (T) candidate; + } + + + private static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory, + String entryValue) { + Object value; + String rawValue = entryValue; + if (rawValue != null) { + rawValue = rawValue.trim(); + } + if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) { + // assume it's spel + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(beanFactory)); + Expression expression = parser.parseExpression(entryValue, + new TemplateParserContext()); + context.lookupVariable("api"); + value = expression.getValue(context); + } else { + value = entryValue; + } + return value; + } +} diff --git a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/utils/WebUtils.java b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/WebUtils.java similarity index 99% rename from spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/utils/WebUtils.java rename to spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/WebUtils.java index ec75e003..4e168e07 100644 --- a/spring-cloud-bamboo/src/main/java/cn/springcloud/bamboo/utils/WebUtils.java +++ b/spring-cloud-gray-utils/src/main/java/cn/springcloud/gray/utils/WebUtils.java @@ -1,4 +1,4 @@ -package cn.springcloud.bamboo.utils; +package cn.springcloud.gray.utils; import org.slf4j.Logger; diff --git a/spring-cloud-gray-utils/src/test/java/cn/springcloud/gray/test/ListUtilsTest.java b/spring-cloud-gray-utils/src/test/java/cn/springcloud/gray/test/ListUtilsTest.java new file mode 100644 index 00000000..317aa10d --- /dev/null +++ b/spring-cloud-gray-utils/src/test/java/cn/springcloud/gray/test/ListUtilsTest.java @@ -0,0 +1,44 @@ +package cn.springcloud.gray.test; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.ComparatorUtils; +import org.apache.commons.collections.ListUtils; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ListUtilsTest { + + @Test + public void testIsEqualList() { + + List a = Arrays.asList("a", "b"); + List b = Arrays.asList("b", "a"); + + + Collections.sort(a); + Collections.sort(b); + + boolean c = ListUtils.isEqualList(a, b); + System.out.println(c); + } + + @Test + public void testContainsAny() { + List a = Arrays.asList("a", "b", "c"); + List b = Arrays.asList("b", "a", "d"); + boolean c = CollectionUtils.containsAny(a, b); + System.out.println(c); + } + + @Test + public void testContainsAll() { + List a = Arrays.asList("a", "b", "c"); + List b = Arrays.asList("b", "a"); + boolean c = a.containsAll(b); + System.out.println(c); + } +} diff --git a/spring-cloud-starter-multi-version/pom.xml b/spring-cloud-gray-webui/pom.xml similarity index 57% rename from spring-cloud-starter-multi-version/pom.xml rename to spring-cloud-gray-webui/pom.xml index 1437517c..93cea733 100644 --- a/spring-cloud-starter-multi-version/pom.xml +++ b/spring-cloud-gray-webui/pom.xml @@ -5,19 +5,11 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 - spring-cloud-starter-multi-version - jar + spring-cloud-gray-webui - - - cn.springcloud.gray - spring-cloud-bamboo - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/README.md b/spring-cloud-gray-zookeeper-samples/README.md deleted file mode 100644 index d84e74d6..00000000 --- a/spring-cloud-gray-zookeeper-samples/README.md +++ /dev/null @@ -1,158 +0,0 @@ -## 灰度发布使用说明 -(以下说明都是假设浏览者对spring-cloud-netflix有过了解) -灰度管理的配置和bamboo的配置是一样的, 配置方式差别不大。不过由于gray-server需要注册到eureka,所以要先运行eureka-server。下面先说gray-server的配置。 - -##### gray-server -在项目的pom.xml加入spring-boot相关的依赖,再加入spring-cloud-starter-gray-server,然后启动就可以了。比如spring-cloud-gray-server-sample -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - - cn.springcloud.gray - spring-cloud-starter-gray-server - - - -``` - -在启动类中,需要启用服务发现,以及@EnableGrayServer。 - -```java -@SpringBootApplication -@EnableGrayServer -@EnableDiscoveryClient -public class GrayServerApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(GrayServerApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(GrayServerApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} -``` - -启动后,可以访问 http://localhost:20202/swagger-ui.html 查看接口列表,也可以调用其中的接口。 - -![灰度服务端swagger api list](../doc/img/web-api-gray-server.png) - -以上介绍完了gray-server的配置,下面再看gray-client的配置。 - - -##### gray-client - -1、在pom.xml中加入spring-cloud-starter-gray。比如spring-cloud-gray-service-b-sample -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - org.springframework.cloud - spring-cloud-starter-feign - - - org.apache.commons - commons-lang3 - 3.5 - - - - - cn.springcloud.gray - spring-cloud-starter-gray - - -``` - -2、在application.yaml中加入灰度配置。 -```yaml -spring: - application: - name: service-b - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - # 因zookeeper作为注册中心生成的instanceId不规则且不易获取,所以必须配置instanceId,取值值只要能区分不同的实例即可 - instanceId: ${spring.application.name}:${java.rmi.server.hostname}:${server.port} - -server: - port: 20101 - -gray: - client: - instance: - grayEnroll: false #是否在启动后自动注册成灰度实例 - serverUrl: http://localhost:20202 #灰度服务端的url -``` -更多gray-client配置请查看cn.springcloud.gray.client.GrayClientConfig接口 - -3、在启动类中加入灰度客户端的注解@EnableGrayClient -```java -@SpringBootApplication -@EnableDiscoveryClient -@EnableGrayClient -@EnableFeignClients -public class ServiceBApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(ServiceBApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(ServiceBApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} -``` - -这样灰略度的服务端和客户端都配置好了, 只要在灰度服务端开启灰度实例和灰度策,在灰度客户端就会自动进行灰度路由。 - - - diff --git a/spring-cloud-gray-zookeeper-samples/pom.xml b/spring-cloud-gray-zookeeper-samples/pom.xml deleted file mode 100644 index 4a4200d6..00000000 --- a/spring-cloud-gray-zookeeper-samples/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - spring-cloud-gray - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-gray-zookeeper-samples - pom - - spring-cloud-gray-zookeeper-zuul-sample - spring-cloud-gray-zookeeper-service-a-sample - spring-cloud-gray-zookeeper-service-b-sample - spring-cloud-gray-zookeeper-server-sample - - - - - - - - maven-deploy-plugin - - true - - - - - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java deleted file mode 100644 index e31c2a46..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/java/cn/springcloud/gray/server/GrayServerApplication.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.springcloud.gray.server; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -@SpringBootApplication -@EnableGrayServer -@EnableDiscoveryClient -public class GrayServerApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(GrayServerApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(GrayServerApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/resources/config/application.yml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/resources/config/application.yml deleted file mode 100644 index bf4ae0d6..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-server-sample/src/main/resources/config/application.yml +++ /dev/null @@ -1,12 +0,0 @@ -spring: - application: - name: gray-server - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - -server: - port: 20202 diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/pom.xml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/pom.xml deleted file mode 100644 index 11e10887..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - spring-cloud-gray-zookeeper-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-gray-zookeeper-service-a-sample - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - org.apache.commons - commons-lang3 - 3.5 - - - - cn.springcloud.gray - spring-cloud-starter-gray - - - - - - - - maven-deploy-plugin - - true - - - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/resources/config/application.yml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/resources/config/application.yml deleted file mode 100644 index f1fe7d4e..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-a-sample/src/main/resources/config/application.yml +++ /dev/null @@ -1,19 +0,0 @@ -spring: - application: - name: service-a - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - instanceId: ${spring.application.name}:${java.rmi.server.hostname}:${server.port} -server: - port: 20101 - -gray: - client: - instance: - grayEnroll: false #是否在启动后自动注册成灰度实例 - serverUrl: http://localhost:20202 #灰度服务端的url \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/pom.xml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/pom.xml deleted file mode 100644 index 23bbcfe7..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - spring-cloud-gray-zookeeper-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-gray-zookeeper-service-b-sample - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - org.springframework.cloud - spring-cloud-starter-feign - - - org.apache.commons - commons-lang3 - 3.5 - - - - - cn.springcloud.gray - spring-cloud-starter-gray - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/ServiceBApplication.java b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/ServiceBApplication.java deleted file mode 100644 index 842a0df2..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/ServiceBApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.springcloud.gray.service.b; - -import cn.springcloud.gray.client.EnableGrayClient; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.feign.EnableFeignClients; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * Created by saleson on 2017/10/18. - */ -@SpringBootApplication -@EnableDiscoveryClient -@EnableGrayClient -@EnableFeignClients -public class ServiceBApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(ServiceBApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(ServiceBApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/feign/TestClient.java b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/feign/TestClient.java deleted file mode 100644 index dc12a37a..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/feign/TestClient.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.springcloud.gray.service.b.feign; - -import org.springframework.cloud.netflix.feign.FeignClient; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; - -import java.util.Map; - -/** - * Created by saleson on 2017/11/10. - */ -@FeignClient(name = "service-a") -public interface TestClient { - - @RequestMapping(path = "/api/test/get", method = RequestMethod.GET) - Map testGet(@RequestParam(value = "version", required = false) String version); - -} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/rest/TestResource.java b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/rest/TestResource.java deleted file mode 100644 index 9e114290..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/java/cn/springcloud/gray/service/b/rest/TestResource.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.springcloud.gray.service.b.rest; - -import cn.springcloud.gray.service.b.feign.TestClient; -import com.google.common.collect.ImmutableMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -/** - * Created by saleson on 2017/10/18. - */ -@RestController -@RequestMapping("/api/test") -public class TestResource { - @Autowired - private RestTemplate restTemplate; - @Autowired - private TestClient testClient; - - - @RequestMapping(value = "/testGet", method = RequestMethod.GET) - @ResponseBody - public Map testGet(HttpServletRequest request) { - return ImmutableMap.of("restTemplateGet", "success.", "service-b-result", ""); - } - - - /** - * test rest template invoke service-a - * - * @param request HttpServletRequest - * @return 内容 - */ - @RequestMapping(value = "/restTemplateGet", method = RequestMethod.GET) - @ResponseBody - public Map restTemplateGet(HttpServletRequest request) { - String url = "http://service-a/api/test/get"; - String query = request.getQueryString(); - if (!StringUtils.isEmpty(query)) { - url = url + "?" + query; - } - - Map map = restTemplate.getForObject(url, Map.class); - return ImmutableMap.of("restTemplateGet", "success.", "service-a-result", map); - } - - - /** - * test feign invoke service-a - * - * @param version 请求的版本 - * @return 内容 - */ - @RequestMapping(value = "/feignGet", method = RequestMethod.GET) - @ResponseBody - public Map feignGet(@RequestParam(value = "version", required = false) String version) { - Map map = testClient.testGet(version); - return ImmutableMap.of("feignGet", "success.", "service-a-result", map); - } -} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/resources/config/application.yml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/resources/config/application.yml deleted file mode 100644 index eeabd291..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-service-b-sample/src/main/resources/config/application.yml +++ /dev/null @@ -1,24 +0,0 @@ -spring: - application: - name: service-b - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - instanceId: ${spring.application.name}:${java.rmi.server.hostname}:${server.port} - -server: - port: 20102 - -feign: - hystrix: - enabled: true - -gray: - client: - instance: - grayEnroll: false #是否在启动后自动注册成灰度实例 - serverUrl: http://localhost:20202 #灰度服务端的url \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/pom.xml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/pom.xml deleted file mode 100644 index 7e65962c..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - spring-cloud-gray-zookeeper-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-gray-zookeeper-zuul-sample - - - - org.springframework.cloud - spring-cloud-starter-zuul - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - - cn.springcloud.gray - spring-cloud-starter-gray - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/java/cn/springcloud/gray/zuul/GrayZuulApplication.java b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/java/cn/springcloud/gray/zuul/GrayZuulApplication.java deleted file mode 100644 index ff63148f..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/java/cn/springcloud/gray/zuul/GrayZuulApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.springcloud.gray.zuul; - -import cn.springcloud.gray.client.EnableGrayClient; -import org.slf4j.LoggerFactory; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.SpringCloudApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * Created by saleson on 2017/10/18. - */ -@SpringCloudApplication -@EnableZuulProxy -@EnableDiscoveryClient -@EnableGrayClient -public class GrayZuulApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(GrayZuulApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(GrayZuulApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/resources/config/application.yml b/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/resources/config/application.yml deleted file mode 100644 index e179a36c..00000000 --- a/spring-cloud-gray-zookeeper-samples/spring-cloud-gray-zookeeper-zuul-sample/src/main/resources/config/application.yml +++ /dev/null @@ -1,30 +0,0 @@ -spring: - application: - name: api-gateway - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - instanceId: ${spring.application.name}:${java.rmi.server.hostname}:${server.port} - -server: - port: 20301 - -ribbon: -# ReadTimeout: 30000 -# ConnectTimeout: 30000 - -zuul: - prefix: /gateway #为zuul设置一个公共的前缀 -# routes: -# eureka-client: #随便定义,当不存在serviceId时,默认该值为serviceId(就是注册服务的名称,属性spring.application.name) -# path: /client/** #匹配/techouse/usersystem/** 均路由到cloud-client -# serviceId: eureka-client #指定路由到的serviceId -gray: - client: - server-url: http://localhost:20202 #灰度服务端的url - instance: - use-multi-version: false \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/README.md b/spring-cloud-mult-version-samples/README.md deleted file mode 100644 index d904dadd..00000000 --- a/spring-cloud-mult-version-samples/README.md +++ /dev/null @@ -1,95 +0,0 @@ -## 多版本控制使用说明 -(以下说明都是假设浏览者对spring-cloud-netflix有过了解) -在使用多版本控制时,需要修改服务提供方和服务消费方,分别是application.yaml和pom.xml。 - -1、在服务提供方的application.yaml中添加versions属性,标明服务支持哪些版本。 -* eureka -```yaml -spring: - application: - name: service-a -server: - port: 10101 -eureka: - client: - register-with-eureka: true - fetch-registry: true - serviceUrl: - defaultZone: http://localhost:10001/eureka/ - instance: - instanceId: ${spring.application.name}:${server.port} - metadata-map: - versions: 1,2 -``` -* zookeeper -```yaml -spring: - application: - name: service-c - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - versions: 1,2 -server: - port: 10101 -``` - -2、在服务消费方,只需要在pom.xml添加spring-cloud-starter-multi-version到pom.xml依赖中即可,eureka和zookeeper的依赖二选一。 -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - - org.springframework.cloud - spring-cloud-starter-eureka - - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - org.springframework.cloud - spring-cloud-starter-feign - - - - org.apache.commons - commons-lang3 - 3.5 - - - - cn.springcloud.gray - spring-cloud-starter-multi-version - - -``` - -以下例子中,在一个名为spring-cloud-bamboo-service-a-samples的项目中加入第1个步骤, 启动服务。网关spring-cloud-bamboo-zuul-samples做为服务消费方,在pom.xml中加入spring-cloud-starter-multi-version, 并在application.yaml中加入zuul的配置: -```yaml -ribbon: - eureka: - enabled: true -zuul: - prefix: /gateway #为zuul设置一个公共的前缀 -``` - -启动服务后,访问 http://localhost:10301/gateway/service-a/api/test/get?version=2 会返回数据,因为service-a支持version=2 -![访问成功](../doc/img/bamboo-access-success.png) - -如果访问http://localhost:10301/gateway/service-a/api/test/get?version=3 会报错, 因为找不到支持版本3的service-a服务实例 -![访问失败](../doc/img/bamboo-access-fail.png) -![访问失败](../doc/img/bamboo-access-fail-thread-statck.png) - diff --git a/spring-cloud-mult-version-samples/pom.xml b/spring-cloud-mult-version-samples/pom.xml deleted file mode 100644 index 997c620e..00000000 --- a/spring-cloud-mult-version-samples/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - spring-cloud-gray - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-mult-version-samples - pom - - spring-cloud-bamboo-eureka-samples - spring-cloud-bamboo-zuul-samples - spring-cloud-bamboo-zuul-zookeeper-samples - spring-cloud-bamboo-service-a-samples - spring-cloud-bamboo-service-b-samples - spring-cloud-bamboo-service-c-samples - spring-cloud-bamboo-service-d-samples - - - - - - - - maven-deploy-plugin - - true - - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/pom.xml deleted file mode 100644 index 29ef9fb7..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-eureka-samples - - - - org.springframework.cloud - spring-cloud-starter-eureka-server - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/resources/config/application.yml deleted file mode 100644 index 6b2aaa22..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-eureka-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,13 +0,0 @@ -spring: - application: - name: eureka-server -server: - port: 10001 -eureka: - instance: - hostname: localhost - client: - register-with-eureka: false - fetch-registry: false - serviceUrl: - defaultZone: http://localhost:${server.port}/eureka/ diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceAApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceAApplication.java deleted file mode 100644 index 6ce4f873..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceAApplication.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.springcloud.bamboo.service.c; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -@SpringBootApplication -@EnableDiscoveryClient -public class BambooServiceAApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooServiceAApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooServiceAApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java deleted file mode 100644 index 8db7bd7c..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.springcloud.bamboo.service.c.web.rest; - -import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -/** - * Created by saleson on 2017/10/18. - */ -@RestController -@RequestMapping("/api/test") -public class TestResource { - @Autowired - Environment env; - - @RequestMapping(value = "/get", method = RequestMethod.GET) - @ResponseBody - public Map testGet(@RequestParam(value = "version", required = false) String version) { - return ImmutableMap.of("test", "success.", "version", StringUtils.defaultIfEmpty(version, ""), "serverPort", env.getProperty("server.port")); - } - - - @RequestMapping(value = "/post", method = RequestMethod.POST) - @ResponseBody - public Map testPost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - return ImmutableMap.of( - "test", "success.", "version", StringUtils.defaultIfEmpty(version, ""), - "serverPort", env.getProperty("server.port"), "body", body); - } - -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/resources/config/application.yml deleted file mode 100644 index 5508ad40..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-a-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,15 +0,0 @@ -spring: - application: - name: service-a -server: - port: 10101 -eureka: - client: - register-with-eureka: true - fetch-registry: true - serviceUrl: - defaultZone: http://localhost:10001/eureka/ - instance: - instanceId: ${spring.application.name}:${server.port} - metadata-map: - versions: 1,2 \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/pom.xml deleted file mode 100644 index cbe55abd..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-service-b-samples - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-eureka - - - org.springframework.cloud - spring-cloud-starter-feign - - - - org.apache.commons - commons-lang3 - 3.5 - - - - cn.springcloud.gray - spring-cloud-starter-multi-version - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceBApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceBApplication.java deleted file mode 100644 index 3bf5d609..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceBApplication.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.springcloud.bamboo.service.d; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.feign.EnableFeignClients; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -@SpringBootApplication -@EnableDiscoveryClient -@EnableFeignClients -public class BambooServiceBApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooServiceBApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooServiceBApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java deleted file mode 100644 index 6e2502ae..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.springcloud.bamboo.service.d.feign; - -import org.springframework.cloud.netflix.feign.FeignClient; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -/** - * Created by saleson on 2017/11/10. - */ -@FeignClient(name = "service-a") -public interface TestClient { - - @RequestMapping(path = "/api/test/get", method = RequestMethod.GET) - Map testGet(@RequestParam(value = "version", required = false) String version); - - - - @RequestMapping(value = "/api/test/post", method = RequestMethod.POST) - Map testPost(@RequestParam(value = "version") String version, @RequestBody String body); - -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java deleted file mode 100644 index ed517cad..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.springcloud.bamboo.service.d.rest; - -import cn.springcloud.bamboo.service.d.feign.TestClient; -import com.google.common.collect.ImmutableMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -/** - * Created by saleson on 2017/10/18. - */ -@RestController -@RequestMapping("/api/test") -public class TestResource { - @Autowired - private RestTemplate restTemplate; - @Autowired - private TestClient testClient; - - /** - * test rest template invoke service-a - * - * @param request HttpServletRequest - * @return 消息体 - */ - @RequestMapping(value = "/restTemplateGet", method = RequestMethod.GET) - @ResponseBody - public Map restTemplateGet(HttpServletRequest request) { - String url = "http://service-a/api/test/get"; - String query = request.getQueryString(); - if (!StringUtils.isEmpty(query)) { - url = url + "?" + query; - } - - Map map = restTemplate.getForObject(url, Map.class); - return ImmutableMap.of("restTemplateGet", "success.", "service-a-result", map); - } - - - /** - * test feign invoke service-a - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/feignPost", method = RequestMethod.POST) - @ResponseBody - public Map feignPost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - Map map = testClient.testPost(version, body); - return ImmutableMap.of("feignPost", "success.", "service-a-result", map); - } - - - /** - * test feign invoke service-a - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/restTemplatePost", method = RequestMethod.POST) - @ResponseBody - public Map restTemplatePost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - String url = "http://service-a/api/test/post?version="+version; - Map map = restTemplate.postForObject(url, body, Map.class); - return ImmutableMap.of("restTemplatePost", "success.", "service-a-result", map); - } - - - /** - * test feign invoke service-a - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/feignGet", method = RequestMethod.GET) - @ResponseBody - public Map feignGet(@RequestParam(value = "version", required = false) String version) { - Map map = testClient.testGet(version); - return ImmutableMap.of("feignGet", "success.", "service-a-result", map); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/resources/config/application.yml deleted file mode 100644 index efd7e131..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-b-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,26 +0,0 @@ -spring: - application: - name: service-b -server: - port: 10102 -eureka: - client: - register-with-eureka: true - fetch-registry: true - serviceUrl: - defaultZone: http://localhost:10001/eureka/ - instance: - instanceId: ${spring.application.name}:${server.port} - -feign: - hystrix: - enabled: true - -hystrix: - command: - default: - execution: - isolation: - strategy: THREAD - thread: - timeoutInMilliseconds: 5000 \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/pom.xml deleted file mode 100644 index 34a826fd..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-service-c-samples - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - - - - - - maven-deploy-plugin - - true - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceCApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceCApplication.java deleted file mode 100644 index 9e010db0..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/BambooServiceCApplication.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.springcloud.bamboo.service.c; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -@SpringBootApplication -@EnableDiscoveryClient -public class BambooServiceCApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooServiceCApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooServiceCApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java deleted file mode 100644 index 8db7bd7c..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/java/cn/springcloud/bamboo/service/c/web/rest/TestResource.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.springcloud.bamboo.service.c.web.rest; - -import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -/** - * Created by saleson on 2017/10/18. - */ -@RestController -@RequestMapping("/api/test") -public class TestResource { - @Autowired - Environment env; - - @RequestMapping(value = "/get", method = RequestMethod.GET) - @ResponseBody - public Map testGet(@RequestParam(value = "version", required = false) String version) { - return ImmutableMap.of("test", "success.", "version", StringUtils.defaultIfEmpty(version, ""), "serverPort", env.getProperty("server.port")); - } - - - @RequestMapping(value = "/post", method = RequestMethod.POST) - @ResponseBody - public Map testPost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - return ImmutableMap.of( - "test", "success.", "version", StringUtils.defaultIfEmpty(version, ""), - "serverPort", env.getProperty("server.port"), "body", body); - } - -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/resources/config/application.yml deleted file mode 100644 index 0e20155e..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-c-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,14 +0,0 @@ -spring: - application: - name: service-c - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev - metadata: - versions: 1,2 - -server: - port: 10101 \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/pom.xml deleted file mode 100644 index a3fd12f0..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-service-d-samples - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - org.springframework.cloud - spring-cloud-starter-feign - - - - org.apache.commons - commons-lang3 - 3.5 - - - - cn.springcloud.gray - spring-cloud-starter-multi-version - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceDApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceDApplication.java deleted file mode 100644 index 981701eb..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/BambooServiceDApplication.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.springcloud.bamboo.service.d; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.feign.EnableFeignClients; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -@SpringBootApplication -@EnableDiscoveryClient -@EnableFeignClients -public class BambooServiceDApplication { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooServiceDApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooServiceDApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java deleted file mode 100644 index 7687e610..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/feign/TestClient.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.springcloud.bamboo.service.d.feign; - -import org.springframework.cloud.netflix.feign.FeignClient; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -/** - * Created by saleson on 2017/11/10. - */ -@FeignClient(name = "service-c") -public interface TestClient { - - @RequestMapping(path = "/api/test/get", method = RequestMethod.GET) - Map testGet(@RequestParam(value = "version", required = false) String version); - - - - @RequestMapping(value = "/api/test/post", method = RequestMethod.POST) - Map testPost(@RequestParam(value = "version") String version, @RequestBody String body); - -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java deleted file mode 100644 index 3909f89f..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/java/cn/springcloud/bamboo/service/d/rest/TestResource.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.springcloud.bamboo.service.d.rest; - -import cn.springcloud.bamboo.service.d.feign.TestClient; -import com.google.common.collect.ImmutableMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -/** - * Created by saleson on 2017/10/18. - */ -@RestController -@RequestMapping("/api/test") -public class TestResource { - @Autowired - private RestTemplate restTemplate; - @Autowired - private TestClient testClient; - - /** - * test rest template invoke service-c - * - * @param request HttpServletRequest - * @return 消息体 - */ - @RequestMapping(value = "/restTemplateGet", method = RequestMethod.GET) - @ResponseBody - public Map restTemplateGet(HttpServletRequest request) { - String url = "http://service-c/api/test/get"; - String query = request.getQueryString(); - if (!StringUtils.isEmpty(query)) { - url = url + "?" + query; - } - - Map map = restTemplate.getForObject(url, Map.class); - return ImmutableMap.of("restTemplateGet", "success.", "service-c-result", map); - } - - - /** - * test feign invoke service-c - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/feignPost", method = RequestMethod.POST) - @ResponseBody - public Map feignPost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - Map map = testClient.testPost(version, body); - return ImmutableMap.of("feignPost", "success.", "service-c-result", map); - } - - - /** - * test feign invoke service-c - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/restTemplatePost", method = RequestMethod.POST) - @ResponseBody - public Map restTemplatePost(@RequestParam(value = "version", required = false) String version, @RequestBody String body) { - String url = "http://service-c/api/test/post?version="+version; - Map map = restTemplate.postForObject(url, body, Map.class); - return ImmutableMap.of("restTemplatePost", "success.", "service-c-result", map); - } - - - /** - * test feign invoke service-c - * - * @param version 请求版本 - * @return 消息体 - */ - @RequestMapping(value = "/feignGet", method = RequestMethod.GET) - @ResponseBody - public Map feignGet(@RequestParam(value = "version", required = false) String version) { - Map map = testClient.testGet(version); - return ImmutableMap.of("feignGet", "success.", "service-c-result", map); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/resources/config/application.yml deleted file mode 100644 index 3c299552..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-service-d-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,24 +0,0 @@ -spring: - application: - name: service-d - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev -server: - port: 10102 - -feign: - hystrix: - enabled: true - -hystrix: - command: - default: - execution: - isolation: - strategy: THREAD - thread: - timeoutInMilliseconds: 5000 \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/pom.xml deleted file mode 100644 index 57cb004d..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-zuul-samples - - - - - org.springframework.cloud - spring-cloud-starter-zuul - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-eureka - - - cn.springcloud.gray - spring-cloud-starter-multi-version - - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java deleted file mode 100644 index 81cbd735..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.springcloud.bamboo.zuul; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.SpringCloudApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * Created by saleson on 2017/10/18. - */ -@SpringCloudApplication -@EnableZuulProxy -@EnableDiscoveryClient -public class BambooZuulApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooZuulApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooZuulApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/resources/config/application.yml deleted file mode 100644 index a9998f06..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,21 +0,0 @@ -spring: - application: - name: api-gateway -server: - port: 10301 -eureka: - client: - serviceUrl: - defaultZone: http://localhost:10001/eureka/ - -ribbon: -# ReadTimeout: 30000 -# ConnectTimeout: 30000 - eureka: - enabled: true -zuul: - prefix: /gateway #为zuul设置一个公共的前缀 -# routes: -# eureka-client: #随便定义,当不存在serviceId时,默认该值为serviceId(就是注册服务的名称,属性spring.application.name) -# path: /client/** #匹配/techouse/usersystem/** 均路由到cloud-client -# serviceId: eureka-client #指定路由到的serviceId \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/pom.xml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/pom.xml deleted file mode 100644 index 1644dce3..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - spring-cloud-mult-version-samples - cn.springcloud.gray - 1.1.0 - - 4.0.0 - - spring-cloud-bamboo-zuul-zookeeper-samples - - - - - org.springframework.cloud - spring-cloud-starter-zuul - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - - - cn.springcloud.gray - spring-cloud-starter-multi-version - - - - - - - - - maven-deploy-plugin - - true - - - - - - \ No newline at end of file diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java deleted file mode 100644 index 81cbd735..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/java/cn/springcloud/bamboo/zuul/BambooZuulApplication.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.springcloud.bamboo.zuul; - -import org.slf4j.LoggerFactory; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.SpringCloudApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.core.env.Environment; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * Created by saleson on 2017/10/18. - */ -@SpringCloudApplication -@EnableZuulProxy -@EnableDiscoveryClient -public class BambooZuulApplication { - private static final org.slf4j.Logger log = LoggerFactory.getLogger(BambooZuulApplication.class); - - - public static void main(String[] args) throws UnknownHostException { - Environment env = new SpringApplicationBuilder(BambooZuulApplication.class).web(true).run(args).getEnvironment(); - log.info( - "\n----------------------------------------------------------\n\t" - + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://127.0.0.1:{}\n\t" - + "External: \thttp://{}:{}\n----------------------------------------------------------", - env.getProperty("spring.application.name"), env.getProperty("server.port"), - InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); - - String configServerStatus = env.getProperty("configserver.status"); - log.info( - "\n----------------------------------------------------------\n\t" - + "Config Server: \t{}\n----------------------------------------------------------", - configServerStatus == null ? "Not found or not setup for this application" : configServerStatus); - } -} diff --git a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/resources/config/application.yml b/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/resources/config/application.yml deleted file mode 100644 index b5b1ddfc..00000000 --- a/spring-cloud-mult-version-samples/spring-cloud-bamboo-zuul-zookeeper-samples/src/main/resources/config/application.yml +++ /dev/null @@ -1,23 +0,0 @@ -spring: - application: - name: api-gateway - cloud: - zookeeper: - connect-string: 127.0.0.1:2181 - discovery: - register: true - root: dev -server: - port: 10301 - -#ribbon: -# ReadTimeout: 30000 -# ConnectTimeout: 30000 -# eureka: -# enabled: true -zuul: - prefix: /gateway #为zuul设置一个公共的前缀 -# routes: -# eureka-client: #随便定义,当不存在serviceId时,默认该值为serviceId(就是注册服务的名称,属性spring.application.name) -# path: /client/** #匹配/techouse/usersystem/** 均路由到cloud-client -# serviceId: eureka-client #指定路由到的serviceId \ No newline at end of file diff --git a/spring-cloud-starter-gray/pom.xml b/spring-cloud-starter-gray-client/pom.xml similarity index 64% rename from spring-cloud-starter-gray/pom.xml rename to spring-cloud-starter-gray-client/pom.xml index 9ddc0c2b..1c71865c 100644 --- a/spring-cloud-starter-gray/pom.xml +++ b/spring-cloud-starter-gray-client/pom.xml @@ -5,11 +5,11 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 - spring-cloud-starter-gray + spring-cloud-starter-gray-client @@ -19,8 +19,13 @@ cn.springcloud.gray - spring-cloud-starter-multi-version + spring-cloud-gray-client-netflix + + + + + \ No newline at end of file diff --git a/spring-cloud-starter-gray-client/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-gray-client/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..a0c8923c --- /dev/null +++ b/spring-cloud-starter-gray-client/src/main/resources/META-INF/spring.factories @@ -0,0 +1,10 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.springcloud.gray.client.config.GrayClientAutoConfiguration,\ + cn.springcloud.gray.client.config.GrayLoadAutoConfigration,\ + cn.springcloud.gray.client.netflix.configuration.NetflixGrayAutoConfiguration,\ + cn.springcloud.gray.client.netflix.configuration.GrayClientEurekaAutoConfiguration,\ + cn.springcloud.gray.client.netflix.feign.configuration.GrayFeignAutoConfiguration,\ + cn.springcloud.gray.client.netflix.zuul.configuration.GrayZuulAutoConfiguration,\ + cn.springcloud.gray.client.netflix.resttemplate.configuration.GrayRestTemplateAutoConfiguration,\ + cn.springcloud.gray.client.config.GrayEventAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-starter-gray-server/pom.xml b/spring-cloud-starter-gray-server/pom.xml index bc03a9e2..f8ec415b 100644 --- a/spring-cloud-starter-gray-server/pom.xml +++ b/spring-cloud-starter-gray-server/pom.xml @@ -5,7 +5,7 @@ spring-cloud-gray cn.springcloud.gray - 1.1.0 + 2.0.0 4.0.0 @@ -16,6 +16,21 @@ cn.springcloud.gray spring-cloud-gray-server + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-starter-gray-server/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-gray-server/src/main/resources/META-INF/spring.factories index d2899dee..37dd0bec 100644 --- a/spring-cloud-starter-gray-server/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-starter-gray-server/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.springcloud.gray.server.config.GrayServerAutoConfiguration,\ - cn.springcloud.gray.server.config.GrayServiceEurekaAutoConfiguration,\ - cn.springcloud.gray.server.config.GrayServiceZookeeperAutoConfiguration \ No newline at end of file + cn.springcloud.gray.server.configuration.GrayServerAutoConfiguration,\ + cn.springcloud.gray.server.configuration.DBStorageConfiguration,\ + cn.springcloud.gray.server.netflix.eureka.configuration.GrayServiceEurekaAutoConfiguration,\ + cn.springcloud.gray.server.configuration.GrayServerEventAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-starter-gray/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-gray/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 3dc90898..00000000 --- a/spring-cloud-starter-gray/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,5 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.springcloud.gray.client.config.GrayClientAutoConfiguration,\ - cn.springcloud.gray.client.config.GrayClientEurekaAutoConfiguration,\ - cn.springcloud.gray.client.config.GrayClientZookeeperAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-starter-multi-version/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-multi-version/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 2fb3d1a5..00000000 --- a/spring-cloud-starter-multi-version/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,5 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.springcloud.bamboo.autoconfig.BambooAutoConfiguration,\ - cn.springcloud.bamboo.feign.config.BambooFeignConfiguration,\ - cn.springcloud.bamboo.zuul.config.BambooZuulConfiguration \ No newline at end of file