diff --git a/blade-core-boot/pom.xml b/blade-core-boot/pom.xml index 7382cba..e633f7a 100644 --- a/blade-core-boot/pom.xml +++ b/blade-core-boot/pom.xml @@ -5,7 +5,7 @@ org.springblade blade-tool - 2.4.1 + 2.5.0 4.0.0 @@ -111,6 +111,12 @@ ehcache 2.10.5 + + + com.alibaba + druid-spring-boot-starter + 1.1.18 + mysql diff --git a/blade-core-boot/src/main/resources/bootstrap.yml b/blade-core-boot/src/main/resources/bootstrap.yml index a3a0049..67386e1 100644 --- a/blade-core-boot/src/main/resources/bootstrap.yml +++ b/blade-core-boot/src/main/resources/bootstrap.yml @@ -29,12 +29,37 @@ spring: add-mappings: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver - hikari: - connection-test-query: SELECT 1 FROM DUAL - connection-timeout: 30000 - maximum-pool-size: 5 - max-lifetime: 1800000 - minimum-idle: 1 + druid: + initial-size: 5 + max-active: 20 + min-idle: 5 + max-wait: 60000 + # MySql、PostgreSQL校验 + validation-query: select 1 + # Oracle校验 + #validation-query: select 1 from dual + validation-query-timeout: 2000 + test-on-borrow: false + test-on-return: false + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + stat-view-servlet: + enabled: true + login-username: blade + login-password: 1qaz@WSX + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*' + session-stat-enable: true + session-stat-max-count: 10 + #hikari: + #connection-test-query: SELECT 1 FROM DUAL + #connection-timeout: 30000 + #maximum-pool-size: 5 + #max-lifetime: 1800000 + #minimum-idle: 1 devtools: restart: log-condition-evaluation-delta: false @@ -75,7 +100,7 @@ mybatis-plus: swagger: title: SpringBlade 接口文档系统 description: SpringBlade 接口文档系统 - version: 2.4.1 + version: 2.5.0 license: Powered By SpringBlade licenseUrl: https://bladex.vip terms-of-service-url: https://bladex.vip diff --git a/blade-core-cloud/pom.xml b/blade-core-cloud/pom.xml index 878f352..e17aa1a 100644 --- a/blade-core-cloud/pom.xml +++ b/blade-core-cloud/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 @@ -16,34 +16,27 @@ + - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - + org.springblade + blade-core-secure + ${blade.tool.version} + - org.springframework.boot - spring-boot-starter-undertow - - - de.codecentric - spring-boot-admin-starter-client - ${spring.boot.admin.version} + org.springframework.retry + spring-retry org.springframework.cloud - spring-cloud-starter-netflix-hystrix - - - commons-logging - commons-logging - - + spring-cloud-stream + 2.2.1.RELEASE + + + + io.github.openfeign + feign-okhttp + 10.4.0 org.springframework.cloud @@ -55,6 +48,28 @@ + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + + commons-logging + commons-logging + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + de.codecentric + spring-boot-admin-starter-client + ${spring.boot.admin.version} + com.alibaba.cloud diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java new file mode 100644 index 0000000..5127bc2 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.annotation; + +import java.lang.annotation.*; + +/** + * header 版本 处理 + * + * @author L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ApiVersion { + + /** + * header 路径中的版本 + * + * @return 版本号 + */ + String value() default ""; + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java new file mode 100644 index 0000000..c0622a8 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.annotation; + +import java.lang.annotation.*; + +/** + * 注解用于生成 requestMappingInfo 时候直接拼接路径规则,自动放置于方法路径开始部分 + * + * @author L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface UrlVersion { + + /** + * url 路径中的版本 + * + * @return 版本号 + */ + String value() default ""; +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java new file mode 100644 index 0000000..93a0127 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.*; + +/** + * 版本号处理 + * + *

+ * 1. url 版本号:添加到 url 前 + * 2. Accept 版本:application/vnd.blade.VERSION+json + *

+ * + * @author L.cm + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping +@UrlVersion +@ApiVersion +@Validated +public @interface VersionMapping { + /** + * Alias for {@link RequestMapping#name}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + * default json utf-8 + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + + /** + * Alias for {@link UrlVersion#value}. + * @return {String} + */ + @AliasFor(annotation = UrlVersion.class, attribute = "value") + String urlVersion() default ""; + + /** + * Alias for {@link ApiVersion#value}. + * @return {String} + */ + @AliasFor(annotation = ApiVersion.class, attribute = "value") + String apiVersion() default ""; + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/config/BladeFeignConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/config/BladeFeignConfiguration.java deleted file mode 100644 index 71ad4a9..0000000 --- a/blade-core-cloud/src/main/java/org/springblade/core/cloud/config/BladeFeignConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). - *

- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springblade.core.cloud.config; - -import feign.RequestInterceptor; -import lombok.extern.slf4j.Slf4j; -import org.springblade.core.cloud.feign.BladeFeignRequestHeaderInterceptor; -import org.springblade.core.cloud.feign.FeignHystrixConcurrencyStrategy; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -/** - * WEB配置 - * - * @author Chill - */ -@Slf4j -@Configuration -@EnableCaching -@Order(Ordered.HIGHEST_PRECEDENCE) -public class BladeFeignConfiguration implements WebMvcConfigurer { - - @Bean - @ConditionalOnMissingBean - public RequestInterceptor requestInterceptor() { - return new BladeFeignRequestHeaderInterceptor(); - } - - @Bean - public FeignHystrixConcurrencyStrategy feignHystrixConcurrencyStrategy() { - return new FeignHystrixConcurrencyStrategy(); - } - -} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java new file mode 100644 index 0000000..b6a524d --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.feign; + +import feign.Target; +import feign.hystrix.FallbackFactory; +import lombok.AllArgsConstructor; +import org.springframework.cglib.proxy.Enhancer; + +/** + * 默认 Fallback,避免写过多fallback类 + * + * @param 泛型标记 + * @author L.cm + */ +@AllArgsConstructor +public class BladeFallbackFactory implements FallbackFactory { + private final Target target; + + @Override + @SuppressWarnings("unchecked") + public T create(Throwable cause) { + final Class targetType = target.type(); + final String targetName = target.name(); + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(targetType); + enhancer.setUseCache(true); + enhancer.setCallback(new BladeFeignFallback<>(targetType, targetName, cause)); + return (T) enhancer.create(); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignAutoConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignAutoConfiguration.java new file mode 100644 index 0000000..64e6710 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignAutoConfiguration.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.cloud.feign; + +import com.netflix.hystrix.HystrixCommand; +import feign.Contract; +import feign.Feign; +import feign.RequestInterceptor; +import feign.hystrix.HystrixFeign; +import org.springblade.core.tool.convert.EnumToStringConverter; +import org.springblade.core.tool.convert.StringToEnumConverter; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.openfeign.BladeFeignClientsRegistrar; +import org.springframework.cloud.openfeign.BladeHystrixTargeter; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.Targeter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; + +import java.util.ArrayList; + + +/** + * blade feign 增强配置 + * + * @author L.cm + */ +@Configuration +@ConditionalOnClass(Feign.class) +@Import(BladeFeignClientsRegistrar.class) +@AutoConfigureAfter(EnableFeignClients.class) +public class BladeFeignAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public Targeter bladeFeignTargeter() { + return new BladeHystrixTargeter(); + } + + @Configuration("hystrixFeignConfiguration") + @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) + protected static class HystrixFeignConfiguration { + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + @ConditionalOnProperty("feign.hystrix.enabled") + public Feign.Builder feignHystrixBuilder( + RequestInterceptor requestInterceptor, Contract feignContract) { + return HystrixFeign.builder() + .contract(feignContract) + .decode404() + .requestInterceptor(requestInterceptor); + } + + @Bean + @ConditionalOnMissingBean + public RequestInterceptor requestInterceptor() { + return new BladeFeignRequestHeaderInterceptor(); + } + } + + /** + * blade enum 《-》 String 转换配置 + * @param conversionService ConversionService + * @return SpringMvcContract + */ + @Bean + public Contract feignContract(@Qualifier("mvcConversionService") ConversionService conversionService) { + ConverterRegistry converterRegistry = ((ConverterRegistry) conversionService); + converterRegistry.addConverter(new EnumToStringConverter()); + converterRegistry.addConverter(new StringToEnumConverter()); + return new BladeSpringMvcContract(new ArrayList<>(), conversionService); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java new file mode 100644 index 0000000..17c51c3 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.feign; + +import com.fasterxml.jackson.databind.JsonNode; +import feign.FeignException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * blade fallBack 代理处理 + * + * @author L.cm + */ +@Slf4j +@AllArgsConstructor +public class BladeFeignFallback implements MethodInterceptor { + private final Class targetType; + private final String targetName; + private final Throwable cause; + private final String code = "code"; + + @Nullable + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + String errorMessage = cause.getMessage(); + log.error("BladeFeignFallback:[{}.{}] serviceId:[{}] message:[{}]", targetType.getName(), method.getName(), targetName, errorMessage); + Class returnType = method.getReturnType(); + // 暂时不支持 flux,rx,异步等,返回值不是 R,直接返回 null。 + if (R.class != returnType) { + return null; + } + // 非 FeignException + if (!(cause instanceof FeignException)) { + return R.fail(ResultCode.INTERNAL_SERVER_ERROR, errorMessage); + } + FeignException exception = (FeignException) cause; + byte[] content = exception.content(); + // 如果返回的数据为空 + if (ObjectUtil.isEmpty(content)) { + return R.fail(ResultCode.INTERNAL_SERVER_ERROR, errorMessage); + } + // 转换成 jsonNode 读取,因为直接转换,可能 对方放回的并 不是 R 的格式。 + JsonNode resultNode = JsonUtil.readTree(content); + // 判断是否 R 格式 返回体 + if (resultNode.has(code)) { + return JsonUtil.getInstance().convertValue(resultNode, R.class); + } + return R.fail(resultNode.toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BladeFeignFallback that = (BladeFeignFallback) o; + return targetType.equals(that.targetType); + } + + @Override + public int hashCode() { + return Objects.hash(targetType); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestHeaderInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestHeaderInterceptor.java index b393f87..1960d0d 100644 --- a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestHeaderInterceptor.java +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestHeaderInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). *

* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; * you may not use this file except in compliance with the License. @@ -17,36 +17,29 @@ package org.springblade.core.cloud.feign; import feign.RequestInterceptor; import feign.RequestTemplate; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import javax.servlet.http.HttpServletRequest; -import java.util.Enumeration; +import org.springblade.core.cloud.hystrix.BladeHttpHeadersContextHolder; +import org.springframework.http.HttpHeaders; /** * feign 传递Request header * - * @author Chill + *

+ * https://blog.csdn.net/u014519194/article/details/77160958 + * http://tietang.wang/2016/02/25/hystrix/Hystrix%E5%8F%82%E6%95%B0%E8%AF%A6%E8%A7%A3/ + * https://github.com/Netflix/Hystrix/issues/92#issuecomment-260548068 + *

+ * + * @author L.cm */ -@Slf4j public class BladeFeignRequestHeaderInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { - ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attrs != null) { - HttpServletRequest request = attrs.getRequest(); - Enumeration headerNames = request.getHeaderNames(); - if (headerNames != null) { - while (headerNames.hasMoreElements()) { - String name = headerNames.nextElement(); - String value = request.getHeader(name); - if ("blade-auth".equals(name.toLowerCase())) { - requestTemplate.header(name, value); - } - } - } + HttpHeaders headers = BladeHttpHeadersContextHolder.get(); + if (headers != null && !headers.isEmpty()) { + headers.forEach((key, values) -> { + values.forEach(value -> requestTemplate.header(key, value)); + }); } } diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeSpringMvcContract.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeSpringMvcContract.java new file mode 100644 index 0000000..10cf4b7 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeSpringMvcContract.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.feign; + +import feign.MethodMetadata; +import org.springblade.core.cloud.annotation.ApiVersion; +import org.springblade.core.cloud.annotation.UrlVersion; +import org.springblade.core.cloud.version.BladeMediaType; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; +import org.springframework.cloud.openfeign.support.SpringMvcContract; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; + +/** + * 支持 blade-boot 的 版本 处理 + * + * @see org.springblade.core.cloud.annotation.UrlVersion + * @see org.springblade.core.cloud.annotation.ApiVersion + * @author L.cm + */ +public class BladeSpringMvcContract extends SpringMvcContract { + + public BladeSpringMvcContract(List annotatedParameterProcessors, ConversionService conversionService) { + super(annotatedParameterProcessors, conversionService); + } + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + if (RequestMapping.class.isInstance(methodAnnotation) || methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) { + Class targetType = method.getDeclaringClass(); + // url 上的版本,优先获取方法上的版本 + UrlVersion urlVersion = AnnotatedElementUtils.findMergedAnnotation(method, UrlVersion.class); + // 再次尝试类上的版本 + if (urlVersion == null || StringUtil.isBlank(urlVersion.value())) { + urlVersion = AnnotatedElementUtils.findMergedAnnotation(targetType, UrlVersion.class); + } + if (urlVersion != null && StringUtil.isNotBlank(urlVersion.value())) { + String versionUrl = "/" + urlVersion.value(); + data.template().uri(versionUrl); + } + + // 注意:在父类之前 添加 url版本,在父类之后,处理 Media Types 版本 + super.processAnnotationOnMethod(data, methodAnnotation, method); + + // 处理 Media Types 版本信息 + ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class); + // 再次尝试类上的版本 + if (apiVersion == null || StringUtil.isBlank(apiVersion.value())) { + apiVersion = AnnotatedElementUtils.findMergedAnnotation(targetType, ApiVersion.class); + } + if (apiVersion != null && StringUtil.isNotBlank(apiVersion.value())) { + BladeMediaType BladeMediaType = new BladeMediaType(apiVersion.value()); + data.template().header(HttpHeaders.ACCEPT, BladeMediaType.toString()); + } + } + } + + /** + * 参考:https://gist.github.com/rmfish/0ed59a9af6c05157be2a60c9acea2a10 + * @param annotations 注解 + * @param paramIndex 参数索引 + * @return 是否 http 注解 + */ + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + boolean httpAnnotation = super.processAnnotationsOnParameter(data, annotations, paramIndex); + // 在 springMvc 中如果是 Get 请求且参数中是对象 没有声明为@RequestBody 则默认为 Param + if (!httpAnnotation && StringPool.GET.equals(data.template().method().toUpperCase())) { + for (Annotation parameterAnnotation : annotations) { + if (!(parameterAnnotation instanceof RequestBody)) { + return false; + } + } + data.queryMapIndex(paramIndex); + return true; + } + return httpAnnotation; + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java new file mode 100644 index 0000000..c29b5b4 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.cloud.feign; + + +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.cloud.openfeign.EnableFeignClients; + +import java.lang.annotation.*; + +/** + * 开启Feign注解 + * + * @author Chill + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@EnableFeignClients(AppConstant.BASE_PACKAGES) +public @interface EnableBladeFeign { + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + * + * @return the array of 'basePackages'. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * + * @return the array of 'basePackages'. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * + * @return the array of 'basePackageClasses'. + */ + Class[] basePackageClasses() default {}; + + /** + * A custom @Configuration for all feign clients. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + */ + Class[] defaultConfiguration() default {}; + + /** + * List of classes annotated with @FeignClient. If not empty, disables classpath scanning. + * + * @return + */ + Class[] clients() default {}; +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/FeignHystrixConcurrencyStrategy.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/FeignHystrixConcurrencyStrategy.java deleted file mode 100644 index 32d883e..0000000 --- a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/FeignHystrixConcurrencyStrategy.java +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). - *

- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springblade.core.cloud.feign; - -import com.netflix.hystrix.HystrixThreadPoolKey; -import com.netflix.hystrix.HystrixThreadPoolProperties; -import com.netflix.hystrix.strategy.HystrixPlugins; -import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; -import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; -import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; -import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; -import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; -import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; -import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; -import com.netflix.hystrix.strategy.properties.HystrixProperty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * 自定义Feign的隔离策略 - * - * @author Chill - */ -public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { - - private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategy.class); - private HystrixConcurrencyStrategy delegate; - - public FeignHystrixConcurrencyStrategy() { - try { - this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); - if (this.delegate instanceof FeignHystrixConcurrencyStrategy) { - // Welcome to singleton hell... - return; - } - HystrixCommandExecutionHook commandExecutionHook = - HystrixPlugins.getInstance().getCommandExecutionHook(); - HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); - HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); - HystrixPropertiesStrategy propertiesStrategy = - HystrixPlugins.getInstance().getPropertiesStrategy(); - this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy); - HystrixPlugins.reset(); - HystrixPlugins.getInstance().registerConcurrencyStrategy(this); - HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); - HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); - HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); - HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); - } catch (Exception e) { - log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e); - } - } - - private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier, - HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) { - if (log.isDebugEnabled()) { - log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy [" - + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher [" - + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]"); - log.debug("Registering Sleuth Hystrix Concurrency Strategy."); - } - } - - @Override - public Callable wrapCallable(Callable callable) { - RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - return new WrappedCallable<>(callable, requestAttributes); - } - - @Override - public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, - HystrixProperty corePoolSize, HystrixProperty maximumPoolSize, - HystrixProperty keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { - return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, - unit, workQueue); - } - - @Override - public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, - HystrixThreadPoolProperties threadPoolProperties) { - return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties); - } - - @Override - public BlockingQueue getBlockingQueue(int maxQueueSize) { - return this.delegate.getBlockingQueue(maxQueueSize); - } - - @Override - public HystrixRequestVariable getRequestVariable(HystrixRequestVariableLifecycle rv) { - return this.delegate.getRequestVariable(rv); - } - - static class WrappedCallable implements Callable { - private final Callable target; - private final RequestAttributes requestAttributes; - - public WrappedCallable(Callable target, RequestAttributes requestAttributes) { - this.target = target; - this.requestAttributes = requestAttributes; - } - - @Override - public T call() throws Exception { - try { - RequestContextHolder.setRequestAttributes(requestAttributes); - return target.call(); - } finally { - RequestContextHolder.resetRequestAttributes(); - } - } - } -} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/HttpLoggingInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/HttpLoggingInterceptor.java new file mode 100644 index 0000000..712c887 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/HttpLoggingInterceptor.java @@ -0,0 +1,330 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http; + +import okhttp3.*; +import okhttp3.internal.http.HttpHeaders; +import okhttp3.internal.platform.Platform; +import okio.Buffer; +import okio.BufferedSource; +import okio.GzipSource; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static okhttp3.internal.platform.Platform.INFO; + +/** + * An OkHttp interceptor which logs request and response information. Can be applied as an + * {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain + * OkHttpClient#networkInterceptors() network interceptor}.

The format of the logs created by + * this class should not be considered stable and may change slightly between releases. If you need + * a stable logging format, use your own interceptor. + * + * @author L.cm + */ +public final class HttpLoggingInterceptor implements Interceptor { + private static final Charset UTF8 = StandardCharsets.UTF_8; + + public enum Level { + /** + * No logs. + */ + NONE, + /** + * Logs request and response lines. + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1 (3-byte body)
+		 *
+		 * <-- 200 OK (22ms, 6-byte body)
+		 * }
+ */ + BASIC, + /** + * Logs request and response lines and their respective headers. + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1
+		 * Host: example.com
+		 * Content-Type: plain/text
+		 * Content-Length: 3
+		 * --> END POST
+		 *
+		 * <-- 200 OK (22ms)
+		 * Content-Type: plain/text
+		 * Content-Length: 6
+		 * <-- END HTTP
+		 * }
+ */ + HEADERS, + /** + * Logs request and response lines and their respective headers and bodies (if present). + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1
+		 * Host: example.com
+		 * Content-Type: plain/text
+		 * Content-Length: 3
+		 *
+		 * Hi?
+		 * --> END POST
+		 *
+		 * <-- 200 OK (22ms)
+		 * Content-Type: plain/text
+		 * Content-Length: 6
+		 *
+		 * Hello!
+		 * <-- END HTTP
+		 * }
+ */ + BODY + } + + public interface Logger { + /** + * log + * @param message message + */ + void log(String message); + + /** + * A {@link Logger} defaults output appropriate for the current platform. + */ + Logger DEFAULT = message -> Platform.get().log(INFO, message, null); + } + + public HttpLoggingInterceptor() { + this(Logger.DEFAULT); + } + + public HttpLoggingInterceptor(Logger logger) { + this.logger = logger; + } + + private final Logger logger; + + private volatile Level level = Level.NONE; + + /** + * Change the level at which this interceptor logs. + * @param level log Level + * @return HttpLoggingInterceptor + */ + public HttpLoggingInterceptor setLevel(Level level) { + Objects.requireNonNull(level, "level == null. Use Level.NONE instead."); + this.level = level; + return this; + } + + public Level getLevel() { + return level; + } + + private String gzip = "gzip"; + private String contentEncoding = "Content-Encoding"; + + @Override + public Response intercept(Chain chain) throws IOException { + Level level = this.level; + + Request request = chain.request(); + if (level == Level.NONE) { + return chain.proceed(request); + } + + boolean logBody = level == Level.BODY; + boolean logHeaders = logBody || level == Level.HEADERS; + + RequestBody requestBody = request.body(); + boolean hasRequestBody = requestBody != null; + + Connection connection = chain.connection(); + String requestStartMessage = "--> " + + request.method() + + ' ' + request.url() + + (connection != null ? " " + connection.protocol() : ""); + if (!logHeaders && hasRequestBody) { + requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; + } + logger.log(requestStartMessage); + + if (logHeaders) { + if (hasRequestBody) { + // Request body headers are only present when installed as a network interceptor. Force + // them to be included (when available) so there values are known. + if (requestBody.contentType() != null) { + logger.log("Content-Type: " + requestBody.contentType()); + } + if (requestBody.contentLength() != -1) { + logger.log("Content-Length: " + requestBody.contentLength()); + } + } + + Headers headers = request.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + String name = headers.name(i); + // Skip headers from the request body as they are explicitly logged above. + if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) { + logger.log(name + ": " + headers.value(i)); + } + } + + if (!logBody || !hasRequestBody) { + logger.log("--> END " + request.method()); + } else if (bodyHasUnknownEncoding(request.headers())) { + logger.log("--> END " + request.method() + " (encoded body omitted)"); + } else { + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + + Charset charset = UTF8; + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + logger.log(""); + if (isPlaintext(buffer)) { + logger.log(buffer.readString(charset)); + logger.log("--> END " + request.method() + + " (" + requestBody.contentLength() + "-byte body)"); + } else { + logger.log("--> END " + request.method() + " (binary " + + requestBody.contentLength() + "-byte body omitted)"); + } + } + } + + long startNs = System.nanoTime(); + Response response; + try { + response = chain.proceed(request); + } catch (Exception e) { + logger.log("<-- HTTP FAILED: " + e); + throw e; + } + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; + logger.log("<-- " + + response.code() + + (response.message().isEmpty() ? "" : ' ' + response.message()) + + ' ' + response.request().url() + + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')'); + + if (logHeaders) { + Headers headers = response.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + logger.log(headers.name(i) + ": " + headers.value(i)); + } + + if (!logBody || !HttpHeaders.hasBody(response)) { + logger.log("<-- END HTTP"); + } else if (bodyHasUnknownEncoding(response.headers())) { + logger.log("<-- END HTTP (encoded body omitted)"); + } else { + BufferedSource source = responseBody.source(); + // Buffer the entire body. + source.request(Long.MAX_VALUE); + Buffer buffer = source.buffer(); + + Long gzippedLength = null; + if (gzip.equalsIgnoreCase(headers.get(contentEncoding))) { + gzippedLength = buffer.size(); + GzipSource gzippedResponseBody = null; + try { + gzippedResponseBody = new GzipSource(buffer.clone()); + buffer = new Buffer(); + buffer.writeAll(gzippedResponseBody); + } finally { + if (gzippedResponseBody != null) { + gzippedResponseBody.close(); + } + } + } + + Charset charset = UTF8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + if (!isPlaintext(buffer)) { + logger.log(""); + logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); + return response; + } + + if (contentLength != 0) { + logger.log(""); + logger.log(buffer.clone().readString(charset)); + } + + if (gzippedLength != null) { + logger.log("<-- END HTTP (" + buffer.size() + "-byte, " + + gzippedLength + "-gzipped-byte body)"); + } else { + logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); + } + } + } + + return response; + } + + /** + * Returns true if the body in question probably contains human readable text. Uses a small sample + * of code points to detect unicode control characters commonly used in binary file signatures. + */ + private static int plainCnt = 16; + private static boolean isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < plainCnt; i++) { + if (prefix.exhausted()) { + break; + } + int codePoint = prefix.readUtf8CodePoint(); + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false; + } + } + return true; + } catch (EOFException e) { + // Truncated UTF-8 sequence. + return false; + } + } + + private boolean bodyHasUnknownEncoding(Headers headers) { + String contentEncoding = headers.get("Content-Encoding"); + return contentEncoding != null + && !"identity".equalsIgnoreCase(contentEncoding) + && !"gzip".equalsIgnoreCase(contentEncoding); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java new file mode 100644 index 0000000..b4ac109 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java @@ -0,0 +1,27 @@ +package org.springblade.core.cloud.http; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + * Loadbalancer RestTemplate + * + * @author L.cm + */ +public class LbRestTemplate extends RestTemplate { + + public LbRestTemplate() { + super(); + } + + public LbRestTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + } + + public LbRestTemplate(List> messageConverters) { + super(messageConverters); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/OkHttpSlf4jLogger.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/OkHttpSlf4jLogger.java new file mode 100644 index 0000000..ccc33f9 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/OkHttpSlf4jLogger.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http; + +import lombok.extern.slf4j.Slf4j; + +/** + * OkHttp Slf4j logger + * + * @author L.cm + */ +@Slf4j +public class OkHttpSlf4jLogger implements HttpLoggingInterceptor.Logger { + @Override + public void log(String message) { + log.info(message); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java new file mode 100644 index 0000000..cc4f8e0 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.springblade.core.cloud.hystrix.BladeHystrixAccountGetter; +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springblade.core.tool.utils.Charsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; +import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Http RestTemplateHeaderInterceptor 配置 + * + * @author L.cm + */ +@Configuration +@ConditionalOnClass(okhttp3.OkHttpClient.class) +@AllArgsConstructor +public class RestTemplateConfiguration { + private final ObjectMapper objectMapper; + + /** + * dev, test 环境打印出BODY + * @return HttpLoggingInterceptor + */ + @Bean("httpLoggingInterceptor") + @Profile({"dev", "test"}) + public HttpLoggingInterceptor testLoggingInterceptor() { + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new OkHttpSlf4jLogger()); + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + return interceptor; + } + + /** + * ontest 环境 打印 请求头 + * @return HttpLoggingInterceptor + */ + @Bean("httpLoggingInterceptor") + @Profile("ontest") + public HttpLoggingInterceptor onTestLoggingInterceptor() { + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new OkHttpSlf4jLogger()); + interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); + return interceptor; + } + + /** + * prod 环境只打印请求url + * @return HttpLoggingInterceptor + */ + @Bean("httpLoggingInterceptor") + @Profile("prod") + public HttpLoggingInterceptor prodLoggingInterceptor() { + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new OkHttpSlf4jLogger()); + interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); + return interceptor; + } + + /** + * okhttp3 链接池配置 + * @param connectionPoolFactory 链接池配置 + * @param httpClientProperties httpClient配置 + * @return okhttp3.ConnectionPool + */ + @Bean + @ConditionalOnMissingBean(okhttp3.ConnectionPool.class) + public okhttp3.ConnectionPool httpClientConnectionPool( + FeignHttpClientProperties httpClientProperties, + OkHttpClientConnectionPoolFactory connectionPoolFactory) { + Integer maxTotalConnections = httpClientProperties.getMaxConnections(); + Long timeToLive = httpClientProperties.getTimeToLive(); + TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); + return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); + } + + /** + * 配置OkHttpClient + * @param httpClientFactory httpClient 工厂 + * @param connectionPool 链接池配置 + * @param httpClientProperties httpClient配置 + * @param interceptor 拦截器 + * @return OkHttpClient + */ + @Bean + @ConditionalOnMissingBean(okhttp3.OkHttpClient.class) + public okhttp3.OkHttpClient httpClient( + OkHttpClientFactory httpClientFactory, + okhttp3.ConnectionPool connectionPool, + FeignHttpClientProperties httpClientProperties, + HttpLoggingInterceptor interceptor) { + Boolean followRedirects = httpClientProperties.isFollowRedirects(); + Integer connectTimeout = httpClientProperties.getConnectionTimeout(); + return httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()) + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(followRedirects) + .connectionPool(connectionPool) + .addInterceptor(interceptor) + .build(); + } + + @Bean + public RestTemplateHeaderInterceptor requestHeaderInterceptor( + @Autowired(required = false) @Nullable BladeHystrixAccountGetter accountGetter, + BladeHystrixHeadersProperties properties) { + return new RestTemplateHeaderInterceptor(accountGetter,properties); + } + + /** + * 普通的 RestTemplate,不透传请求头,一般只做外部 http 调用 + * @param httpClient OkHttpClient + * @return RestTemplate + */ + @Bean + @ConditionalOnMissingBean(RestTemplate.class) + public RestTemplate restTemplate(okhttp3.OkHttpClient httpClient) { + RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory(httpClient)); + configMessageConverters(restTemplate.getMessageConverters()); + return restTemplate; + } + + /** + * 支持负载均衡的 LbRestTemplate + * @param httpClient OkHttpClient + * @param interceptor RestTemplateHeaderInterceptor + * @return LbRestTemplate + */ + @Bean + @LoadBalanced + @ConditionalOnMissingBean(LbRestTemplate.class) + public LbRestTemplate lbRestTemplate(okhttp3.OkHttpClient httpClient, RestTemplateHeaderInterceptor interceptor) { + LbRestTemplate lbRestTemplate = new LbRestTemplate(new OkHttp3ClientHttpRequestFactory(httpClient)); + lbRestTemplate.setInterceptors(Collections.singletonList(interceptor)); + configMessageConverters(lbRestTemplate.getMessageConverters()); + return lbRestTemplate; + } + + private void configMessageConverters(List> converters) { + converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof MappingJackson2HttpMessageConverter); + converters.add(new StringHttpMessageConverter(Charsets.UTF_8)); + converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java new file mode 100644 index 0000000..2950676 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http; + +import lombok.AllArgsConstructor; +import org.springblade.core.cloud.hystrix.BladeHttpHeadersContextHolder; +import org.springblade.core.cloud.hystrix.BladeHystrixAccountGetter; +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; + +import java.io.IOException; + +/** + * RestTemplateHeaderInterceptor 传递Request header + * + * @author L.cm + */ +@AllArgsConstructor +public class RestTemplateHeaderInterceptor implements ClientHttpRequestInterceptor { + @Nullable + private final BladeHystrixAccountGetter accountGetter; + private final BladeHystrixHeadersProperties properties; + + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] bytes, + ClientHttpRequestExecution execution) throws IOException { + HttpHeaders headers = BladeHttpHeadersContextHolder.get(); + // 考虑2中情况 1. RestTemplate 不是用 hystrix 2. 使用 hystrix + if (headers == null) { + headers = BladeHttpHeadersContextHolder.toHeaders(accountGetter, properties); + } + if (headers != null && !headers.isEmpty()) { + HttpHeaders httpHeaders = request.getHeaders(); + headers.forEach((key, values) -> { + values.forEach(value -> httpHeaders.add(key, value)); + }); + } + return execution.execute(request, bytes); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeAccountGetter.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeAccountGetter.java new file mode 100644 index 0000000..43c967c --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeAccountGetter.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.UrlUtil; + +import javax.servlet.http.HttpServletRequest; + +/** + * 用户信息获取器 + * + * @author Chill + */ +public class BladeAccountGetter implements BladeHystrixAccountGetter { + + @Override + public String get(HttpServletRequest request) { + BladeUser account = SecureUtil.getUser(); + if (account == null) { + return null; + } + // 增加用户头, 123[admin] + String xAccount = String.format("%s[%s]", account.getUserId(), account.getUserName()); + return UrlUtil.encodeURL(xAccount, Charsets.UTF_8); + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersCallable.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersCallable.java new file mode 100644 index 0000000..2bd1d02 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersCallable.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import java.util.concurrent.Callable; + +/** + * HttpHeaders hystrix Callable + * + * @param 泛型标记 + * @author L.cm + */ +public class BladeHttpHeadersCallable implements Callable { + private final Callable delegate; + @Nullable + private HttpHeaders httpHeaders; + + public BladeHttpHeadersCallable(Callable delegate, + @Nullable BladeHystrixAccountGetter accountGetter, + BladeHystrixHeadersProperties properties) { + this.delegate = delegate; + this.httpHeaders = BladeHttpHeadersContextHolder.toHeaders(accountGetter, properties); + } + + @Override + public V call() throws Exception { + if (httpHeaders == null) { + return delegate.call(); + } + try { + BladeHttpHeadersContextHolder.set(httpHeaders); + return delegate.call(); + } finally { + BladeHttpHeadersContextHolder.remove(); + httpHeaders.clear(); + httpHeaders = null; + } + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersContextHolder.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersContextHolder.java new file mode 100644 index 0000000..0712098 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersContextHolder.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.core.NamedThreadLocal; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.PatternMatchUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; + +/** + * HttpHeadersContext + * + * @author L.cm + */ +public class BladeHttpHeadersContextHolder { + private static final ThreadLocal HTTP_HEADERS_HOLDER = new NamedThreadLocal<>("Blade hystrix HttpHeaders"); + + /** + * 请求和转发的ip + */ + private static final String[] ALLOW_HEADS = new String[]{ + "X-Real-IP", "x-forwarded-for", "authorization", "blade-auth", "Authorization", "Blade-Auth" + }; + + static void set(HttpHeaders httpHeaders) { + HTTP_HEADERS_HOLDER.set(httpHeaders); + } + + @Nullable + public static HttpHeaders get() { + return HTTP_HEADERS_HOLDER.get(); + } + + static void remove() { + HTTP_HEADERS_HOLDER.remove(); + } + + @Nullable + public static HttpHeaders toHeaders( + @Nullable BladeHystrixAccountGetter accountGetter, + BladeHystrixHeadersProperties properties) { + HttpServletRequest request = WebUtil.getRequest(); + if (request == null) { + return null; + } + HttpHeaders headers = new HttpHeaders(); + String accountHeaderName = properties.getAccount(); + // 如果配置有 account 读取器 + if (accountGetter != null) { + String xAccountHeader = accountGetter.get(request); + if (StringUtil.isNotBlank(xAccountHeader)) { + headers.add(accountHeaderName, xAccountHeader); + } + } + List allowHeadsList = new ArrayList<>(Arrays.asList(ALLOW_HEADS)); + // 如果有传递 account header 继续往下层传递 + allowHeadsList.add(accountHeaderName); + // 传递请求头 + Enumeration headerNames = request.getHeaderNames(); + if (headerNames != null) { + List allowed = properties.getAllowed(); + String pattern = properties.getPattern(); + while (headerNames.hasMoreElements()) { + String key = headerNames.nextElement(); + // 只支持配置的 header + if (allowHeadsList.contains(key) || allowed.contains(key) || PatternMatchUtils.simpleMatch(pattern, key)) { + String values = request.getHeader(key); + // header value 不为空的 传递 + if (StringUtil.isNotBlank(values)) { + headers.add(key, values); + } + } + + } + } + return headers.isEmpty() ? null : headers; + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAccountGetter.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAccountGetter.java new file mode 100644 index 0000000..57952de --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAccountGetter.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + + +import org.springframework.lang.Nullable; + +import javax.servlet.http.HttpServletRequest; + +/** + * Blade 用户信息获取器,用于请求头传递 + * + * @author L.cm + */ +public interface BladeHystrixAccountGetter { + + /** + * 账号信息获取器 + * + * @param request HttpServletRequest + * @return account 信息 + */ + @Nullable + String get(HttpServletRequest request); +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAutoConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAutoConfiguration.java new file mode 100644 index 0000000..9462c8a --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAutoConfiguration.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + +import com.netflix.hystrix.Hystrix; +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; +import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +import javax.annotation.PostConstruct; + +/** + * Hystrix 配置 + * + * @author L.cm + */ +@Configuration +@ConditionalOnClass(Hystrix.class) +@EnableConfigurationProperties(BladeHystrixHeadersProperties.class) +public class BladeHystrixAutoConfiguration { + @Nullable + @Autowired(required = false) + private HystrixConcurrencyStrategy existingConcurrencyStrategy; + @Nullable + @Autowired(required = false) + private BladeHystrixAccountGetter accountGetter; + @Autowired + private BladeHystrixHeadersProperties properties; + + @PostConstruct + public void init() { + // Keeps references of existing Hystrix plugins. + HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy(); + HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook(); + + HystrixPlugins.reset(); + + // Registers existing plugins excepts the Concurrent Strategy plugin. + HystrixConcurrencyStrategy strategy = new BladeHystrixConcurrencyStrategy(existingConcurrencyStrategy, accountGetter, properties); + HystrixPlugins.getInstance().registerConcurrencyStrategy(strategy); + HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); + HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); + HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); + HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixConcurrencyStrategy.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixConcurrencyStrategy.java new file mode 100644 index 0000000..76c1fa5 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixConcurrencyStrategy.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.hystrix; + +import com.netflix.hystrix.HystrixThreadPoolKey; +import com.netflix.hystrix.HystrixThreadPoolProperties; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; +import com.netflix.hystrix.strategy.properties.HystrixProperty; +import lombok.AllArgsConstructor; +import org.springblade.core.cloud.props.BladeHystrixHeadersProperties; +import org.springframework.lang.Nullable; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Hystrix传递ThreaLocal中的一些变量 + * + *

+ * https://github.com/Netflix/Hystrix/issues/92#issuecomment-260548068 + * https://github.com/spring-cloud/spring-cloud-sleuth/issues/39 + * https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/security + * https://github.com/spring-projects/spring-security/blob/master/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextCallable.java + *

+ * + * @author L.cm + */ +@AllArgsConstructor +public class BladeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { + @Nullable + private final HystrixConcurrencyStrategy existingConcurrencyStrategy; + @Nullable + private final BladeHystrixAccountGetter accountGetter; + private final BladeHystrixHeadersProperties properties; + + @Override + public BlockingQueue getBlockingQueue(int maxQueueSize) { + return existingConcurrencyStrategy != null + ? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize) + : super.getBlockingQueue(maxQueueSize); + } + + @Override + public HystrixRequestVariable getRequestVariable( + HystrixRequestVariableLifecycle rv) { + return existingConcurrencyStrategy != null + ? existingConcurrencyStrategy.getRequestVariable(rv) + : super.getRequestVariable(rv); + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, + HystrixProperty corePoolSize, + HystrixProperty maximumPoolSize, + HystrixProperty keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) { + return existingConcurrencyStrategy != null + ? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue) + : super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) { + return existingConcurrencyStrategy != null + ? existingConcurrencyStrategy.getThreadPool(threadPoolKey, threadPoolProperties) + : super.getThreadPool(threadPoolKey, threadPoolProperties); + } + + @Override + public Callable wrapCallable(Callable callable) { + Callable wrapCallable = new BladeHttpHeadersCallable<>(callable, accountGetter, properties); + return existingConcurrencyStrategy != null + ? existingConcurrencyStrategy.wrapCallable(wrapCallable) + : super.wrapCallable(wrapCallable); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/props/BladeHystrixHeadersProperties.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/props/BladeHystrixHeadersProperties.java new file mode 100644 index 0000000..a9364fd --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/props/BladeHystrixHeadersProperties.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.props; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.lang.Nullable; + +import java.util.Arrays; +import java.util.List; + +/** + * Hystrix Headers 配置 + * + * @author L.cm + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties("blade.hystrix.headers") +public class BladeHystrixHeadersProperties { + + /** + * 用于 聚合层 向调用层传递用户信息 的请求头,默认:x-blade-account + */ + private String account = "X-Blade-Account"; + + /** + * RestTemplate 和 Fegin 透传到下层的 Headers 名称表达式 + */ + @Nullable + private String pattern = "Blade*"; + + /** + * RestTemplate 和 Fegin 透传到下层的 Headers 名称列表 + */ + private List allowed = Arrays.asList("X-Real-IP", "x-forwarded-for", "authorization", "blade-auth", "Authorization", "Blade-Auth"); + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/stream/ServiceErrorStreams.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/stream/ServiceErrorStreams.java new file mode 100644 index 0000000..5398109 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/stream/ServiceErrorStreams.java @@ -0,0 +1,32 @@ +package org.springblade.core.cloud.stream; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.SubscribableChannel; + +/** + * 服务异常 Streams + * + * @author L.cm + */ +public interface ServiceErrorStreams { + String INPUT = "service-error-in"; + String OUTPUT = "service-error-out"; + + /** + * input + * + * @return SubscribableChannel + */ + @Input(INPUT) + SubscribableChannel subscribablebChannel(); + + /** + * output + * + * @return MessageChannel + */ + @Output(OUTPUT) + MessageChannel messageChannel(); +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java new file mode 100644 index 0000000..2b94032 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.cloud.version; + +import lombok.Getter; +import org.springframework.http.MediaType; + +/** + * blade Media Types,application/vnd.github.VERSION+json + * + *

+ * https://developer.github.com/v3/media/ + *

+ * + * @author L.cm + */ +@Getter +public class BladeMediaType { + private static final String MEDIA_TYPE_TEMP = "application/vnd.%s.%s+json"; + + private final String appName = "blade"; + private final String version; + private final MediaType mediaType; + + public BladeMediaType(String version) { + this.version = version; + this.mediaType = MediaType.valueOf(String.format(MEDIA_TYPE_TEMP, appName, version)); + } + + @Override + public String toString() { + return mediaType.toString(); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java new file mode 100644 index 0000000..49d9580 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.version; + +import org.springblade.core.cloud.annotation.ApiVersion; +import org.springblade.core.cloud.annotation.UrlVersion; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * url版本号处理 和 header 版本处理 + * + *

+ * url: /v1/user/{id} + * header: Accept application/vnd.blade.VERSION+json + *

+ * + * 注意:c 代表客户端版本 + * + * @author L.cm + */ +public class BladeRequestMappingHandlerMapping extends RequestMappingHandlerMapping { + + @Nullable + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + RequestMappingInfo mappinginfo = super.getMappingForMethod(method, handlerType); + if (mappinginfo != null) { + RequestMappingInfo apiVersionMappingInfo = getApiVersionMappingInfo(method, handlerType); + return apiVersionMappingInfo == null ? mappinginfo : apiVersionMappingInfo.combine(mappinginfo); + } + return null; + } + + @Nullable + private RequestMappingInfo getApiVersionMappingInfo(Method method, Class handlerType) { + // url 上的版本,优先获取方法上的版本 + UrlVersion urlVersion = AnnotatedElementUtils.findMergedAnnotation(method, UrlVersion.class); + // 再次尝试类上的版本 + if (urlVersion == null || StringUtil.isBlank(urlVersion.value())) { + urlVersion = AnnotatedElementUtils.findMergedAnnotation(handlerType, UrlVersion.class); + } + // Media Types 版本信息 + ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class); + // 再次尝试类上的版本 + if (apiVersion == null || StringUtil.isBlank(apiVersion.value())) { + apiVersion = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion.class); + } + boolean nonUrlVersion = urlVersion == null || StringUtil.isBlank(urlVersion.value()); + boolean nonApiVersion = apiVersion == null || StringUtil.isBlank(apiVersion.value()); + // 先判断同时不纯在 + if (nonUrlVersion && nonApiVersion) { + return null; + } + // 如果 header 版本不存在 + RequestMappingInfo.Builder mappingInfoBuilder = null; + if (nonApiVersion) { + mappingInfoBuilder = RequestMappingInfo.paths(urlVersion.value()); + } else { + mappingInfoBuilder = RequestMappingInfo.paths(StringPool.EMPTY); + } + // 如果url版本不存在 + if (nonUrlVersion) { + String vsersionMediaTypes = new BladeMediaType(apiVersion.value()).toString(); + mappingInfoBuilder.produces(vsersionMediaTypes); + } + return mappingInfoBuilder.build(); + } + + @Override + protected void handlerMethodsInitialized(Map handlerMethods) { + // 打印路由信息 spring boot 2.1 去掉了这个 日志的打印 + if (logger.isInfoEnabled()) { + for (Map.Entry entry : handlerMethods.entrySet()) { + RequestMappingInfo mapping = entry.getKey(); + HandlerMethod handlerMethod = entry.getValue(); + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + } + super.handlerMethodsInitialized(handlerMethods); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java new file mode 100644 index 0000000..5ad6503 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.version; + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * url版本号处理 + * + * @author L.cm + */ +public class BladeWebMvcRegistrations implements WebMvcRegistrations { + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return new BladeRequestMappingHandlerMapping(); + } + + @Override + public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { + return null; + } + + @Override + public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() { + return null; + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java new file mode 100644 index 0000000..2276e23 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.version; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * url版本号处理 + * + * 参考:https://gitee.com/lianqu1990/spring-boot-starter-version-mapping + * + * @author L.cm + */ +@Configuration +@ConditionalOnWebApplication +public class VersionMappingAutoConfiguration { + @Bean + public WebMvcRegistrations bladeWebMvcRegistrations() { + return new BladeWebMvcRegistrations(); + } +} diff --git a/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeFeignClientsRegistrar.java b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeFeignClientsRegistrar.java new file mode 100644 index 0000000..b6c635c --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeFeignClientsRegistrar.java @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.openfeign; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springblade.core.cloud.feign.BladeFeignAutoConfiguration; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * feign 自动配置 + * + * @author L.cm + */ +@NoArgsConstructor +public class BladeFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware { + @Getter + private ClassLoader beanClassLoader; + @Getter + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + registerFeignClients(metadata, registry); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + private void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + List feignClients = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); + // 如果 spring.factories 里为空 + if (feignClients.isEmpty()) { + return; + } + for (String className : feignClients) { + try { + Class clazz = beanClassLoader.loadClass(className); + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, FeignClient.class); + if (attributes == null) { + continue; + } + // 如果已经存在该 bean,支持原生的 Feign + if (registry.containsBeanDefinition(className)) { + continue; + } + registerClientConfiguration(registry, getClientName(attributes), attributes.get("configuration")); + + validate(attributes); + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); + definition.addPropertyValue("url", getUrl(attributes)); + definition.addPropertyValue("path", getPath(attributes)); + String name = getName(attributes); + definition.addPropertyValue("name", name); + + // 兼容最新版本的 spring-cloud-openfeign,尚未发布 + StringBuilder aliasBuilder = new StringBuilder(18); + if (attributes.containsKey("contextId")) { + String contextId = getContextId(attributes); + aliasBuilder.append(contextId); + definition.addPropertyValue("contextId", contextId); + } else { + aliasBuilder.append(name); + } + + definition.addPropertyValue("type", className); + definition.addPropertyValue("decode404", attributes.get("decode404")); + definition.addPropertyValue("fallback", attributes.get("fallback")); + definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + + AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); + + // alias + String alias = aliasBuilder.append("FeignClient").toString(); + + // has a default, won't be null + boolean primary = (Boolean)attributes.get("primary"); + + beanDefinition.setPrimary(primary); + + String qualifier = getQualifier(attributes); + if (StringUtils.hasText(qualifier)) { + alias = qualifier; + } + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + + /** + * Return the class used by {@link SpringFactoriesLoader} to load configuration + * candidates. + * @return the factory class + */ + private Class getSpringFactoriesLoaderFactoryClass() { + return BladeFeignAutoConfiguration.class; + } + + private void validate(Map attributes) { + AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); + // This blows up if an aliased property is overspecified + // FIXME annotation.getAliasedString("name", FeignClient.class, null); + FeignClientsRegistrar.validateFallback(annotation.getClass("fallback")); + FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory")); + } + + private String getName(Map attributes) { + String name = (String) attributes.get("serviceId"); + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("name"); + } + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("value"); + } + name = resolve(name); + return FeignClientsRegistrar.getName(name); + } + + private String getContextId(Map attributes) { + String contextId = (String) attributes.get("contextId"); + if (!StringUtils.hasText(contextId)) { + return getName(attributes); + } + + contextId = resolve(contextId); + return FeignClientsRegistrar.getName(contextId); + } + + private String resolve(String value) { + if (StringUtils.hasText(value)) { + return this.environment.resolvePlaceholders(value); + } + return value; + } + + private String getUrl(Map attributes) { + String url = resolve((String) attributes.get("url")); + return FeignClientsRegistrar.getUrl(url); + } + + private String getPath(Map attributes) { + String path = resolve((String) attributes.get("path")); + return FeignClientsRegistrar.getPath(path); + } + + @Nullable + private String getQualifier(@Nullable Map client) { + if (client == null) { + return null; + } + String qualifier = (String) client.get("qualifier"); + if (StringUtils.hasText(qualifier)) { + return qualifier; + } + return null; + } + + @Nullable + private String getClientName(@Nullable Map client) { + if (client == null) { + return null; + } + String value = (String) client.get("contextId"); + if (!StringUtils.hasText(value)) { + value = (String) client.get("value"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("name"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("serviceId"); + } + if (StringUtils.hasText(value)) { + return value; + } + + throw new IllegalStateException("Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); + } + + private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + +} diff --git a/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeHystrixTargeter.java b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeHystrixTargeter.java new file mode 100644 index 0000000..f875cdd --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeHystrixTargeter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.openfeign; + +import feign.Feign; +import feign.Target; +import feign.hystrix.FallbackFactory; +import feign.hystrix.HystrixFeign; +import feign.hystrix.SetterFactory; +import org.springblade.core.cloud.feign.BladeFallbackFactory; +import org.springframework.lang.Nullable; + +/** + * 添加 blade 默认的 fallbackFactory L.cm 2019.01.19 + * + * @author L.cm + * @author Spencer Gibb + * @author Erik Kringen + */ +@SuppressWarnings("unchecked") +public class BladeHystrixTargeter implements Targeter { + + @Override + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + Target.HardCodedTarget target) { + if (!(feign instanceof HystrixFeign.Builder)) { + return feign.target(target); + } + HystrixFeign.Builder builder = (HystrixFeign.Builder) feign; + SetterFactory setterFactory = getOptional(factory.getName(), context, SetterFactory.class); + if (setterFactory != null) { + builder.setterFactory(setterFactory); + } + Class fallback = factory.getFallback(); + if (fallback != void.class) { + return targetWithFallback(factory.getName(), context, target, builder, fallback); + } + Class fallbackFactory = factory.getFallbackFactory(); + if (fallbackFactory != void.class) { + return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory); + } + // blade 默认的 fallbackFactory + BladeFallbackFactory bladeFallbackFactory = new BladeFallbackFactory(target); + return (T) builder.target(target, bladeFallbackFactory); + } + + private T targetWithFallbackFactory(String feignClientName, FeignContext context, + Target.HardCodedTarget target, + HystrixFeign.Builder builder, + Class fallbackFactoryClass) { + FallbackFactory fallbackFactory = (FallbackFactory) + getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class); + return builder.target(target, fallbackFactory); + } + + + private T targetWithFallback(String feignClientName, FeignContext context, + Target.HardCodedTarget target, + HystrixFeign.Builder builder, Class fallback) { + T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); + return builder.target(target, fallbackInstance); + } + + private T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, Class beanType, + Class targetType) { + Object fallbackInstance = context.getInstance(feignClientName, beanType); + if (fallbackInstance == null) { + throw new IllegalStateException(String.format("No " + fallbackMechanism + + " instance of type %s found for feign client %s", beanType, feignClientName)); + } + + if (!targetType.isAssignableFrom(beanType)) { + throw new IllegalStateException(String.format( + "Incompatible " + fallbackMechanism + " instance. Fallback/fallbackFactory of " + + "type %s is not assignable to %s for feign client %s", beanType, targetType, feignClientName)); + } + return (T) fallbackInstance; + } + + @Nullable + private T getOptional(String feignClientName, FeignContext context, Class beanType) { + return context.getInstance(feignClientName, beanType); + } +} diff --git a/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/Targeter.java b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/Targeter.java new file mode 100644 index 0000000..bb49bd2 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/Targeter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.openfeign; + +import feign.Feign; +import feign.Target; + +/** + * @author Spencer Gibb + */ +public interface Targeter { + /** + * target + * + * @param factory + * @param feign + * @param context + * @param target + * @param + * @return T + */ + T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + Target.HardCodedTarget target); +} diff --git a/blade-core-develop/pom.xml b/blade-core-develop/pom.xml index cc9d1a9..a7a47e1 100644 --- a/blade-core-develop/pom.xml +++ b/blade-core-develop/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-launch/pom.xml b/blade-core-launch/pom.xml index 152c13f..4e88be7 100644 --- a/blade-core-launch/pom.xml +++ b/blade-core-launch/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java index 0f84897..b7915fe 100644 --- a/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java @@ -100,6 +100,7 @@ public class BladeApplication { props.setProperty("spring.cloud.nacos.config.prefix", NacosConstant.NACOS_CONFIG_PREFIX); props.setProperty("spring.cloud.nacos.config.file-extension", NacosConstant.NACOS_CONFIG_FORMAT); props.setProperty("spring.cloud.sentinel.transport.dashboard", SentinelConstant.SENTINEL_ADDR); + props.setProperty("spring.cloud.alibaba.seata.tx-service-group", appName.concat(NacosConstant.NACOS_GROUP_SUFFIX)); // 加载自定义组件 List launcherList = new ArrayList<>(); ServiceLoader.load(LauncherService.class).forEach(launcherList::add); diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java index 2d33343..e20a276 100644 --- a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java @@ -25,7 +25,7 @@ public interface AppConstant { /** * 应用版本 */ - String APPLICATION_VERSION = "2.4.1"; + String APPLICATION_VERSION = "2.5.0"; /** * 基础包 diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java index d7e2ea6..f7bcbff 100644 --- a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java @@ -32,6 +32,11 @@ public interface NacosConstant { */ String NACOS_CONFIG_PREFIX = "blade"; + /** + * nacos 组配置后缀 + */ + String NACOS_GROUP_SUFFIX = "-group"; + /** * nacos 配置文件类型 */ diff --git a/blade-core-log/pom.xml b/blade-core-log/pom.xml index a31ab5e..91237e9 100644 --- a/blade-core-log/pom.xml +++ b/blade-core-log/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-log/src/main/java/org/springblade/core/log/model/LogAbstract.java b/blade-core-log/src/main/java/org/springblade/core/log/model/LogAbstract.java index 429234a..956d924 100644 --- a/blade-core-log/src/main/java/org/springblade/core/log/model/LogAbstract.java +++ b/blade-core-log/src/main/java/org/springblade/core/log/model/LogAbstract.java @@ -24,7 +24,7 @@ import org.springblade.core.tool.utils.DateUtil; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; -import java.time.LocalDateTime; +import java.util.Date; /** * logApi、logError、logUsual的父类,拥有相同的属性值 @@ -101,6 +101,6 @@ public class LogAbstract implements Serializable { */ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) - protected LocalDateTime createTime; + protected Date createTime; } diff --git a/blade-core-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java b/blade-core-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java index a39269f..176f956 100644 --- a/blade-core-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java +++ b/blade-core-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2029, DreamLu 卢春梦 (596392912@qq.com & www.dreamlu.net). + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). *

* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; * you may not use this file except in compliance with the License. @@ -20,17 +20,17 @@ import org.springblade.core.launch.props.BladeProperties; import org.springblade.core.launch.server.ServerInfo; import org.springblade.core.log.model.LogAbstract; import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.UrlUtil; import org.springblade.core.tool.utils.WebUtil; import javax.servlet.http.HttpServletRequest; -import java.time.LocalDateTime; /** - * INet 相关工具 + * Log 相关工具 * - * @author L.cm + * @author Chill */ public class LogAbstractUtil { @@ -61,7 +61,7 @@ public class LogAbstractUtil { logAbstract.setServerHost(serverInfo.getHostName()); logAbstract.setServerIp(serverInfo.getIpWithPort()); logAbstract.setEnv(bladeProperties.getEnv()); - logAbstract.setCreateTime(LocalDateTime.now()); + logAbstract.setCreateTime(DateUtil.now()); //这里判断一下params为null的情况,否则blade-log服务在解析该字段的时候,可能会报出NPE if (logAbstract.getParams() == null) { diff --git a/blade-core-mybatis/pom.xml b/blade-core-mybatis/pom.xml index 0550881..439025d 100644 --- a/blade-core-mybatis/pom.xml +++ b/blade-core-mybatis/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java index ced5140..ed7c5ac 100644 --- a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java @@ -24,7 +24,7 @@ import org.springblade.core.tool.utils.DateUtil; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; -import java.time.LocalDateTime; +import java.util.Date; /** * 基础实体类 @@ -46,7 +46,7 @@ public class BaseEntity implements Serializable { @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) @ApiModelProperty(value = "创建时间") - private LocalDateTime createTime; + private Date createTime; /** * 更新人 @@ -60,7 +60,7 @@ public class BaseEntity implements Serializable { @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) @ApiModelProperty(value = "更新时间") - private LocalDateTime updateTime; + private Date updateTime; /** * 状态[1:正常] diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java index 69d9728..0b18187 100644 --- a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java @@ -20,12 +20,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springblade.core.secure.BladeUser; import org.springblade.core.secure.utils.SecureUtil; import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.DateUtil; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.NotEmpty; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.time.LocalDateTime; +import java.util.Date; import java.util.List; /** @@ -53,7 +54,7 @@ public class BaseServiceImpl, T extends BaseEntity> exte entity.setCreateUser(user.getUserId()); entity.setUpdateUser(user.getUserId()); } - LocalDateTime now = LocalDateTime.now(); + Date now = DateUtil.now(); entity.setCreateTime(now); entity.setUpdateTime(now); if (entity.getStatus() == null) { @@ -69,7 +70,7 @@ public class BaseServiceImpl, T extends BaseEntity> exte if (user != null) { entity.setUpdateUser(user.getUserId()); } - entity.setUpdateTime(LocalDateTime.now()); + entity.setUpdateTime(DateUtil.now()); return super.updateById(entity); } diff --git a/blade-core-oss/pom.xml b/blade-core-oss/pom.xml index 869d611..7938f8d 100644 --- a/blade-core-oss/pom.xml +++ b/blade-core-oss/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-secure/pom.xml b/blade-core-secure/pom.xml index eb06ca9..bfde604 100644 --- a/blade-core-secure/pom.xml +++ b/blade-core-secure/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-swagger/pom.xml b/blade-core-swagger/pom.xml index 0fd3e60..e25b815 100644 --- a/blade-core-swagger/pom.xml +++ b/blade-core-swagger/pom.xml @@ -5,7 +5,7 @@ blade-tool org.springblade - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java index e7e274a..a6b725d 100644 --- a/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java +++ b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java @@ -55,7 +55,7 @@ public class SwaggerProperties { /** * 版本 **/ - private String version = "2.4.1"; + private String version = "2.5.0"; /** * 许可证 **/ diff --git a/blade-core-test/pom.xml b/blade-core-test/pom.xml index bf77602..ab7c1cf 100644 --- a/blade-core-test/pom.xml +++ b/blade-core-test/pom.xml @@ -5,7 +5,7 @@ org.springblade blade-tool - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-tool/pom.xml b/blade-core-tool/pom.xml index cd7b379..72cdb6c 100644 --- a/blade-core-tool/pom.xml +++ b/blade-core-tool/pom.xml @@ -6,7 +6,7 @@ org.springblade blade-tool - 2.4.1 + 2.5.0 4.0.0 diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java new file mode 100644 index 0000000..f946200 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java @@ -0,0 +1,50 @@ +package org.springblade.core.tool.convert; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * 类型 转换 服务,添加了 IEnum 转换 + * + * @author L.cm + */ +public class BladeConversionService extends ApplicationConversionService { + @Nullable + private static volatile BladeConversionService SHARED_INSTANCE; + + public BladeConversionService() { + this(null); + } + + public BladeConversionService(@Nullable StringValueResolver embeddedValueResolver) { + super(embeddedValueResolver); + super.addConverter(new EnumToStringConverter()); + super.addConverter(new StringToEnumConverter()); + } + + /** + * Return a shared default application {@code ConversionService} instance, lazily + * building it once needed. + *

+ * Note: This method actually returns an {@link BladeConversionService} + * instance. However, the {@code ConversionService} signature has been preserved for + * binary compatibility. + * @return the shared {@code BladeConversionService} instance (never{@code null}) + */ + public static GenericConversionService getInstance() { + BladeConversionService sharedInstance = BladeConversionService.SHARED_INSTANCE; + if (sharedInstance == null) { + synchronized (BladeConversionService.class) { + sharedInstance = BladeConversionService.SHARED_INSTANCE; + if (sharedInstance == null) { + sharedInstance = new BladeConversionService(); + BladeConversionService.SHARED_INSTANCE = sharedInstance; + } + } + } + return sharedInstance; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java new file mode 100644 index 0000000..b0af763 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java @@ -0,0 +1,65 @@ +package org.springblade.core.tool.convert; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.support.Try; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springblade.core.tool.utils.ReflectUtil; +import org.springframework.cglib.core.Converter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Field; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 组合 spring cglib Converter 和 spring ConversionService + * + * @author L.cm + */ +@Slf4j +@AllArgsConstructor +public class BladeConverter implements Converter { + private static final ConcurrentMap TYPE_CACHE = new ConcurrentHashMap<>(); + private final Class targetClazz; + + /** + * cglib convert + * @param value 源对象属性 + * @param target 目标对象属性类 + * @param fieldName 目标的field名,原为 set 方法名,BladeBeanCopier 里做了更改 + * @return {Object} + */ + @Override + @Nullable + public Object convert(Object value, Class target, final Object fieldName) { + if (value == null) { + return null; + } + // 类型一样,不需要转换 + if (ClassUtil.isAssignableValue(target, value)) { + return value; + } + try { + TypeDescriptor targetDescriptor = BladeConverter.getTypeDescriptor(targetClazz, (String) fieldName); + return ConvertUtil.convert(value, targetDescriptor); + } catch (Throwable e) { + log.warn("BladeConverter error", e); + return null; + } + } + + private static TypeDescriptor getTypeDescriptor(final Class clazz, final String fieldName) { + String srcCacheKey = clazz.getName() + fieldName; + return TYPE_CACHE.computeIfAbsent(srcCacheKey, Try.of(k -> { + // 这里 property 理论上不会为 null + Field field = ReflectUtil.getField(clazz, fieldName); + if (field == null) { + throw new NoSuchFieldException(fieldName); + } + return new TypeDescriptor(field); + })); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java new file mode 100644 index 0000000..a7a2b2d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java @@ -0,0 +1,109 @@ +package org.springblade.core.tool.convert; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 接收参数 同 jackson Enum -》 String 转换 + * + * @author L.cm + */ +@Slf4j +public class EnumToStringConverter implements ConditionalGenericConverter { + /** + * 缓存 Enum 类信息,提供性能 + */ + private static final ConcurrentMap, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8); + + @Nullable + private static AccessibleObject getAnnotation(Class clazz) { + Set accessibleObjects = new HashSet<>(); + // JsonValue METHOD, FIELD + Field[] fields = clazz.getDeclaredFields(); + Collections.addAll(accessibleObjects, fields); + // methods + Method[] methods = clazz.getDeclaredMethods(); + Collections.addAll(accessibleObjects, methods); + for (AccessibleObject accessibleObject : accessibleObjects) { + // 复用 jackson 的 JsonValue 注解 + JsonValue jsonValue = accessibleObject.getAnnotation(JsonValue.class); + if (jsonValue != null && jsonValue.value()) { + accessibleObject.setAccessible(true); + return accessibleObject; + } + } + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return true; + } + + @Override + public Set getConvertibleTypes() { + Set pairSet = new HashSet<>(3); + pairSet.add(new ConvertiblePair(Enum.class, String.class)); + pairSet.add(new ConvertiblePair(Enum.class, Integer.class)); + pairSet.add(new ConvertiblePair(Enum.class, Long.class)); + return Collections.unmodifiableSet(pairSet); + } + + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Class sourceClazz = sourceType.getType(); + AccessibleObject accessibleObject = ENUM_CACHE_MAP.computeIfAbsent(sourceClazz, EnumToStringConverter::getAnnotation); + Class targetClazz = targetType.getType(); + // 如果为null,走默认的转换 + if (accessibleObject == null) { + if (String.class == targetClazz) { + return ((Enum) source).name(); + } + int ordinal = ((Enum) source).ordinal(); + return ConvertUtil.convert(ordinal, targetClazz); + } + try { + return EnumToStringConverter.invoke(sourceClazz, accessibleObject, source, targetClazz); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + @Nullable + private static Object invoke(Class clazz, AccessibleObject accessibleObject, Object source, Class targetClazz) + throws IllegalAccessException, InvocationTargetException { + Object value = null; + if (accessibleObject instanceof Field) { + Field field = (Field) accessibleObject; + value = field.get(source); + } else if (accessibleObject instanceof Method) { + Method method = (Method) accessibleObject; + Class paramType = method.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(source, paramType); + value = method.invoke(clazz, object); + } + if (value == null) { + return null; + } + return ConvertUtil.convert(value, targetClazz); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java new file mode 100644 index 0000000..9574adf --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java @@ -0,0 +1,109 @@ +package org.springblade.core.tool.convert; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 接收参数 同 jackson String -》 Enum 转换 + * + * @author L.cm + */ +@Slf4j +public class StringToEnumConverter implements ConditionalGenericConverter { + /** + * 缓存 Enum 类信息,提供性能 + */ + private static final ConcurrentMap, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8); + + @Nullable + private static AccessibleObject getAnnotation(Class clazz) { + Set accessibleObjects = new HashSet<>(); + // JsonCreator METHOD, CONSTRUCTOR + Constructor[] constructors = clazz.getConstructors(); + Collections.addAll(accessibleObjects, constructors); + // methods + Method[] methods = clazz.getDeclaredMethods(); + Collections.addAll(accessibleObjects, methods); + for (AccessibleObject accessibleObject : accessibleObjects) { + // 复用 jackson 的 JsonCreator注解 + JsonCreator jsonCreator = accessibleObject.getAnnotation(JsonCreator.class); + if (jsonCreator != null && JsonCreator.Mode.DISABLED != jsonCreator.mode()) { + accessibleObject.setAccessible(true); + return accessibleObject; + } + } + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return true; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Enum.class)); + } + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (StringUtil.isBlank((String) source)) { + return null; + } + Class clazz = targetType.getType(); + AccessibleObject accessibleObject = ENUM_CACHE_MAP.computeIfAbsent(clazz, StringToEnumConverter::getAnnotation); + String value = ((String) source).trim(); + // 如果为null,走默认的转换 + if (accessibleObject == null) { + return valueOf(clazz, value); + } + try { + return StringToEnumConverter.invoke(clazz, accessibleObject, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings("unchecked") + private static > T valueOf(Class clazz, String value){ + return Enum.valueOf((Class) clazz, value); + } + + @Nullable + private static Object invoke(Class clazz, AccessibleObject accessibleObject, String value) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + if (accessibleObject instanceof Constructor) { + Constructor constructor = (Constructor) accessibleObject; + Class paramType = constructor.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(value, paramType); + return constructor.newInstance(object); + } + if (accessibleObject instanceof Method) { + Method method = (Method) accessibleObject; + Class paramType = method.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(value, paramType); + return method.invoke(clazz, object); + } + return null; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java new file mode 100644 index 0000000..12344e6 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java @@ -0,0 +1,83 @@ +package org.springblade.core.tool.utils; + +import lombok.experimental.UtilityClass; +import org.springblade.core.tool.convert.BladeConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; + +/** + * 基于 spring ConversionService 类型转换 + * + * @author L.cm + */ +@UtilityClass +@SuppressWarnings("unchecked") +public class ConvertUtil { + + /** + * Convenience operation for converting a source object to the specified targetType. + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, Class targetType) { + if (source == null) { + return null; + } + if (ClassUtil.isAssignableValue(targetType, source)) { + return (T) source; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return conversionService.convert(source, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the target type is a descriptor that provides additional conversion context. + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param sourceType the source type + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return (T) conversionService.convert(source, sourceType, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the target type is a descriptor that provides additional conversion context. + * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and + * encapsulates the construction of the source type descriptor using + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor targetType) { + if (source == null) { + return null; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return (T) conversionService.convert(source, targetType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java index 05db0e9..2ae2cea 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java @@ -39,6 +39,15 @@ public class DateUtil { public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATE); public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_TIME); + /** + * 获取当前日期 + * + * @return 当前日期 + */ + public static Date now() { + return new Date(); + } + /** * 添加年 * diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java index 9ade2de..b05140d 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java @@ -1,18 +1,17 @@ -/* - * Copyright (c) 2018-2028, DreamLu All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * Neither the name of the dreamlu.net developer nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * Author: DreamLu 卢春梦 (596392912@qq.com) +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.springblade.core.tool.utils; diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java new file mode 100644 index 0000000..6b54d02 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java @@ -0,0 +1,166 @@ +package org.springblade.core.tool.utils; + +import lombok.experimental.UtilityClass; +import org.springframework.beans.BeansException; +import org.springframework.cglib.core.CodeGenerationException; +import org.springframework.core.convert.Property; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * 反射工具类 + * + * @author L.cm + */ +@UtilityClass +public class ReflectUtil extends ReflectionUtils { + + /** + * 获取 Bean 的所有 get方法 + * + * @param type 类 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getBeanGetters(Class type) { + return getPropertiesHelper(type, true, false); + } + + /** + * 获取 Bean 的所有 set方法 + * + * @param type 类 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getBeanSetters(Class type) { + return getPropertiesHelper(type, false, true); + } + + /** + * 获取 Bean 的所有 PropertyDescriptor + * + * @param type 类 + * @param read 读取方法 + * @param write 写方法 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getPropertiesHelper(Class type, boolean read, boolean write) { + try { + PropertyDescriptor[] all = BeanUtil.getPropertyDescriptors(type); + if (read && write) { + return all; + } else { + List properties = new ArrayList<>(all.length); + for (PropertyDescriptor pd : all) { + if (read && pd.getReadMethod() != null) { + properties.add(pd); + } else if (write && pd.getWriteMethod() != null) { + properties.add(pd); + } + } + return properties.toArray(new PropertyDescriptor[0]); + } + } catch (BeansException ex) { + throw new CodeGenerationException(ex); + } + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyName 属性名 + * @return {Property} + */ + @Nullable + public static Property getProperty(Class propertyType, String propertyName) { + PropertyDescriptor propertyDescriptor = BeanUtil.getPropertyDescriptor(propertyType, propertyName); + if (propertyDescriptor == null) { + return null; + } + return ReflectUtil.getProperty(propertyType, propertyDescriptor, propertyName); + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyDescriptor PropertyDescriptor + * @param propertyName 属性名 + * @return {Property} + */ + public static Property getProperty(Class propertyType, PropertyDescriptor propertyDescriptor, String propertyName) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + return new Property(propertyType, readMethod, writeMethod, propertyName); + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyName 属性名 + * @return {Property} + */ + @Nullable + public static TypeDescriptor getTypeDescriptor(Class propertyType, String propertyName) { + Property property = ReflectUtil.getProperty(propertyType, propertyName); + if (property == null) { + return null; + } + return new TypeDescriptor(property); + } + + /** + * 获取 类属性信息 + * @param propertyType 类型 + * @param propertyDescriptor PropertyDescriptor + * @param propertyName 属性名 + * @return {Property} + */ + public static TypeDescriptor getTypeDescriptor(Class propertyType, PropertyDescriptor propertyDescriptor, String propertyName) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + Property property = new Property(propertyType, readMethod, writeMethod, propertyName); + return new TypeDescriptor(property); + } + + /** + * 获取 类属性 + * @param clazz 类信息 + * @param fieldName 属性名 + * @return Field + */ + @Nullable + public static Field getField(Class clazz, String fieldName) { + while (clazz != Object.class) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + /** + * 获取 所有 field 属性上的注解 + * @param clazz 类 + * @param fieldName 属性名 + * @param annotationClass 注解 + * @param 注解泛型 + * @return 注解 + */ + @Nullable + public static T getAnnotation(Class clazz, String fieldName, Class annotationClass) { + Field field = ReflectUtil.getField(clazz, fieldName); + if (field == null) { + return null; + } + return field.getAnnotation(annotationClass); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java index c4a964d..c541302 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java @@ -82,5 +82,8 @@ public interface StringPool { char L_A = 'a'; char U_Z = 'Z'; char L_Z = 'z'; + String UNKNOWN = "unknown"; + String GET = "GET"; + String POST = "POST"; } diff --git a/blade-core-transaction/pom.xml b/blade-core-transaction/pom.xml new file mode 100644 index 0000000..abe885f --- /dev/null +++ b/blade-core-transaction/pom.xml @@ -0,0 +1,43 @@ + + + + blade-tool + org.springblade + 2.5.0 + + 4.0.0 + + blade-core-transaction + ${project.artifactId} + ${blade.tool.version} + jar + + + + org.springblade + blade-core-mybatis + ${blade.tool.version} + + + + org.springframework.cloud + spring-cloud-commons + + + + com.alibaba.cloud + spring-cloud-alibaba-seata + ${alibaba.cloud.version} + + + io.seata + seata-all + ${alibaba.seata.version} + + + + + + diff --git a/blade-core-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java b/blade-core-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java new file mode 100644 index 0000000..2a9ed85 --- /dev/null +++ b/blade-core-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2018-2028, lengleng (wangiegie@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.transaction.annotation; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +import java.lang.annotation.*; + +/** + * Seata启动注解配置 + * + * @author Chill + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@EnableDiscoveryClient +@EnableCircuitBreaker +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +public @interface SeataCloudApplication { + +} diff --git a/blade-core-transaction/src/main/java/org/springblade/core/transaction/config/DataSourceConfiguration.java b/blade-core-transaction/src/main/java/org/springblade/core/transaction/config/DataSourceConfiguration.java new file mode 100644 index 0000000..194c03c --- /dev/null +++ b/blade-core-transaction/src/main/java/org/springblade/core/transaction/config/DataSourceConfiguration.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2018-2028, lengleng (wangiegie@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.transaction.config; + +import com.alibaba.druid.pool.DruidDataSource; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import io.seata.rm.datasource.DataSourceProxy; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import javax.sql.DataSource; + +/** + * 分布式事务数据源配置 + * + * @author Chill + */ +@Configuration +public class DataSourceConfiguration { + + @Bean(name = "sqlSessionFactory") + public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { + MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); + bean.setDataSource(dataSourceProxy); + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + bean.setMapperLocations(resolver.getResources("classpath:org/springblade/**/mapper/*Mapper.xml")); + + SqlSessionFactory factory = null; + try { + factory = bean.getObject(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return factory; + } + + @Bean + public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { + return new SqlSessionTemplate(sqlSessionFactory); + } + + /** + * 从配置文件获取属性构造datasource + */ + @Bean + @ConfigurationProperties(prefix = "spring.datasource") + public DruidDataSource druidDataSource() { + return new DruidDataSource(); + } + + /** + * 构造datasource代理对象 + */ + @Primary + @Bean("dataSource") + public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { + return new DataSourceProxy(druidDataSource); + } + +} diff --git a/blade-core-transaction/src/main/resources/registry.conf b/blade-core-transaction/src/main/resources/registry.conf new file mode 100644 index 0000000..da0dda8 --- /dev/null +++ b/blade-core-transaction/src/main/resources/registry.conf @@ -0,0 +1,20 @@ +registry { + # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa + type = "nacos" + + nacos { + serverAddr = "localhost" + namespace = "" + cluster = "default" + } +} + +config { + # file、nacos 、apollo、zk、consul、etcd3 + type = "nacos" + + nacos { + serverAddr = "localhost" + namespace = "" + } +} diff --git a/pom.xml b/pom.xml index 0bc975f..bbd5817 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springblade blade-tool - 2.4.1 + 2.5.0 pom blade-tool @@ -36,13 +36,13 @@ - 2.4.1 + 2.5.0 1.8 3.8.0 2.9.2 1.5.21 - 1.9.4 + 1.9.6 3.1.2 4.0.1 1.6.0 @@ -50,11 +50,12 @@ 2.1.5 1.1.0 2.1.0.RELEASE + 0.8.1 - 2.1.7.RELEASE - Greenwich.SR2 + 2.1.8.RELEASE + Greenwich.SR3 Cairo-SR8 - + UTF-8 UTF-8 @@ -71,6 +72,7 @@ blade-core-test blade-core-tool blade-core-oss + blade-core-transaction