From 9005d6b94ef6c5b4b64abb40572c9dbbc3c6bb6c Mon Sep 17 00:00:00 2001 From: chentianming Date: Sat, 30 Apr 2022 17:19:45 +0800 Subject: [PATCH] =?UTF-8?q?degrade=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_EN.md | 852 ------------------ .../spring/boot/config/DegradeProperty.java | 20 +- .../config/RetrofitAutoConfiguration.java | 54 +- .../boot/config/RetrofitConfigBean.java | 19 +- .../spring/boot/core/RetrofitFactoryBean.java | 331 +++---- .../boot/core/RetrofitInvocationHandler.java | 59 -- .../boot/degrade/BaseDegradeInterceptor.java | 51 -- .../boot/degrade/BaseResourceNameParser.java | 86 -- .../degrade/DefaultResourceNameParser.java | 43 +- .../boot/degrade/DegradeInterceptor.java | 9 + .../spring/boot/degrade/DegradeProxy.java | 76 ++ .../boot/degrade/ResourceNameParser.java | 72 ++ .../degrade/SentinelDegradeInterceptor.java | 37 - .../{ => sentinel}/SentinelDegrade.java | 4 +- .../sentinel/SentinelDegradeInterceptor.java | 52 ++ .../boot/retry/BaseRetryInterceptor.java | 22 +- .../boot/util/AnnotationExtendUtils.java | 48 + .../boot/util/ApplicationContextUtils.java | 16 +- .../spring/boot/util/RetrofitUtils.java | 19 +- .../spring/boot/test/http/DegradeApi.java | 2 +- 20 files changed, 508 insertions(+), 1364 deletions(-) delete mode 100644 README_EN.md delete mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/core/RetrofitInvocationHandler.java delete mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseDegradeInterceptor.java delete mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseResourceNameParser.java create mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeInterceptor.java create mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeProxy.java create mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/ResourceNameParser.java delete mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegradeInterceptor.java rename src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/{ => sentinel}/SentinelDegrade.java (76%) create mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegradeInterceptor.java create mode 100644 src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/AnnotationExtendUtils.java diff --git a/README_EN.md b/README_EN.md deleted file mode 100644 index a455dbd..0000000 --- a/README_EN.md +++ /dev/null @@ -1,852 +0,0 @@ - -## retrofit-spring-boot-starter - -[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![Build Status](https://api.travis-ci.com/LianjiaTech/retrofit-spring-boot-starter.svg?branch=master)](https://travis-ci.com/github/LianjiaTech/retrofit-spring-boot-starter) -[![Maven central](https://maven-badges.herokuapp.com/maven-central/com.github.lianjiatech/retrofit-spring-boot-starter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.lianjiatech/retrofit-spring-boot-starter) -[![GitHub release](https://img.shields.io/github/v/release/lianjiatech/retrofit-spring-boot-starter.svg)](https://github.com/LianjiaTech/retrofit-spring-boot-starter/releases) -[![License](https://img.shields.io/badge/JDK-1.8+-4EB1BA.svg)](https://docs.oracle.com/javase/8/docs/index.html) -[![License](https://img.shields.io/badge/SpringBoot-1.5+-green.svg)](https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/) -[![Author](https://img.shields.io/badge/Author-chentianming-orange.svg?style=flat-square)](https://juejin.im/user/3562073404738584/posts) -[![QQ-Group](https://img.shields.io/badge/QQ%E7%BE%A4-806714302-orange.svg?style=flat-square) ](https://img.ljcdn.com/hc-picture/HTTP-exception-information-formatter6302d742-ebc8-4649-95cf-62ccf57a1add) - - -[中文文档](https://github.com/LianjiaTech/retrofit-spring-boot-starter) - -`Retrofit` is a type safe HTTP client for `Android` and `Java`. **Supporting HTTP requests through `interfaces`** is the strongest feature of `Retrofit`. `Spring-boot` is the most widely used java development framework, but there is no official `retrofit` support for rapid integration with `spring-boot` framework, so we developed `retrofit-spring-boot-starter`. - -**`Retrofit-spring-boot-starter` realizes the rapid integration of `Retrofit` and `spring-boot`, supports many enhanced features and greatly simplifies development**. - -🚀The project is in continuous optimization iteration. We welcome everyone to mention ISSUE and PR! Your star✨ is our power for continuous updating! - - - -## Features - -- [x] [Custom injection OkHttpClient](#Custom-injection-OkHttpClient) -- [x] [Annotation interceptor](#Annotation-interceptor) -- [x] [Connection pool management](#Connection-pool-management) -- [x] [Log printing](#Log-printing) -- [x] [Request retry](#Request-retry) -- [x] [Error decoder](#Error-decoder) -- [x] [Global interceptor](#Global-interceptor) -- [x] [Fuse degrade](#Fuse-degrade) -- [x] [HTTP calls between microservices](#HTTP-calls-between-microservices) -- [x] [CallAdapter](#CallAdapter) -- [x] [Converter](#Converter) - -## Quick start - -### Introduce dependency - -```xml - - com.github.lianjiatech - retrofit-spring-boot-starter - 2.3.0 - -``` - -This project depends on Retrofit-2.9.0, okhttp-3.14.9, and okio-1.17.5 versions. If there is a conflict, please manually import related jar packages. The complete dependencies are as follows: - -```xml - - com.github.lianjiatech - retrofit-spring-boot-starter - 2.3.0 - - - com.squareup.okhttp3 - logging-interceptor - 3.14.9 - - - com.squareup.okhttp3 - okhttp - 3.14.9 - - - com.squareup.okio - okio - 1.17.5 - - - com.squareup.retrofit2 - retrofit - 2.9.0 - - - com.squareup.retrofit2 - converter-jackson - 2.9.0 - -``` - -### Define HTTP interface - -**The interface must be marked with `@RetrofitClient` annotation**! Related annotations of HTTP can refer to the official documents: [Retrofit official documents](https://square.github.io/retrofit/). - -```java -@RetrofitClient(baseUrl = "${test.baseUrl}") -public interface HttpApi { - - @GET("person") - Result getPerson(@Query("id") Long id); -} -``` - -### Inject and use - -**Inject the interface into other Service and use!** - -```java -@Service -public class TestService { - - @Autowired - private HttpApi httpApi; - - public void test() { - // Initiate HTTP request via HTTP Api - } -} -``` - -**By default, the SpringBoot scan path is automatically used for retrofitClient registration**. You can also add `@RetrofitScan` to the configuration class to manually specify the scan path. - -## Related annotations of HTTP request - -All of the related annotations of `HTTP` request use native annotations of `retrofit`. **For more information, please refer to the official document: [Retrofit official documents](https://square.github.io/retrofit/)**. The following is a brief description: - -| Annotation classification|Supported annotations | -|------------|-----------| -|Request method|`@GET` `@HEAD` `@POST` `@PUT` `@DELETE` `@OPTIONS` `@HTTP`| -|Request header|`@Header` `@HeaderMap` `@Headers`| -|Query param|`@Query` `@QueryMap` `@QueryName`| -|Path param|`@Path`| -|Form-encoded param|`@Field` `@FieldMap` `@FormUrlEncoded`| -| Request body |`@Body`| -|File upload|`@Multipart` `@Part` `@PartMap`| -|Url param|`@Url`| - -## Configuration item description - -`Retrofit-spring-boot-starter` supports multiple configurable properties to deal with different business scenarios.For more information, please refer to the [configuration example](https://github.com/LianjiaTech/retrofit-spring-boot-starter/blob/master/src/test/resources/application.yml) - -## Advanced feature - -### Custom injection OkHttpClient - -In general, dynamic creation of `OkHttpClient` object through the `@RetrofitClient` annotation can satisfy most usage scenarios. But in some cases, users may need to customize `OkHttpClient`. At this time, you can define a static method with the return type of `OkHttpClient.Builder` on the interface to achieve this. The code example is as follows: - -```java -@RetrofitClient(baseUrl = "http://ke.com") -public interface HttpApi3 { - - @OkHttpClientBuilder - static OkHttpClient.Builder okhttpClientBuilder() { - return new OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(1, TimeUnit.SECONDS) - .writeTimeout(1, TimeUnit.SECONDS); - - } - - @GET - Result getPerson(@Url String url, @Query("id") Long id); -} -``` - -> The method must be marked with `@OkHttpClientBuilder` annotation! - - - -### Annotation interceptor - -In many cases, we hope that certain http requests in a certain interface execute a unified interception processing logic. So as to support this feature, `retrofit-spring-boot-starter` provide **annotation interceptor** and at the same time achieves **matching interception based on URL path**. The use is mainly divided into 2 steps: - -1. Inherit `BasePathMatchInterceptor` and write interceptor processor; -2. Mark the interface with `@Intercept`. - -> To configure multiple interceptors, just mark multiple `@Intercept` annotations on the interface! - -The following is an example of how to use annotation interceptors *by splicing timestamp after the URL of a specified request*. - -#### Inherit `BasePathMatchInterceptor` and write interceptor processor - -```java -@Component -public class TimeStampInterceptor extends BasePathMatchInterceptor { - - @Override - public Response doIntercept(Chain chain) throws IOException { - Request request = chain.request(); - HttpUrl url = request.url(); - long timestamp = System.currentTimeMillis(); - HttpUrl newUrl = url.newBuilder() - .addQueryParameter("timestamp", String.valueOf(timestamp)) - .build(); - Request newRequest = request.newBuilder() - .url(newUrl) - .build(); - return chain.proceed(newRequest); - } -} - -``` - -#### Mark the interface with `@Intercept` - -```java -@RetrofitClient(baseUrl = "${test.baseUrl}") -@Intercept(handler = TimeStampInterceptor.class, include = {"/api/**"}, exclude = "/api/test/savePerson") -public interface HttpApi { - - @GET("person") - Result getPerson(@Query("id") Long id); - - @POST("savePerson") - Result savePerson(@Body Person person); -} -``` - -The above `@Intercept`: Intercept the request under the path `/api/**` in the `HttpApi` interface (excluding `/api/test/savePerson`).The interception processor uses `TimeStampInterceptor`. - -### Extended annotation interceptor - -Sometimes, we need to dynamically pass in some parameters in the **intercept annotation** and then use these parameter when performing interception. In this case, we can extend the implementation of **custom intercept annotation**. You must mark `custom intercept annotation` with `@InterceptMark` and **the annotation must include `include(), exclude(), handler()` attribute information**. The use is mainly divided into 3 steps: - -1. Custom intercept annotation -2. Inherit `BasePathMatchInterceptor` and write interceptor processor -3. Mark the interface with custom intercept annotation - -For example, we need to **dynamically add the signature information of `accesskeyid` and `accesskeysecret` in the request header to initiate HTTP requests normally**. In this case, we can **customize a signature interceptor Annotation `@sign` to implement**.The following is an example of the custom `@sign` intercept annotation. - - -#### Custom `@sign` intercept annotation - -```java -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Documented -@InterceptMark -public @interface Sign { - /** - * secret key - * Support the configuration in the form of placeholder. - * - * @return - */ - String accessKeyId(); - - /** - * secret key - * Support the configuration in the form of placeholder. - * - * @return - */ - String accessKeySecret(); - - /** - * Interceptor matching path. - * - * @return - */ - String[] include() default {"/**"}; - - /** - * Interceptor excludes matching and intercepting by specified path. - * - * @return - */ - String[] exclude() default {}; - - /** - * The interceptor class which handles the annotation. - * Get the corresponding bean from the spring container firstly.If not, use - * reflection to create one! - * - * @return - */ - Class handler() default SignInterceptor.class; -} -``` - -There are two points to be noted in the extension of the `custom intercept annotation`: - -1. `Custom intercept annotation` must be marked with `@InterceptMark`. -2. The annotation must include `include(), exclude(), handler()` attribute information. - -#### Realize `SignInterceptor` - -```java -@Component -public class SignInterceptor extends BasePathMatchInterceptor { - - private String accessKeyId; - - private String accessKeySecret; - - public void setAccessKeyId(String accessKeyId) { - this.accessKeyId = accessKeyId; - } - - public void setAccessKeySecret(String accessKeySecret) { - this.accessKeySecret = accessKeySecret; - } - - @Override - public Response doIntercept(Chain chain) throws IOException { - Request request = chain.request(); - Request newReq = request.newBuilder() - .addHeader("accessKeyId", accessKeyId) - .addHeader("accessKeySecret", accessKeySecret) - .build(); - return chain.proceed(newReq); - } -} -``` - -**The above `accessKeyId` and `accessKeySecret` value will be automatically injected according to the `accessKeyId()` and `accessKeySecret()` values of the `@sign` annotation.If `@Sign` specifies a string in the form of a placeholder, the configuration property value will be taken for injection**. In addition, **`accessKeyId` and `accessKeySecret` value must provide `setter` method**. - -#### Mark interface with `@Sign` - -```java -@RetrofitClient(baseUrl = "${test.baseUrl}") -@Sign(accessKeyId = "${test.accessKeyId}", accessKeySecret = "${test.accessKeySecret}", exclude = {"/api/test/person"}) -public interface HttpApi { - - @GET("person") - Result getPerson(@Query("id") Long id); - - @POST("savePerson") - Result savePerson(@Body Person person); -} -``` - -In this way, the signature information can be automatically added to the request of the specified URL. - -### Connection pool management - -By default, all HTTP requests sent through `Retrofit` will use the default connection pool of `max idle connections = 5 keep alive second = 300`. Of course, We can also configure multiple custom connection pools in the configuration file and then specify the usage through the `poolName` attribute of `@retrofitclient`. For example, we want to make all requests under an interface use the connection pool of `poolName = test1`. The code implementation is as follows: - -1. Configure the connection pool. - - ```yaml - retrofit: - # 连接池配置 - pool: - # test1连接池配置 - test1: - # 最大空闲连接数 - max-idle-connections: 3 - # 连接保活时间(秒) - keep-alive-second: 100 - ``` - -2. Use the `poolName` property of `@Retrofitclient` to specify the connection pool to be used. - ```java - @RetrofitClient(baseUrl = "${test.baseUrl}", poolName="test1") - public interface HttpApi { - - @GET("person") - Result getPerson(@Query("id") Long id); - } - ``` - -### Log printing - -In many cases, we want to log HTTP requests. The framework supports the following global log printing configurations: - -```yaml -retrofit: - # 日志打印配置 - log: - # 启用日志打印 - enable: true - # 日志打印拦截器 - logging-interceptor: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor - # 全局日志打印级别 - global-log-level: info - # 全局日志打印策略 - global-log-strategy: body - -``` - -The meanings of the 4 log printing strategies are as follows: - -1. `NONE`:No logs. -2. `BASIC`:Logs request and response lines. -3. `HEADERS`:Logs request and response lines and their respective headers. -4. `BODY`:Logs request and response lines and their respective headers and bodies (if present). - -For each interface, if you need to customize it separately, you can set the `enableLog`, `logLevel` and `logStrategy` of `@RetrofitClient`. - -### Request retry - -`retrofit-spring-boot-starter` supports global retry and declarative retry. - -#### global retry - -Global retry is enabled by default and can be disabled by configuring `retrofit.retry.enable-global-retry=false`. After enabling, all `HTTP` requests will be retried automatically according to the configuration parameters. The detailed configuration items are as follows: - -```yaml -retrofit: - # 重试配置 - retry: - # 是否启用全局重试 - enable-global-retry: true - # 全局重试间隔时间 - global-interval-ms: 20 - # 全局最大重试次数 - global-max-retries: 10 - # 全局重试规则 - global-retry-rules: - - response_status_not_2xx - # 重试拦截器 - retry-interceptor: com.github.lianjiatech.retrofit.spring.boot.retry.DefaultRetryInterceptor -``` - -#### declarative retry - -If you only need to specify certain requests before retrying, you can use declarative retry! Specifically, declare the `@Retry` annotation on the interface or method. - - -### Error decoder - -When an error occurs in the `HTTP` request (including an exception or the response data does not meet expectations), the error decoder can decode `HTTP` related information into a custom exception. You can specify the error decoder of the current interface in the `errorDecoder()` annotated by `@RetrofitClient`. The custom error decoder needs to implement the `ErrorDecoder` interface: - -```java -/** - * When an exception occurs in the request or an invalid response result is received, the HTTP related information is decoded into the exception, - * and the invalid response is determined by the business itself. - * - * @author Tianming Chen - */ -public interface ErrorDecoder { - - /** - * When the response is invalid, decode the HTTP information into the exception, invalid response is determined by business. - * - * @param request request - * @param response response - * @return If it returns null, the processing is ignored and the processing continues with the original response. - */ - default RuntimeException invalidRespDecode(Request request, Response response) { - if (!response.isSuccessful()) { - throw RetrofitException.errorStatus(request, response); - } - return null; - } - - - /** - * When an IO exception occurs in the request, the HTTP information is decoded into the exception. - * - * @param request request - * @param cause IOException - * @return RuntimeException - */ - default RuntimeException ioExceptionDecode(Request request, IOException cause) { - return RetrofitException.errorExecuting(request, cause); - } - - /** - * When the request has an exception other than the IO exception, the HTTP information is decoded into the exception. - * - * @param request request - * @param cause Exception - * @return RuntimeException - */ - default RuntimeException exceptionDecode(Request request, Exception cause) { - return RetrofitException.errorUnknown(request, cause); - } - -} - -``` - -## Global interceptor - -### Global application interceptor - -If we need to implement unified interception processing for HTTP requests of the whole system, we can customize the implementation of global interceptor `BaseGlobalInterceptor` and configure it as a `Bean` in `Spring`! For example, we need to carry source information for all http requests initiated in the entire system. - -```java -@Component -public class SourceInterceptor extends BaseGlobalInterceptor { - @Override - public Response doIntercept(Chain chain) throws IOException { - Request request = chain.request(); - Request newReq = request.newBuilder() - .addHeader("source", "test") - .build(); - return chain.proceed(newReq); - } -} -``` - -### Global network interceptor - -You only need to implement the NetworkInterceptor interface and configure it as a bean in the spring container to support automatic weaving into the global network interceptor. - -### Fuse degrade - -In the distributed service architecture, fuse downgrade of unstable external services is one of the important measures to ensure high service availability. Since the stability of external services cannot be guaranteed, when external services are unstable, the response time will become longer. Correspondingly, the caller's response time will become longer, threads will accumulate, and eventually the caller's thread pool may be exhausted, causing the entire service to be unavailable. Therefore, we need to fuse and downgrade unstable weakly dependent service calls, temporarily cut off unstable calls, and avoid local instability leading to an overall service avalanche. - -retrofit-spring-boot-starter supports the fuse downgrade function, and the bottom layer is based on [Sentinel](https://sentinelguard.io/zh-cn/docs/introduction.html). Specifically, it supports self-discovery of fusing resources and annotated degrade rule configuration. If you need to use the fuse to downgrade, you only need to do the following: - -#### 1. Enable fuse degrade - -By default, the fuse downgrade function is turned off, you need to set the corresponding configuration items to turn on the fuse downgrade function - -```yaml -retrofit: - enable-degrade: true - # the degade type(Currently only Sentinel is supported) - degrade-type: sentinel - # the resource name parser - resource-name-parser: com.github.lianjiatech.retrofit.spring.boot.degrade.DefaultResourceNameParser -``` - -The resource name resolver is used to implement user-defined resource names. The default configuration is `DefaultResourceNameParser`, and the corresponding resource name format is `HTTP_OUT:GET:http://localhost:8080/api/degrade/test`.Users can inherit the `BaseResourceNameParser` class to implement their own resource name parser. - -**In addition, since the fuse downgrade function is optional, enabling fuse downgrade requires users to introduce Sentinel dependencies by themselves**: - -```xml - - com.alibaba.csp - sentinel-core - 1.6.3 - -``` - -### Configure degrade rules (optional) - -**`retrofit-spring-boot-starter` supports annotation-based configuration of downgrade rules, and you can configure downgrade rules through @Degrade annotations**. The @Degrade annotation can be configured on the interface or method, and the priority of the configuration on the method is higher. - - -```java -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -@Documented -public @interface Degrade { - - /** - * RT threshold or exception ratio threshold count. - */ - double count(); - - /** - * Degrade recover timeout (in seconds) when degradation occurs. - */ - int timeWindow() default 5; - - /** - * Degrade strategy (0: average RT, 1: exception ratio). - */ - DegradeStrategy degradeStrategy() default DegradeStrategy.AVERAGE_RT; -} -``` - -> **If the application project already supports the configuration of downgrade rules through the configuration center, you can ignore the annotation configuration method**。 - -#### 3. @RetrofitClient set fallback or fallbackFactory (optional) - -**If `@RetrofitClient` does not set `fallback` or `fallbackFactory`, when the fuse is triggered, `RetrofitBlockException` will be thrown directly. The user can customize the return value of the method when fusing by setting `fallback` or `fallbackFactory`**. The `fallback` class must be the implementation class of the current interface, `fallbackFactory` must be the `FallbackFactory` implementation class, and the generic parameter type is the current interface type. In addition, fallback and fallbackFactory instances must be configured as Spring container beans. - -**The main difference between `fallbackFactory` and `fallback` is that it can sense the cause of each fusing**. The reference example is as follows: - -```java -@Slf4j -@Service -public class HttpDegradeFallback implements HttpDegradeApi { - - @Override - public Result test() { - Result fallback = new Result<>(); - fallback.setCode(100) - .setMsg("fallback") - .setBody(1000000); - return fallback; - } -} -``` - -```java -@Slf4j -@Service -public class HttpDegradeFallbackFactory implements FallbackFactory { - - /** - * Returns an instance of the fallback appropriate for the given cause - * - * @param cause fallback cause - * @return 实现了retrofit接口的实例。an instance that implements the retrofit interface. - */ - @Override - public HttpDegradeApi create(Throwable cause) { - log.error("触发熔断了! ", cause.getMessage(), cause); - return new HttpDegradeApi() { - @Override - public Result test() { - Result fallback = new Result<>(); - fallback.setCode(100) - .setMsg("fallback") - .setBody(1000000); - return fallback; - } - } -} -``` - - - - -### HTTP calls between microservices - -**By configuring the `serviceId` and `path` properties of `@Retrofit`, HTTP calls between microservices can be realized**. - -```java -@RetrofitClient(serviceId = "${jy-helicarrier-api.serviceId}", path = "/m/count", errorDecoder = HelicarrierErrorDecoder.class) -@Retry -public interface ApiCountService { - -} -``` - -Users need to implement the `ServiceInstanceChooser` interface by themselves, complete the selection logic of the service instance, and configure it as the `Bean` of the `Spring` container. -For `Spring Cloud` applications, `retrofit-spring-boot-starter` provides the implementation of `SpringCloudServiceInstanceChooser`, Users only need to configure it as the `Bean` of `Spring`. - -```java -public interface ServiceInstanceChooser { - - /** - * Chooses a ServiceInstance URI from the LoadBalancer for the specified service. - * - * @param serviceId The service ID to look up the LoadBalancer. - * @return Return the uri of ServiceInstance - */ - URI choose(String serviceId); - -} -``` - -```java -@Bean -@Autowired -public ServiceInstanceChooser serviceInstanceChooser(LoadBalancerClient loadBalancerClient) { - return new SpringCloudServiceInstanceChooser(loadBalancerClient); -} -``` - - -## CallAdapter and Converter - -You only need to implement the `NetworkInterceptor` interface and configure it as a `bean` in the `spring` container to support automatic weaving into the global network interceptor. - -### CallAdapter - -`Retrofit` can adapt the `Call` object to the return value type of the interface method by calling the adapter `CallAdapterFactory`. `Retrofit-spring-boot-starter` extends two implementations of `CallAdapterFactory`: -1. `BodyCallAdapterFactory` - - Feature is enabled by default, and can be disabled by configuring `retrofit.enable-body-call-adapter=false`. - - Execute the http request synchronously and adapt the response body to an instance of the return value type of the interface method. - - All return types can use this adapter except `Retrofit.Call`, `Retrofit.Response`, `java.util.concurrent.CompletableFuture`. -2. `ResponseCallAdapterFactory` - - Feature is enabled by default, and can be disabled by configuring `retrofit.enable-response-call-adapter=false`. - - Execute the http request synchronously, adapt the response body content to `Retrofit.Response` and return. - - If the return value type of the method is `Retrofit.Response`, you can use this adapter. - -**Retrofit automatically selects the corresponding `CallAdapterFactory` to perform adaptation processing according to the method return value type! With the default `CallAdapterFactory` of retrofit, it can support various types of method return values:** - -- `Call`: Do not perform adaptation processing, directly return the `Call` object. -- `CompletableFuture`: Adapt the response body content to a `CompletableFuture` object and return. -- `Void`: You can use `Void` regardless of the return type. If the http status code is not 2xx, just throw an error! -- `Response`: Adapt the response content to a `Response` object and return. -- Any other Java type: Adapt the response body content to a corresponding Java type object and return. If the http status code is not 2xx, just throw an error! - -```java - /** - * Call - * do not perform adaptation processing, directly return the Call object. - * @param id - * @return - */ - @GET("person") - Call> getPersonCall(@Query("id") Long id); - - /** - * CompletableFuture - * Adapt the response body content to a CompletableFuture object and return. - * @param id - * @return - */ - @GET("person") - CompletableFuture> getPersonCompletableFuture(@Query("id") Long id); - - /** - * Void - * You can use Void regardless of the return type. If the http status code is not 2xx, just throw an error! - * @param id - * @return - */ - @GET("person") - Void getPersonVoid(@Query("id") Long id); - - /** - * Response - * Adapt the response content to a Response object and return. - * @param id - * @return - */ - @GET("person") - Response> getPersonResponse(@Query("id") Long id); - - /** - * Any other Java type - * Adapt the response body content to a corresponding Java type object and return. If the http status code is not 2xx, just throw an error! - * @param id - * @return - */ - @GET("person") - Result getPerson(@Query("id") Long id); - -``` - -**We can also implement our own `CallAdapter`** by inheriting the `CallAdapter.Factory`! - -`retrofit-spring-boot-starter` supports configuring the global `CallAdapter.Factory` through `retrofit.global-call-adapter-factories`. The call adapter factory instance is first obtained from the Spring container. If it is not obtained, it is created by reflection. The default global call adapter factory is `[BodyCallAdapterFactory, ResponseCallAdapterFactory]`. - -```yaml -retrofit: - global-call-adapter-factories: - - com.github.lianjiatech.retrofit.spring.boot.core.BodyCallAdapterFactory - - com.github.lianjiatech.retrofit.spring.boot.core.ResponseCallAdapterFactory -``` - -For each Java interface, you can also specify the `CallAdapter.Factory` used by the current interface through `callAdapterFactories()` annotated by `@RetrofitClient`, and the specified call adapter factory instance is still preferentially obtained from the Spring container. - -**Note: If `CallAdapter.Factory` does not have a parameterless constructor of `public`, please manually configure it as the `Bean` object of the `Spring` container**! - - -### Converter - -`Retrofit` uses `Converter` to convert the object annotated with `@Body` into the request body, and the response body data into a `Java` object. The following types of `Converter` can be used: - -- [Gson](https://github.com/google/gson): com.squareup.Retrofit:converter-gson -- [Jackson](https://github.com/FasterXML/jackson): com.squareup.Retrofit:converter-jackson -- [Moshi](https://github.com/square/moshi/): com.squareup.Retrofit:converter-moshi -- [Protobuf](https://developers.google.com/protocol-buffers/): com.squareup.Retrofit:converter-protobuf -- [Wire](https://github.com/square/wire): com.squareup.Retrofit:converter-wire -- [Simple XML](http://simple.sourceforge.net/): com.squareup.Retrofit:converter-simplexml -- [JAXB](https://docs.oracle.com/javase/tutorial/jaxb/intro/index.html): com.squareup.retrofit2:converter-jaxb -- fastJson:com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory - -`retrofit-spring-boot-starter` supports configuring the global `converter factory` through `retrofit.global-converter-factories`. The converter factory instance is first obtained from the Spring container. If it is not obtained, it is created by reflection. The default global data converter factory is `retrofit2.converter.jackson.JacksonConverterFactory`, you can directly configure the `jackson` serialization rules through `spring.jackson.*`, please refer to [Customize the Jackson ObjectMapper](https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#howto-customize-the-jackson-objectmapper) - -```yaml -retrofit: - global-converter-factories: - - retrofit2.converter.jackson.JacksonConverterFactory -``` - -For each Java interface, you can also specify the `Converter.Factory` used by the current interface through `converterFactories()` annotated by `@RetrofitClient`, and the specified converter factory instance is still preferentially obtained from the Spring container. - -**Note: If `Converter.Factory` does not have a parameterless constructor of `public`, please manually configure it as the `Bean` object of the `Spring` container**! - - -## Other features - -### Upload file example - -#### Build MultipartBody.Part - -```java -// Encode file names with URLEncoder -String fileName = URLEncoder.encode(Objects.requireNonNull(file.getOriginalFilename()), "utf-8"); -okhttp3.RequestBody requestBody = okhttp3.RequestBody.create(MediaType.parse("multipart/form-data"),file.getBytes()); -MultipartBody.Part file = MultipartBody.Part.createFormData("file", fileName, requestBody); -apiService.upload(file); -``` - -#### Http upload interface - -```java -@POST("upload") -@Multipart -Void upload(@Part MultipartBody.Part file); - -``` - -### download file - -#### http download interface - -```java -@RetrofitClient(baseUrl = "https://img.ljcdn.com/hc-picture/") -public interface DownloadApi { - - @GET("{fileKey}") - Response download(@Path("fileKey") String fileKey); -} - -``` - -#### http download usage - -```java -@SpringBootTest(classes = RetrofitTestApplication.class) -@RunWith(SpringRunner.class) -public class DownloadTest { - - @Autowired - DownloadApi downLoadApi; - - @Test - public void download() throws Exception { - String fileKey = "6302d742-ebc8-4649-95cf-62ccf57a1add"; - Response response = downLoadApi.download(fileKey); - ResponseBody responseBody = response.body(); - // InputStream - InputStream is = responseBody.byteStream(); - - // The specific handling of binary streams is controlled by the business itself. Here is an example of writing a file. - File tempDirectory = new File("temp"); - if (!tempDirectory.exists()) { - tempDirectory.mkdir(); - } - File file = new File(tempDirectory, UUID.randomUUID().toString()); - if (!file.exists()) { - file.createNewFile(); - } - FileOutputStream fos = new FileOutputStream(file); - byte[] b = new byte[1024]; - int length; - while ((length = is.read(b)) > 0) { - fos.write(b, 0, length); - } - is.close(); - fos.close(); - } -} -``` - -### Dynamic URL example - -Realize dynamic URL through `@url` annotation - -**Note: `@url` must be placed in the first position of the method parameter. The original definition of `@GET`, `@POST` and other annotations do not need to define the endpoint path**! - -```java - @GET - Map test3(@Url String url,@Query("name") String name); - -``` - -## Feedback - -If you have any questions, welcome to raise issue or add QQ group to feedback. - -QQ Group Number: 806714302 - -![QQ Group](https://github.com/LianjiaTech/retrofit-spring-boot-starter/blob/master/qun.png) diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/DegradeProperty.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/DegradeProperty.java index 8c1ce39..781ae48 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/DegradeProperty.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/DegradeProperty.java @@ -1,7 +1,5 @@ package com.github.lianjiatech.retrofit.spring.boot.config; -import com.github.lianjiatech.retrofit.spring.boot.degrade.BaseResourceNameParser; -import com.github.lianjiatech.retrofit.spring.boot.degrade.DefaultResourceNameParser; import com.github.lianjiatech.retrofit.spring.boot.degrade.DegradeType; /** @@ -17,17 +15,11 @@ public class DegradeProperty { private boolean enable = false; /** - * 熔断降级类型,暂时只支持SENTINEL - * degrade type, Only SENTINEL is currently supported + * 熔断降级类型 + * degrade type */ private DegradeType degradeType = DegradeType.SENTINEL; - /** - * 资源名称解析器 - * resource name parser - */ - private Class resourceNameParser = DefaultResourceNameParser.class; - public boolean isEnable() { return enable; @@ -44,12 +36,4 @@ public DegradeType getDegradeType() { public void setDegradeType(DegradeType degradeType) { this.degradeType = degradeType; } - - public Class getResourceNameParser() { - return resourceNameParser; - } - - public void setResourceNameParser(Class resourceNameParser) { - this.resourceNameParser = resourceNameParser; - } } diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitAutoConfiguration.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitAutoConfiguration.java index 652e5e6..fe69f5f 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitAutoConfiguration.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitAutoConfiguration.java @@ -11,7 +11,9 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -28,7 +30,10 @@ import com.github.lianjiatech.retrofit.spring.boot.core.PrototypeInterceptorBdfProcessor; import com.github.lianjiatech.retrofit.spring.boot.core.RetrofitFactoryBean; import com.github.lianjiatech.retrofit.spring.boot.core.ServiceInstanceChooser; -import com.github.lianjiatech.retrofit.spring.boot.degrade.BaseResourceNameParser; +import com.github.lianjiatech.retrofit.spring.boot.degrade.DefaultResourceNameParser; +import com.github.lianjiatech.retrofit.spring.boot.degrade.DegradeInterceptor; +import com.github.lianjiatech.retrofit.spring.boot.degrade.ResourceNameParser; +import com.github.lianjiatech.retrofit.spring.boot.degrade.sentinel.SentinelDegradeInterceptor; import com.github.lianjiatech.retrofit.spring.boot.interceptor.GlobalAndNetworkInterceptorFinder; import com.github.lianjiatech.retrofit.spring.boot.interceptor.ServiceInstanceChooserInterceptor; import com.github.lianjiatech.retrofit.spring.boot.retry.BaseRetryInterceptor; @@ -49,11 +54,14 @@ public class RetrofitAutoConfiguration implements ApplicationContextAware { private static final Logger logger = LoggerFactory.getLogger(RetrofitAutoConfiguration.class); - @Autowired - private RetrofitProperties retrofitProperties; + private final RetrofitProperties retrofitProperties; private ApplicationContext applicationContext; + public RetrofitAutoConfiguration(RetrofitProperties retrofitProperties) { + this.retrofitProperties = retrofitProperties; + } + @Configuration public static class RetrofitProcessorAutoConfiguration { @@ -70,7 +78,9 @@ public GlobalAndNetworkInterceptorFinder globalAndNetworkInterceptorFinder() { @Bean @ConditionalOnMissingBean - public RetrofitConfigBean retrofitConfigBean() throws IllegalAccessException, InstantiationException { + public RetrofitConfigBean retrofitConfigBean(@Autowired(required = false) ResourceNameParser resourceNameParser, + @Autowired(required = false) DegradeInterceptor degradeInterceptor) + throws IllegalAccessException, InstantiationException { RetrofitConfigBean retrofitConfigBean = new RetrofitConfigBean(retrofitProperties, globalAndNetworkInterceptorFinder()); // Initialize the connection pool @@ -80,24 +90,28 @@ public RetrofitConfigBean retrofitConfigBean() throws IllegalAccessException, In pool.forEach((poolName, poolConfig) -> { long keepAliveSecond = poolConfig.getKeepAliveSecond(); int maxIdleConnections = poolConfig.getMaxIdleConnections(); - ConnectionPool connectionPool = new ConnectionPool(maxIdleConnections, keepAliveSecond, TimeUnit.SECONDS); + ConnectionPool connectionPool = + new ConnectionPool(maxIdleConnections, keepAliveSecond, TimeUnit.SECONDS); poolRegistry.put(poolName, connectionPool); }); } retrofitConfigBean.setPoolRegistry(poolRegistry); // callAdapterFactory - Class[] globalCallAdapterFactories = retrofitProperties.getGlobalCallAdapterFactories(); + Class[] globalCallAdapterFactories = + retrofitProperties.getGlobalCallAdapterFactories(); retrofitConfigBean.setGlobalCallAdapterFactoryClasses(globalCallAdapterFactories); // converterFactory - Class[] globalConverterFactories = retrofitProperties.getGlobalConverterFactories(); + Class[] globalConverterFactories = + retrofitProperties.getGlobalConverterFactories(); retrofitConfigBean.setGlobalConverterFactoryClasses(globalConverterFactories); // retryInterceptor RetryProperty retry = retrofitProperties.getRetry(); Class retryInterceptor = retry.getRetryInterceptor(); - BaseRetryInterceptor retryInterceptorInstance = ApplicationContextUtils.getBean(applicationContext, retryInterceptor); + BaseRetryInterceptor retryInterceptorInstance = + ApplicationContextUtils.getBeanOrNull(applicationContext, retryInterceptor); if (retryInterceptorInstance == null) { retryInterceptorInstance = retryInterceptor.newInstance(); } @@ -112,17 +126,29 @@ public RetrofitConfigBean retrofitConfigBean() throws IllegalAccessException, In serviceInstanceChooser = new NoValidServiceInstanceChooser(); } - ServiceInstanceChooserInterceptor serviceInstanceChooserInterceptor = new ServiceInstanceChooserInterceptor(serviceInstanceChooser); + ServiceInstanceChooserInterceptor serviceInstanceChooserInterceptor = + new ServiceInstanceChooserInterceptor(serviceInstanceChooser); retrofitConfigBean.setServiceInstanceChooserInterceptor(serviceInstanceChooserInterceptor); - // resource name parser - DegradeProperty degrade = retrofitProperties.getDegrade(); - Class resourceNameParser = degrade.getResourceNameParser(); - retrofitConfigBean.setResourceNameParser(resourceNameParser.newInstance()); - + retrofitConfigBean.setResourceNameParser(resourceNameParser); + retrofitConfigBean.setDegradeInterceptor(degradeInterceptor); return retrofitConfigBean; } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "retrofit.degrade.enable", havingValue = "true") + public ResourceNameParser resourceNameParser() { + return new DefaultResourceNameParser(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "retrofit.degrade.degrade-type", havingValue = "sentinel") + @ConditionalOnBean(ResourceNameParser.class) + public DegradeInterceptor degradeInterceptor(ResourceNameParser resourceNameParser) { + return new SentinelDegradeInterceptor(resourceNameParser); + } @Bean @ConditionalOnMissingBean diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitConfigBean.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitConfigBean.java index 4f55b4f..d668244 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitConfigBean.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/config/RetrofitConfigBean.java @@ -3,7 +3,8 @@ import java.util.List; import java.util.Map; -import com.github.lianjiatech.retrofit.spring.boot.degrade.BaseResourceNameParser; +import com.github.lianjiatech.retrofit.spring.boot.degrade.DegradeInterceptor; +import com.github.lianjiatech.retrofit.spring.boot.degrade.ResourceNameParser; import com.github.lianjiatech.retrofit.spring.boot.interceptor.GlobalAndNetworkInterceptorFinder; import com.github.lianjiatech.retrofit.spring.boot.interceptor.GlobalInterceptor; import com.github.lianjiatech.retrofit.spring.boot.interceptor.NetworkInterceptor; @@ -35,7 +36,9 @@ public class RetrofitConfigBean { private Class[] globalCallAdapterFactoryClasses; - private BaseResourceNameParser resourceNameParser; + private ResourceNameParser resourceNameParser; + + private DegradeInterceptor degradeInterceptor; public RetrofitProperties getRetrofitProperties() { return retrofitProperties; @@ -98,11 +101,19 @@ public void setGlobalCallAdapterFactoryClasses(Class implements FactoryBean, EnvironmentAware, private final static Logger logger = LoggerFactory.getLogger(RetrofitFactoryBean.class); private static final Map, CallAdapter.Factory> CALL_ADAPTER_FACTORIES_CACHE = - new HashMap<>(4); + new ConcurrentHashMap<>(4); private Class retrofitInterface; @@ -81,51 +83,35 @@ public class RetrofitFactoryBean implements FactoryBean, EnvironmentAware, private ApplicationContext applicationContext; - private RetrofitClient retrofitClient; - private static final Map, Converter.Factory> CONVERTER_FACTORIES_CACHE = - new HashMap<>(4); + new ConcurrentHashMap<>(4); public RetrofitFactoryBean(Class retrofitInterface) { this.retrofitInterface = retrofitInterface; - retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); } @Override - @SuppressWarnings("unchecked") public T getObject() throws Exception { - checkRetrofitInterface(retrofitInterface); - Retrofit retrofit = getRetrofit(retrofitInterface); + checkRetrofitInterface(); // source - T source = retrofit.create(retrofitInterface); - - RetrofitProperties retrofitProperties = retrofitConfigBean.getRetrofitProperties(); - Class fallbackClass = retrofitClient.fallback(); - Object fallback = null; - if (!void.class.isAssignableFrom(fallbackClass)) { - fallback = ApplicationContextUtils.getBean(applicationContext, fallbackClass); - } - Class fallbackFactoryClass = retrofitClient.fallbackFactory(); - FallbackFactory fallbackFactory = null; - if (!void.class.isAssignableFrom(fallbackFactoryClass)) { - fallbackFactory = - (FallbackFactory)ApplicationContextUtils.getBean(applicationContext, fallbackFactoryClass); + T source = createRetrofit().create(retrofitInterface); + if (!isEnableSentinelDegrade(retrofitProperties.getDegrade(), retrofitInterface)) { + return source; } + // 启用代理 loadDegradeRules(); - // proxy - return (T)Proxy.newProxyInstance(retrofitInterface.getClassLoader(), - new Class[] {retrofitInterface}, - new RetrofitInvocationHandler(source, fallback, fallbackFactory, retrofitProperties) + return DegradeProxy.create(source, retrofitInterface, applicationContext); + } - ); + public boolean isEnableSentinelDegrade(DegradeProperty degradeProperty, Class retrofitInterface) { + if (!degradeProperty.isEnable()) { + return false; + } + return AnnotationExtendUtils.isAnnotationPresent(retrofitInterface, SentinelDegrade.class); } private void loadDegradeRules() { - DegradeProperty degrade = retrofitProperties.getDegrade(); - if (!degrade.isEnable()) { - return; - } - if (degrade.getDegradeType() == DegradeType.SENTINEL) { + if (retrofitProperties.getDegrade().getDegradeType() == DegradeType.SENTINEL) { loadSentinelDegradeRules(); } } @@ -143,41 +129,27 @@ private void loadSentinelDegradeRules() { continue; } // 获取熔断配置 - SentinelDegrade sentinelDegrade = getSentinelDegrade(method); + SentinelDegrade sentinelDegrade = AnnotationExtendUtils.findAnnotation(method, SentinelDegrade.class); if (sentinelDegrade == null) { continue; } - String resourceName = retrofitConfigBean.getResourceNameParser().parseResourceName(method, environment); + DegradeRule degradeRule = new DegradeRule() .setCount(sentinelDegrade.count()) .setTimeWindow(sentinelDegrade.timeWindow()) .setGrade(sentinelDegrade.grade()); - degradeRule.setResource(resourceName); + degradeRule.setResource(retrofitConfigBean.getResourceNameParser().extractResourceName(method)); rules.add(degradeRule); } DegradeRuleManager.loadRules(rules); } - private SentinelDegrade getSentinelDegrade(Method method) { - SentinelDegrade sentinelDegrade; - if (method.isAnnotationPresent(SentinelDegrade.class)) { - sentinelDegrade = method.getAnnotation(SentinelDegrade.class); - } else { - sentinelDegrade = retrofitInterface.getAnnotation(SentinelDegrade.class); - } - return sentinelDegrade; - } - - /** - * RetrofitInterface检查 - * - * @param retrofitInterface . - */ - private void checkRetrofitInterface(Class retrofitInterface) { + private void checkRetrofitInterface() { // check class type Assert.isTrue(retrofitInterface.isInterface(), "@RetrofitClient can only be marked on the interface type!"); Method[] methods = retrofitInterface.getMethods(); + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); Assert.isTrue(StringUtils.hasText(retrofitClient.baseUrl()) || StringUtils.hasText(retrofitClient.serviceId()), "@RetrofitClient's baseUrl and serviceId must be configured with one!"); @@ -205,7 +177,7 @@ private void checkRetrofitInterface(Class retrofitInterface) { if (!void.class.isAssignableFrom(fallbackClass)) { Assert.isTrue(retrofitInterface.isAssignableFrom(fallbackClass), "The fallback type must implement the current interface!the fallback type is " + fallbackClass); - Object fallback = ApplicationContextUtils.getBean(applicationContext, fallbackClass); + Object fallback = ApplicationContextUtils.getBeanOrNull(applicationContext, fallbackClass); Assert.notNull(fallback, "fallback must be a valid spring bean! the fallback class is " + fallbackClass); } @@ -214,7 +186,7 @@ private void checkRetrofitInterface(Class retrofitInterface) { Assert.isTrue(FallbackFactory.class.isAssignableFrom(fallbackFactoryClass), "The fallback factory type must implement FallbackFactory!the fallback factory is " + fallbackFactoryClass); - Object fallbackFactory = ApplicationContextUtils.getBean(applicationContext, fallbackFactoryClass); + Object fallbackFactory = ApplicationContextUtils.getBeanOrNull(applicationContext, fallbackFactoryClass); Assert.notNull(fallbackFactory, "fallback factory must be a valid spring bean! the fallback factory class is " + fallbackFactoryClass); @@ -231,14 +203,8 @@ public boolean isSingleton() { return true; } - /** - * Get okhttp3 connection pool - * - * @param retrofitClientInterfaceClass retrofitClientInterfaceClass - * @return okhttp3 connection pool - */ - private synchronized okhttp3.ConnectionPool getConnectionPool(Class retrofitClientInterfaceClass) { - RetrofitClient retrofitClient = retrofitClientInterfaceClass.getAnnotation(RetrofitClient.class); + private okhttp3.ConnectionPool parseConnectionPool() { + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); String poolName = retrofitClient.poolName(); Map poolRegistry = retrofitConfigBean.getPoolRegistry(); Assert.notNull(poolRegistry, "poolRegistry does not exist! Please set retrofitConfigBean.poolRegistry!"); @@ -248,99 +214,32 @@ private synchronized okhttp3.ConnectionPool getConnectionPool(Class retrofitC return connectionPool; } - /** - * Get OkHttpClient instance, one interface corresponds to one OkHttpClient - * - * @param retrofitClientInterfaceClass retrofitClientInterfaceClass - * @return OkHttpClient instance - */ - private synchronized OkHttpClient getOkHttpClient(Class retrofitClientInterfaceClass) + private OkHttpClient createOkHttpClient() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { - RetrofitClient retrofitClient = retrofitClientInterfaceClass.getAnnotation(RetrofitClient.class); - Method method = findOkHttpClientBuilderMethod(retrofitClientInterfaceClass); - OkHttpClient.Builder okHttpClientBuilder; - if (method != null) { - okHttpClientBuilder = (OkHttpClient.Builder)method.invoke(null); - } else { - okhttp3.ConnectionPool connectionPool = getConnectionPool(retrofitClientInterfaceClass); - - final int connectTimeoutMs = retrofitClient.connectTimeoutMs() == -1 - ? retrofitProperties.getGlobalConnectTimeoutMs() : retrofitClient.connectTimeoutMs(); - final int readTimeoutMs = retrofitClient.readTimeoutMs() == -1 ? retrofitProperties.getGlobalReadTimeoutMs() - : retrofitClient.readTimeoutMs(); - final int writeTimeoutMs = retrofitClient.writeTimeoutMs() == -1 - ? retrofitProperties.getGlobalWriteTimeoutMs() : retrofitClient.writeTimeoutMs(); - final int callTimeoutMs = retrofitClient.callTimeoutMs() == -1 ? retrofitProperties.getGlobalCallTimeoutMs() - : retrofitClient.callTimeoutMs(); - - // Construct an OkHttpClient object - okHttpClientBuilder = new OkHttpClient.Builder() - .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS) - .readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS) - .writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS) - .callTimeout(callTimeoutMs, TimeUnit.MILLISECONDS) - .retryOnConnectionFailure(retrofitClient.retryOnConnectionFailure()) - .followRedirects(retrofitClient.followRedirects()) - .followSslRedirects(retrofitClient.followSslRedirects()) - .pingInterval(retrofitClient.pingIntervalMs(), TimeUnit.MILLISECONDS) - .connectionPool(connectionPool); - } - - // add DegradeInterceptor - DegradeProperty degradeProperty = retrofitProperties.getDegrade(); - if (degradeProperty.isEnable()) { - DegradeType degradeType = degradeProperty.getDegradeType(); - switch (degradeType) { - case SENTINEL: { - try { - Class.forName("com.alibaba.csp.sentinel.SphU"); - SentinelDegradeInterceptor sentinelDegradeInterceptor = new SentinelDegradeInterceptor(); - sentinelDegradeInterceptor.setEnvironment(environment); - sentinelDegradeInterceptor.setResourceNameParser(retrofitConfigBean.getResourceNameParser()); - okHttpClientBuilder.addInterceptor(sentinelDegradeInterceptor); - } catch (ClassNotFoundException e) { - logger.warn("com.alibaba.csp.sentinel not found! No SentinelDegradeInterceptor is set."); - } - break; - } - default: { - throw new IllegalArgumentException("Not currently supported! degradeType=" + degradeType); - } - } - } + OkHttpClient.Builder okHttpClientBuilder = createOkHttpClientBuilder(); + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); + addDegradeInterceptor(okHttpClientBuilder); + addServiceInstanceChooserInterceptor(okHttpClientBuilder, retrofitClient); + addErrorDecoderInterceptor(okHttpClientBuilder, retrofitClient); + addDefineOnInterfaceInterceptor(okHttpClientBuilder); + addGlobalInterceptor(okHttpClientBuilder); + addRetryInterceptor(okHttpClientBuilder); + addLoggingInterceptor(okHttpClientBuilder, retrofitClient); + addNetworkInterceptor(okHttpClientBuilder); + return okHttpClientBuilder.build(); + } - // add ServiceInstanceChooserInterceptor - if (StringUtils.hasText(retrofitClient.serviceId())) { - ServiceInstanceChooserInterceptor serviceInstanceChooserInterceptor = - retrofitConfigBean.getServiceInstanceChooserInterceptor(); - if (serviceInstanceChooserInterceptor != null) { - okHttpClientBuilder.addInterceptor(serviceInstanceChooserInterceptor); + private void addNetworkInterceptor(OkHttpClient.Builder okHttpClientBuilder) { + List networkInterceptors = retrofitConfigBean.getNetworkInterceptors(); + if (!CollectionUtils.isEmpty(networkInterceptors)) { + for (NetworkInterceptor networkInterceptor : networkInterceptors) { + okHttpClientBuilder.addNetworkInterceptor(networkInterceptor); } } + } - // add ErrorDecoderInterceptor - Class errorDecoderClass = retrofitClient.errorDecoder(); - ErrorDecoder decoder = ApplicationContextUtils.getBean(applicationContext, errorDecoderClass); - if (decoder == null) { - decoder = errorDecoderClass.newInstance(); - } - ErrorDecoderInterceptor decoderInterceptor = ErrorDecoderInterceptor.create(decoder); - okHttpClientBuilder.addInterceptor(decoderInterceptor); - - // Add the interceptor defined by the annotation on the interface - List interceptors = new ArrayList<>(findInterceptorByAnnotation(retrofitClientInterfaceClass)); - // add global interceptor - List globalInterceptors = retrofitConfigBean.getGlobalInterceptors(); - if (!CollectionUtils.isEmpty(globalInterceptors)) { - interceptors.addAll(globalInterceptors); - } - interceptors.forEach(okHttpClientBuilder::addInterceptor); - - // add retry interceptor - Interceptor retryInterceptor = retrofitConfigBean.getRetryInterceptor(); - okHttpClientBuilder.addInterceptor(retryInterceptor); - - // add log printing interceptor + private void addLoggingInterceptor(OkHttpClient.Builder okHttpClientBuilder, RetrofitClient retrofitClient) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { LogProperty logProperty = retrofitProperties.getLog(); if (logProperty.isEnable() && retrofitClient.enableLog()) { Class loggingInterceptorClass = logProperty.getLoggingInterceptor(); @@ -362,19 +261,82 @@ private synchronized OkHttpClient getOkHttpClient(Class retrofitClientInterfa BaseLoggingInterceptor loggingInterceptor = constructor.newInstance(logLevel, logStrategy); okHttpClientBuilder.addInterceptor(loggingInterceptor); } + } - List networkInterceptors = retrofitConfigBean.getNetworkInterceptors(); - if (!CollectionUtils.isEmpty(networkInterceptors)) { - for (NetworkInterceptor networkInterceptor : networkInterceptors) { - okHttpClientBuilder.addNetworkInterceptor(networkInterceptor); + private void addRetryInterceptor(OkHttpClient.Builder okHttpClientBuilder) { + BaseRetryInterceptor retryInterceptor = retrofitConfigBean.getRetryInterceptor(); + okHttpClientBuilder.addInterceptor(retryInterceptor); + } + + private void addGlobalInterceptor(OkHttpClient.Builder okHttpClientBuilder) { + List globalInterceptors = retrofitConfigBean.getGlobalInterceptors(); + globalInterceptors.forEach(okHttpClientBuilder::addInterceptor); + } + + private void addDefineOnInterfaceInterceptor(OkHttpClient.Builder okHttpClientBuilder) + throws InstantiationException, IllegalAccessException { + findInterceptorByAnnotation().forEach(okHttpClientBuilder::addInterceptor); + } + + private void addErrorDecoderInterceptor(OkHttpClient.Builder okHttpClientBuilder, RetrofitClient retrofitClient) + throws InstantiationException, IllegalAccessException { + Class errorDecoderClass = retrofitClient.errorDecoder(); + ErrorDecoder decoder = ApplicationContextUtils.getBeanOrNull(applicationContext, errorDecoderClass); + if (decoder == null) { + decoder = errorDecoderClass.newInstance(); + } + ErrorDecoderInterceptor decoderInterceptor = ErrorDecoderInterceptor.create(decoder); + okHttpClientBuilder.addInterceptor(decoderInterceptor); + } + + private void addServiceInstanceChooserInterceptor(OkHttpClient.Builder okHttpClientBuilder, + RetrofitClient retrofitClient) { + if (StringUtils.hasText(retrofitClient.serviceId())) { + ServiceInstanceChooserInterceptor serviceInstanceChooserInterceptor = + retrofitConfigBean.getServiceInstanceChooserInterceptor(); + if (serviceInstanceChooserInterceptor != null) { + okHttpClientBuilder.addInterceptor(serviceInstanceChooserInterceptor); } } + } - return okHttpClientBuilder.build(); + private void addDegradeInterceptor(OkHttpClient.Builder okHttpClientBuilder) { + if (isEnableSentinelDegrade(retrofitProperties.getDegrade(), retrofitInterface)) { + okHttpClientBuilder.addInterceptor(retrofitConfigBean.getDegradeInterceptor()); + } } - private Method findOkHttpClientBuilderMethod(Class retrofitClientInterfaceClass) { - Method[] methods = retrofitClientInterfaceClass.getMethods(); + private OkHttpClient.Builder createOkHttpClientBuilder() throws InvocationTargetException, IllegalAccessException { + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); + Method method = findOkHttpClientBuilderMethod(); + if (method != null) { + return (OkHttpClient.Builder)method.invoke(null); + } + okhttp3.ConnectionPool connectionPool = parseConnectionPool(); + final int connectTimeoutMs = retrofitClient.connectTimeoutMs() == -1 + ? retrofitProperties.getGlobalConnectTimeoutMs() : retrofitClient.connectTimeoutMs(); + final int readTimeoutMs = retrofitClient.readTimeoutMs() == -1 ? retrofitProperties.getGlobalReadTimeoutMs() + : retrofitClient.readTimeoutMs(); + final int writeTimeoutMs = retrofitClient.writeTimeoutMs() == -1 + ? retrofitProperties.getGlobalWriteTimeoutMs() : retrofitClient.writeTimeoutMs(); + final int callTimeoutMs = retrofitClient.callTimeoutMs() == -1 ? retrofitProperties.getGlobalCallTimeoutMs() + : retrofitClient.callTimeoutMs(); + + // Construct an OkHttpClient object + return new OkHttpClient.Builder() + .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS) + .readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS) + .writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS) + .callTimeout(callTimeoutMs, TimeUnit.MILLISECONDS) + .retryOnConnectionFailure(retrofitClient.retryOnConnectionFailure()) + .followRedirects(retrofitClient.followRedirects()) + .followSslRedirects(retrofitClient.followSslRedirects()) + .pingInterval(retrofitClient.pingIntervalMs(), TimeUnit.MILLISECONDS) + .connectionPool(connectionPool); + } + + private Method findOkHttpClientBuilderMethod() { + Method[] methods = retrofitInterface.getMethods(); for (Method method : methods) { if (Modifier.isStatic(method.getModifiers()) && method.isAnnotationPresent(OkHttpClientBuilder.class) @@ -385,17 +347,10 @@ private Method findOkHttpClientBuilderMethod(Class retrofitClientInterfaceCla return null; } - /** - * 获取retrofitClient接口类上定义的拦截器集合 - * Get the interceptor set defined on the retrofitClient interface class - * - * @param retrofitClientInterfaceClass retrofitClientInterfaceClass - * @return the interceptor list - */ @SuppressWarnings("unchecked") - private List findInterceptorByAnnotation(Class retrofitClientInterfaceClass) + private List findInterceptorByAnnotation() throws InstantiationException, IllegalAccessException { - Annotation[] classAnnotations = retrofitClientInterfaceClass.getAnnotations(); + Annotation[] classAnnotations = retrofitInterface.getAnnotations(); List interceptors = new ArrayList<>(); // 找出被@InterceptMark标记的注解。Find the annotation marked by @InterceptMark List interceptAnnotations = new ArrayList<>(); @@ -406,9 +361,7 @@ private List findInterceptorByAnnotation(Class retrofitClientInt } if (classAnnotation instanceof Intercepts) { Intercept[] value = ((Intercepts)classAnnotation).value(); - for (Intercept intercept : value) { - interceptAnnotations.add(intercept); - } + interceptAnnotations.addAll(Arrays.asList(value)); } } for (Annotation interceptAnnotation : interceptAnnotations) { @@ -424,7 +377,8 @@ private List findInterceptorByAnnotation(Class retrofitClientInt Class interceptorClass = (Class)handler; BasePathMatchInterceptor interceptor = - ApplicationContextUtils.getTargetInstanceIfNecessary(getInterceptorInstance(interceptorClass)); + ApplicationContextUtils.getTargetInstanceIfNecessary( + ApplicationContextUtils.getBeanOrNew(applicationContext, interceptorClass)); Map annotationResolveAttributes = new HashMap<>(8); // 占位符属性替换。Placeholder attribute replacement annotationAttributes.forEach((key, value) -> { @@ -442,39 +396,14 @@ private List findInterceptorByAnnotation(Class retrofitClientInt return interceptors; } - /** - * 获取路径拦截器实例,优先从spring容器中取。如果spring容器中不存在,则无参构造器实例化一个。 - * Obtain the path interceptor instance, first from the spring container. If it does not exist in the spring container, the no-argument constructor will instantiate one. - * - * @param interceptorClass A subclass of @{@link BasePathMatchInterceptor} - * @return @{@link BasePathMatchInterceptor} instance - */ - private BasePathMatchInterceptor getInterceptorInstance(Class interceptorClass) - throws IllegalAccessException, InstantiationException { - // spring bean - try { - return applicationContext.getBean(interceptorClass); - } catch (BeansException e) { - // spring容器获取失败,反射创建 - return interceptorClass.newInstance(); - } - } - - /** - * 获取Retrofit实例,一个retrofitClient接口对应一个Retrofit实例 - * Obtain a Retrofit instance, a retrofitClient interface corresponds to a Retrofit instance - * - * @param retrofitClientInterfaceClass retrofitClientInterfaceClass - * @return Retrofit instance - */ - private synchronized Retrofit getRetrofit(Class retrofitClientInterfaceClass) + private Retrofit createRetrofit() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { - RetrofitClient retrofitClient = retrofitClientInterfaceClass.getAnnotation(RetrofitClient.class); + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); String baseUrl = retrofitClient.baseUrl(); baseUrl = RetrofitUtils.convertBaseUrl(retrofitClient, baseUrl, environment); - OkHttpClient client = getOkHttpClient(retrofitClientInterfaceClass); + OkHttpClient client = createOkHttpClient(); Retrofit.Builder retrofitBuilder = new Retrofit.Builder() .baseUrl(baseUrl) .validateEagerly(retrofitClient.validateEagerly()) @@ -525,7 +454,7 @@ private List getCallAdapterFactories( for (Class callAdapterFactoryClass : combineCallAdapterFactoryClasses) { CallAdapter.Factory callAdapterFactory = CALL_ADAPTER_FACTORIES_CACHE.get(callAdapterFactoryClass); if (callAdapterFactory == null) { - callAdapterFactory = ApplicationContextUtils.getBean(applicationContext, callAdapterFactoryClass); + callAdapterFactory = ApplicationContextUtils.getBeanOrNull(applicationContext, callAdapterFactoryClass); if (callAdapterFactory == null) { callAdapterFactory = callAdapterFactoryClass.newInstance(); } @@ -558,7 +487,7 @@ private List getConverterFactories(Class converterFactoryClass : combineConverterFactoryClasses) { Converter.Factory converterFactory = CONVERTER_FACTORIES_CACHE.get(converterFactoryClass); if (converterFactory == null) { - converterFactory = ApplicationContextUtils.getBean(applicationContext, converterFactoryClass); + converterFactory = ApplicationContextUtils.getBeanOrNull(applicationContext, converterFactoryClass); if (converterFactory == null) { converterFactory = converterFactoryClass.newInstance(); } diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/core/RetrofitInvocationHandler.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/core/RetrofitInvocationHandler.java deleted file mode 100644 index 7909271..0000000 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/core/RetrofitInvocationHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.lianjiatech.retrofit.spring.boot.core; - -import com.github.lianjiatech.retrofit.spring.boot.config.DegradeProperty; -import com.github.lianjiatech.retrofit.spring.boot.config.RetrofitProperties; -import com.github.lianjiatech.retrofit.spring.boot.degrade.FallbackFactory; -import com.github.lianjiatech.retrofit.spring.boot.degrade.RetrofitBlockException; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; - -/** - * @author 陈添明 - */ -public class RetrofitInvocationHandler implements InvocationHandler { - - private final Object source; - - private final DegradeProperty degradeProperty; - - private Object fallback; - - private FallbackFactory fallbackFactory; - - - public RetrofitInvocationHandler(Object source, Object fallback, FallbackFactory fallbackFactory, RetrofitProperties retrofitProperties) { - this.source = source; - this.degradeProperty = retrofitProperties.getDegrade(); - this.fallback = fallback; - this.fallbackFactory = fallbackFactory; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try { - return method.invoke(source, args); - } catch (Throwable e) { - Throwable cause = e.getCause(); - // 熔断逻辑 - if (cause instanceof RetrofitBlockException && degradeProperty.isEnable()) { - Object fallbackObject = getFallbackObject(cause); - if (fallbackObject != null) { - return method.invoke(fallbackObject, args); - } - } - throw cause; - } - } - - private Object getFallbackObject(Throwable cause) { - if (fallback != null) { - return fallback; - } - - if (fallbackFactory != null) { - return fallbackFactory.create(cause); - } - return null; - } -} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseDegradeInterceptor.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseDegradeInterceptor.java deleted file mode 100644 index 547af27..0000000 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseDegradeInterceptor.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.lianjiatech.retrofit.spring.boot.degrade; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.springframework.core.env.Environment; -import retrofit2.Invocation; - -import java.io.IOException; -import java.lang.reflect.Method; - -/** - * @author 陈添明 - */ -public abstract class BaseDegradeInterceptor implements Interceptor { - - private Environment environment; - - private BaseResourceNameParser resourceNameParser; - - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - public void setResourceNameParser(BaseResourceNameParser resourceNameParser) { - this.resourceNameParser = resourceNameParser; - } - - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - Invocation invocation = request.tag(Invocation.class); - assert invocation != null; - Method method = invocation.method(); - String resourceName = resourceNameParser.parseResourceName(method, environment); - return degradeIntercept(resourceName, chain); - } - - - /** - * 熔断拦截处理 - * - * @param resourceName 资源名称 - * @param chain 请求执行链 - * @return 请求响应 - * @throws RetrofitBlockException 如果触发熔断,抛出RetrofitBlockException异常! - * @throws IOException IOException - * - */ - protected abstract Response degradeIntercept(String resourceName, Chain chain) throws RetrofitBlockException, IOException; -} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseResourceNameParser.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseResourceNameParser.java deleted file mode 100644 index 750287d..0000000 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/BaseResourceNameParser.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.lianjiatech.retrofit.spring.boot.degrade; - -import com.github.lianjiatech.retrofit.spring.boot.annotation.RetrofitClient; -import com.github.lianjiatech.retrofit.spring.boot.util.RetrofitUtils; -import org.springframework.core.env.Environment; -import retrofit2.http.*; - -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author 陈添明 - */ -public abstract class BaseResourceNameParser { - - - private static Map RESOURCE_NAME_CACHE = new ConcurrentHashMap<>(128); - - - public String parseResourceName(Method method, Environment environment) { - String resourceName = RESOURCE_NAME_CACHE.get(method); - if (resourceName != null) { - return resourceName; - } - Class declaringClass = method.getDeclaringClass(); - RetrofitClient retrofitClient = declaringClass.getAnnotation(RetrofitClient.class); - String baseUrl = retrofitClient.baseUrl(); - baseUrl = RetrofitUtils.convertBaseUrl(retrofitClient, baseUrl, environment); - HttpMethodPath httpMethodPath = parseHttpMethodPath(method); - resourceName = defineResourceName(baseUrl, httpMethodPath); - RESOURCE_NAME_CACHE.put(method, resourceName); - return resourceName; - } - - /** - * define resource name. - * - * @param baseUrl baseUrl - * @param httpMethodPath httpMethodPath - * @return resource name. - */ - protected abstract String defineResourceName(String baseUrl, HttpMethodPath httpMethodPath); - - - protected HttpMethodPath parseHttpMethodPath(Method method) { - - if (method.isAnnotationPresent(HTTP.class)) { - HTTP http = method.getAnnotation(HTTP.class); - return new HttpMethodPath(http.method(), http.path()); - } - - if (method.isAnnotationPresent(GET.class)) { - GET get = method.getAnnotation(GET.class); - return new HttpMethodPath("GET", get.value()); - } - - if (method.isAnnotationPresent(POST.class)) { - POST post = method.getAnnotation(POST.class); - return new HttpMethodPath("POST", post.value()); - } - - if (method.isAnnotationPresent(PUT.class)) { - PUT put = method.getAnnotation(PUT.class); - return new HttpMethodPath("PUT", put.value()); - } - - if (method.isAnnotationPresent(DELETE.class)) { - DELETE delete = method.getAnnotation(DELETE.class); - return new HttpMethodPath("DELETE", delete.value()); - } - - if (method.isAnnotationPresent(HEAD.class)) { - HEAD head = method.getAnnotation(HEAD.class); - return new HttpMethodPath("HEAD", head.value()); - } - - if (method.isAnnotationPresent(PATCH.class)) { - PATCH patch = method.getAnnotation(PATCH.class); - return new HttpMethodPath("PATCH", patch.value()); - } - - return null; - } - -} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DefaultResourceNameParser.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DefaultResourceNameParser.java index 2d81554..d8cef9d 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DefaultResourceNameParser.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DefaultResourceNameParser.java @@ -1,22 +1,43 @@ package com.github.lianjiatech.retrofit.spring.boot.degrade; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; + +import com.github.lianjiatech.retrofit.spring.boot.annotation.RetrofitClient; +import com.github.lianjiatech.retrofit.spring.boot.util.RetrofitUtils; + /** * @author 陈添明 */ -public class DefaultResourceNameParser extends BaseResourceNameParser { +public class DefaultResourceNameParser implements ResourceNameParser, EnvironmentAware { - private static String PREFIX = "HTTP_OUT"; + private static final Map RESOURCE_NAME_CACHE = new ConcurrentHashMap<>(128); + + private Environment environment; - /** - * define resource name. - * - * @param baseUrl baseUrl - * @param httpMethodPath httpMethodPath - * @return resource name. - */ @Override - protected String defineResourceName(String baseUrl, HttpMethodPath httpMethodPath) { + public String extractResourceName(Method method) { + String resourceName = RESOURCE_NAME_CACHE.get(method); + if (resourceName != null) { + return resourceName; + } + Class declaringClass = method.getDeclaringClass(); + RetrofitClient retrofitClient = declaringClass.getAnnotation(RetrofitClient.class); + String baseUrl = retrofitClient.baseUrl(); + baseUrl = RetrofitUtils.convertBaseUrl(retrofitClient, baseUrl, environment); + HttpMethodPath httpMethodPath = parseHttpMethodPath(method); + resourceName = + String.format("%s:%s:%s", HTTP_OUT, httpMethodPath.getMethod(), baseUrl + httpMethodPath.getPath()); + RESOURCE_NAME_CACHE.put(method, resourceName); + return resourceName; + } - return String.format("%s:%s:%s", PREFIX, httpMethodPath.getMethod(), baseUrl + httpMethodPath.getPath()); + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; } } diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeInterceptor.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeInterceptor.java new file mode 100644 index 0000000..7562774 --- /dev/null +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeInterceptor.java @@ -0,0 +1,9 @@ +package com.github.lianjiatech.retrofit.spring.boot.degrade; + +import okhttp3.Interceptor; + +/** + * @author 陈添明 + * @since 2022/4/30 3:34 下午 + */ +public interface DegradeInterceptor extends Interceptor {} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeProxy.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeProxy.java new file mode 100644 index 0000000..b991bf0 --- /dev/null +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/DegradeProxy.java @@ -0,0 +1,76 @@ +package com.github.lianjiatech.retrofit.spring.boot.degrade; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.context.ApplicationContext; + +import com.github.lianjiatech.retrofit.spring.boot.annotation.RetrofitClient; +import com.github.lianjiatech.retrofit.spring.boot.util.ApplicationContextUtils; + +/** + * @author 陈添明 + * @since 2022/4/30 2:27 下午 + */ +public class DegradeProxy implements InvocationHandler { + + private final Object source; + + private final Object fallback; + + private final FallbackFactory fallbackFactory; + + @SuppressWarnings("unchecked") + public static T create(Object source, Class retrofitInterface, ApplicationContext applicationContext) { + RetrofitClient retrofitClient = retrofitInterface.getAnnotation(RetrofitClient.class); + Class fallbackClass = retrofitClient.fallback(); + Object fallback = null; + if (!void.class.isAssignableFrom(fallbackClass)) { + fallback = ApplicationContextUtils.getBeanOrNull(applicationContext, fallbackClass); + } + Class fallbackFactoryClass = retrofitClient.fallbackFactory(); + FallbackFactory fallbackFactory = null; + if (!void.class.isAssignableFrom(fallbackFactoryClass)) { + fallbackFactory = + (FallbackFactory)ApplicationContextUtils.getBeanOrNull(applicationContext, fallbackFactoryClass); + } + DegradeProxy degradeProxy = new DegradeProxy(source, fallback, fallbackFactory); + return (T)Proxy.newProxyInstance(retrofitInterface.getClassLoader(), + new Class[] {retrofitInterface}, degradeProxy); + } + + public DegradeProxy(Object source, Object fallback, FallbackFactory fallbackFactory) { + this.source = source; + this.fallback = fallback; + this.fallbackFactory = fallbackFactory; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + return method.invoke(source, args); + } catch (Throwable e) { + Throwable cause = e.getCause(); + // 熔断逻辑 + if (cause instanceof RetrofitBlockException) { + Object fallbackObject = getFallbackObject(cause); + if (fallbackObject != null) { + return method.invoke(fallbackObject, args); + } + } + throw cause; + } + } + + private Object getFallbackObject(Throwable cause) { + if (fallback != null) { + return fallback; + } + + if (fallbackFactory != null) { + return fallbackFactory.create(cause); + } + return null; + } +} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/ResourceNameParser.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/ResourceNameParser.java new file mode 100644 index 0000000..1aaf0ad --- /dev/null +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/ResourceNameParser.java @@ -0,0 +1,72 @@ +package com.github.lianjiatech.retrofit.spring.boot.degrade; + +import java.lang.reflect.Method; + +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.HEAD; +import retrofit2.http.HTTP; +import retrofit2.http.PATCH; +import retrofit2.http.POST; +import retrofit2.http.PUT; + +/** + * @author 陈添明 + * @since 2022/4/30 3:55 下午 + */ +public interface ResourceNameParser { + + String HTTP_OUT = "HTTP_OUT"; + + /** + * 提取资源名称 + * @param method 方法 + * @return 资源名称 + */ + String extractResourceName(Method method); + + /** + * 解析方法路径 + * @param method 方法 + * @return 方法路径 + */ + default HttpMethodPath parseHttpMethodPath(Method method) { + + if (method.isAnnotationPresent(HTTP.class)) { + HTTP http = method.getAnnotation(HTTP.class); + return new HttpMethodPath(http.method(), http.path()); + } + + if (method.isAnnotationPresent(GET.class)) { + GET get = method.getAnnotation(GET.class); + return new HttpMethodPath("GET", get.value()); + } + + if (method.isAnnotationPresent(POST.class)) { + POST post = method.getAnnotation(POST.class); + return new HttpMethodPath("POST", post.value()); + } + + if (method.isAnnotationPresent(PUT.class)) { + PUT put = method.getAnnotation(PUT.class); + return new HttpMethodPath("PUT", put.value()); + } + + if (method.isAnnotationPresent(DELETE.class)) { + DELETE delete = method.getAnnotation(DELETE.class); + return new HttpMethodPath("DELETE", delete.value()); + } + + if (method.isAnnotationPresent(HEAD.class)) { + HEAD head = method.getAnnotation(HEAD.class); + return new HttpMethodPath("HEAD", head.value()); + } + + if (method.isAnnotationPresent(PATCH.class)) { + PATCH patch = method.getAnnotation(PATCH.class); + return new HttpMethodPath("PATCH", patch.value()); + } + + return null; + } +} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegradeInterceptor.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegradeInterceptor.java deleted file mode 100644 index 8fdf478..0000000 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegradeInterceptor.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.lianjiatech.retrofit.spring.boot.degrade; - -import com.alibaba.csp.sentinel.*; -import com.alibaba.csp.sentinel.slots.block.BlockException; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - -/** - * @author 陈添明 - */ -public class SentinelDegradeInterceptor extends BaseDegradeInterceptor { - - /** - * 熔断拦截处理 - * - * @param chain 请求执行链 - * @return 请求响应 - * @throws RetrofitBlockException 如果触发熔断,抛出RetrofitBlockException异常! - */ - @Override - protected Response degradeIntercept(String resourceName, Chain chain) throws RetrofitBlockException, IOException { - Request request = chain.request(); - Entry entry = null; - try { - entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); - return chain.proceed(request); - } catch (BlockException e) { - throw new RetrofitBlockException(e); - } finally { - if (entry != null) { - entry.exit(); - } - } - } -} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegrade.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegrade.java similarity index 76% rename from src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegrade.java rename to src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegrade.java index 7e87f15..ca96b44 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/SentinelDegrade.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegrade.java @@ -1,4 +1,4 @@ -package com.github.lianjiatech.retrofit.spring.boot.degrade; +package com.github.lianjiatech.retrofit.spring.boot.degrade.sentinel; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -15,7 +15,7 @@ public @interface SentinelDegrade { /** - * RT模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 + * 各降级策略对应的阈值。平均响应时间(ms),异常比例(0-1),异常数量(1-N) */ double count(); diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegradeInterceptor.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegradeInterceptor.java new file mode 100644 index 0000000..791396b --- /dev/null +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/degrade/sentinel/SentinelDegradeInterceptor.java @@ -0,0 +1,52 @@ +package com.github.lianjiatech.retrofit.spring.boot.degrade.sentinel; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Objects; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.github.lianjiatech.retrofit.spring.boot.degrade.DegradeInterceptor; +import com.github.lianjiatech.retrofit.spring.boot.degrade.ResourceNameParser; +import com.github.lianjiatech.retrofit.spring.boot.degrade.RetrofitBlockException; +import com.github.lianjiatech.retrofit.spring.boot.util.AnnotationExtendUtils; + +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Invocation; + +/** + * @author 陈添明 + */ +public class SentinelDegradeInterceptor implements DegradeInterceptor { + + private ResourceNameParser resourceNameParser; + + public SentinelDegradeInterceptor(ResourceNameParser resourceNameParser) { + this.resourceNameParser = resourceNameParser; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Method method = Objects.requireNonNull(request.tag(Invocation.class)).method(); + if (AnnotationExtendUtils.findAnnotation(method, SentinelDegrade.class) == null) { + return chain.proceed(request); + } + String resourceName = resourceNameParser.extractResourceName(method); + Entry entry = null; + try { + entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + return chain.proceed(request); + } catch (BlockException e) { + throw new RetrofitBlockException(e); + } finally { + if (entry != null) { + entry.exit(); + } + } + } +} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/retry/BaseRetryInterceptor.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/retry/BaseRetryInterceptor.java index d6b10b6..7677de6 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/retry/BaseRetryInterceptor.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/retry/BaseRetryInterceptor.java @@ -1,13 +1,15 @@ package com.github.lianjiatech.retrofit.spring.boot.retry; +import java.io.IOException; +import java.lang.reflect.Method; + +import com.github.lianjiatech.retrofit.spring.boot.util.AnnotationExtendUtils; + import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; import retrofit2.Invocation; -import java.io.IOException; -import java.lang.reflect.Method; - /** * 请求重试拦截器 * Request retry interceptor @@ -33,7 +35,7 @@ public Response intercept(Chain chain) throws IOException { assert invocation != null; Method method = invocation.method(); // 获取重试配置 - Retry retry = getRetry(method); + Retry retry = AnnotationExtendUtils.findAnnotation(method, Retry.class); if (!needRetry(retry)) { return chain.proceed(request); @@ -71,18 +73,6 @@ private boolean needRetry(Retry retry) { return false; } - private Retry getRetry(Method method) { - Retry retry; - if (method.isAnnotationPresent(Retry.class)) { - retry = method.getAnnotation(Retry.class); - } else { - Class declaringClass = method.getDeclaringClass(); - retry = declaringClass.getAnnotation(Retry.class); - } - return retry; - } - - /** * process a retryable request * The access level here is set to protected, which can facilitate business personalized expansion diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/AnnotationExtendUtils.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/AnnotationExtendUtils.java new file mode 100644 index 0000000..2162d9f --- /dev/null +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/AnnotationExtendUtils.java @@ -0,0 +1,48 @@ +package com.github.lianjiatech.retrofit.spring.boot.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * @author 陈添明 + * @since 2022/4/30 3:02 下午 + */ +public class AnnotationExtendUtils { + + /** + * 查找方法上的Annotation,如果不存在,则查找类上的。 + * @param method 方法 + * @param annotationType 注解类型 + * @param 注解泛型参数 + * @return 方法或者类上指定的注解。 + */ + public static A findAnnotation(Method method, @Nullable Class annotationType) { + A annotation = method.getAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + return method.getDeclaringClass().getAnnotation(annotationType); + } + + /** + * 判断某个类上指定的Annotation是否存在。如果类上不存在,则继续判断每个公有方法是否存在。 + * @param clazz 类 + * @param annotationType 注解类型 + * @param 注解泛型参数 + * @return 某个类上指定的Annotation是否存在。类或公有方法存在,则返回true。 + */ + public static boolean isAnnotationPresent(Class clazz, Class annotationType) { + if (clazz.isAnnotationPresent(annotationType)) { + return true; + } + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(annotationType)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/ApplicationContextUtils.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/ApplicationContextUtils.java index 2500677..7af1396 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/ApplicationContextUtils.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/ApplicationContextUtils.java @@ -14,16 +14,24 @@ private ApplicationContextUtils() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - - public static T getBean(ApplicationContext context, Class clz) { + public static T getBeanOrNull(ApplicationContext context, Class clz) { try { - T bean = context.getBean(clz); - return bean; + return context.getBean(clz); } catch (BeansException e) { return null; } } + public static T getBeanOrNew(ApplicationContext context, Class clz) + throws InstantiationException, IllegalAccessException { + try { + return context.getBean(clz); + } catch (BeansException e) { + return clz.newInstance(); + } + } + + @SuppressWarnings("unchecked") public static T getTargetInstanceIfNecessary(T bean) { Object object = bean; while (AopUtils.isAopProxy(object)) { diff --git a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/RetrofitUtils.java b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/RetrofitUtils.java index f41ce73..59f508a 100644 --- a/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/RetrofitUtils.java +++ b/src/main/java/com/github/lianjiatech/retrofit/spring/boot/util/RetrofitUtils.java @@ -1,7 +1,15 @@ package com.github.lianjiatech.retrofit.spring.boot.util; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + import com.github.lianjiatech.retrofit.spring.boot.annotation.RetrofitClient; import com.github.lianjiatech.retrofit.spring.boot.exception.ReadResponseBodyException; + import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.Response; @@ -9,29 +17,24 @@ import okio.Buffer; import okio.BufferedSource; import okio.GzipSource; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; - -import java.io.IOException; -import java.nio.charset.Charset; /** * @author 陈添明 */ public final class RetrofitUtils { - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; public static final String GZIP = "gzip"; public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String IDENTITY = "identity"; private static final String SUFFIX = "/"; + public static final String HTTP_PREFIX = "http://"; private RetrofitUtils() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** * read ResponseBody as String * @@ -100,7 +103,7 @@ public static String convertBaseUrl(RetrofitClient retrofitClient, String baseUr if (!path.endsWith(SUFFIX)) { path += SUFFIX; } - baseUrl = "http://" + (serviceId + SUFFIX + path).replaceAll("/+", SUFFIX); + baseUrl = HTTP_PREFIX + (serviceId + SUFFIX + path).replaceAll("/+", SUFFIX); baseUrl = environment.resolveRequiredPlaceholders(baseUrl); } return baseUrl; diff --git a/src/test/java/com/github/lianjiatech/retrofit/spring/boot/test/http/DegradeApi.java b/src/test/java/com/github/lianjiatech/retrofit/spring/boot/test/http/DegradeApi.java index 5f8a6f7..49ae5d3 100644 --- a/src/test/java/com/github/lianjiatech/retrofit/spring/boot/test/http/DegradeApi.java +++ b/src/test/java/com/github/lianjiatech/retrofit/spring/boot/test/http/DegradeApi.java @@ -1,7 +1,7 @@ package com.github.lianjiatech.retrofit.spring.boot.test.http; import com.github.lianjiatech.retrofit.spring.boot.annotation.RetrofitClient; -import com.github.lianjiatech.retrofit.spring.boot.degrade.SentinelDegrade; +import com.github.lianjiatech.retrofit.spring.boot.degrade.sentinel.SentinelDegrade; import com.github.lianjiatech.retrofit.spring.boot.test.entity.Person; import com.github.lianjiatech.retrofit.spring.boot.test.entity.Result;