Browse Source

:tada: 2.5.0.RELEASE

smallchill 5 years ago
parent
commit
6cb32aefd5
65 changed files with 3216 additions and 276 deletions
  1. 7 1
      blade-core-boot/pom.xml
  2. 32 7
      blade-core-boot/src/main/resources/bootstrap.yml
  3. 31 16
      blade-core-cloud/pom.xml
  4. 38 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java
  5. 37 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java
  6. 106 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java
  7. 0 52
      blade-core-cloud/src/main/java/org/springblade/core/cloud/config/BladeFeignConfiguration.java
  8. 44 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java
  9. 96 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignAutoConfiguration.java
  10. 91 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java
  11. 15 22
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestHeaderInterceptor.java
  12. 101 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeSpringMvcContract.java
  13. 80 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java
  14. 0 133
      blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/FeignHystrixConcurrencyStrategy.java
  15. 330 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/http/HttpLoggingInterceptor.java
  16. 27 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java
  17. 31 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/http/OkHttpSlf4jLogger.java
  18. 177 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java
  19. 59 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java
  20. 43 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeAccountGetter.java
  21. 56 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersCallable.java
  22. 100 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHttpHeadersContextHolder.java
  23. 38 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAccountGetter.java
  24. 71 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixAutoConfiguration.java
  25. 93 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/hystrix/BladeHystrixConcurrencyStrategy.java
  26. 54 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/props/BladeHystrixHeadersProperties.java
  27. 32 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/stream/ServiceErrorStreams.java
  28. 48 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java
  29. 103 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java
  30. 43 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java
  31. 37 0
      blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java
  32. 230 0
      blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeFeignClientsRegistrar.java
  33. 99 0
      blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeHystrixTargeter.java
  34. 39 0
      blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/Targeter.java
  35. 1 1
      blade-core-develop/pom.xml
  36. 1 1
      blade-core-launch/pom.xml
  37. 1 0
      blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java
  38. 1 1
      blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java
  39. 5 0
      blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java
  40. 1 1
      blade-core-log/pom.xml
  41. 2 2
      blade-core-log/src/main/java/org/springblade/core/log/model/LogAbstract.java
  42. 5 5
      blade-core-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java
  43. 1 1
      blade-core-mybatis/pom.xml
  44. 3 3
      blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java
  45. 4 3
      blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java
  46. 1 1
      blade-core-oss/pom.xml
  47. 1 1
      blade-core-secure/pom.xml
  48. 1 1
      blade-core-swagger/pom.xml
  49. 1 1
      blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java
  50. 1 1
      blade-core-test/pom.xml
  51. 1 1
      blade-core-tool/pom.xml
  52. 50 0
      blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java
  53. 65 0
      blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java
  54. 109 0
      blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java
  55. 109 0
      blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java
  56. 83 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java
  57. 9 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java
  58. 14 15
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java
  59. 166 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java
  60. 3 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java
  61. 43 0
      blade-core-transaction/pom.xml
  62. 39 0
      blade-core-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java
  63. 79 0
      blade-core-transaction/src/main/java/org/springblade/core/transaction/config/DataSourceConfiguration.java
  64. 20 0
      blade-core-transaction/src/main/resources/registry.conf
  65. 8 6
      pom.xml

+ 7 - 1
blade-core-boot/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springblade</groupId>
         <artifactId>blade-tool</artifactId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>
@@ -111,6 +111,12 @@
             <artifactId>ehcache</artifactId>
             <version>2.10.5</version>
         </dependency>
+        <!-- Druid -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>1.1.18</version>
+        </dependency>
         <!-- MySQL -->
         <dependency>
             <groupId>mysql</groupId>

+ 32 - 7
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

+ 31 - 16
blade-core-cloud/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -16,28 +16,31 @@
 
 
     <dependencies>
+        <!--Blade-->
         <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-web</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.springframework.boot</groupId>
-                    <artifactId>spring-boot-starter-tomcat</artifactId>
-                </exclusion>
-            </exclusions>
+            <groupId>org.springblade</groupId>
+            <artifactId>blade-core-secure</artifactId>
+            <version>${blade.tool.version}</version>
         </dependency>
+        <!--Spring-->
         <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-undertow</artifactId>
+            <groupId>org.springframework.retry</groupId>
+            <artifactId>spring-retry</artifactId>
         </dependency>
         <dependency>
-            <groupId>de.codecentric</groupId>
-            <artifactId>spring-boot-admin-starter-client</artifactId>
-            <version>${spring.boot.admin.version}</version>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-stream</artifactId>
+            <version>2.2.1.RELEASE</version>
+        </dependency>
+        <!--Feign-->
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+            <version>10.4.0</version>
         </dependency>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
-            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
             <exclusions>
                 <exclusion>
                     <groupId>commons-logging</groupId>
@@ -45,9 +48,10 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!--Hystrix-->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
-            <artifactId>spring-cloud-starter-openfeign</artifactId>
+            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
             <exclusions>
                 <exclusion>
                     <groupId>commons-logging</groupId>
@@ -55,6 +59,17 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- Actuator -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!-- Admin -->
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId>
+            <version>${spring.boot.admin.version}</version>
+        </dependency>
         <!-- Nacos -->
         <dependency>
             <groupId>com.alibaba.cloud</groupId>

+ 38 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "";
+
+}

+ 37 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "";
+}

+ 106 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.*;
+
+/**
+ * 版本号处理
+ *
+ * <p>
+ *     1. url 版本号:添加到 url 前
+ *     2. Accept 版本:application/vnd.blade.VERSION+json
+ * </p>
+ *
+ * @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 "";
+
+}

+ 0 - 52
blade-core-cloud/src/main/java/org/springblade/core/cloud/config/BladeFeignConfiguration.java

@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
- * <p>
- * 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
- * <p>
- * http://www.gnu.org/licenses/lgpl.html
- * <p>
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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();
-	}
-
-}

+ 44 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 <T> 泛型标记
+ * @author L.cm
+ */
+@AllArgsConstructor
+public class BladeFallbackFactory<T> implements FallbackFactory<T> {
+	private final Target<T> target;
+
+	@Override
+	@SuppressWarnings("unchecked")
+	public T create(Throwable cause) {
+		final Class<T> 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();
+	}
+}

+ 96 - 0
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);
+	}
+}

+ 91 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<T> implements MethodInterceptor {
+	private final Class<T> 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);
+	}
+}

+ 15 - 22
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).
  * <p>
  * 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
+ * <p>
+ *     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
+ * </p>
+ *
+ * @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<String> 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));
+			});
 		}
 	}
 

+ 101 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<AnnotatedParameterProcessor> 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;
+	}
+}

+ 80 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.
+	 * <p>
+	 * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
+	 * <p>
+	 * 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.
+	 * <p>
+	 * 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 <code>@Configuration</code> for all feign clients. Can contain override
+	 * <code>@Bean</code> 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 {};
+}

+ 0 - 133
blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/FeignHystrixConcurrencyStrategy.java

@@ -1,133 +0,0 @@
-/**
- * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
- * <p>
- * 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
- * <p>
- * http://www.gnu.org/licenses/lgpl.html
- * <p>
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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 <T> Callable<T> wrapCallable(Callable<T> callable) {
-		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
-		return new WrappedCallable<>(callable, requestAttributes);
-	}
-
-	@Override
-	public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
-											HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize,
-											HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> 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<Runnable> getBlockingQueue(int maxQueueSize) {
-		return this.delegate.getBlockingQueue(maxQueueSize);
-	}
-
-	@Override
-	public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
-		return this.delegate.getRequestVariable(rv);
-	}
-
-	static class WrappedCallable<T> implements Callable<T> {
-		private final Callable<T> target;
-		private final RequestAttributes requestAttributes;
-
-		public WrappedCallable(Callable<T> 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();
-			}
-		}
-	}
-}

+ 330 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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}. <p> 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.
+		 *
+		 * <p>Example:
+		 * <pre>{@code
+		 * --> POST /greeting http/1.1 (3-byte body)
+		 *
+		 * <-- 200 OK (22ms, 6-byte body)
+		 * }</pre>
+		 */
+		BASIC,
+		/**
+		 * Logs request and response lines and their respective headers.
+		 *
+		 * <p>Example:
+		 * <pre>{@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
+		 * }</pre>
+		 */
+		HEADERS,
+		/**
+		 * Logs request and response lines and their respective headers and bodies (if present).
+		 *
+		 * <p>Example:
+		 * <pre>{@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
+		 * }</pre>
+		 */
+		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);
+	}
+}

+ 27 - 0
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<HttpMessageConverter<?>> messageConverters) {
+		super(messageConverters);
+	}
+}

+ 31 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+	}
+}

+ 177 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<HttpMessageConverter<?>> converters) {
+		converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof MappingJackson2HttpMessageConverter);
+		converters.add(new StringHttpMessageConverter(Charsets.UTF_8));
+		converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
+	}
+}

+ 59 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+	}
+}

+ 43 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+	}
+
+}

+ 56 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 <V> 泛型标记
+ * @author L.cm
+ */
+public class BladeHttpHeadersCallable<V> implements Callable<V> {
+	private final Callable<V> delegate;
+	@Nullable
+	private HttpHeaders httpHeaders;
+
+	public BladeHttpHeadersCallable(Callable<V> 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;
+		}
+	}
+}

+ 100 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<HttpHeaders> 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<String> allowHeadsList = new ArrayList<>(Arrays.asList(ALLOW_HEADS));
+		// 如果有传递 account header 继续往下层传递
+		allowHeadsList.add(accountHeaderName);
+		// 传递请求头
+		Enumeration<String> headerNames = request.getHeaderNames();
+		if (headerNames != null) {
+			List<String> 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;
+	}
+}

+ 38 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+}

+ 71 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+	}
+
+}

+ 93 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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中的一些变量
+ *
+ * <p>
+ * 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
+ * </p>
+ *
+ * @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<Runnable> getBlockingQueue(int maxQueueSize) {
+		return existingConcurrencyStrategy != null
+			? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)
+			: super.getBlockingQueue(maxQueueSize);
+	}
+
+	@Override
+	public <T> HystrixRequestVariable<T> getRequestVariable(
+		HystrixRequestVariableLifecycle<T> rv) {
+		return existingConcurrencyStrategy != null
+			? existingConcurrencyStrategy.getRequestVariable(rv)
+			: super.getRequestVariable(rv);
+	}
+
+	@Override
+	public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
+											HystrixProperty<Integer> corePoolSize,
+											HystrixProperty<Integer> maximumPoolSize,
+											HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
+											BlockingQueue<Runnable> 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 <T> Callable<T> wrapCallable(Callable<T> callable) {
+		Callable<T> wrapCallable = new BladeHttpHeadersCallable<>(callable, accountGetter, properties);
+		return existingConcurrencyStrategy != null
+			? existingConcurrencyStrategy.wrapCallable(wrapCallable)
+			: super.wrapCallable(wrapCallable);
+	}
+}

+ 54 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<String> allowed = Arrays.asList("X-Real-IP", "x-forwarded-for", "authorization", "blade-auth", "Authorization", "Blade-Auth");
+
+}

+ 32 - 0
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();
+}

+ 48 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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
+ *
+ * <p>
+ * https://developer.github.com/v3/media/
+ * </p>
+ *
+ * @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();
+	}
+}

+ 103 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 版本处理
+ *
+ * <p>
+ *     url: /v1/user/{id}
+ *     header: Accept application/vnd.blade.VERSION+json
+ * </p>
+ *
+ * 注意: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<RequestMappingInfo, HandlerMethod> handlerMethods) {
+		// 打印路由信息 spring boot 2.1 去掉了这个 日志的打印
+		if (logger.isInfoEnabled()) {
+			for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
+				RequestMappingInfo mapping = entry.getKey();
+				HandlerMethod handlerMethod = entry.getValue();
+				logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
+			}
+		}
+		super.handlerMethodsInitialized(handlerMethods);
+	}
+}

+ 43 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;
+	}
+}

+ 37 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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();
+	}
+}

+ 230 - 0
blade-core-cloud/src/main/java/org/springframework/cloud/openfeign/BladeFeignClientsRegistrar.java

@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> attributes) {
+		String url = resolve((String) attributes.get("url"));
+		return FeignClientsRegistrar.getUrl(url);
+	}
+
+	private String getPath(Map<String, Object> attributes) {
+		String path = resolve((String) attributes.get("path"));
+		return FeignClientsRegistrar.getPath(path);
+	}
+
+	@Nullable
+	private String getQualifier(@Nullable Map<String, Object> 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<String, Object> 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;
+	}
+
+}

+ 99 - 0
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> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
+						Target.HardCodedTarget<T> 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> T targetWithFallbackFactory(String feignClientName, FeignContext context,
+											Target.HardCodedTarget<T> target,
+											HystrixFeign.Builder builder,
+											Class<?> fallbackFactoryClass) {
+		FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>)
+			getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
+		return builder.target(target, fallbackFactory);
+	}
+
+
+	private <T> T targetWithFallback(String feignClientName, FeignContext context,
+									 Target.HardCodedTarget<T> target,
+									 HystrixFeign.Builder builder, Class<?> fallback) {
+		T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
+		return builder.target(target, fallbackInstance);
+	}
+
+	private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, Class<?> beanType,
+								 Class<T> 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> T getOptional(String feignClientName, FeignContext context, Class<T> beanType) {
+		return context.getInstance(feignClientName, beanType);
+	}
+}

+ 39 - 0
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 <T>
+	 * @return T
+	 */
+	<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
+				 Target.HardCodedTarget<T> target);
+}

+ 1 - 1
blade-core-develop/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
blade-core-launch/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 0
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<LauncherService> launcherList = new ArrayList<>();
 		ServiceLoader.load(LauncherService.class).forEach(launcherList::add);

+ 1 - 1
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";
 
 	/**
 	 * 基础包

+ 5 - 0
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 配置文件类型
 	 */

+ 1 - 1
blade-core-log/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 2 - 2
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;
 
 }

+ 5 - 5
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).
  * <p>
  * 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) {

+ 1 - 1
blade-core-mybatis/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 3 - 3
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:正常]

+ 4 - 3
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<M extends BaseMapper<T>, 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<M extends BaseMapper<T>, T extends BaseEntity> exte
 		if (user != null) {
 			entity.setUpdateUser(user.getUserId());
 		}
-		entity.setUpdateTime(LocalDateTime.now());
+		entity.setUpdateTime(DateUtil.now());
 		return super.updateById(entity);
 	}
 

+ 1 - 1
blade-core-oss/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
blade-core-secure/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
blade-core-swagger/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
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";
 	/**
 	 * 许可证
 	 **/

+ 1 - 1
blade-core-test/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springblade</groupId>
         <artifactId>blade-tool</artifactId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
blade-core-tool/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <groupId>org.springblade</groupId>
         <artifactId>blade-tool</artifactId>
-        <version>2.4.1</version>
+        <version>2.5.0</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 50 - 0
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.
+	 * <p>
+	 * 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;
+	}
+
+}

+ 65 - 0
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<String, TypeDescriptor> 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);
+		}));
+	}
+}

+ 109 - 0
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<Class<?>, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8);
+
+	@Nullable
+	private static AccessibleObject getAnnotation(Class<?> clazz) {
+		Set<AccessibleObject> 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<ConvertiblePair> getConvertibleTypes() {
+		Set<ConvertiblePair> 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);
+	}
+}

+ 109 - 0
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<Class<?>, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8);
+
+	@Nullable
+	private static AccessibleObject getAnnotation(Class<?> clazz) {
+		Set<AccessibleObject> 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<ConvertiblePair> 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 extends Enum<T>> T valueOf(Class<?> clazz, String value){
+		return Enum.valueOf((Class<T>) 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;
+	}
+
+}

+ 83 - 0
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 <T> 泛型标记
+	 * @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> T convert(@Nullable Object source, Class<T> 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 <T> 泛型标记
+	 * @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> 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 <T> 泛型标记
+	 * @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> T convert(@Nullable Object source, TypeDescriptor targetType) {
+		if (source == null) {
+			return null;
+		}
+		GenericConversionService conversionService = BladeConversionService.getInstance();
+		return (T) conversionService.convert(source, targetType);
+	}
+
+}

+ 9 - 0
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();
+	}
+
 	/**
 	 * 添加年
 	 *

+ 14 - 15
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;

+ 166 - 0
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<PropertyDescriptor> 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 <T> 注解泛型
+	 * @return 注解
+	 */
+	@Nullable
+	public static <T extends Annotation> T getAnnotation(Class<?> clazz, String fieldName, Class<T> annotationClass) {
+		Field field = ReflectUtil.getField(clazz, fieldName);
+		if (field == null) {
+			return null;
+		}
+		return field.getAnnotation(annotationClass);
+	}
+}

+ 3 - 0
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";
 
 }

+ 43 - 0
blade-core-transaction/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>blade-tool</artifactId>
+        <groupId>org.springblade</groupId>
+        <version>2.5.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>blade-core-transaction</artifactId>
+    <name>${project.artifactId}</name>
+    <version>${blade.tool.version}</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springblade</groupId>
+            <artifactId>blade-core-mybatis</artifactId>
+            <version>${blade.tool.version}</version>
+        </dependency>
+        <!-- Cloud-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-commons</artifactId>
+        </dependency>
+        <!-- Seata-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-alibaba-seata</artifactId>
+            <version>${alibaba.cloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.seata</groupId>
+            <artifactId>seata-all</artifactId>
+            <version>${alibaba.seata.version}</version>
+        </dependency>
+    </dependencies>
+
+
+
+</project>

+ 39 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 {
+
+}

+ 79 - 0
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).
+ * <p>
+ * 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
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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);
+	}
+
+}

+ 20 - 0
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 = ""
+  }
+}

+ 8 - 6
pom.xml

@@ -5,7 +5,7 @@
 
     <groupId>org.springblade</groupId>
     <artifactId>blade-tool</artifactId>
-    <version>2.4.1</version>
+    <version>2.5.0</version>
     <packaging>pom</packaging>
     <name>blade-tool</name>
     <description>
@@ -36,13 +36,13 @@
     </scm>
 
     <properties>
-        <blade.tool.version>2.4.1</blade.tool.version>
+        <blade.tool.version>2.5.0</blade.tool.version>
 
         <java.version>1.8</java.version>
         <maven.plugin.version>3.8.0</maven.plugin.version>
         <swagger.version>2.9.2</swagger.version>
         <swagger.models.version>1.5.21</swagger.models.version>
-        <swagger.bootstrapui.version>1.9.4</swagger.bootstrapui.version>
+        <swagger.bootstrapui.version>1.9.6</swagger.bootstrapui.version>
         <mybatis.plus.version>3.1.2</mybatis.plus.version>
         <curator.framework.version>4.0.1</curator.framework.version>
         <protostuff.version>1.6.0</protostuff.version>
@@ -50,11 +50,12 @@
         <spring.boot.admin.version>2.1.5</spring.boot.admin.version>
         <mica.auto.version>1.1.0</mica.auto.version>
         <alibaba.cloud.version>2.1.0.RELEASE</alibaba.cloud.version>
+        <alibaba.seata.version>0.8.1</alibaba.seata.version>
 
-        <spring.boot.version>2.1.7.RELEASE</spring.boot.version>
-        <spring.cloud.version>Greenwich.SR2</spring.cloud.version>
+        <spring.boot.version>2.1.8.RELEASE</spring.boot.version>
+        <spring.cloud.version>Greenwich.SR3</spring.cloud.version>
         <spring.platform.version>Cairo-SR8</spring.platform.version>
-        
+
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
     </properties>
@@ -71,6 +72,7 @@
         <module>blade-core-test</module>
         <module>blade-core-tool</module>
         <module>blade-core-oss</module>
+        <module>blade-core-transaction</module>
     </modules>
 
     <dependencyManagement>