├── .gitignore ├── LICENSE ├── README-en.md ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── dadiyang │ └── httpinvoker │ ├── HttpApiInvoker.java │ ├── HttpApiProxyFactory.java │ ├── annotation │ ├── ContentType.java │ ├── Cookies.java │ ├── ExpectedCode.java │ ├── Form.java │ ├── Headers.java │ ├── HttpApi.java │ ├── HttpApiScan.java │ ├── HttpReq.java │ ├── NotResultBean.java │ ├── Param.java │ ├── RetryPolicy.java │ └── UserAgent.java │ ├── enumeration │ └── ReqMethod.java │ ├── exception │ └── UnexpectedResultException.java │ ├── mocker │ ├── MockRequestor.java │ ├── MockResponse.java │ └── MockRule.java │ ├── propertyresolver │ ├── EnvironmentBasePropertyResolver.java │ ├── MultiSourcePropertyResolver.java │ ├── PropertiesBasePropertyResolver.java │ └── PropertyResolver.java │ ├── requestor │ ├── DefaultHttpRequestor.java │ ├── DefaultResponseProcessor.java │ ├── HttpClientRequestor.java │ ├── HttpClientResponse.java │ ├── HttpRequest.java │ ├── HttpResponse.java │ ├── JsoupHttpResponse.java │ ├── JsoupRequestor.java │ ├── MultiPart.java │ ├── RequestPreprocessor.java │ ├── Requestor.java │ ├── ResponseProcessor.java │ ├── ResultBeanResponseProcessor.java │ └── Status.java │ ├── serializer │ ├── FastJsonJsonSerializer.java │ ├── GsonJsonSerializer.java │ ├── JsonSerializer.java │ └── JsonSerializerDecider.java │ ├── spring │ ├── ClassPathHttpApiScanner.java │ ├── HttpApiConfigurer.java │ └── HttpApiProxyFactoryBean.java │ └── util │ ├── IoUtils.java │ ├── ObjectUtils.java │ ├── ParamUtils.java │ ├── ReflectionUtils.java │ └── StringUtils.java └── test ├── java └── com │ └── github │ └── dadiyang │ └── httpinvoker │ ├── HttpApiProxyFactoryTest.java │ ├── TestApplication.java │ ├── entity │ ├── City.java │ ├── ComplicatedInfo.java │ ├── ResultBean.java │ └── ResultBeanWithStatusAsCode.java │ ├── interfaces │ ├── CityService.java │ ├── CityServiceErrorTest.java │ ├── CityServiceMockRequestorTest.java │ ├── CityServiceSpringTest.java │ └── CityServiceTest.java │ ├── requestor │ └── DefaultHttpRequestorTest.java │ ├── serializer │ ├── JsonSerializerDeciderTest.java │ └── JsonSerializerTest.java │ └── util │ ├── CityUtil.java │ └── ParamUtilsTest.java └── resources ├── conf.properties ├── conf2.properties └── log4j.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # for eclipse 2 | .classpath 3 | .project 4 | /.settings/ 5 | /target/ 6 | 7 | # IntelliJ project files 8 | *.idea 9 | *.iml 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dadiyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # HTTP API INVOKER 2 | 3 | **Make HTTP api invokes as natural and elegant as calling local methods.** 4 | 5 | Binding HTTP url to interface, the framework will generate a proxy class which send HTTP request and handle response when we call the interface's method. Generic is supported. 6 | 7 | The only thing we need to do is defining the interface. 8 | 9 | # FEATURE 10 | 11 | 1. Just define interface, and the framework provide the proxy implement like MyBatis. 12 | 2. Light-weigh. 13 | 3. Upload and download file is supported. 14 | 4. Autowired annotation is supported if integrate with Spring. 15 | 5. Well documented and unit tested. 16 | 6. Mock supported 17 | 7. JDK6+ (1.2.0 and above) 18 | 19 | # TECHNOLOGY STACK 20 | 21 | * dynamic proxy 22 | * reflection 23 | * annotation 24 | * auto package scanning 25 | 26 | # GET STARTED 27 | 28 | ## I. Add maven dependency 29 | 30 | ```xml 31 | 32 | com.github.dadiyang 33 | http-api-invoker 34 | 1.2.4 35 | 36 | 37 | 38 | com.alibaba 39 | fastjson 40 | 1.2.75 41 | 42 | ``` 43 | ## II. Define interface 44 | 45 | Provided `http://localhost:8080/city/allCities` response: 46 | ```json 47 | [ 48 | { 49 | "id":1, 50 | "name":"beijing" 51 | }, 52 | { 53 | "id":2, 54 | "name":"shanghai" 55 | } 56 | ] 57 | ``` 58 | 59 | Define an interface like this: 60 | ```java 61 | @HttpApi 62 | public interface CityService { 63 | @HttpReq("http://localhost:8080/city/allCities") 64 | List getAllCities(); 65 | } 66 | ``` 67 | 68 | ## III. Get proxy 69 | 70 | ### HttpApiProxyFactory 71 | 72 | Now we can get a proxy implement of the interface by calling 73 | 74 | `CityService cityService = HttpApiProxyFactory.getProxy(CityService.class)` 75 | 76 | And just use this instance to send a request to the HTTP api: 77 | 78 | `List cities = cityService.getAllCities()` 79 | 80 | ### Spring Integration 81 | 82 | #### Configuration 83 | 84 | Add @HttpApiScan to a @Configuration class for enabling package scanning. 85 | 86 | ```java 87 | @Configuration 88 | @HttpApiScan 89 | public class TestApplication { 90 | } 91 | ``` 92 | 93 | #### Autowired the interface 94 | 95 | ```java 96 | @Autowired 97 | private CityService cityService; 98 | 99 | public void test() { 100 | List cities = cityService.getAllCities(); 101 | for (City city : cities) { 102 | System.out.println(city.getId() + ", " + city.getName()); 103 | } 104 | } 105 | ``` 106 | 107 | Note: if your IDE complain "Could not autowired. no beans of type 'xxx' type found", just ignore that message. 108 | 109 | ## IV. PLACEHOLDER 110 | 111 | Placeholder is supported for reading config properties (using **${}**, like ${api.url.city}) and path variables (using **{}**, like {cityName}). 112 | 113 | Note: Params matched the path variables will be removed from the request body, but we can use placeholder **#{id}** to keep it. 114 | 115 | We can use placeholder in **@HttpApi's prefix and @HttpReq's url**. 116 | 117 | Note: 118 | 119 | - path variable using **`{}`**, 120 | - path variable and keep it in request params using **`#{}`**, 121 | - and config using **`${}`**. 122 | - set default value through : , such as ${api.url.city:beijing} {cityId:1} #{cityId:} 123 | 124 | The framework will get the config property from: 125 | 126 | * **property file** set by **configPaths** in @HttpApiScan, 127 | * **System property**: System.getProperty("property"), 128 | * and **Spring Environment** in Spring integration scenario. 129 | 130 | ## V. Retry policy 131 | 132 | In some cases, we need to retry a request if network is not available, response status code is not 2xx etc. We can use `@RetryPolicy` annotation to indicate that this method need to be retry when an unexpected condition occur. It can be annotated on method and class. The class's policy prior to the method's. 133 | 134 | * times: try times, 3 by default; 135 | * retryFor: what exception to retry, IOException by default; 136 | * retryForStatus: what status code would retry, other than 20x by default; 137 | * fixedBackOffPeriod: back off strategy, the number of seconds to sleep when retry is required, not to sleep by default. 138 | 139 | 140 | ## VI. EXTENSION 141 | 142 | ### RequestPreprocessor 143 | 144 | Sometimes, we need to get all request added some specific headers, cookies or params. 145 | 146 | It would be redundant to add these stuff. 147 | 148 | Now we can register a RequestPreprocessor. RequestPreprocessor's process() method will be call when a request is prepared but not yet send. 149 | 150 | We are provided a chance to access the request and set anything we need. 151 | 152 | ```java 153 | public void preprocessorTest() { 154 | HttpApiProxyFactory factory = new HttpApiProxyFactory(request -> { 155 | // we add cookie and header for all request invoked by the proxy get from this factory 156 | request.addCookie("authCookies", authKey); 157 | request.addHeader("authHeaders", authKey); 158 | // get current proxied method from CURRENT_METHOD_THREAD_LOCAL 159 | Method method = CURRENT_METHOD_THREAD_LOCAL.get(); 160 | }); 161 | CityService cityService = factory.getProxy(CityService.class); 162 | City city = cityService.getCity(id); 163 | } 164 | ``` 165 | 166 | or in Spring scenario, register a RequestPreprocessor Bean. 167 | 168 | 169 | ### ResponseProcessor 170 | 171 | Similar to RequestPreprocessor, ResponseProcessor enable us to get access to take over the response by implementing the ResponseProcessor interface. 172 | 173 | ```java 174 | ResponseProcessor cityResultProcessor = (response, method) -> { 175 | ResultBean cityResultBean = JSON.parseObject(response.getBody(), 176 | new TypeReference>() { 177 | }); 178 | return cityResultBean.getData(); 179 | }; 180 | HttpApiProxyFactory factory = new HttpApiProxyFactory(cityResultProcessor); 181 | CityService cityServiceWithResponseProcessor = factory.getProxy(CityService.class); 182 | City city = cityServiceWithResponseProcessor.getCity(id); 183 | ``` 184 | 185 | or in Spring scenario, register a RequestPreprocessor Bean. 186 | 187 | Usually, an HTTP api would response a ResultBean formed with code/msg/data fields, like: 188 | 189 | ```json 190 | { 191 | "code": 0, 192 | "data": { 193 | "name": "Hello" 194 | }, 195 | "msg or message": "xx" 196 | } 197 | ``` 198 | 199 | We provided a `ResultBeanResponseProcessor` for this scenario. 200 | 201 | It will check the code specify by @ExpectedCode annotation or 0 by default. 202 | 203 | If the code in response body is not equals to the expected one, an IllegalStateException will be thrown. 204 | 205 | If they are equal, the data field will be parse to return value, unless the method do not need it or its return type is a ResultBean. 206 | 207 | ### JsonSerializer 208 | 209 | Fastjson is used for JSON serialization and deserialization in this project. However, some users reported that they could not introduce fastjson due to objective reasons such as the company's regulations. Therefore, we decouple the serializer and **still use fastjson by default**. If there are special requirements, the specific implementation can be specified by the following methods: 210 | 211 | ```java 212 | JsonSerializerDecider.registerJsonSerializer("Gson", GsonJsonSerializer.getInstance()); 213 | JsonSerializerDecider.setJsonInstanceKey("Gson"); 214 | ``` 215 | 216 | We provided two implementations, fastjson and gson, and makes the behavior of these two implementations consistent to the maximum extent through some configuration, so that the replacement of JSON implementation will not affect the original code. If you need other implementations, you can do so through implementing ` com.github.dadiyang . httpinvoker.serializer.JsonSerializer` interface, and then according to the above way to replace your own implementation. 217 | 218 | # CORE ANNOTATION 219 | 220 | ## @HttpApiScan 221 | 222 | Enable package scanning similar to @ComponentScan 223 | 224 | * value: to set basePackage,the annotated class's package by default 225 | * configPaths: to specify config files 226 | 227 | ## @HttpApi 228 | 229 | Declare a class it HTTP api binding class which need to be scanned. Similar to Spring's @Component 230 | 231 | ## @HttpReq 232 | 233 | Set the url and request method (GET/POST/DELETE etc.) binding to the method. 234 | 235 | ## @Param 236 | 237 | value: the key of request param 238 | isBody: mark that the argument is the request body, if the argument is an non-primary object, all the field-value will be a part of request params. 239 | 240 | value and isBody should not both be empty/false, otherwise the param will be ignored 241 | 242 | ## @Headers 243 | 244 | Headers of the request, must be `Map` otherwise an `IllegalArgumentException` will be thrown. 245 | 246 | ## @Cookies 247 | 248 | Cookies of the request, must be `Map` otherwise an `IllegalArgumentException` will be thrown. 249 | 250 | ## @Form 251 | 252 | indicate a method or all methods in a class would send a form request, Content-Type of application/x-www-form-urlencoded. 253 | 254 | ## @RetryPolicy 255 | 256 | Retry policy can be annotated to both class and method. 257 | 258 | * times: try times, 3 by default; 259 | * retryFor: what exception to retry, IOException by default; 260 | * retryForStatus: what status code would retry, other than 20x by default; 261 | * fixedBackOffPeriod: back off strategy, the number of seconds to sleep when retry is required, not to sleep by default. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [**English**](./README-en.md) 2 | 3 | # HTTP接口调用框架 4 | 5 | **让 HTTP 接口调用跟调用本地方法一样自然优雅** 6 | 7 | 将 HTTP 请求和接口绑定,然后由框架生成接口的代理类,直接调用接口的方法就会自动构建请求参数并发送请求,然后处理请求响应转换为接口方法的返回值返回(**支持泛型**)。 8 | 9 | 若与 **Spring 集成(可选)**,更能使用 @Autowired 进行自动注入接口的代理实现。 10 | 11 | # 特色 12 | 13 | 1. 像 MyBatis 一样,只写接口,由框架提供实现 14 | 2. 轻量级,不要求依赖Spring,只使用少量注解 15 | 3. 支持上传和下载文件 16 | 4. 若使用 Spring ,则可以使用 Autowired 自动注入接口的实现 17 | 5. 完善的文档用例和单元测试 18 | 6. 支持 Mock 19 | 7. JDK6+(注:1.2.0版本后支持JDK6,之前的版本必须JDK8) 20 | 21 | # 技术栈 22 | 23 | * 动态代理 24 | * 反射 25 | * 注解 26 | * 自动包扫描 27 | 28 | # 快速开始 29 | 30 | ## 一、添加maven依赖 31 | 32 | ```xml 33 | 34 | com.github.dadiyang 35 | http-api-invoker 36 | 1.2.4 37 | 38 | 39 | 40 | com.alibaba 41 | fastjson 42 | 1.2.75 43 | 44 | ``` 45 | 46 | ## 二、定义接口 47 | 48 | 假设有一个 GET 请求 `http://localhost:8080/city/allCities` 会响应: 49 | 50 | ```json 51 | [ 52 | { 53 | "id": 1, 54 | "name": "beijing" 55 | }, 56 | { 57 | "id": 2, 58 | "name": "shanghai" 59 | } 60 | ] 61 | ``` 62 | 63 | 我们定义一个接口来调用这个请求: 64 | 65 | ```java 66 | @HttpApi 67 | public interface CityService { 68 | @HttpReq("http://localhost:8080/city/allCities") 69 | List getAllCities(); 70 | } 71 | ``` 72 | 73 | 注: 74 | * 如果有参数,需要添加 `@Param("参数名")` 75 | * 这里只展示最简单的使用方法,完整功能的示例可查看[单元测试中的CityService接口](./src/test/java/com/github/dadiyang/httpinvoker/interfaces/CityService.java) 76 | 77 | ## 三、获取代理 78 | 79 | 获取代理有两种方式,一种是直接通过工厂方法获取,一种是集成Spring通过 @Autowired 注入 80 | 81 | ### HttpApiProxyFactory 82 | 83 | 通过调用 `HttpApiProxyFactory.getProxy` 方法获取,如: 84 | 85 | ```java 86 | CityService cityService = HttpApiProxyFactory.getProxy(CityService.class); 87 | List cities = cityService.getAllCities() 88 | System.out.println(cities); 89 | ``` 90 | 91 | ### Spring 集成 92 | 93 | #### 配置开启 HTTP API 扫描 94 | 95 | 只需添加`@HttpApiScan`到任意一个 `@Configuration` 的类上即可: 96 | 97 | ```java 98 | @Configuration 99 | @HttpApiScan 100 | public class TestApplication { 101 | } 102 | ``` 103 | **注**:加上 @HttpApiScan 后会自动扫描这个 Configuration 类所在的包及其子包中所有带有 @HttpApi 注解的接口并生成代理类注册到Spring容器中。你也可以通过设置 `@HttpApiScan` 中的 value 值来指定要扫描的包。 104 | 105 | #### @Autowired 注入接口代理 106 | 107 | ```java 108 | @Autowired 109 | private CityService cityService; 110 | 111 | public void test() { 112 | List cities = cityService.getAllCities(); 113 | System.out.println(cities); 114 | } 115 | ``` 116 | 117 | **注**:因为是动态代理生成并注册到Spring容器中的,所以IDE可能会警告 "Could not autowired. no beans of type 'xxx' type found." 忽略即可。 118 | 119 | ## 四、占位符 120 | 121 | 在 `@HttpApi` 注解的 prefix 和 `@HttpReq` 注解的 url 中都支持配置和路径参数占位符 122 | 123 | * 配置占位符:${},如 ${api.url.city} 124 | * 路径参数占位符:{},如 {cityId} 125 | * 保留到请求参数中的路径参数占位符:#{},如 #{cityId} 126 | * 可以通过 : 分隔来指定默认值,如 ${api.url.city:北京} {cityId:1} #{cityId:} 127 | 128 | 配置占位符中的配置项将会从以下几个来源中获取: 129 | 130 | * 在 `@HttpApiScan` 中设置的 **configPaths** 对应的配置文件中 131 | * **系统配置**,即 System.getProperty("property") 132 | * 与Spring集成时,也会从**Spring Environment**中获取 133 | 134 | ## 五、重试策略 135 | 136 | 当调用接口失败时,可能是网络不通或者接口返回的状态码不是2xx时,我们可能需要重试几次。这种情况下,我们可以使用`@RetryPolicy`注解。这个注解可以打在类和方法上,方法上的策略优先于类上的。支持的参数如下: 137 | 138 | * times 尝试调用次数,默认 3 次 139 | * retryFor 当发生该异常时才重试,默认只在 IOException 时触发重试 140 | * retryForStatus 当服务器返回的状态码为某一类型时触发,默认只要服务器返回非 20x 的状态都进行重试 141 | * fixedBackOffPeriod 退避策略,当需要进行重试时休眠的秒数,默认不休眠 142 | 143 | ## 六、扩展 144 | 145 | ### 请求前置处理器 146 | 147 | 有些情况下,我们需要给所有的请求添加一个请求头、Cookie或者固定的参数,这时候如果我们在接口里添加这些参数会很冗余 148 | 149 | 此时,我们可以实现 RequestPreprocessor 接口,并在初始化代理工厂时使用该接口,此时所有的请求都会通过这个接口进行预处理 150 | 151 | 我们可以在框架发送请求之前对请求体做任何修改 152 | 153 | ```java 154 | public void preprocessorTest() { 155 | HttpApiProxyFactory factory = new HttpApiProxyFactory(request -> { 156 | // 我们为所有的请求都加上 cookie 和 header 157 | request.addCookie("authCookies", authKey); 158 | request.addHeader("authHeaders", authKey); 159 | // 可以通过 CURRENT_METHOD_THREAD_LOCAL 获取到当前的被代理的方法 160 | Method method = CURRENT_METHOD_THREAD_LOCAL.get(); 161 | }); 162 | CityService cityService = factory.getProxy(CityService.class); 163 | City city = cityService.getCity(id); 164 | } 165 | ``` 166 | 167 | ### 响应处理器 168 | 169 | 接管响应结果的处理逻辑。通过实现 `ResponseProcessor` 接口并在初始化代理工厂时使用,可以拿到响应结果,并根据自己的需求对响应结果进行反序列化等操作 170 | 171 | ```java 172 | ResponseProcessor cityResultProcessor = (response, method) -> { 173 | ResultBean cityResultBean = JSON.parseObject(response.getBody(), 174 | new TypeReference>() { 175 | }); 176 | return cityResultBean.getData(); 177 | }; 178 | HttpApiProxyFactory factory = new HttpApiProxyFactory(cityResultProcessor); 179 | CityService cityServiceWithResponseProcessor = factory.getProxy(CityService.class); 180 | City city = cityServiceWithResponseProcessor.getCity(id); 181 | ``` 182 | 183 | 整合 Spring 时,可以使用 @Import(ResultBeanResponseProcessor.class) 加入全局的结果处理器 184 | 185 | 可以通过 @NotResultBean 注解声明此接口无需 ResultBeanResponseProcessor 来处理 186 | 187 | 实践中,很多 HTTP 接口通常使用具有 code、msg或message、data 三个字段的类作为返回值,即 ResultBean 的形式: 188 | 189 | ```json 190 | { 191 | "code": 0, 192 | "data": { 193 | "name": "Hello" 194 | }, 195 | "msg或message": "xx" 196 | } 197 | ``` 198 | 199 | 因此,我们提供了 ResultBeanResponseProcessor 来处理这种请求。 200 | 201 | 配合 @ExpectedCode 注解,来表明期望的 code 值,默认为 0。 202 | 203 | 若响应体返回的 code 值与期望值: 204 | 205 | * 不等时抛出 UnexpectedResultException 异常,异常信息为 message 的内容 206 | * 相等时解析 data 中的内容 207 | 208 | ### JSON序列化器 209 | 210 | 本项目JSON序列化/反序列化使用 FastJson,但是出现一些用户反馈由于公司规定等客观原因,他们无法引入 FastJson 的情况,所以我们对 序列化器 进行了解耦,**默认仍然采用 FastJson 实现**,如果有特殊需求,可以通过以下方法指定具体的实现: 211 | 212 | ```java 213 | JsonSerializerDecider.registerJsonSerializer("Gson", GsonJsonSerializer.getInstance()); 214 | JsonSerializerDecider.setJsonInstanceKey("Gson"); 215 | ``` 216 | 217 | 我们提供了 FastJson 和 Gson 两种实现,并通过个性化的配置使这两种实现的行为最大限度地保持一致,以让更换 JSON 实现不会影响到原有的代码。若你需要其他的实现,可以通过实现 `com.github.dadiyang.httpinvoker.serializer.JsonSerializer` 接口编写自己的实现,然后根据上面的 方式更换为自己的实现。 218 | 219 | ## 七、文件上传 220 | 221 | 只要方法参数是 MultiPart 222 | 示例: 223 | 224 | ```java 225 | /** 226 | * 提交 multipart/form-data 表单,实现多文件上传 227 | * 228 | * @param multiPart 表单 229 | */ 230 | @HttpReq(value = "/files/upload", method = "POST") 231 | String multiPartForm(MultiPart multiPart); 232 | ``` 233 | 234 | 调用示例: 235 | 236 | ```java 237 | String fileName1 = "conf.properties"; 238 | String fileName2 = "conf2.properties"; 239 | try (InputStream in1 = new FileInputStream(fileName1); 240 | InputStream in2 = new FileInputStream(fileName2);) { 241 | // 支持一个或多个文件 242 | MultiPart.Part part1 = new MultiPart.Part("conf1", fileName1, in1); 243 | MultiPart.Part part2 = new MultiPart.Part("conf2", fileName2, in2); 244 | MultiPart multiPart = new MultiPart(); 245 | multiPart.addPart(part1); 246 | multiPart.addPart(part2); 247 | cityService.multiPartForm(multiPart); 248 | } catch (IOException e) { 249 | e.printStackTrace(); 250 | } 251 | ``` 252 | 253 | ## 八、Mock 254 | 255 | 在实际开发过程中,我们依赖的接口可能由于尚未开发完成、服务不稳定或者没有需要的测试数据等原因,导致我们在**开发过程中浪费掉很多时间** 256 | 257 | 如果服务接口能在我们开发调试的时候**随心所欲地返回我们设定好的响应**,等到开发完再进行真实接口的联调,就会**大大提高我们的开发效率** 258 | 259 | 因此,本项目提供 Mock 的功能,**根据给定的规则匹配请求,若与给定的规则能匹配上,则使用给定的 Response 做为请求响应直接返回,没有匹配到则发送真实请求** 260 | 261 | 其原理就是实现 Requestor 接口以**接管发送请求的方法** 262 | 263 | ### 直接使用 264 | ```java 265 | // 生成 MockRequestor 对象 266 | MockRequestor requestor = new MockRequestor(); 267 | HttpApiProxyFactory factory = new HttpApiProxyFactory.Builder() 268 | // 注册到 代理工厂 中,以接管请求发送过程 269 | .setRequestor(requestor) 270 | // 配置文件 271 | .addProperties(in) 272 | .build(); 273 | // 获取代理对象 274 | CityService cityService = factory.getProxy(CityService.class); 275 | Map params = new HashMap<>(); 276 | params.put("id", 1); 277 | // 返回模拟的请求响应:MockResponse 278 | MockResponse response = new MockResponse(200, "北京"); 279 | // 添加匹配规则 280 | MockRule rule = new MockRule("http://localhost:18888/city/getCityName", params, response); 281 | requestor.addRule(rule); 282 | String name = cityService.getCityName(1); 283 | System.out.println(name); 284 | ``` 285 | 286 | ### 整合Spring 287 | 288 | 在有 @Configuration 注解的类中添加方法: 289 | 290 | * 注意,**千万不要在生产环境中使用**,可以通过 @Profile 或者 @Conditional 注解来有条件地添加,或者只在单元测试中使用 291 | 292 | ```java 293 | @Bean 294 | // 注意,千万不要在生产环境中使用,可以使用 @Profile("dev") 注解声明只在开发环境中自动扫包 295 | @Profile("dev") 296 | public MockRequestor requestor() { 297 | MockRequestor requestor = new MockRequestor(); 298 | MockRule rule = ... 299 | // 添加 mock 规则 300 | requestor.addRule(rule); 301 | return requestor; 302 | } 303 | ``` 304 | 305 | Mock请求器会在每次发送请求的时候打印警告 306 | 307 | # 核心注解 308 | 309 | ## @HttpApiScan 310 | 311 | 启动包扫描,类似@ComponentScan。 312 | * value属性设定扫包的 basePackage,如果没有设置则使用被标注的类所在的包为基包 313 | * configPaths属性指定配置文件 314 | 315 | ## @HttpApi 316 | 317 | 标注一个类是与Http接口绑定的,需要被包扫描的接口。类似Spring中的@Component注解 318 | 319 | ## @HttpReq 320 | 321 | 标注方法对应的url 322 | 323 | ## @Param 324 | 325 | value: 指定方法参数名对应的请求参数名称 326 | isBody: 指定是否将该参数的所有字段都做为单独的参数 327 | 这两个参数不能同时为空 328 | 329 | ## @Headers 330 | 331 | 指定方法参数为 Headers,目前只允许打在类型为 `Map` 的参数上,否则会抛出 `IllegalArgumentException` 332 | 333 | 打在方法上和类上则 keys 和 values 来指定请求头,keys 和 values 数组元素必须一一对应 334 | 335 | ## @Cookies 336 | 337 | 指定方法参数为 Cookies,目前只允许打在类型为 `Map` 的参数上,否则会抛出 `IllegalArgumentException` 338 | 339 | 打在方法上和类上则 keys 和 values 来指定 cookie,keys 和 values 数组元素必须一一对应 340 | 341 | ## @Form 342 | 343 | 指定方法或类中的所有方法都为 Form 表单形式提交,即 Content-Type 为 application/x-www-form-urlencoded 344 | 345 | ## @RetryPolicy 重试策略 346 | 347 | 重试策略。可以打在类和方法上,方法上的策略优先于类上的。 348 | 349 | * times 尝试调用次数,默认 3 次 350 | * retryFor 当发生该异常时才重试,默认只在 IOException 时触发重试 351 | * retryForStatus 当服务器返回的状态码为某一类型时触发,默认只要服务器返回非 20x 的状态都进行重试 352 | * fixedBackOffPeriod 退避策略,当需要进行重试时休眠的秒数,默认不休眠 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.dadiyang 8 | http-api-invoker 9 | 1.2.4 10 | jar 11 | 12 | org.sonatype.oss 13 | oss-parent 14 | 9 15 | 16 | 17 | 18 | MIT License 19 | http://www.opensource.org/licenses/mit-license.php 20 | repo 21 | 22 | 23 | 24 | https://github.com/dadiyang/http-api-invoker 25 | https://github.com/dadiyang/http-api-invoker.git 26 | https://github.com/dadiyang/http-api-invoker 27 | 28 | 29 | 30 | Xuyang Huang 31 | dadiyang@aliyun.com 32 | https://github.com/dadiyang/http-api-invoker 33 | 34 | 35 | 36 | UTF-8 37 | 4.3.24.RELEASE 38 | 1.2.75 39 | 2.8.6 40 | 1.7.21 41 | 1.11.2 42 | 0.7.5.201505241946 43 | 4.5.9 44 | 4.5.9 45 | 46 | 47 | 48 | org.slf4j 49 | slf4j-api 50 | ${slf4j.version} 51 | 52 | 53 | com.alibaba 54 | fastjson 55 | ${fastjson.version} 56 | provided 57 | 58 | 59 | com.google.code.gson 60 | gson 61 | ${gson.version} 62 | provided 63 | 64 | 65 | org.jsoup 66 | jsoup 67 | ${jsoup.version} 68 | 69 | 70 | org.apache.httpcomponents 71 | httpclient 72 | ${httpclient.version} 73 | provided 74 | 75 | 76 | org.apache.httpcomponents 77 | httpmime 78 | ${httpmime.version} 79 | provided 80 | 81 | 82 | org.springframework 83 | spring-context 84 | ${spring.version} 85 | provided 86 | 87 | 88 | junit 89 | junit 90 | 4.13.1 91 | test 92 | 93 | 94 | org.slf4j 95 | slf4j-log4j12 96 | ${slf4j.version} 97 | test 98 | 99 | 100 | org.springframework 101 | spring-test 102 | 4.3.5.RELEASE 103 | test 104 | 105 | 106 | com.github.tomakehurst 107 | wiremock-standalone 108 | 2.23.2 109 | test 110 | 111 | 112 | 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-compiler-plugin 118 | 3.8.0 119 | 120 | 1.6 121 | 1.6 122 | true 123 | 124 | 125 | 126 | org.jacoco 127 | jacoco-maven-plugin 128 | ${jacoco.version} 129 | 130 | 131 | prepare-agent 132 | 133 | prepare-agent 134 | 135 | 136 | 137 | report 138 | prepare-package 139 | 140 | report 141 | 142 | 143 | 144 | post-unit-test 145 | test 146 | 147 | report 148 | 149 | 150 | target/jacoco.exec 151 | target/jacoco-report 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/HttpApiProxyFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker; 2 | 3 | import com.github.dadiyang.httpinvoker.propertyresolver.EnvironmentBasePropertyResolver; 4 | import com.github.dadiyang.httpinvoker.propertyresolver.MultiSourcePropertyResolver; 5 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertiesBasePropertyResolver; 6 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertyResolver; 7 | import com.github.dadiyang.httpinvoker.requestor.DefaultHttpRequestor; 8 | import com.github.dadiyang.httpinvoker.requestor.RequestPreprocessor; 9 | import com.github.dadiyang.httpinvoker.requestor.Requestor; 10 | import com.github.dadiyang.httpinvoker.requestor.ResponseProcessor; 11 | import com.github.dadiyang.httpinvoker.util.IoUtils; 12 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 13 | import org.springframework.core.env.Environment; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.lang.reflect.InvocationHandler; 19 | import java.lang.reflect.Proxy; 20 | import java.util.Map; 21 | import java.util.Properties; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | /** 25 | * A factory to create HttpApiInvoker 26 | * 27 | * @author huangxuyang 28 | * date 2018/10/30 29 | */ 30 | public class HttpApiProxyFactory { 31 | private Map, Object> instances = new ConcurrentHashMap, Object>(); 32 | private Requestor requestor; 33 | private PropertyResolver propertyResolver; 34 | private RequestPreprocessor requestPreprocessor; 35 | private ResponseProcessor responseProcessor; 36 | 37 | /** 38 | * the builder of HttpApiProxyFactory 39 | * 40 | * @author dadiyang 41 | * @since 2019/5/20 42 | */ 43 | public static class Builder { 44 | private Requestor requestor; 45 | private MultiSourcePropertyResolver propertyResolvers = new MultiSourcePropertyResolver(); 46 | private RequestPreprocessor requestPreprocessor; 47 | private ResponseProcessor responseProcessor; 48 | 49 | public Builder setRequestor(Requestor requestor) { 50 | this.requestor = requestor; 51 | return this; 52 | } 53 | 54 | public Builder setRequestPreprocessor(RequestPreprocessor requestPreprocessor) { 55 | this.requestPreprocessor = requestPreprocessor; 56 | return this; 57 | } 58 | 59 | public Builder setResponseProcessor(ResponseProcessor responseProcessor) { 60 | this.responseProcessor = responseProcessor; 61 | return this; 62 | } 63 | 64 | public Builder addPropertyResolver(PropertyResolver propertyResolver) { 65 | this.propertyResolvers.addPropertyResolver(propertyResolver); 66 | return this; 67 | } 68 | 69 | public Builder addProperties(Properties properties) { 70 | propertyResolvers.addPropertyResolver(new PropertiesBasePropertyResolver(properties)); 71 | return this; 72 | } 73 | 74 | public Builder addProperties(InputStream in) throws IOException { 75 | try { 76 | Properties properties = new Properties(); 77 | properties.load(in); 78 | return addProperties(properties); 79 | } finally { 80 | in.close(); 81 | } 82 | } 83 | 84 | public Builder addProperties(File file) throws IOException { 85 | ObjectUtils.requireNonNull(file, "properties file should not be null"); 86 | Properties properties = IoUtils.getPropertiesFromFile(file.getAbsolutePath()); 87 | return addProperties(properties); 88 | } 89 | 90 | public Builder addEnvironment(Environment environment) { 91 | propertyResolvers.addPropertyResolver(new EnvironmentBasePropertyResolver(environment)); 92 | return this; 93 | } 94 | 95 | public HttpApiProxyFactory build() { 96 | HttpApiProxyFactory factory = new HttpApiProxyFactory(); 97 | factory.requestor = requestor != null ? requestor : factory.requestor; 98 | factory.responseProcessor = responseProcessor != null ? responseProcessor : factory.responseProcessor; 99 | factory.requestPreprocessor = requestPreprocessor != null ? requestPreprocessor : factory.requestPreprocessor; 100 | propertyResolvers.addPropertyResolver(factory.propertyResolver); 101 | factory.propertyResolver = propertyResolvers; 102 | return factory; 103 | } 104 | } 105 | 106 | public HttpApiProxyFactory() { 107 | this(new DefaultHttpRequestor(), System.getProperties()); 108 | } 109 | 110 | public HttpApiProxyFactory(Requestor requestor) { 111 | this(requestor, System.getProperties()); 112 | } 113 | 114 | public HttpApiProxyFactory(Properties properties) { 115 | this(new DefaultHttpRequestor(), properties); 116 | } 117 | 118 | public HttpApiProxyFactory(Requestor requestor, Properties properties) { 119 | this(requestor, properties, null); 120 | } 121 | 122 | public HttpApiProxyFactory(Properties properties, RequestPreprocessor requestPreprocessor) { 123 | this(null, properties, requestPreprocessor); 124 | } 125 | 126 | public HttpApiProxyFactory(Properties properties, ResponseProcessor responseProcessor) { 127 | this(properties, null, responseProcessor); 128 | } 129 | 130 | public HttpApiProxyFactory(Properties properties, RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 131 | this(null, properties, requestPreprocessor, responseProcessor); 132 | } 133 | 134 | public HttpApiProxyFactory(RequestPreprocessor requestPreprocessor) { 135 | this(requestPreprocessor, null); 136 | } 137 | 138 | public HttpApiProxyFactory(ResponseProcessor responseProcessor) { 139 | this(System.getProperties(), responseProcessor); 140 | } 141 | 142 | public HttpApiProxyFactory(RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 143 | this(System.getProperties(), requestPreprocessor, responseProcessor); 144 | } 145 | 146 | 147 | public HttpApiProxyFactory(PropertyResolver propertyResolver) { 148 | this(new DefaultHttpRequestor(), propertyResolver); 149 | } 150 | 151 | public HttpApiProxyFactory(Requestor requestor, PropertyResolver propertyResolver) { 152 | this(requestor, propertyResolver, null); 153 | } 154 | 155 | public HttpApiProxyFactory(PropertyResolver propertyResolver, RequestPreprocessor requestPreprocessor) { 156 | this(null, propertyResolver, requestPreprocessor); 157 | } 158 | 159 | public HttpApiProxyFactory(PropertyResolver propertyResolver, ResponseProcessor responseProcessor) { 160 | this(null, propertyResolver, null, responseProcessor); 161 | } 162 | 163 | public HttpApiProxyFactory(PropertyResolver propertyResolver, RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 164 | this(null, propertyResolver, requestPreprocessor, responseProcessor); 165 | } 166 | 167 | public HttpApiProxyFactory(Requestor requestor, 168 | Properties properties, 169 | RequestPreprocessor requestPreprocessor) { 170 | this(requestor, properties, requestPreprocessor, null); 171 | } 172 | 173 | public HttpApiProxyFactory(Requestor requestor, 174 | Properties properties, 175 | RequestPreprocessor requestPreprocessor, 176 | ResponseProcessor responseProcessor) { 177 | this(requestor, properties == null ? null : new PropertiesBasePropertyResolver(properties), requestPreprocessor, responseProcessor); 178 | } 179 | 180 | public HttpApiProxyFactory(Requestor requestor, 181 | PropertyResolver propertyResolver, 182 | RequestPreprocessor requestPreprocessor) { 183 | this(requestor, propertyResolver, requestPreprocessor, null); 184 | } 185 | 186 | public HttpApiProxyFactory(Requestor requestor, 187 | PropertyResolver propertyResolver, 188 | RequestPreprocessor requestPreprocessor, 189 | ResponseProcessor responseProcessor) { 190 | this.requestor = requestor; 191 | this.propertyResolver = propertyResolver == null ? new PropertiesBasePropertyResolver(System.getProperties()) : propertyResolver; 192 | this.requestPreprocessor = requestPreprocessor; 193 | this.responseProcessor = responseProcessor; 194 | } 195 | 196 | public static T newProxy(Class clazz) { 197 | return newProxy(clazz, System.getProperties()); 198 | } 199 | 200 | public static T newProxy(Class clazz, Properties properties) { 201 | return newProxy(clazz, null, properties); 202 | } 203 | 204 | public static T newProxy(Class clazz, RequestPreprocessor requestPreprocessor) { 205 | return newProxy(clazz, System.getProperties(), requestPreprocessor); 206 | } 207 | 208 | public static T newProxy(Class clazz, Requestor requestor) { 209 | return newProxy(clazz, requestor, System.getProperties()); 210 | } 211 | 212 | public static T newProxy(Class clazz, Requestor requestor, Properties properties) { 213 | return newProxy(clazz, requestor, properties, null); 214 | } 215 | 216 | public static T newProxy(Class clazz, Properties properties, RequestPreprocessor requestPreprocessor) { 217 | return newProxy(clazz, null, properties, requestPreprocessor); 218 | } 219 | 220 | public static T newProxy(Class clazz, Requestor requestor, 221 | Properties properties, 222 | RequestPreprocessor requestPreprocessor) { 223 | return newProxyInstance(requestor, properties, clazz, requestPreprocessor, null); 224 | } 225 | 226 | public static T newProxy(Class clazz, Requestor requestor, 227 | Properties properties, 228 | RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 229 | return newProxyInstance(requestor, properties, clazz, requestPreprocessor, responseProcessor); 230 | } 231 | 232 | public static T newProxy(Class clazz, PropertyResolver propertyResolver) { 233 | return newProxy(clazz, null, propertyResolver); 234 | } 235 | 236 | public static T newProxy(Class clazz, Requestor requestor, PropertyResolver propertyResolver) { 237 | return newProxy(clazz, requestor, propertyResolver, null); 238 | } 239 | 240 | public static T newProxy(Class clazz, PropertyResolver propertyResolver, RequestPreprocessor requestPreprocessor) { 241 | return newProxy(clazz, null, propertyResolver, requestPreprocessor); 242 | } 243 | 244 | public static T newProxy(Class clazz, PropertyResolver propertyResolver, RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 245 | return newProxy(clazz, null, propertyResolver, requestPreprocessor, responseProcessor); 246 | } 247 | 248 | public static T newProxy(Class clazz, PropertyResolver propertyResolver, ResponseProcessor responseProcessor) { 249 | return newProxy(clazz, null, propertyResolver, null, responseProcessor); 250 | } 251 | 252 | public static T newProxy(Class clazz, ResponseProcessor responseProcessor) { 253 | return newProxy(clazz, null, new PropertiesBasePropertyResolver(System.getProperties()), null, responseProcessor); 254 | } 255 | 256 | public static T newProxy(Class clazz, Requestor requestor, 257 | PropertyResolver propertyResolver, 258 | RequestPreprocessor requestPreprocessor) { 259 | return newProxyInstance(requestor, propertyResolver, clazz, requestPreprocessor, null); 260 | } 261 | 262 | public static T newProxy(Class clazz, Requestor requestor, 263 | PropertyResolver propertyResolver, 264 | RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 265 | return newProxyInstance(requestor, propertyResolver, clazz, requestPreprocessor, responseProcessor); 266 | } 267 | 268 | private static T newProxyInstance(Requestor requestor, PropertyResolver propertyResolver, 269 | Class clazz, RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 270 | InvocationHandler handler = new HttpApiInvoker(requestor, propertyResolver, clazz, requestPreprocessor, responseProcessor); 271 | //noinspection unchecked 272 | return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler); 273 | } 274 | 275 | private static T newProxyInstance(Requestor requestor, Properties properties, 276 | Class clazz, RequestPreprocessor requestPreprocessor, ResponseProcessor responseProcessor) { 277 | InvocationHandler handler = new HttpApiInvoker(requestor, properties, clazz, requestPreprocessor, responseProcessor); 278 | //noinspection unchecked 279 | return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler); 280 | } 281 | 282 | /** 283 | * dynamic proxy the given interface whose methods annotated with @HttpReq 284 | * 285 | * @param clazz an interface whose methods annotated with @HttpReq 286 | * @param this interface's type 287 | * @return the generated dynamic proxy 288 | * @throws IllegalStateException thrown when the method without annotated with @HttpReq was invoke 289 | */ 290 | public T getProxy(Class clazz) { 291 | if (!instances.containsKey(clazz)) { 292 | synchronized (HttpApiProxyFactory.class) { 293 | if (!instances.containsKey(clazz)) { 294 | instances.put(clazz, newProxyInstance(requestor, propertyResolver, 295 | clazz, requestPreprocessor, responseProcessor)); 296 | } 297 | } 298 | } 299 | //noinspection unchecked 300 | return (T) instances.get(clazz); 301 | } 302 | 303 | public Requestor getRequestor() { 304 | return requestor; 305 | } 306 | 307 | public PropertyResolver getPropertyResolver() { 308 | return propertyResolver; 309 | } 310 | 311 | public RequestPreprocessor getRequestPreprocessor() { 312 | return requestPreprocessor; 313 | } 314 | 315 | public ResponseProcessor getResponseProcessor() { 316 | return responseProcessor; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/ContentType.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * @author dadiyang 7 | * @since 2019-06-15 8 | */ 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ElementType.METHOD, ElementType.TYPE}) 12 | public @interface ContentType { 13 | String value() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/Cookies.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Indicates the parameter is a cookies map. 7 | *

8 | * This annotation should only be annotated on parameter of Map<String, String> type 9 | * @author huangxuyang 10 | * date 2018/12/21 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE}) 14 | @Documented 15 | public @interface Cookies { 16 | String[] keys() default ""; 17 | 18 | String[] values() default ""; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/ExpectedCode.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 正确的code是什么 7 | *

8 | * 默认为 0 9 | *

10 | * 接口返回值 code 不统一,有些接口 code 为 0 时表明成功,有些则为 1 11 | *

12 | * 因此创建此注解 13 | * 14 | * @author huangxuyang 15 | * @since 1.1.4 16 | */ 17 | @Documented 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Target({ElementType.METHOD, ElementType.TYPE}) 20 | public @interface ExpectedCode { 21 | int value() default 0; 22 | 23 | /** 24 | * code 字段名,默认是 code 25 | */ 26 | String codeFieldName() default "code"; 27 | 28 | /** 29 | * 是否忽略 code 字段首字母大小写 30 | */ 31 | boolean ignoreFieldInitialCase() default true; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/Form.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * indicate a request with Content-Type of application/x-www-form-urlencoded 7 | * 8 | * @author dadiyang 9 | * @since 1.1.2 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Documented 13 | @Target({ElementType.TYPE, ElementType.METHOD}) 14 | public @interface Form { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/Headers.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Indicates the parameter is a header map. 7 | * 8 | * @author huangxuyang 9 | * date 2018/12/21 10 | */ 11 | @Documented 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE}) 14 | public @interface Headers { 15 | String[] keys() default ""; 16 | 17 | String[] values() default ""; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/HttpApi.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Indicate the interface is a http api interface. 7 | *

8 | * To simplified the urls, use {@link #prefix} attribute to set the prefix of url in @HttpReq. ie. http://localhost:8080 9 | *

10 | * Those interfaces annotated by this annotation will be scanned by {@link com.github.dadiyang.httpinvoker.spring.HttpApiConfigurer} 11 | * so that users can autowire the interface. 12 | *

13 | * 14 | * @author huangxuyang 15 | */ 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target(ElementType.TYPE) 18 | @Documented 19 | public @interface HttpApi { 20 | /** 21 | * when {@link #prefix} is empty, this value will be used 22 | * 23 | * @return the same as prefix 24 | */ 25 | String value() default ""; 26 | 27 | /** 28 | * @return the prefix 29 | */ 30 | String prefix() default ""; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/HttpApiScan.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import com.github.dadiyang.httpinvoker.spring.HttpApiConfigurer; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.*; 7 | 8 | /** 9 | * Similar to @ComponentScan in Spring, 10 | * the {@link #value} specify the base packages the {@link HttpApiConfigurer } would scan. 11 | *

12 | * {@link #configPaths} specify the config files paths 13 | *

14 | * If specific packages are not defined, scanning will occur from the package of the class that declares this annotation. 15 | * 16 | * @author huangxuyang 17 | * date 2018/11/1 18 | */ 19 | @Documented 20 | @Target(ElementType.TYPE) 21 | @Import(HttpApiConfigurer.class) 22 | @Retention(RetentionPolicy.RUNTIME) 23 | public @interface HttpApiScan { 24 | /** 25 | * base packages, which the http api interfaces contain 26 | * 27 | * @return base packages 28 | */ 29 | String[] value() default ""; 30 | 31 | /** 32 | * the config file path, if you use ${...} placeholder, this is needed. 33 | * 34 | * @return the config file path 35 | */ 36 | String[] configPaths() default ""; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/HttpReq.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Indicates the http request related information. 7 | *

8 | * The url is specified by {@link #value}, and the {@link #method} declares the request method such as GET/POST/PUT 9 | * and {@link #timeout} provides the request timeout. 10 | * 11 | * @author huangxuyang 12 | * date 2018/10/30 13 | */ 14 | @Target(ElementType.METHOD) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | public @interface HttpReq { 18 | 19 | /** 20 | * the service's url, path variable is supported 21 | * 22 | * @return the service's url 23 | */ 24 | String value(); 25 | 26 | /** 27 | * GET,POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE 28 | * 29 | * @return 请求方式 30 | */ 31 | String method() default "GET"; 32 | 33 | /** 34 | * request timeout in millisecond 35 | * 36 | * @return request timeout 37 | */ 38 | int timeout() default 5000; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/NotResultBean.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 表明一个接口的返回值不是 ResultBean,不需要 ResultBeanResponseProcessor 进行处理 7 | * 8 | * @author dadiyang 9 | * @since 2019-09-02 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target({ElementType.METHOD, ElementType.TYPE}) 13 | @Documented 14 | public @interface NotResultBean { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/Param.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * The {@link #value} stand for the key of request param and the annotated parameter represents the according value 7 | *

8 | * value and isBody should not both be empty/false, otherwise the param will be ignored 9 | * 10 | * @author huangxuyang 11 | * date 2018/10/31 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.PARAMETER) 15 | @Documented 16 | public @interface Param { 17 | /** 18 | * value and isBody should not both be empty/false 19 | * 20 | * @return key of request param 21 | */ 22 | String value() default ""; 23 | 24 | /** 25 | * mark that the argument is the request body 26 | *

27 | * if the argument is an non-primary object, all the field-value will be a part of request params. 28 | * 29 | * @return if the argument is the request body 30 | */ 31 | boolean isBody() default false; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/RetryPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import com.github.dadiyang.httpinvoker.requestor.Status; 4 | 5 | import java.io.IOException; 6 | import java.lang.annotation.*; 7 | 8 | import static com.github.dadiyang.httpinvoker.requestor.Status.NOT_FOUND; 9 | import static com.github.dadiyang.httpinvoker.requestor.Status.REDIRECT; 10 | import static com.github.dadiyang.httpinvoker.requestor.Status.SERVER_ERROR; 11 | 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.TYPE, ElementType.METHOD}) 14 | @Documented 15 | public @interface RetryPolicy { 16 | /** 17 | * retry times. 18 | *

19 | * Default to 3 20 | * 21 | * @return retry times 22 | */ 23 | int times() default 3; 24 | 25 | /** 26 | * Default for IOException 27 | * 28 | * @return retry if the provided exception occur 29 | */ 30 | Class[] retryFor() default IOException.class; 31 | 32 | /** 33 | * Default for all not 20x code 34 | * 35 | * @return retry if the status codes gotten 36 | */ 37 | Status[] retryForStatus() default {NOT_FOUND, REDIRECT, SERVER_ERROR}; 38 | 39 | /** 40 | * fixed milli to sleep before retry 41 | *

42 | * Default for 0 43 | * 44 | * @return fixed milli 45 | */ 46 | long fixedBackOffPeriod() default 0; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/annotation/UserAgent.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * @author dadiyang 7 | * @since 2019-06-15 8 | */ 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ElementType.METHOD, ElementType.TYPE}) 12 | public @interface UserAgent { 13 | String value() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/enumeration/ReqMethod.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.enumeration; 2 | 3 | /** 4 | * request methods 5 | * 6 | * @author dadiyang 7 | * @since 2019-06-13 8 | */ 9 | public class ReqMethod { 10 | public static final String GET = "GET"; 11 | public static final String POST = "POST"; 12 | public static final String PUT = "PUT"; 13 | public static final String DELETE = "DELETE"; 14 | public static final String PATCH = "PATCH"; 15 | public static final String HEAD = "HEAD"; 16 | public static final String OPTIONS = "OPTIONS"; 17 | public static final String TRACE = "TRACE"; 18 | } -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/exception/UnexpectedResultException.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.exception; 2 | 3 | /** 4 | * Signals that a request has received an unexpected result. 5 | * 6 | * @author dadiyang 7 | * @since 2019-07-09 8 | */ 9 | public class UnexpectedResultException extends IllegalStateException { 10 | public UnexpectedResultException() { 11 | } 12 | 13 | public UnexpectedResultException(String s) { 14 | super(s); 15 | } 16 | 17 | public UnexpectedResultException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | public UnexpectedResultException(Throwable cause) { 22 | super(cause); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/mocker/MockRequestor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.mocker; 2 | 3 | import com.github.dadiyang.httpinvoker.requestor.DefaultHttpRequestor; 4 | import com.github.dadiyang.httpinvoker.requestor.HttpRequest; 5 | import com.github.dadiyang.httpinvoker.requestor.HttpResponse; 6 | import com.github.dadiyang.httpinvoker.requestor.Requestor; 7 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.IOException; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.ArrayList; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Mock 请求器,使用这个请求器可以配置一些规则,当发起的请求符合这个规则时,直接返回给定的结果而不发起真实请求 21 | *

22 | * 没有匹配的规则才发起真实请求 23 | *

24 | * 注:只用于开发环境使用,生产环境千万不要使用此请求器!! 25 | * 26 | * @author dadiyang 27 | * @since 2019-05-31 28 | */ 29 | public class MockRequestor implements Requestor { 30 | private static final Logger log = LoggerFactory.getLogger(MockRequestor.class); 31 | /** 32 | * 是否忽略环境警告信息,默认每次使用本请求器时都会打印警告 33 | */ 34 | private boolean ignoreWarning; 35 | private final List mockRules; 36 | private final Requestor realRequestor; 37 | 38 | public MockRequestor() { 39 | this(new ArrayList(), new DefaultHttpRequestor()); 40 | } 41 | 42 | public MockRequestor(List mockRules, Requestor realRequestor) { 43 | if (realRequestor == null) { 44 | throw new IllegalArgumentException("必须配置一个真实请求的请求器"); 45 | } 46 | this.mockRules = mockRules; 47 | this.realRequestor = realRequestor; 48 | log.info("初始化 MOCK 请求器,注意:一般只用于开发环境使用,生产环境千万不要使用此请求器!!"); 49 | } 50 | 51 | public MockRequestor(List mockRules) { 52 | this(new ArrayList(mockRules), new DefaultHttpRequestor()); 53 | } 54 | 55 | public void addRule(MockRule rule) { 56 | mockRules.add(rule); 57 | } 58 | 59 | @Override 60 | public HttpResponse sendRequest(HttpRequest request) throws IOException { 61 | ObjectUtils.requireNonNull(request, "请求不能为 null"); 62 | ObjectUtils.requireNonNull(request.getUrl(), "请求 url 不能为 null"); 63 | if (!ignoreWarning) { 64 | log.warn("当前使用 MOCK 请求器,注意:一般只在开发环境使用,生产环境千万不要使用此请求器!!"); 65 | } 66 | List matchedRule = new LinkedList(); 67 | for (MockRule rule : mockRules) { 68 | if (isMatch(request, rule)) { 69 | matchedRule.add(rule); 70 | } 71 | } 72 | // 没有匹配则发起真实请求 73 | if (matchedRule.isEmpty()) { 74 | log.info("请求没有找到对应的 mock,所以发起真实请求: " + request.getUrl()); 75 | return realRequestor.sendRequest(request); 76 | } 77 | if (matchedRule.size() > 1) { 78 | List exactlyMatches = new LinkedList(); 79 | // 在匹配到的规则器里找url或uri完全匹配的 80 | for (MockRule rule : matchedRule) { 81 | if (ObjectUtils.equals(rule.getUrlReg(), request.getUrl()) 82 | || ObjectUtils.equals(rule.getUriReg(), getUri(request.getUrl()))) { 83 | exactlyMatches.add(rule); 84 | } 85 | } 86 | matchedRule = exactlyMatches; 87 | // 如果还是有多个,则抛出异常 88 | if (matchedRule.size() > 1) { 89 | throw new IllegalStateException("一个请求匹配到 " + matchedRule.size() + " 个 mock 规则,请确认是否重复添加: " + request.getUrl()); 90 | } 91 | } 92 | MockRule rule = matchedRule.get(0); 93 | log.info("mock匹配成功,使用匹配到的规则,请求url: " + request.getUrl() + ", 规则: " + rule); 94 | return rule.getResponse(); 95 | } 96 | 97 | private String getUri(String url) { 98 | try { 99 | return new URL(url).getPath(); 100 | } catch (MalformedURLException e) { 101 | return url; 102 | } 103 | } 104 | 105 | private boolean isMatch(HttpRequest request, MockRule rule) { 106 | if (rule == null) { 107 | return false; 108 | } 109 | if (rule.getMethod() != null && !rule.getMethod().isEmpty() 110 | && !ObjectUtils.equals(request.getMethod().toUpperCase(), rule.getMethod().toUpperCase())) { 111 | log.info("请求方法规则不匹配: requestMethod: " + request.getMethod() + ", ruleMethod: " + rule.getMethod()); 112 | return false; 113 | } 114 | if (!isUrlOrUriMatch(request, rule)) { 115 | return false; 116 | } 117 | if (!isMapMatch(rule.getData(), request.getData())) { 118 | log.info("参数规则不匹配: requestData: " + request.getData() + ", ruleData: " + rule.getData()); 119 | return false; 120 | } 121 | if (!ObjectUtils.equals(rule.getBody(), request.getBody())) { 122 | log.info("请求体规则不匹配: requestBody: " + request.getBody() + ", ruleBody: " + rule.getBody()); 123 | return false; 124 | } 125 | // 校验 cookie 126 | if (!isMapMatch(rule.getCookies(), request.getCookies())) { 127 | log.info("Cookie规则不匹配: requestCookies: " + request.getCookies() + ", ruleCookies: " + rule.getCookies()); 128 | return false; 129 | } 130 | // 校验 header 131 | if (!isMapMatch(rule.getHeaders(), request.getHeaders())) { 132 | log.info("Header规则不匹配: requestHeaders: " + request.getHeaders() + ", ruleHeaders: " + rule.getHeaders()); 133 | return false; 134 | } 135 | // 全部校验通过,则匹配 136 | return true; 137 | } 138 | 139 | private boolean isUrlOrUriMatch(HttpRequest request, MockRule m) { 140 | // url 规则 141 | if (m.getUrlReg() != null && !m.getUrlReg().isEmpty()) { 142 | boolean urlMatch = isStringMatch(request.getUrl(), m.getUrlReg()); 143 | if (!urlMatch) { 144 | log.info("url规则不匹配: requestUrl: " + request.getUrl() + ", ruleUrl: " + m.getUrlReg()); 145 | return false; 146 | } 147 | } else if (m.getUriReg() != null && !m.getUriReg().isEmpty()) { 148 | // uri 规则 149 | String uri = getUri(request.getUrl()); 150 | boolean uriMatch = isStringMatch(uri, m.getUriReg()); 151 | if (!uriMatch) { 152 | log.info("uri 规则不匹配: requestUri: " + uri + ", ruleUri: " + m.getUriReg()); 153 | return false; 154 | } 155 | } else { 156 | log.info("url 和 uri 规则不能同时为空不匹配: requestUrl: " + request.getUrl() + ", ruleUrl: " + m.getUrlReg()); 157 | return false; 158 | } 159 | return true; 160 | } 161 | 162 | private boolean isStringMatch(String uri, String urlReg) { 163 | return ObjectUtils.equals(uri, urlReg) 164 | || uri.matches(urlReg); 165 | } 166 | 167 | private boolean isMapMatch(Map mapFromMockRule, Map mapFromRequest) { 168 | // 无需匹配 169 | if (mapFromMockRule == null || mapFromMockRule.isEmpty()) { 170 | return true; 171 | } 172 | // 请求中没有 cookies 173 | if (mapFromRequest == null || mapFromRequest.isEmpty()) { 174 | return false; 175 | } 176 | for (Map.Entry entry : mapFromMockRule.entrySet()) { 177 | Object value = mapFromRequest.get(entry.getKey()); 178 | // 只要有一个 cookie 与请求不符,则不匹配 179 | if (!ObjectUtils.equals(entry.getValue(), value)) { 180 | return false; 181 | } 182 | } 183 | return true; 184 | } 185 | 186 | public void setIgnoreWarning(boolean ignoreWarning) { 187 | this.ignoreWarning = ignoreWarning; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/mocker/MockResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.mocker; 2 | 3 | import com.github.dadiyang.httpinvoker.requestor.HttpResponse; 4 | 5 | import java.io.InputStream; 6 | import java.util.Arrays; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author huangxuyang 13 | * date 2018/12/6 14 | */ 15 | public class MockResponse implements HttpResponse { 16 | private static final int STATUS_CODE_SUC = 200; 17 | private int statusCode = STATUS_CODE_SUC; 18 | private String statusMessage; 19 | private String charset; 20 | private String contentType; 21 | private byte[] bodyAsBytes; 22 | private String body; 23 | private InputStream bodyStream; 24 | private Map> headers; 25 | private Map cookies; 26 | 27 | public MockResponse() { 28 | } 29 | 30 | public MockResponse(int statusCode, String body) { 31 | setStatusCode(statusCode); 32 | setBody(body); 33 | } 34 | 35 | public MockResponse(String statusMessage, int statusCode) { 36 | setStatusCode(statusCode); 37 | setStatusMessage(statusMessage); 38 | } 39 | 40 | public MockResponse(String body) { 41 | setStatusCode(STATUS_CODE_SUC); 42 | setBody(body); 43 | } 44 | 45 | public MockResponse(int statusCode, String statusMessage, String contentType) { 46 | this.statusCode = statusCode; 47 | this.statusMessage = statusMessage; 48 | this.contentType = contentType; 49 | } 50 | 51 | @Override 52 | public int getStatusCode() { 53 | return statusCode; 54 | } 55 | 56 | public void setStatusCode(int statusCode) { 57 | this.statusCode = statusCode; 58 | } 59 | 60 | @Override 61 | public String getStatusMessage() { 62 | return statusMessage; 63 | } 64 | 65 | public void setStatusMessage(String statusMessage) { 66 | this.statusMessage = statusMessage; 67 | } 68 | 69 | @Override 70 | public String getCharset() { 71 | return charset; 72 | } 73 | 74 | public void setCharset(String charset) { 75 | this.charset = charset; 76 | } 77 | 78 | @Override 79 | public String getContentType() { 80 | return contentType; 81 | } 82 | 83 | public void setContentType(String contentType) { 84 | this.contentType = contentType; 85 | } 86 | 87 | @Override 88 | public byte[] getBodyAsBytes() { 89 | return bodyAsBytes; 90 | } 91 | 92 | public void setBodyAsBytes(byte[] bodyAsBytes) { 93 | this.bodyAsBytes = bodyAsBytes; 94 | } 95 | 96 | @Override 97 | public InputStream getBodyStream() { 98 | return bodyStream; 99 | } 100 | 101 | public void setBodyStream(InputStream bodyStream) { 102 | this.bodyStream = bodyStream; 103 | } 104 | 105 | @Override 106 | public String getBody() { 107 | return body; 108 | } 109 | 110 | public void setBody(String body) { 111 | this.body = body; 112 | } 113 | 114 | @Override 115 | public Map getHeaders() { 116 | LinkedHashMap map = new LinkedHashMap(headers.size()); 117 | for (Map.Entry> entry : headers.entrySet()) { 118 | String header = entry.getKey(); 119 | List values = entry.getValue(); 120 | if (values.size() > 0) { 121 | map.put(header, values.get(0)); 122 | } 123 | } 124 | return map; 125 | } 126 | 127 | public void setHeaders(Map> headers) { 128 | this.headers = headers; 129 | } 130 | 131 | @Override 132 | public Map> multiHeaders() { 133 | return headers; 134 | } 135 | 136 | @Override 137 | public List getHeaders(String name) { 138 | return Arrays.asList(getHeader(name).split(";\\s?")); 139 | } 140 | 141 | @Override 142 | public String getHeader(String name) { 143 | return getHeaders().get(name); 144 | } 145 | 146 | @Override 147 | public Map getCookies() { 148 | return cookies; 149 | } 150 | 151 | public void setCookies(Map cookies) { 152 | this.cookies = cookies; 153 | } 154 | 155 | @Override 156 | public String getCookie(String name) { 157 | return getCookies().get(name); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/mocker/MockRule.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.mocker; 2 | 3 | import com.github.dadiyang.httpinvoker.requestor.HttpResponse; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Mock规则 9 | * 10 | * @author huangxuyang 11 | * @since 2019-05-31 12 | */ 13 | public class MockRule { 14 | /** 15 | * url 正则表达式 16 | */ 17 | private String urlReg; 18 | /** 19 | * uri 正则,即忽略协议和域名,如 http://localhost:8080/city/getName,则 uri 为 /city/getName 20 | */ 21 | private String uriReg; 22 | /** 23 | * 请求方法:GET/POST/PUT 等 24 | */ 25 | private String method; 26 | private Map headers; 27 | private Map cookies; 28 | private Map data; 29 | private Object body; 30 | private HttpResponse response; 31 | 32 | public MockRule() { 33 | } 34 | 35 | public MockRule(String urlReg) { 36 | this.urlReg = urlReg; 37 | } 38 | 39 | public MockRule(String urlReg, HttpResponse response) { 40 | this.urlReg = urlReg; 41 | this.response = response; 42 | } 43 | 44 | public MockRule(String urlReg, String method, HttpResponse response) { 45 | this.urlReg = urlReg; 46 | this.method = method; 47 | this.response = response; 48 | } 49 | 50 | public MockRule(String urlReg, Map data, HttpResponse response) { 51 | this.urlReg = urlReg; 52 | this.data = data; 53 | this.response = response; 54 | } 55 | 56 | public String getUrlReg() { 57 | return urlReg; 58 | } 59 | 60 | public void setUrlReg(String urlReg) { 61 | this.urlReg = urlReg; 62 | } 63 | 64 | public HttpResponse getResponse() { 65 | return response; 66 | } 67 | 68 | public void setResponse(HttpResponse response) { 69 | this.response = response; 70 | } 71 | 72 | public Map getHeaders() { 73 | return headers; 74 | } 75 | 76 | public void setHeaders(Map headers) { 77 | this.headers = headers; 78 | } 79 | 80 | public Map getCookies() { 81 | return cookies; 82 | } 83 | 84 | public void setCookies(Map cookies) { 85 | this.cookies = cookies; 86 | } 87 | 88 | public Map getData() { 89 | return data; 90 | } 91 | 92 | public void setData(Map data) { 93 | this.data = data; 94 | } 95 | 96 | public Object getBody() { 97 | return body; 98 | } 99 | 100 | public void setBody(Object body) { 101 | this.body = body; 102 | } 103 | 104 | public String getMethod() { 105 | return method; 106 | } 107 | 108 | public void setMethod(String method) { 109 | this.method = method; 110 | } 111 | 112 | public String getUriReg() { 113 | return uriReg; 114 | } 115 | 116 | public void setUriReg(String uriReg) { 117 | this.uriReg = uriReg; 118 | } 119 | 120 | @Override 121 | public String toString() { 122 | return "MockRule{" + 123 | "urlReg='" + urlReg + '\'' + 124 | ", uriReg='" + uriReg + '\'' + 125 | ", method='" + method + '\'' + 126 | ", headers=" + headers + 127 | ", cookies=" + cookies + 128 | ", data=" + data + 129 | ", body=" + body + 130 | ", response=" + response + 131 | '}'; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/propertyresolver/EnvironmentBasePropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.propertyresolver; 2 | 3 | import org.springframework.core.env.Environment; 4 | 5 | /** 6 | * A PropertyResolver base on Spring Environment object. 7 | * 8 | * @author dadiyang 9 | * @since 1.0.9 10 | */ 11 | public class EnvironmentBasePropertyResolver implements PropertyResolver { 12 | private Environment environment; 13 | 14 | public EnvironmentBasePropertyResolver(Environment environment) { 15 | this.environment = environment; 16 | } 17 | 18 | @Override 19 | public boolean containsProperty(String key) { 20 | return environment.containsProperty(key); 21 | } 22 | 23 | @Override 24 | public String getProperty(String key) { 25 | return environment.getProperty(key); 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) { 31 | return true; 32 | } 33 | if (o == null || getClass() != o.getClass()) { 34 | return false; 35 | } 36 | EnvironmentBasePropertyResolver that = (EnvironmentBasePropertyResolver) o; 37 | return environment != null ? environment.equals(that.environment) : that.environment == null; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return environment != null ? environment.hashCode() : 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/propertyresolver/MultiSourcePropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.propertyresolver; 2 | 3 | import java.util.Collections; 4 | import java.util.LinkedHashSet; 5 | import java.util.Set; 6 | 7 | /** 8 | * A PropertyResolver which includes a set of PropertyResolvers 9 | * 10 | * @author huangxuyang 11 | * @since 1.0.9 12 | */ 13 | public class MultiSourcePropertyResolver implements PropertyResolver { 14 | private Set resolvers; 15 | 16 | /** 17 | * Construct by a given resolvers list. 18 | *

19 | * Note that a new HashSet will be use, 20 | * so if you want to add a new Resolver, call {@link #addPropertyResolver} please 21 | * 22 | * @param resolvers resolvers list 23 | * @throws IllegalArgumentException if param resolvers is null 24 | */ 25 | public MultiSourcePropertyResolver(Set resolvers) { 26 | if (resolvers == null) { 27 | throw new IllegalArgumentException("resolvers must not be null"); 28 | } 29 | this.resolvers = new LinkedHashSet(resolvers); 30 | } 31 | 32 | public MultiSourcePropertyResolver() { 33 | resolvers = new LinkedHashSet(); 34 | } 35 | 36 | public Set getResolvers() { 37 | return Collections.unmodifiableSet(resolvers); 38 | } 39 | 40 | /** 41 | * Add a new PropertyResolver; 42 | * 43 | * @param resolver a propertyResolver 44 | */ 45 | public void addPropertyResolver(PropertyResolver resolver) { 46 | this.resolvers.add(resolver); 47 | } 48 | 49 | @Override 50 | public boolean containsProperty(String key) { 51 | for (PropertyResolver resolver : resolvers) { 52 | if (resolver.containsProperty(key)) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | @Override 60 | public String getProperty(String key) { 61 | for (PropertyResolver resolver : resolvers) { 62 | if (resolver.containsProperty(key)) { 63 | return resolver.getProperty(key); 64 | } 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/propertyresolver/PropertiesBasePropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.propertyresolver; 2 | 3 | import java.util.Properties; 4 | 5 | /** 6 | * A PropertyResolver base on a Properties object. 7 | * 8 | * @author dadiyang 9 | * @since 1.0.9 10 | */ 11 | public class PropertiesBasePropertyResolver implements PropertyResolver { 12 | private Properties properties; 13 | 14 | public PropertiesBasePropertyResolver(Properties properties) { 15 | this.properties = properties; 16 | } 17 | 18 | @Override 19 | public boolean containsProperty(String key) { 20 | return properties.containsKey(key); 21 | } 22 | 23 | @Override 24 | public String getProperty(String key) { 25 | return properties.getProperty(key); 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) { 31 | return true; 32 | } 33 | if (o == null || getClass() != o.getClass()) { 34 | return false; 35 | } 36 | PropertiesBasePropertyResolver that = (PropertiesBasePropertyResolver) o; 37 | return properties != null ? properties.equals(that.properties) : that.properties == null; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return properties != null ? properties.hashCode() : 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/propertyresolver/PropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.propertyresolver; 2 | 3 | /** 4 | * Interface for resolving properties. 5 | * 6 | * @author dadiyang 7 | * @since 1.0.9 8 | */ 9 | public interface PropertyResolver { 10 | /** 11 | * Check whether the given property key is available. 12 | * 13 | * @param key the property name to resolve 14 | * @return whether the given property key is available. 15 | */ 16 | boolean containsProperty(String key); 17 | 18 | /** 19 | * Return the property value associated with the given key, 20 | * or {@code null} if the key cannot be resolved. 21 | * 22 | * @param key the property name to resolve 23 | * @return the property value associated with the given key 24 | */ 25 | String getProperty(String key); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/DefaultHttpRequestor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | /** 4 | * The default implementation of {@link Requestor} that send the request using Jsoup which is a elegant http client I've ever use. 5 | *

6 | * the parameter will be formatted according to the request method. 7 | * 8 | * @author huangxuyang 9 | * date 2018/11/1 10 | */ 11 | public class DefaultHttpRequestor extends JsoupRequestor { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/DefaultResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import com.github.dadiyang.httpinvoker.serializer.JsonSerializerDecider; 4 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Type; 9 | 10 | /** 11 | * @author huangxuyang 12 | * date 2019/2/21 13 | */ 14 | public class DefaultResponseProcessor implements ResponseProcessor { 15 | 16 | @Override 17 | public Object process(HttpResponse response, Method method) { 18 | // not need a return value 19 | if (ObjectUtils.equals(method.getReturnType(), Void.class) 20 | || ObjectUtils.equals(method.getReturnType(), void.class)) { 21 | return null; 22 | } 23 | String body = response.getBody(); 24 | if (body == null || body.trim().isEmpty()) { 25 | return null; 26 | } 27 | // return body if return type is Object 28 | if (method.getReturnType() == Object.class) { 29 | return response.getBody(); 30 | } 31 | if (method.getReturnType() == String.class 32 | || method.getReturnType() == CharSequence.class) { 33 | return body; 34 | } 35 | if (method.getReturnType() == byte[].class) { 36 | return response.getBodyAsBytes(); 37 | } 38 | if (method.getReturnType().isAssignableFrom(BufferedInputStream.class)) { 39 | return response.getBodyStream(); 40 | } 41 | if (method.getReturnType().isAssignableFrom(response.getClass())) { 42 | return response; 43 | } 44 | // get generic return type 45 | Type type = method.getGenericReturnType(); 46 | type = type == null ? method.getReturnType() : type; 47 | return JsonSerializerDecider.getJsonSerializer().parseObject(response.getBody(), type); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/HttpClientRequestor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import com.github.dadiyang.httpinvoker.serializer.JsonSerializerDecider; 4 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 5 | import com.github.dadiyang.httpinvoker.util.ParamUtils; 6 | import com.github.dadiyang.httpinvoker.util.StringUtils; 7 | import org.apache.http.HttpEntity; 8 | import org.apache.http.HttpMessage; 9 | import org.apache.http.client.config.RequestConfig; 10 | import org.apache.http.client.entity.UrlEncodedFormEntity; 11 | import org.apache.http.client.methods.*; 12 | import org.apache.http.entity.BasicHttpEntity; 13 | import org.apache.http.entity.BufferedHttpEntity; 14 | import org.apache.http.entity.ByteArrayEntity; 15 | import org.apache.http.entity.ContentType; 16 | import org.apache.http.entity.mime.MultipartEntityBuilder; 17 | import org.apache.http.impl.client.CloseableHttpClient; 18 | import org.apache.http.impl.client.HttpClients; 19 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 20 | import org.apache.http.message.BasicNameValuePair; 21 | import org.apache.http.util.EntityUtils; 22 | 23 | import java.io.IOException; 24 | import java.nio.charset.Charset; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | import static com.github.dadiyang.httpinvoker.enumeration.ReqMethod.*; 30 | import static com.github.dadiyang.httpinvoker.util.ParamUtils.*; 31 | 32 | /** 33 | * an http requestor base on HttpClient 34 | * 35 | * @author huangxuyang 36 | * @since 2019-06-13 37 | */ 38 | public class HttpClientRequestor implements Requestor { 39 | private static final String FORM_URLENCODED = "application/x-www-form-urlencoded"; 40 | private static final String APPLICATION_JSON = "application/json"; 41 | private static final String CONTENT_TYPE = "Content-Type"; 42 | private CloseableHttpClient httpClient; 43 | 44 | /** 45 | * 使用默认的 httpClient 实现和配置 46 | */ 47 | public HttpClientRequestor() { 48 | httpClient = createHttpClient(); 49 | } 50 | 51 | /** 52 | * 自定义配置 httpClient 53 | */ 54 | public HttpClientRequestor(CloseableHttpClient httpClient) { 55 | this.httpClient = httpClient; 56 | } 57 | 58 | private CloseableHttpClient createHttpClient() { 59 | PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(); 60 | poolingHttpClientConnectionManager.setMaxTotal(32); 61 | poolingHttpClientConnectionManager.setDefaultMaxPerRoute(16); 62 | return HttpClients.custom() 63 | .setConnectionManager(poolingHttpClientConnectionManager) 64 | .build(); 65 | } 66 | 67 | @Override 68 | public HttpResponse sendRequest(HttpRequest request) throws IOException { 69 | String method = StringUtils.upperCase(request.getMethod()); 70 | if (ObjectUtils.equals(method, GET)) { 71 | return sendGet(request); 72 | } else if (ObjectUtils.equals(method, POST)) { 73 | return sendPost(request); 74 | } else if (ObjectUtils.equals(method, PUT)) { 75 | return sendPut(request); 76 | } else if (ObjectUtils.equals(method, DELETE)) { 77 | return sendDelete(request); 78 | } else if (ObjectUtils.equals(method, PATCH)) { 79 | return sendPatch(request); 80 | } else if (ObjectUtils.equals(method, HEAD)) { 81 | return sendHead(request); 82 | } else if (ObjectUtils.equals(method, OPTIONS)) { 83 | return sendOptions(request); 84 | } else if (ObjectUtils.equals(method, TRACE)) { 85 | return sendTrace(request); 86 | } else { 87 | throw new IllegalArgumentException("Unsupported http method: " + method); 88 | } 89 | } 90 | 91 | private HttpResponse sendTrace(HttpRequest request) throws IOException { 92 | String fullUrl = request.getUrl() + toQueryString(request.getData()); 93 | HttpTrace httpTrace = new HttpTrace(fullUrl); 94 | return sendRequest(request, httpTrace); 95 | } 96 | 97 | private HttpResponse sendOptions(HttpRequest request) throws IOException { 98 | String fullUrl = request.getUrl() + toQueryString(request.getData()); 99 | HttpOptions httpOptions = new HttpOptions(fullUrl); 100 | return sendRequest(request, httpOptions); 101 | } 102 | 103 | private HttpResponse sendHead(HttpRequest request) throws IOException { 104 | String fullUrl = request.getUrl() + toQueryString(request.getData()); 105 | HttpHead httpHead = new HttpHead(fullUrl); 106 | return sendRequest(request, httpHead); 107 | } 108 | 109 | private HttpResponse sendPatch(HttpRequest request) throws IOException { 110 | HttpEntity entity = createHttpEntity(request); 111 | HttpPatch httpPatch = new HttpPatch(request.getUrl()); 112 | httpPatch.setEntity(entity); 113 | return sendRequest(request, httpPatch); 114 | } 115 | 116 | private HttpResponse sendDelete(HttpRequest request) throws IOException { 117 | String fullUrl = request.getUrl() + toQueryString(request.getData()); 118 | HttpDelete httpDelete = new HttpDelete(fullUrl); 119 | return sendRequest(request, httpDelete); 120 | } 121 | 122 | private HttpResponse sendPut(HttpRequest request) throws IOException { 123 | HttpEntity entity = createHttpEntity(request); 124 | HttpPut httpPut = new HttpPut(request.getUrl()); 125 | httpPut.setEntity(entity); 126 | return sendRequest(request, httpPut); 127 | } 128 | 129 | private HttpResponse sendPost(HttpRequest request) throws IOException { 130 | // handle MultiPart 131 | if (isUploadRequest(request.getBody())) { 132 | MultiPart multiPart; 133 | if (!(request.getBody() instanceof MultiPart)) { 134 | multiPart = ParamUtils.convertInputStreamAndFile(request); 135 | } else { 136 | multiPart = (MultiPart) request.getBody(); 137 | } 138 | MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 139 | builder.setLaxMode(); 140 | for (MultiPart.Part part : multiPart.getParts()) { 141 | if (part.getInputStream() != null) { 142 | builder.addBinaryBody(part.getKey(), part.getInputStream(), ContentType.DEFAULT_BINARY, part.getValue()); 143 | } else { 144 | ContentType contentType = ContentType.create("text/plain", "UTF-8"); 145 | builder.addTextBody(part.getKey(), part.getValue(), contentType); 146 | } 147 | } 148 | HttpEntity entity = builder.build(); 149 | HttpPost httpPost = new HttpPost(request.getUrl()); 150 | httpPost.setEntity(entity); 151 | return sendMultiPartRequest(request, httpPost); 152 | } 153 | HttpEntity entity = createHttpEntity(request); 154 | HttpPost httpPost = new HttpPost(request.getUrl()); 155 | httpPost.setEntity(entity); 156 | return sendRequest(request, httpPost); 157 | } 158 | 159 | private HttpEntity createHttpEntity(HttpRequest request) throws IOException { 160 | HttpEntity entity; 161 | // handle x-www-form-urlencoded 162 | if (request.getHeaders() != null 163 | && ObjectUtils.equals(FORM_URLENCODED, request.getHeaders().get(CONTENT_TYPE))) { 164 | List parameters = new ArrayList(); 165 | Map map = toMapStringString(request.getData(), ""); 166 | for (Map.Entry entry : map.entrySet()) { 167 | parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); 168 | } 169 | entity = new UrlEncodedFormEntity(parameters, "UTF-8"); 170 | } else { 171 | if (request.getBody() != null) { 172 | entity = new ByteArrayEntity(JsonSerializerDecider.getJsonSerializer().serialize(request.getBody()).getBytes(Charset.forName("UTF-8")), 173 | ContentType.create(APPLICATION_JSON, "UTF-8")); 174 | } else if (request.getData() != null) { 175 | entity = new ByteArrayEntity(JsonSerializerDecider.getJsonSerializer().serialize(request.getData()).getBytes(Charset.forName("UTF-8")), 176 | ContentType.create(APPLICATION_JSON, "UTF-8")); 177 | } else { 178 | BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); 179 | basicHttpEntity.setContentLength(0); 180 | entity = basicHttpEntity; 181 | } 182 | } 183 | return entity; 184 | } 185 | 186 | private HttpResponse sendGet(HttpRequest request) throws IOException { 187 | String fullUrl = request.getUrl() + toQueryString(request.getData()); 188 | HttpGet httpGet = new HttpGet(fullUrl); 189 | return sendRequest(request, httpGet); 190 | } 191 | 192 | private HttpResponse sendRequest(HttpRequest request, HttpRequestBase httpRequestBase) throws IOException { 193 | prepare(request, httpRequestBase); 194 | CloseableHttpResponse response = httpClient.execute(httpRequestBase); 195 | if (response.getEntity() != null) { 196 | response.setEntity(new BufferedHttpEntity(response.getEntity())); 197 | EntityUtils.consume(response.getEntity()); 198 | } 199 | return new HttpClientResponse(response); 200 | } 201 | 202 | private HttpResponse sendMultiPartRequest(HttpRequest request, HttpRequestBase httpRequestBase) throws IOException { 203 | prepare(request, httpRequestBase); 204 | CloseableHttpClient httpClient = null; 205 | try { 206 | httpClient = createHttpClient(); 207 | CloseableHttpResponse response = httpClient.execute(httpRequestBase); 208 | response.setEntity(new BufferedHttpEntity(response.getEntity())); 209 | EntityUtils.consume(response.getEntity()); 210 | return new HttpClientResponse(response); 211 | } finally { 212 | if (httpClient != null) { 213 | httpClient.close(); 214 | } 215 | } 216 | } 217 | 218 | private void prepare(HttpRequest request, HttpRequestBase httpRequestBase) { 219 | addHeaders(request, httpRequestBase); 220 | addCookies(request, httpRequestBase); 221 | RequestConfig requestConfig = RequestConfig.custom() 222 | .setConnectionRequestTimeout(request.getTimeout()) 223 | .build(); 224 | httpRequestBase.setConfig(requestConfig); 225 | } 226 | 227 | private void addCookies(HttpRequest request, HttpMessage msg) { 228 | Map cookies = request.getCookies(); 229 | if (cookies == null || cookies.isEmpty()) { 230 | return; 231 | } 232 | StringBuilder sb = new StringBuilder(); 233 | for (Map.Entry entry : cookies.entrySet()) { 234 | sb.append(entry.getKey()).append("=").append(entry.getValue()).append(";"); 235 | } 236 | msg.addHeader("Cookie", sb.substring(0, sb.length())); 237 | } 238 | 239 | private void addHeaders(HttpRequest request, HttpMessage msg) { 240 | Map headers = request.getHeaders(); 241 | if (headers != null && !headers.isEmpty()) { 242 | for (Map.Entry entry : headers.entrySet()) { 243 | msg.addHeader(entry.getKey(), entry.getValue()); 244 | } 245 | } 246 | } 247 | 248 | public CloseableHttpClient getHttpClient() { 249 | return httpClient; 250 | } 251 | 252 | public void setHttpClient(CloseableHttpClient httpClient) { 253 | this.httpClient = httpClient; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/HttpClientResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import org.apache.http.Header; 4 | import org.apache.http.HeaderElement; 5 | import org.apache.http.client.methods.CloseableHttpResponse; 6 | import org.apache.http.util.EntityUtils; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.*; 11 | 12 | /** 13 | * @author huangxuyang 14 | * @since 2019-06-13 15 | */ 16 | public class HttpClientResponse implements HttpResponse { 17 | public static final String SET_COOKIE = "set-cookie"; 18 | private final CloseableHttpResponse response; 19 | 20 | public HttpClientResponse(CloseableHttpResponse response) { 21 | this.response = response; 22 | } 23 | 24 | @Override 25 | public int getStatusCode() { 26 | return response.getStatusLine().getStatusCode(); 27 | } 28 | 29 | @Override 30 | public String getStatusMessage() { 31 | return response.getStatusLine().getReasonPhrase(); 32 | } 33 | 34 | @Override 35 | public String getCharset() { 36 | return response.getEntity().getContentEncoding().getValue(); 37 | } 38 | 39 | @Override 40 | public String getContentType() { 41 | return response.getEntity().getContentType().getValue(); 42 | } 43 | 44 | @Override 45 | public byte[] getBodyAsBytes() { 46 | try { 47 | return EntityUtils.toByteArray(response.getEntity()); 48 | } catch (IOException e) { 49 | throw new IllegalStateException("cannot read bytes from response!", e); 50 | } 51 | } 52 | 53 | @Override 54 | public InputStream getBodyStream() { 55 | try { 56 | return response.getEntity().getContent(); 57 | } catch (IOException e) { 58 | throw new IllegalStateException("cannot read stream from response!", e); 59 | } 60 | } 61 | 62 | @Override 63 | public String getBody() { 64 | try { 65 | return EntityUtils.toString(response.getEntity(), "UTF-8"); 66 | } catch (IOException e) { 67 | throw new IllegalStateException("cannot read String from response!", e); 68 | } 69 | } 70 | 71 | @Override 72 | public Map getHeaders() { 73 | Header[] headers = response.getAllHeaders(); 74 | if (headers == null || headers.length == 0) { 75 | return Collections.emptyMap(); 76 | } 77 | Map map = new HashMap(headers.length); 78 | for (Header header : headers) { 79 | map.put(header.getName(), header.getValue()); 80 | } 81 | return map; 82 | } 83 | 84 | @Override 85 | public String getHeader(String name) { 86 | return response.getFirstHeader(name).getValue(); 87 | } 88 | 89 | @Override 90 | public Map getCookies() { 91 | Header[] headers = response.getAllHeaders(); 92 | if (headers == null || headers.length == 0) { 93 | return Collections.emptyMap(); 94 | } 95 | Map map = new HashMap(); 96 | for (Header header : headers) { 97 | if (SET_COOKIE.equalsIgnoreCase(header.getName())) { 98 | for (HeaderElement element : header.getElements()) { 99 | map.put(element.getName(), element.getValue()); 100 | } 101 | } 102 | } 103 | return map; 104 | } 105 | 106 | @Override 107 | public String getCookie(String name) { 108 | return getCookies().get(name); 109 | } 110 | 111 | @Override 112 | public Map> multiHeaders() { 113 | Header[] headers = response.getAllHeaders(); 114 | if (headers == null || headers.length == 0) { 115 | return Collections.emptyMap(); 116 | } 117 | Map> map = new HashMap>(headers.length); 118 | for (Header header : headers) { 119 | List values = map.containsKey(header.getName()) ? map.get(header.getName()) : new LinkedList(); 120 | values.add(header.getValue()); 121 | map.put(header.getName(), values); 122 | } 123 | return map; 124 | } 125 | 126 | @Override 127 | public List getHeaders(String name) { 128 | Header[] headers = response.getHeaders(name); 129 | if (headers == null || headers.length == 0) { 130 | return Collections.emptyList(); 131 | } 132 | List values = new LinkedList(); 133 | for (Header header : headers) { 134 | values.add(header.getValue()); 135 | } 136 | return values; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/HttpRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author huangxuyang 8 | * date 2018/12/7 9 | */ 10 | public class HttpRequest { 11 | private String method = "GET"; 12 | private int timeout = 5000; 13 | private String url; 14 | private Map headers; 15 | private Map cookies; 16 | private Map data; 17 | private Object body; 18 | private String fileFormKey; 19 | 20 | public HttpRequest(String url) { 21 | this.url = url; 22 | } 23 | 24 | public HttpRequest(String url, String method, int timeout) { 25 | this.method = method; 26 | this.timeout = timeout; 27 | this.url = url; 28 | } 29 | 30 | public HttpRequest(int timeout, String method) { 31 | this.method = method; 32 | this.timeout = timeout; 33 | } 34 | 35 | public String getMethod() { 36 | return method; 37 | } 38 | 39 | public void setMethod(String method) { 40 | this.method = method; 41 | } 42 | 43 | public int getTimeout() { 44 | return timeout; 45 | } 46 | 47 | public void setTimeout(int timeout) { 48 | this.timeout = timeout; 49 | } 50 | 51 | public Map getHeaders() { 52 | return headers; 53 | } 54 | 55 | public void setHeaders(Map headers) { 56 | this.headers = new HashMap(headers); 57 | } 58 | 59 | public void addHeader(String key, String value) { 60 | if (headers == null) { 61 | headers = new HashMap(8); 62 | } 63 | headers.put(key, value); 64 | } 65 | 66 | public void addCookie(String key, String value) { 67 | if (cookies == null) { 68 | cookies = new HashMap(8); 69 | } 70 | cookies.put(key, value); 71 | } 72 | 73 | public Map getCookies() { 74 | return cookies; 75 | } 76 | 77 | public void setCookies(Map cookies) { 78 | this.cookies = new HashMap(cookies); 79 | } 80 | 81 | public Map getData() { 82 | return data; 83 | } 84 | 85 | public void setData(Map data) { 86 | this.data = data; 87 | } 88 | 89 | public void addParam(String key, String value) { 90 | if (data == null) { 91 | data = new HashMap(8); 92 | } 93 | data.put(key, value); 94 | } 95 | 96 | public Object getBody() { 97 | return body; 98 | } 99 | 100 | public void setBody(Object body) { 101 | this.body = body; 102 | } 103 | 104 | public String getUrl() { 105 | return url; 106 | } 107 | 108 | public void setUrl(String url) { 109 | this.url = url; 110 | } 111 | 112 | public String getFileFormKey() { 113 | return fileFormKey; 114 | } 115 | 116 | public void setFileFormKey(String fileFormKey) { 117 | this.fileFormKey = fileFormKey; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.io.InputStream; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * @author huangxuyang 9 | * date 2018/12/6 10 | */ 11 | public interface HttpResponse { 12 | 13 | int getStatusCode(); 14 | 15 | String getStatusMessage(); 16 | 17 | String getCharset(); 18 | 19 | String getContentType(); 20 | 21 | byte[] getBodyAsBytes(); 22 | 23 | InputStream getBodyStream(); 24 | 25 | String getBody(); 26 | 27 | Map getHeaders(); 28 | 29 | Map> multiHeaders(); 30 | 31 | List getHeaders(String name); 32 | 33 | String getHeader(String name); 34 | 35 | Map getCookies(); 36 | 37 | String getCookie(String name); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/JsoupHttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import org.jsoup.Connection; 4 | 5 | import java.io.InputStream; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author huangxuyang 11 | * date 2018/12/6 12 | */ 13 | public class JsoupHttpResponse implements HttpResponse { 14 | private final Connection.Response response; 15 | 16 | public JsoupHttpResponse(Connection.Response response) { 17 | this.response = response; 18 | } 19 | 20 | @Override 21 | public int getStatusCode() { 22 | return response.statusCode(); 23 | } 24 | 25 | @Override 26 | public String getStatusMessage() { 27 | return response.statusMessage(); 28 | } 29 | 30 | @Override 31 | public String getCharset() { 32 | return response.charset(); 33 | } 34 | 35 | @Override 36 | public String getContentType() { 37 | return response.contentType(); 38 | } 39 | 40 | @Override 41 | public byte[] getBodyAsBytes() { 42 | return response.bodyAsBytes(); 43 | } 44 | 45 | @Override 46 | public InputStream getBodyStream() { 47 | return response.bodyStream(); 48 | } 49 | 50 | @Override 51 | public String getBody() { 52 | return response.body(); 53 | } 54 | 55 | @Override 56 | public Map getHeaders() { 57 | return response.headers(); 58 | } 59 | 60 | @Override 61 | public String getHeader(String name) { 62 | return response.header(name); 63 | } 64 | 65 | @Override 66 | public Map getCookies() { 67 | return response.cookies(); 68 | } 69 | 70 | @Override 71 | public String getCookie(String name) { 72 | return response.cookie(name); 73 | } 74 | 75 | @Override 76 | public Map> multiHeaders() { 77 | return response.multiHeaders(); 78 | } 79 | 80 | @Override 81 | public List getHeaders(String name) { 82 | return response.headers(name); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/JsoupRequestor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import com.github.dadiyang.httpinvoker.serializer.JsonSerializerDecider; 4 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 5 | import org.jsoup.Connection; 6 | import org.jsoup.Jsoup; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.IOException; 11 | import java.util.Map; 12 | 13 | import static com.github.dadiyang.httpinvoker.util.ParamUtils.*; 14 | import static org.jsoup.Connection.Method; 15 | import static org.jsoup.Connection.Response; 16 | 17 | /** 18 | * The default implementation of {@link Requestor} that send the request using Jsoup which is a elegant http client I've ever use. 19 | *

20 | * the parameter will be formatted according to the request method. 21 | * 22 | * @author huangxuyang 23 | * date 2018/11/1 24 | */ 25 | public class JsoupRequestor implements Requestor { 26 | private static final Logger log = LoggerFactory.getLogger(JsoupRequestor.class); 27 | private static final String CONTENT_TYPE = "Content-Type"; 28 | private static final String APPLICATION_JSON = "application/json"; 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | @Override 34 | public HttpResponse sendRequest(HttpRequest request) throws IOException { 35 | // send request 36 | Method m = Method.valueOf(request.getMethod().toUpperCase()); 37 | Response response; 38 | String url = request.getUrl(); 39 | int timeout = request.getTimeout(); 40 | if (!m.hasBody()) { 41 | String qs = toQueryString(request.getData()); 42 | String fullUrl = request.getUrl() + qs; 43 | log.debug("send {} request to {}", m, fullUrl); 44 | Connection conn = Jsoup.connect(fullUrl) 45 | .method(m) 46 | .timeout(timeout) 47 | // unlimited size 48 | .maxBodySize(0) 49 | .ignoreContentType(true) 50 | .ignoreHttpErrors(true); 51 | addHeadersAndCookies(request, conn); 52 | setContentType(request, conn); 53 | response = conn.execute(); 54 | } else { 55 | Connection conn = Jsoup.connect(url) 56 | .method(m) 57 | .timeout(timeout) 58 | // unlimited size 59 | .maxBodySize(0) 60 | .ignoreContentType(true) 61 | .ignoreHttpErrors(true); 62 | addHeadersAndCookies(request, conn); 63 | setContentType(request, conn); 64 | Map data = request.getData(); 65 | // body first 66 | if (request.getBody() != null) { 67 | Object bodyParam = request.getBody(); 68 | // if the body param is MultiPart or InputStream, submit a multipart form 69 | if (isUploadRequest(bodyParam)) { 70 | log.debug("upload file {} request to {} ", m, url); 71 | response = uploadFile(request); 72 | } else { 73 | if (useJson(request, bodyParam)) { 74 | response = conn.requestBody(JsonSerializerDecider.getJsonSerializer().serialize(bodyParam)).execute(); 75 | } else { 76 | Map map = toMapStringString(bodyParam, ""); 77 | response = conn.data(map).execute(); 78 | } 79 | } 80 | } else if (data == null 81 | || data.isEmpty()) { 82 | log.debug("send {} request to {}", m, url); 83 | response = conn.execute(); 84 | } else { 85 | if (useJson(request, data)) { 86 | if (m == Method.PATCH) { 87 | // use X-HTTP-Method-Override header to send a fake PATCH request 88 | conn.method(Method.POST).header("X-HTTP-Method-Override", "PATCH"); 89 | } 90 | response = conn.requestBody(JsonSerializerDecider.getJsonSerializer().serialize(data)).execute(); 91 | } else { 92 | Map map = toMapStringString(data, ""); 93 | response = conn.data(map).execute(); 94 | } 95 | } 96 | } 97 | return new JsoupHttpResponse(response); 98 | } 99 | 100 | private void setContentType(HttpRequest request, Connection conn) { 101 | // set a default Content-Type if not provided 102 | if (request.getHeaders() == null || !request.getHeaders().containsKey(CONTENT_TYPE)) { 103 | conn.header(CONTENT_TYPE, APPLICATION_JSON); 104 | } 105 | } 106 | 107 | /** 108 | * either param is a collection or Content-Type absence or equals to APPLICATION_JSON 109 | */ 110 | private boolean useJson(HttpRequest request, Object param) { 111 | // collection can only be send by json currently 112 | return isCollection(param) || request.getHeaders() == null 113 | || !request.getHeaders().containsKey(CONTENT_TYPE) 114 | || ObjectUtils.equals(request.getHeaders().get(CONTENT_TYPE), APPLICATION_JSON); 115 | } 116 | 117 | private void addHeadersAndCookies(HttpRequest request, Connection conn) { 118 | if (request.getHeaders() != null) { 119 | conn.headers(request.getHeaders()); 120 | } 121 | if (request.getCookies() != null) { 122 | conn.cookies(request.getCookies()); 123 | } 124 | } 125 | 126 | /** 127 | * @param request the request 128 | */ 129 | private Response uploadFile(HttpRequest request) throws IOException { 130 | Connection conn = Jsoup.connect(request.getUrl()); 131 | conn.method(Method.POST) 132 | .timeout(request.getTimeout()) 133 | .ignoreHttpErrors(true) 134 | // unlimited size 135 | .maxBodySize(0) 136 | .ignoreContentType(true); 137 | addHeadersAndCookies(request, conn); 138 | Object body = request.getBody(); 139 | // handle MultiPart 140 | if (body instanceof MultiPart) { 141 | return handleMultiPart(conn, (MultiPart) body); 142 | } else { 143 | return handleMultiPart(conn, convertInputStreamAndFile(request)); 144 | } 145 | } 146 | 147 | private Response handleMultiPart(Connection conn, MultiPart body) throws IOException { 148 | for (MultiPart.Part part : body.getParts()) { 149 | if (part.getKey() == null || part.getValue() == null) { 150 | throw new IllegalArgumentException("both key and value of part must not be null"); 151 | } 152 | if (part.getInputStream() != null) { 153 | conn.data(part.getKey(), part.getValue(), part.getInputStream()); 154 | } else { 155 | conn.data(part.getKey(), part.getValue()); 156 | } 157 | } 158 | return conn.execute(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/MultiPart.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.io.InputStream; 4 | import java.util.Collections; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | /** 9 | * multipart/form-data params 10 | * 11 | * @author dadiyang 12 | * @since 2019/4/28 13 | */ 14 | public class MultiPart { 15 | 16 | private List parts; 17 | 18 | public MultiPart() { 19 | parts = new LinkedList(); 20 | } 21 | 22 | public MultiPart(List parts) { 23 | if (parts == null) { 24 | throw new IllegalArgumentException("parts cannot be null"); 25 | } 26 | this.parts = parts; 27 | } 28 | 29 | public void addPart(Part part) { 30 | if (part == null) { 31 | throw new IllegalArgumentException("part cannot be null"); 32 | } 33 | parts.add(part); 34 | } 35 | 36 | /** 37 | * return an unmodifiable parts list 38 | */ 39 | public List getParts() { 40 | return Collections.unmodifiableList(parts); 41 | } 42 | 43 | /** 44 | * remove a part 45 | */ 46 | public boolean remove(Part part) { 47 | if (part == null) { 48 | throw new IllegalArgumentException("part cannot be null"); 49 | } 50 | return parts.remove(part); 51 | } 52 | 53 | /** 54 | * a part of MultiPart params 55 | */ 56 | public static class Part { 57 | private String key; 58 | private String value; 59 | private InputStream inputStream; 60 | 61 | public Part() { 62 | } 63 | 64 | public Part(String key, String value) { 65 | this.key = key; 66 | this.value = value; 67 | } 68 | 69 | public Part(String key, String value, InputStream inputStream) { 70 | this.key = key; 71 | this.value = value; 72 | this.inputStream = inputStream; 73 | } 74 | 75 | public String getKey() { 76 | return key; 77 | } 78 | 79 | public void setKey(String key) { 80 | this.key = key; 81 | } 82 | 83 | public String getValue() { 84 | return value; 85 | } 86 | 87 | public void setValue(String value) { 88 | this.value = value; 89 | } 90 | 91 | public InputStream getInputStream() { 92 | return inputStream; 93 | } 94 | 95 | public void setInputStream(InputStream inputStream) { 96 | this.inputStream = inputStream; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/RequestPreprocessor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | /** 6 | * pre-process request before send it. 7 | *

8 | * we can modify headers, cookies, params or even url of the request before actually send it. 9 | * 10 | * @author dadiyang 11 | * date 2019/1/9 12 | */ 13 | public interface RequestPreprocessor { 14 | /** 15 | * for compatible, we use a ThreadLocal to store current method 16 | */ 17 | ThreadLocal CURRENT_METHOD_THREAD_LOCAL = new ThreadLocal(); 18 | 19 | /** 20 | * Pre-processing the request 21 | * 22 | * @param request the request to send 23 | */ 24 | void process(HttpRequest request); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/Requestor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * 发送请求的工具 7 | * 8 | * @author huangxuyang 9 | * date 2018/11/1 10 | */ 11 | public interface Requestor { 12 | /** 13 | * 发送请求 14 | * 15 | * @param request the request info 16 | * @return 发送请求后的返回值 17 | * @throws IOException IO异常 18 | */ 19 | HttpResponse sendRequest(HttpRequest request) throws IOException; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/ResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | /** 6 | * process response after got response. 7 | *

8 | * we will be able to handle result before method return. 9 | * 10 | * @author dadiyang 11 | * date 2019/2/21 12 | */ 13 | public interface ResponseProcessor { 14 | 15 | /** 16 | * processing response before method return 17 | * 18 | * @param response response 19 | * @param method the proxied method 20 | * @return the proxied method's return value 21 | */ 22 | Object process(HttpResponse response, Method method); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/ResultBeanResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import com.github.dadiyang.httpinvoker.annotation.ExpectedCode; 4 | import com.github.dadiyang.httpinvoker.annotation.HttpReq; 5 | import com.github.dadiyang.httpinvoker.annotation.NotResultBean; 6 | import com.github.dadiyang.httpinvoker.exception.UnexpectedResultException; 7 | import com.github.dadiyang.httpinvoker.serializer.JsonSerializerDecider; 8 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 9 | import com.github.dadiyang.httpinvoker.util.ParamUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.io.BufferedInputStream; 15 | import java.lang.reflect.Field; 16 | import java.lang.reflect.Method; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | /** 24 | * 注册响应处理器,用于对后台返回的结果都是类似 {code: 0, msg/message: 'success', data: 'OK'} 的结构, 25 | *

26 | * 此时我们只需要判断 code 是否为期望的值 (ExpectedCode中设置),是的话,解析 data 的值否则抛出异常 27 | * 28 | * @author huangxuyang 29 | * @since 1.1.4 30 | */ 31 | @Component 32 | public class ResultBeanResponseProcessor implements ResponseProcessor { 33 | private static final Logger log = LoggerFactory.getLogger(ResultBeanResponseProcessor.class); 34 | private static final String CODE = "code"; 35 | private static final String DATA = "data"; 36 | private static final String MESSAGE = "message"; 37 | private static final String MSG = "msg"; 38 | private Map, Boolean> isResultBeanCache = new ConcurrentHashMap, Boolean>(); 39 | 40 | @Override 41 | public Object process(HttpResponse response, Method method) throws UnexpectedResultException { 42 | String rs = response.getBody(); 43 | // 声明接口返回值不是 ResultBean,则直接解析 44 | if (method.isAnnotationPresent(NotResultBean.class) 45 | || method.getDeclaringClass().isAnnotationPresent(NotResultBean.class)) { 46 | return parseObject(method, rs); 47 | } 48 | // 以下几种情况下,无需解析响应 49 | if (rs == null || rs.trim().isEmpty()) { 50 | return null; 51 | } 52 | Class returnType = method.getReturnType(); 53 | if (returnType == byte[].class) { 54 | return response.getBodyAsBytes(); 55 | } 56 | if (returnType != Object.class && returnType.isAssignableFrom(BufferedInputStream.class)) { 57 | return response.getBodyStream(); 58 | } 59 | if (returnType != Object.class && returnType.isAssignableFrom(response.getClass())) { 60 | return response; 61 | } 62 | ExpectedCode expectedCode = getExpectedAnnotation(method); 63 | // 如果返回值要求的就是一个 ResultBean,则不做处理 64 | if (ObjectUtils.equals(isResultBean(expectedCode, returnType), true)) { 65 | return parseObject(method, rs); 66 | } 67 | Map obj = JsonSerializerDecider.getJsonSerializer().toMap(rs); 68 | if (isResponseNotResultBean(expectedCode, obj)) { 69 | // 非 ResultBean 则解析整个返回结果 70 | return parseObject(method, rs); 71 | } 72 | 73 | // 标准的 ResultBean 包装类处理,进行解包处理,即只取 data 的值 74 | if (isExpectedCode(expectedCode, obj)) { 75 | // code 为期望的值时说明返回结果是正确的 76 | return parseObject(method, ObjectUtils.toString(obj.get(DATA))); 77 | } else { 78 | // 否则为接口返回错误 79 | HttpReq req = method.getAnnotation(HttpReq.class); 80 | String uri = req != null ? req.value() : method.getName(); 81 | // 兼容两种错误信息的写法 82 | String errMsg = obj.containsKey(MESSAGE) ? ObjectUtils.toString(obj.get(MESSAGE)) : ObjectUtils.toString(obj.get(MSG)); 83 | log.warn("请求api失败, uri: " + uri + ", 错误信息: " + errMsg); 84 | throw new UnexpectedResultException(errMsg); 85 | } 86 | } 87 | 88 | private boolean isExpectedCode(ExpectedCode expectedCode, Map obj) { 89 | if (expectedCode != null) { 90 | return isExpectedCode(obj, expectedCode.value(), expectedCode.codeFieldName(), expectedCode.ignoreFieldInitialCase()); 91 | } 92 | // 默认 期望 0, 字段 code, 忽略首字母大小写 93 | return isExpectedCode(obj, 0, CODE, true); 94 | } 95 | 96 | 97 | private boolean isExpectedCode(Map obj, int expectedCode, String codeField, boolean ignoreFieldInitialCase) { 98 | // 如果没有,而且需要忽略首字母大小写,则改变首字母大小写 99 | if (!obj.containsKey(codeField) && ignoreFieldInitialCase) { 100 | codeField = ParamUtils.changeInitialCase(codeField); 101 | } 102 | return ObjectUtils.equals(expectedCode, Integer.parseInt(obj.get(codeField).toString())); 103 | } 104 | 105 | private ExpectedCode getExpectedAnnotation(Method method) { 106 | // 方法是有打注解,则使用方法上的 107 | if (method.isAnnotationPresent(ExpectedCode.class)) { 108 | return method.getAnnotation(ExpectedCode.class); 109 | } 110 | // 否则使用类上的注解 111 | Class clazz = method.getDeclaringClass(); 112 | if (clazz.isAnnotationPresent(ExpectedCode.class)) { 113 | return clazz.getAnnotation(ExpectedCode.class); 114 | } 115 | return null; 116 | } 117 | 118 | /** 119 | * 判断一个 Class 是否为 ResultBean,即是否同时包含 code/msg/data 三个字段 120 | */ 121 | private Boolean isResultBean(final ExpectedCode expectedCode, final Class returnType) { 122 | if (isResultBeanCache.containsKey(returnType)) { 123 | return isResultBeanCache.get(returnType); 124 | } 125 | synchronized (this) { 126 | if (isResultBeanCache.containsKey(returnType)) { 127 | return isResultBeanCache.get(returnType); 128 | } 129 | if (ParamUtils.isBasicType(returnType) || returnType.isInterface()) { 130 | return false; 131 | } 132 | Field[] fields = getDeclaredFields(returnType); 133 | boolean hasCode = false; 134 | boolean hasMsg = false; 135 | boolean hasData = false; 136 | String codeField = expectedCode == null ? CODE : expectedCode.codeFieldName(); 137 | boolean ignoreInitialCase = expectedCode == null || expectedCode.ignoreFieldInitialCase(); 138 | for (Field field : fields) { 139 | String fieldName = field.getName().toLowerCase(); 140 | hasCode = hasCode || ObjectUtils.equals(codeField, fieldName) || (ignoreInitialCase && ObjectUtils.equals(fieldName, ParamUtils.changeInitialCase(codeField))); 141 | hasMsg = hasMsg || ObjectUtils.equals(MSG, fieldName) || ObjectUtils.equals(MESSAGE, fieldName); 142 | hasData = hasData || ObjectUtils.equals(DATA, fieldName); 143 | } 144 | boolean isResultBean = hasCode && hasMsg && hasData; 145 | isResultBeanCache.put(returnType, isResultBean); 146 | return isResultBean; 147 | } 148 | } 149 | 150 | /** 151 | * 获取字段,包含父级 152 | */ 153 | private Field[] getDeclaredFields(Class returnType) { 154 | if (returnType.getSuperclass() == Object.class) { 155 | return returnType.getDeclaredFields(); 156 | } else { 157 | List fields = new ArrayList(); 158 | Class type = returnType; 159 | while (type != null && type != Object.class && !type.isInterface()) { 160 | fields.addAll(Arrays.asList(type.getDeclaredFields())); 161 | type = type.getSuperclass(); 162 | } 163 | return fields.toArray(new Field[]{}); 164 | } 165 | } 166 | 167 | /** 168 | * 没有包含 code、msg/message 和 data 则不是 ResultBean 169 | */ 170 | private boolean isResponseNotResultBean(ExpectedCode expectedCode, Map obj) { 171 | boolean hasCode = false; 172 | boolean hasMsg = false; 173 | boolean hasData = false; 174 | String codeField = expectedCode == null ? CODE : expectedCode.codeFieldName(); 175 | boolean ignoreInitialCase = expectedCode == null || expectedCode.ignoreFieldInitialCase(); 176 | for (String key : obj.keySet()) { 177 | String fieldName = key == null ? "" : key.toLowerCase(); 178 | hasCode = hasCode || ObjectUtils.equals(codeField, fieldName) || (ignoreInitialCase && ObjectUtils.equals(fieldName, ParamUtils.changeInitialCase(codeField))); 179 | hasMsg = hasMsg || ObjectUtils.equals(MSG, fieldName) || ObjectUtils.equals(MESSAGE, fieldName); 180 | hasData = hasData || ObjectUtils.equals(DATA, fieldName); 181 | } 182 | return !hasCode || (!hasMsg && !hasData); 183 | } 184 | 185 | /** 186 | * 支持泛型的反序列化方法 187 | */ 188 | private Object parseObject(Method method, String dataString) { 189 | if (dataString == null || dataString.trim().isEmpty()) { 190 | return null; 191 | } 192 | // 方法无需返回值 193 | Class returnType = method.getReturnType(); 194 | if (returnType == Void.class || returnType == void.class) { 195 | return null; 196 | } else if (returnType == Object.class 197 | || returnType == String.class 198 | || method.getReturnType() == CharSequence.class) { 199 | return dataString; 200 | } 201 | return JsonSerializerDecider.getJsonSerializer().parseObject(dataString, method.getGenericReturnType()); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/requestor/Status.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | /** 4 | * enum for http status classification 5 | * 6 | * @author dadiyang 7 | * date 2019/1/10 8 | */ 9 | public enum Status { 10 | /** 11 | * 20x 12 | */ 13 | OK(200, 299), 14 | /** 15 | * 30x 16 | */ 17 | REDIRECT(300, 399), 18 | /** 19 | * 40x 20 | */ 21 | NOT_FOUND(400, 499), 22 | /** 23 | * 50x 24 | */ 25 | SERVER_ERROR(500, 599); 26 | private int from; 27 | private int to; 28 | 29 | Status(int from, int to) { 30 | this.from = from; 31 | this.to = to; 32 | } 33 | 34 | public int getFrom() { 35 | return from; 36 | } 37 | 38 | public int getTo() { 39 | return to; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/serializer/FastJsonJsonSerializer.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * 基于 FastJson 的序列化器,仅在类路径中有 fastjson 且没有其他 Json 序列化器时使用 11 | *

12 | * 默认首选的序列化器 13 | * 14 | * @author dadiyang 15 | * @since 2019/3/1 16 | */ 17 | public class FastJsonJsonSerializer implements JsonSerializer { 18 | private static final FastJsonJsonSerializer INSTANCE = new FastJsonJsonSerializer(); 19 | 20 | public static FastJsonJsonSerializer getInstance() { 21 | return INSTANCE; 22 | } 23 | 24 | @Override 25 | public String serialize(Object object) { 26 | if (object == null) { 27 | return ""; 28 | } 29 | return JSON.toJSONString(object); 30 | } 31 | 32 | @Override 33 | public T parseObject(String json, Type type) { 34 | return JSON.parseObject(json, type); 35 | } 36 | 37 | @Override 38 | public List parseArray(String json) { 39 | return JSON.parseArray(json); 40 | } 41 | 42 | @Override 43 | public Map toMap(String json) { 44 | return JSON.parseObject(json); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/serializer/GsonJsonSerializer.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import com.google.gson.*; 4 | import com.google.gson.internal.LinkedTreeMap; 5 | import com.google.gson.reflect.TypeToken; 6 | import com.google.gson.stream.JsonReader; 7 | import com.google.gson.stream.JsonToken; 8 | import com.google.gson.stream.JsonWriter; 9 | 10 | import java.io.IOException; 11 | import java.lang.reflect.Type; 12 | import java.math.BigDecimal; 13 | import java.text.DateFormat; 14 | import java.util.ArrayList; 15 | import java.util.Date; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * 基于 gson 的 json 序列化器,仅在类路径中有 Gson 并且没有注册其他的 json 序列化器时使用 21 | * 22 | * @author dadiyang 23 | * @since 2019/3/1 24 | */ 25 | public class GsonJsonSerializer implements JsonSerializer { 26 | private static final Gson GSON = new GsonBuilder() 27 | .registerTypeAdapter(new TypeToken>() { 28 | }.getType(), NumberTypeAdapter.INSTANCE) 29 | .registerTypeAdapter(new TypeToken>() { 30 | }.getType(), NumberTypeAdapter.INSTANCE) 31 | .registerTypeAdapter(java.util.Date.class, new DateSerializer()).setDateFormat(DateFormat.LONG) 32 | .registerTypeAdapter(java.util.Date.class, new DateDeserializer()).setDateFormat(DateFormat.LONG) 33 | .create(); 34 | 35 | private static final GsonJsonSerializer INSTANCE = new GsonJsonSerializer(); 36 | 37 | public static GsonJsonSerializer getInstance() { 38 | return INSTANCE; 39 | } 40 | 41 | @Override 42 | public String serialize(Object object) { 43 | return GSON.toJson(object); 44 | } 45 | 46 | @Override 47 | public T parseObject(String json, Type type) { 48 | return GSON.fromJson(json, type); 49 | } 50 | 51 | @Override 52 | public List parseArray(String json) { 53 | Type type = new TypeToken>() { 54 | }.getType(); 55 | return GSON.fromJson(json, type); 56 | } 57 | 58 | @Override 59 | public Map toMap(String json) { 60 | return GSON.fromJson(json, new TypeToken>() { 61 | }.getType()); 62 | } 63 | 64 | /** 65 | * 解决 int 类型序列化时变成 double 类型的问题 66 | */ 67 | public static class NumberTypeAdapter extends TypeAdapter { 68 | private static final NumberTypeAdapter INSTANCE = new NumberTypeAdapter(); 69 | 70 | @Override 71 | public Object read(JsonReader in) throws IOException { 72 | JsonToken token = in.peek(); 73 | switch (token) { 74 | case BEGIN_ARRAY: 75 | List list = new ArrayList(); 76 | in.beginArray(); 77 | while (in.hasNext()) { 78 | list.add(read(in)); 79 | } 80 | in.endArray(); 81 | return list; 82 | case BEGIN_OBJECT: 83 | Map map = new LinkedTreeMap(); 84 | in.beginObject(); 85 | while (in.hasNext()) { 86 | map.put(in.nextName(), read(in)); 87 | } 88 | in.endObject(); 89 | return map; 90 | case STRING: 91 | return in.nextString(); 92 | case NUMBER: 93 | // 改写数字的处理逻辑,将数字值分为整型与浮点型。 94 | String str = in.nextString(); 95 | // 有小数点直接返回 BigDecimal 类弄浮点数 96 | if (str.contains(".")) { 97 | return new BigDecimal(str); 98 | } 99 | long lngNum = Long.parseLong(str); 100 | if (lngNum > Integer.MAX_VALUE || lngNum < Integer.MIN_VALUE) { 101 | return lngNum; 102 | } else { 103 | return (int) lngNum; 104 | } 105 | case BOOLEAN: 106 | return in.nextBoolean(); 107 | case NULL: 108 | in.nextNull(); 109 | return null; 110 | default: 111 | throw new IllegalStateException(); 112 | } 113 | } 114 | 115 | @Override 116 | public void write(JsonWriter out, Object value) { 117 | // 序列化无需实现 118 | } 119 | } 120 | 121 | /** 122 | * 日期类型的字段反序列化器 123 | */ 124 | public static class DateDeserializer implements JsonDeserializer { 125 | 126 | @Override 127 | public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 128 | return new Date(json.getAsJsonPrimitive().getAsLong()); 129 | } 130 | } 131 | 132 | /** 133 | * 时间类型序列化为时间戳 134 | */ 135 | public static class DateSerializer implements com.google.gson.JsonSerializer { 136 | @Override 137 | public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { 138 | return new JsonPrimitive(src.getTime()); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/serializer/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import java.lang.reflect.Type; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * 序列化器 9 | * 10 | * @author dadiyang 11 | * @since 2019/3/1 12 | */ 13 | public interface JsonSerializer { 14 | /** 15 | * 将对象序列化为字符串 16 | * 17 | * @param object 对象 18 | * @return 字符串 19 | */ 20 | String serialize(Object object); 21 | 22 | T parseObject(String json, Type type); 23 | 24 | List parseArray(String json); 25 | 26 | Map toMap(String json); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/serializer/JsonSerializerDecider.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import com.github.dadiyang.httpinvoker.util.ReflectionUtils; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | /** 9 | * 默认依次检测当前类路径是否有 FastJson 和 Gson 以决定采用哪种实现 10 | *

11 | * 使用者可以通过 registerJsonSerializer 注册自己指定的 Json 实现,然后调用 setJsonInstanceKey 指定已注册的 Json 实现 12 | * 13 | * @author dadiyang 14 | * @since 2020/12/5 15 | */ 16 | public class JsonSerializerDecider { 17 | private static final String FAST_JSON_CLASS = "com.alibaba.fastjson.JSON"; 18 | private static final String GSON_CLASS = "com.google.gson.Gson"; 19 | private static Map map = new ConcurrentHashMap(); 20 | private static String jsonInstanceKey; 21 | 22 | /** 23 | * 根据规则获取JSON序列化器实例 24 | * 25 | * @return JSON序列化器实例 26 | */ 27 | public static JsonSerializer getJsonSerializer() { 28 | if (jsonInstanceKey != null) { 29 | JsonSerializer instance = map.get(jsonInstanceKey); 30 | if (instance == null) { 31 | throw new IllegalStateException("已指定了JSON序列化实现为: " + jsonInstanceKey + ",但是没有实际注册这个实现,请调用 registerJsonSerializer 方法先注册"); 32 | } 33 | return instance; 34 | } 35 | return getDefaultInstance(); 36 | } 37 | 38 | private static JsonSerializer getDefaultInstance() { 39 | // 默认使用 FAST_JSON 40 | if (ReflectionUtils.classExists(FAST_JSON_CLASS)) { 41 | return FastJsonJsonSerializer.getInstance(); 42 | } 43 | if (ReflectionUtils.classExists(GSON_CLASS)) { 44 | return GsonJsonSerializer.getInstance(); 45 | } 46 | throw new IllegalStateException("当前没有可用的JSON序列化器"); 47 | } 48 | 49 | public static String getJsonInstanceKey() { 50 | return jsonInstanceKey; 51 | } 52 | 53 | /** 54 | * 指定使用哪一个 jsonSerializer 实现,使用这个特性必须先使用 registerJsonSerializer 方法把这个key对应的序列化进行注册,否则无法正常使用 55 | * 56 | * @param jsonInstanceKey 实例key 57 | */ 58 | public static void setJsonInstanceKey(String jsonInstanceKey) { 59 | JsonSerializerDecider.jsonInstanceKey = jsonInstanceKey; 60 | } 61 | 62 | /** 63 | * 注册 json 序列化器 64 | * 65 | * @param key 实例key 66 | * @param jsonSerializer 序列化器实例 67 | */ 68 | public static void registerJsonSerializer(String key, JsonSerializer jsonSerializer) { 69 | map.put(key, jsonSerializer); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/spring/ClassPathHttpApiScanner.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.spring; 2 | 3 | import com.github.dadiyang.httpinvoker.annotation.HttpApi; 4 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertyResolver; 5 | import com.github.dadiyang.httpinvoker.requestor.RequestPreprocessor; 6 | import com.github.dadiyang.httpinvoker.requestor.Requestor; 7 | import com.github.dadiyang.httpinvoker.requestor.ResponseProcessor; 8 | import org.springframework.beans.factory.FactoryBean; 9 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; 10 | import org.springframework.beans.factory.config.BeanDefinition; 11 | import org.springframework.beans.factory.config.BeanDefinitionHolder; 12 | import org.springframework.beans.factory.support.*; 13 | import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; 14 | import org.springframework.core.type.filter.AnnotationTypeFilter; 15 | 16 | import java.lang.annotation.Annotation; 17 | import java.util.Arrays; 18 | import java.util.Set; 19 | 20 | /** 21 | * scan the given basePackages and register bean of includeAnn-annotated interfaces' implementation that HttpProxyBeanFactory generated 22 | * 23 | * @author huangxuyang 24 | * date 2018/11/1 25 | */ 26 | public class ClassPathHttpApiScanner extends ClassPathBeanDefinitionScanner { 27 | private static final String HTTP_API_PREFIX = "$HttpApi$"; 28 | private Class factoryBean; 29 | private Class includeAnn; 30 | private PropertyResolver propertyResolver; 31 | private Requestor requestor; 32 | private RequestPreprocessor requestPreprocessor; 33 | private ResponseProcessor responseProcessor; 34 | private BeanDefinitionRegistry registry; 35 | 36 | public ClassPathHttpApiScanner(BeanDefinitionRegistry registry, PropertyResolver propertyResolver, 37 | Requestor requestor, RequestPreprocessor requestPreprocessor, 38 | ResponseProcessor responseProcessor) { 39 | super(registry, false); 40 | // use DefaultBeanNameGenerator to prevent bean name conflict 41 | setBeanNameGenerator(new DefaultBeanNameGenerator()); 42 | this.registry = registry; 43 | this.propertyResolver = propertyResolver; 44 | this.factoryBean = HttpApiProxyFactoryBean.class; 45 | this.includeAnn = HttpApi.class; 46 | addIncludeFilter(new AnnotationTypeFilter(includeAnn)); 47 | this.requestor = requestor; 48 | this.requestPreprocessor = requestPreprocessor; 49 | this.responseProcessor = responseProcessor; 50 | } 51 | 52 | @Override 53 | public Set doScan(String... basePackages) { 54 | Set beanDefinitions = super.doScan(basePackages); 55 | if (beanDefinitions.isEmpty()) { 56 | logger.warn("No " + includeAnn.getSimpleName() + " was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); 57 | } 58 | return beanDefinitions; 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | @Override 65 | protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { 66 | return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); 67 | } 68 | 69 | /** 70 | * {@inheritDoc} 71 | */ 72 | @Override 73 | protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) { 74 | // Sometimes, the package scan will be conflicted with MyBatis 75 | // so the existing is not the expected one, we remove it. 76 | if (this.registry.containsBeanDefinition(beanName)) { 77 | // an HttpApi bean exists, we ignore the others. 78 | if (isHttpApiBean(beanName)) { 79 | logger.info("an HttpApi bean [" + beanName + "] exists, we ignore the others"); 80 | return false; 81 | } 82 | logger.warn("an not HttpApi bean named [" + beanName + "] exists, we remove it, so that we can generate a new bean."); 83 | registry.removeBeanDefinition(beanName); 84 | } 85 | if (super.checkCandidate(beanName, beanDefinition)) { 86 | return true; 87 | } else { 88 | logger.warn("Skipping " + factoryBean.getSimpleName() + " with name '" + beanName 89 | + "' and '" + beanDefinition.getBeanClassName() + "' interface" 90 | + ". Bean already defined with the same name!"); 91 | return false; 92 | } 93 | } 94 | 95 | private boolean isHttpApiBean(String beanName) { 96 | if (registry instanceof DefaultListableBeanFactory) { 97 | DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) registry; 98 | return beanFactory.containsBean(beanName) && beanFactory.getBean(beanName).toString().startsWith(HTTP_API_PREFIX); 99 | } 100 | return false; 101 | } 102 | 103 | /** 104 | * registry the bean with FactoryBean 105 | */ 106 | @Override 107 | protected void registerBeanDefinition(BeanDefinitionHolder holder, BeanDefinitionRegistry registry) { 108 | GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition(); 109 | if (logger.isDebugEnabled()) { 110 | logger.debug(includeAnn.getSimpleName() + ": Bean with name '" + holder.getBeanName() 111 | + "' and '" + definition.getBeanClassName() + "' interface"); 112 | } 113 | if (logger.isDebugEnabled()) { 114 | logger.debug("Enabling autowire by type for " + factoryBean.getSimpleName() + " with name '" + holder.getBeanName() + "'."); 115 | } 116 | definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); 117 | // 需要被代理的接口 the interface 118 | definition.getPropertyValues().add("interfaceClass", definition.getBeanClassName()); 119 | // 配置项 120 | definition.getPropertyValues().add("propertyResolver", propertyResolver); 121 | if (requestor != null) { 122 | definition.getPropertyValues().add("requestor", requestor); 123 | } 124 | if (requestPreprocessor != null) { 125 | definition.getPropertyValues().add("requestPreprocessor", requestPreprocessor); 126 | } 127 | if (responseProcessor != null) { 128 | definition.getPropertyValues().add("responseProcessor", responseProcessor); 129 | } 130 | // 获取bean名,注意:获取 BeanName 要在setBeanClass之前,否则BeanName就会被覆盖 131 | // caution! we nned to getBeanName first before setBeanClass 132 | String beanName = holder.getBeanName(); 133 | // 将BeanClass设置成Bean工厂 134 | definition.setBeanClass(factoryBean); 135 | // 注册Bean 136 | registry.registerBeanDefinition(beanName, definition); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/spring/HttpApiConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.spring; 2 | 3 | import com.github.dadiyang.httpinvoker.annotation.HttpApiScan; 4 | import com.github.dadiyang.httpinvoker.propertyresolver.EnvironmentBasePropertyResolver; 5 | import com.github.dadiyang.httpinvoker.propertyresolver.MultiSourcePropertyResolver; 6 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertiesBasePropertyResolver; 7 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertyResolver; 8 | import com.github.dadiyang.httpinvoker.requestor.RequestPreprocessor; 9 | import com.github.dadiyang.httpinvoker.requestor.Requestor; 10 | import com.github.dadiyang.httpinvoker.requestor.ResponseProcessor; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.BeansException; 14 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 15 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 16 | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; 17 | import org.springframework.context.ApplicationContext; 18 | import org.springframework.context.ApplicationContextAware; 19 | import org.springframework.stereotype.Component; 20 | 21 | import java.io.IOException; 22 | import java.util.*; 23 | 24 | import static com.github.dadiyang.httpinvoker.util.IoUtils.*; 25 | 26 | /** 27 | * Scanning the base packages that {@link HttpApiScan} specified. 28 | * 29 | * @author huangxuyang 30 | * date 2018/10/31 31 | */ 32 | @Component 33 | public class HttpApiConfigurer implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { 34 | private static final Logger logger = LoggerFactory.getLogger(HttpApiConfigurer.class); 35 | private static final String CLASSPATH_PRE = "classpath:"; 36 | private static final String FILE_PRE = "file:"; 37 | private ApplicationContext ctx; 38 | 39 | @Override 40 | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException { 41 | Map beans = ctx.getBeansWithAnnotation(HttpApiScan.class); 42 | Set basePackages = new HashSet(); 43 | Properties properties = new Properties(); 44 | for (Map.Entry entry : beans.entrySet()) { 45 | HttpApiScan ann = entry.getValue().getClass().getAnnotation(HttpApiScan.class); 46 | if (ann.value().length <= 0 || ann.value()[0].isEmpty()) { 47 | // add the annotated class' package as a basePackage 48 | basePackages.add(entry.getValue().getClass().getPackage().getName()); 49 | } else { 50 | basePackages.addAll(Arrays.asList(ann.value())); 51 | } 52 | String[] configPaths = ann.configPaths(); 53 | if (configPaths.length > 0) { 54 | for (String path : configPaths) { 55 | if (path == null || path.isEmpty()) { 56 | continue; 57 | } 58 | try { 59 | Properties p = getProperties(path); 60 | properties.putAll(p); 61 | } catch (IOException e) { 62 | throw new IllegalStateException("read config error: " + path, e); 63 | } 64 | } 65 | } 66 | } 67 | if (logger.isDebugEnabled()) { 68 | logger.debug("HttpApiScan packages: " + basePackages); 69 | } 70 | Requestor requestor = null; 71 | try { 72 | requestor = ctx.getBean(Requestor.class); 73 | } catch (Exception e) { 74 | logger.debug("Requestor bean does not exist: " + e.getMessage()); 75 | } 76 | RequestPreprocessor requestPreprocessor = null; 77 | try { 78 | requestPreprocessor = ctx.getBean(RequestPreprocessor.class); 79 | } catch (Exception e) { 80 | logger.debug("RequestPreprocessor bean does not exist" + e.getMessage()); 81 | } 82 | ResponseProcessor responseProcessor = null; 83 | try { 84 | responseProcessor = ctx.getBean(ResponseProcessor.class); 85 | } catch (Exception e) { 86 | logger.debug("ResponseProcessor bean does not exist" + e.getMessage()); 87 | } 88 | PropertyResolver resolver; 89 | if (properties.size() > 0) { 90 | MultiSourcePropertyResolver multi = new MultiSourcePropertyResolver(); 91 | // use properties both from config files and environment 92 | multi.addPropertyResolver(new PropertiesBasePropertyResolver(properties)); 93 | multi.addPropertyResolver(new EnvironmentBasePropertyResolver(ctx.getEnvironment())); 94 | resolver = multi; 95 | } else { 96 | // use properties from environment 97 | resolver = new EnvironmentBasePropertyResolver(ctx.getEnvironment()); 98 | } 99 | ClassPathHttpApiScanner scanner = new ClassPathHttpApiScanner(beanDefinitionRegistry, resolver, requestor, requestPreprocessor, responseProcessor); 100 | scanner.doScan(basePackages.toArray(new String[]{})); 101 | } 102 | 103 | @Override 104 | public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws 105 | BeansException { 106 | } 107 | 108 | @Override 109 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 110 | this.ctx = applicationContext; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/spring/HttpApiProxyFactoryBean.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.spring; 2 | 3 | import com.github.dadiyang.httpinvoker.HttpApiProxyFactory; 4 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertyResolver; 5 | import com.github.dadiyang.httpinvoker.requestor.RequestPreprocessor; 6 | import com.github.dadiyang.httpinvoker.requestor.Requestor; 7 | import com.github.dadiyang.httpinvoker.requestor.ResponseProcessor; 8 | import org.springframework.beans.factory.FactoryBean; 9 | 10 | /** 11 | * A factory bean which produce HttpApi interface's implement by using proxyFactory 12 | * 13 | * @author huangxuyang 14 | * date 2018/11/1 15 | */ 16 | public class HttpApiProxyFactoryBean implements FactoryBean { 17 | private HttpApiProxyFactory proxyFactory; 18 | private Requestor requestor; 19 | private Class interfaceClass; 20 | private PropertyResolver propertyResolver; 21 | private RequestPreprocessor requestPreprocessor; 22 | private ResponseProcessor responseProcessor; 23 | 24 | public Class getInterfaceClass() { 25 | return interfaceClass; 26 | } 27 | 28 | public void setInterfaceClass(Class interfaceClass) { 29 | this.interfaceClass = interfaceClass; 30 | } 31 | 32 | public void setRequestor(Requestor requestor) { 33 | this.requestor = requestor; 34 | } 35 | 36 | public void setPropertyResolver(PropertyResolver propertyResolver) { 37 | this.propertyResolver = propertyResolver; 38 | } 39 | 40 | public void setRequestPreprocessor(RequestPreprocessor requestPreprocessor) { 41 | this.requestPreprocessor = requestPreprocessor; 42 | } 43 | 44 | public void setResponseProcessor(ResponseProcessor responseProcessor) { 45 | this.responseProcessor = responseProcessor; 46 | } 47 | 48 | @Override 49 | public T getObject() throws Exception { 50 | if (proxyFactory == null) { 51 | proxyFactory = new HttpApiProxyFactory(requestor, propertyResolver, requestPreprocessor, responseProcessor); 52 | } 53 | return (T) proxyFactory.getProxy(interfaceClass); 54 | } 55 | 56 | @Override 57 | public Class getObjectType() { 58 | return interfaceClass; 59 | } 60 | 61 | @Override 62 | public boolean isSingleton() { 63 | return true; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/util/IoUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.Closeable; 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.Properties; 11 | 12 | /** 13 | * IO Utils 14 | * 15 | * @author dadiyang 16 | * @since 2019-06-12 17 | */ 18 | public class IoUtils { 19 | private static final Logger logger = LoggerFactory.getLogger(IoUtils.class); 20 | private static final String CLASSPATH_PRE = "classpath:"; 21 | private static final String FILE_PRE = "file:"; 22 | 23 | private IoUtils() { 24 | throw new UnsupportedOperationException("utils should not be initialized!"); 25 | } 26 | 27 | public static void closeStream(Closeable in) { 28 | if (in != null) { 29 | try { 30 | in.close(); 31 | } catch (IOException e) { 32 | logger.error("close config file error", e); 33 | } 34 | } 35 | } 36 | 37 | public static Properties getPropertiesFromFile(String path) throws IOException { 38 | if (path.startsWith(FILE_PRE)) { 39 | path = path.replaceFirst(FILE_PRE, ""); 40 | } 41 | Properties p = new Properties(); 42 | InputStream in = null; 43 | try { 44 | in = new FileInputStream(path); 45 | p.load(in); 46 | } finally { 47 | closeStream(in); 48 | } 49 | return p; 50 | } 51 | 52 | public static Properties getPropertiesFromClassPath(String path) throws IOException { 53 | path = path.replaceFirst(CLASSPATH_PRE, ""); 54 | Properties p = new Properties(); 55 | InputStream in = null; 56 | try { 57 | in = IoUtils.class.getClassLoader().getResourceAsStream(path); 58 | p.load(in); 59 | } finally { 60 | closeStream(in); 61 | } 62 | return p; 63 | } 64 | 65 | /** 66 | * read properties from either classpath or file 67 | * 68 | * @param path file in classpath (starts with classpath:) or file path 69 | */ 70 | public static Properties getProperties(String path) throws IOException { 71 | if (path.startsWith(CLASSPATH_PRE)) { 72 | return getPropertiesFromClassPath(path); 73 | } else { 74 | return getPropertiesFromFile(path); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/util/ObjectUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | /** 4 | * @author dadiyang 5 | * @since 2019-06-12 6 | */ 7 | public class ObjectUtils { 8 | private ObjectUtils() { 9 | throw new UnsupportedOperationException("utils should not be initialized!"); 10 | } 11 | 12 | public static boolean equals(Object a, Object b) { 13 | return (a == b) || (a != null && a.equals(b)); 14 | } 15 | 16 | public static void requireNonNull(Object obj, String message) { 17 | if (obj == null) { 18 | throw new NullPointerException(message); 19 | } 20 | } 21 | 22 | public static String toString(Object obj, String defaultVal) { 23 | if (obj == null) { 24 | return defaultVal; 25 | } 26 | return obj.toString(); 27 | } 28 | 29 | public static String toString(Object obj) { 30 | if (obj == null) { 31 | return null; 32 | } 33 | return obj.toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/util/ParamUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | import com.github.dadiyang.httpinvoker.requestor.HttpRequest; 4 | import com.github.dadiyang.httpinvoker.requestor.MultiPart; 5 | import com.github.dadiyang.httpinvoker.serializer.JsonSerializerDecider; 6 | 7 | import java.io.*; 8 | import java.lang.reflect.Array; 9 | import java.net.URLEncoder; 10 | import java.util.*; 11 | 12 | /** 13 | * utils for handling param 14 | * 15 | * @author dadiyang 16 | * @since 1.1.2 17 | */ 18 | public class ParamUtils { 19 | private static final char UPPER_A = 'A'; 20 | private static final char UPPER_Z = 'Z'; 21 | private static final char LOWER_A = 'a'; 22 | private static final char LOWER_Z = 'z'; 23 | private static final String FILE_NAME = "fileName"; 24 | private static final String DEFAULT_UPLOAD_FORM_KEY = "media"; 25 | private static final String FORM_KEY = "formKey"; 26 | private static final List> BASIC_TYPE = Arrays.asList(Byte.class, Short.class, 27 | Integer.class, Long.class, Float.class, Double.class, Character.class, 28 | Boolean.class, String.class, Void.class, Date.class); 29 | /** 30 | * for JDK6/7 compatibility 31 | */ 32 | private static final List BASIC_TYPE_NAME = Arrays.asList("java.time.LocalDate", "java.time.LocalDateTime"); 33 | 34 | private ParamUtils() { 35 | throw new UnsupportedOperationException("utils should not be initialized!"); 36 | } 37 | 38 | /** 39 | * check if the clz is primary type, primary type's wrapper, String or Void 40 | * 41 | * @param clz the type 42 | * @return check if the clz is basic type 43 | */ 44 | public static boolean isBasicType(Class clz) { 45 | if (clz == null) { 46 | return false; 47 | } 48 | // for JDK6/7 compatibility 49 | if (BASIC_TYPE_NAME.contains(clz.getName())) { 50 | return true; 51 | } 52 | return clz.isPrimitive() || BASIC_TYPE.contains(clz); 53 | } 54 | 55 | /** 56 | * check if the arg is a collection 57 | * 58 | * @param arg object to be checked 59 | * @return if the arg is a array/collection 60 | */ 61 | public static boolean isCollection(Object arg) { 62 | if (arg == null) { 63 | return false; 64 | } 65 | return arg.getClass().isArray() 66 | || arg instanceof Collection; 67 | } 68 | 69 | /** 70 | * convert an object to Map<String, String> 71 | * 72 | * @param value object to be converted 73 | * @param prefix key's prefix 74 | * @return Map<String, String> represent the value 75 | */ 76 | public static Map toMapStringString(Object value, String prefix) { 77 | if (value == null) { 78 | return Collections.emptyMap(); 79 | } 80 | if (isBasicType(value.getClass())) { 81 | return Collections.singletonMap(prefix, String.valueOf(value)); 82 | } 83 | Map map = new HashMap(); 84 | if (value.getClass().isArray()) { 85 | for (int i = 0; i < Array.getLength(value); i++) { 86 | String key = prefix + "[" + i++ + "]"; 87 | Object item = Array.get(value, 0); 88 | if (isBasicType(item.getClass())) { 89 | map.put(prefix + "[" + i + "]", String.valueOf(item)); 90 | } else { 91 | map.putAll(toMapStringString(item, key)); 92 | } 93 | 94 | } 95 | } else if (value instanceof Collection) { 96 | Collection collection = (Collection) value; 97 | Iterator it = collection.iterator(); 98 | int i = 0; 99 | while (it.hasNext()) { 100 | String key = prefix + "[" + i++ + "]"; 101 | Object item = it.next(); 102 | if (isBasicType(item.getClass())) { 103 | map.put(prefix + "[" + i++ + "]", String.valueOf(item)); 104 | } else { 105 | map.putAll(toMapStringString(item, key)); 106 | } 107 | } 108 | } else { 109 | Map obj = JsonSerializerDecider.getJsonSerializer().toMap(JsonSerializerDecider.getJsonSerializer().serialize(value)); 110 | for (Map.Entry entry : obj.entrySet()) { 111 | String key; 112 | if (prefix == null || prefix.isEmpty()) { 113 | key = entry.getKey(); 114 | } else { 115 | key = prefix + "[" + entry.getKey() + "]"; 116 | } 117 | if (isBasicType(value.getClass())) { 118 | map.put(key, String.valueOf(value)); 119 | } else { 120 | map.putAll(toMapStringString(entry.getValue(), key)); 121 | } 122 | } 123 | } 124 | return map; 125 | } 126 | 127 | /** 128 | * convert param object to query string 129 | *

130 | * collection fields will be convert to a form of duplicated key such as id=1&id=2&id=3 131 | * 132 | * @param arg the param args 133 | * @return query string 134 | */ 135 | public static String toQueryString(Object arg) { 136 | if (arg == null) { 137 | return ""; 138 | } 139 | StringBuilder qs = new StringBuilder("?"); 140 | Map obj = JsonSerializerDecider.getJsonSerializer().toMap(JsonSerializerDecider.getJsonSerializer().serialize(arg)); 141 | for (Map.Entry entry : obj.entrySet()) { 142 | if (isCollection(entry.getValue())) { 143 | qs.append(collectionToQueryString(obj, entry)); 144 | } else { 145 | String value = entry.getValue() == null ? "" : entry.getValue().toString(); 146 | try { 147 | value = URLEncoder.encode(value, "UTF-8"); 148 | } catch (UnsupportedEncodingException ignored) { 149 | } 150 | qs.append(entry.getKey()).append("=").append(value).append("&"); 151 | } 152 | } 153 | return qs.substring(0, qs.length() - 1); 154 | } 155 | 156 | private static String collectionToQueryString(Map obj, Map.Entry entry) { 157 | List arr = JsonSerializerDecider.getJsonSerializer().parseArray(ObjectUtils.toString(obj.get(entry.getKey()))); 158 | StringBuilder valBuilder = new StringBuilder(); 159 | for (Object item : arr) { 160 | valBuilder.append(entry.getKey()).append("=").append(item).append("&"); 161 | } 162 | return valBuilder.toString(); 163 | } 164 | 165 | public static char changeCase(char c) { 166 | if (c >= UPPER_A && c <= UPPER_Z) { 167 | return c += 32; 168 | } else if (c >= LOWER_A && c <= LOWER_Z) { 169 | return c -= 32; 170 | } else { 171 | return c; 172 | } 173 | } 174 | 175 | public static String changeInitialCase(String c) { 176 | if (c == null || c.isEmpty()) { 177 | return c; 178 | } 179 | return changeCase(c.charAt(0)) + c.substring(1); 180 | } 181 | 182 | public static MultiPart convertInputStreamAndFile(HttpRequest request) throws IOException { 183 | Map paramMap = request.getData(); 184 | String formKey = DEFAULT_UPLOAD_FORM_KEY; 185 | if (request.getFileFormKey() != null 186 | && !request.getFileFormKey().isEmpty()) { 187 | formKey = request.getFileFormKey(); 188 | } else if (paramMap != null && paramMap.containsKey(FORM_KEY)) { 189 | formKey = paramMap.get(FORM_KEY).toString(); 190 | } 191 | List parts = new ArrayList(); 192 | String fileName = FILE_NAME; 193 | if (paramMap != null) { 194 | for (Map.Entry entry : paramMap.entrySet()) { 195 | if (entry.getKey() != null && entry.getValue() != null) { 196 | if (ObjectUtils.equals(entry.getKey(), FILE_NAME)) { 197 | fileName = String.valueOf(entry.getValue()); 198 | } 199 | parts.add(new MultiPart.Part(entry.getKey(), String.valueOf(entry.getValue()))); 200 | } 201 | } 202 | } 203 | InputStream in; 204 | if (File.class.isAssignableFrom(request.getBody().getClass())) { 205 | File file = (File) request.getBody(); 206 | in = new FileInputStream(file); 207 | fileName = file.getName(); 208 | } else { 209 | in = (InputStream) request.getBody(); 210 | } 211 | parts.add(new MultiPart.Part(formKey, fileName, in)); 212 | return new MultiPart(parts); 213 | } 214 | 215 | public static boolean isUploadRequest(Object bodyParam) { 216 | return bodyParam != null && (bodyParam instanceof MultiPart 217 | || InputStream.class.isAssignableFrom(bodyParam.getClass()) 218 | || File.class.isAssignableFrom(bodyParam.getClass())); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/util/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | /** 7 | * 反射工具类 8 | * 9 | * @author dadiyang 10 | * @since 2019/3/1 11 | */ 12 | public class ReflectionUtils { 13 | private static Map existCache = new ConcurrentHashMap(); 14 | 15 | private ReflectionUtils() { 16 | throw new UnsupportedOperationException("静态工具类不允许被实例化"); 17 | } 18 | 19 | /** 20 | * 检查某个全类名是否存在于 classpath 中 21 | */ 22 | public static boolean classExists(String clzFullName) { 23 | if (StringUtils.isBlank(clzFullName)) { 24 | return false; 25 | } 26 | Boolean rs = existCache.get(clzFullName); 27 | if (rs != null && rs) { 28 | return true; 29 | } 30 | try { 31 | Class clz = Class.forName(clzFullName); 32 | existCache.put(clzFullName, clz != null); 33 | return clz != null; 34 | } catch (ClassNotFoundException e) { 35 | existCache.put(clzFullName, false); 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/dadiyang/httpinvoker/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | /** 4 | * @author dadiyang 5 | * @since 2019-06-13 6 | */ 7 | public class StringUtils { 8 | private StringUtils() { 9 | throw new UnsupportedOperationException("utils should not be initialized!"); 10 | } 11 | 12 | public static String upperCase(String str) { 13 | return str == null ? null : str.toUpperCase(); 14 | } 15 | 16 | public static boolean isBlank(final CharSequence cs) { 17 | int strLen; 18 | if (cs == null || (strLen = cs.length()) == 0) { 19 | return true; 20 | } 21 | for (int i = 0; i < strLen; i++) { 22 | if (!Character.isWhitespace(cs.charAt(i))) { 23 | return false; 24 | } 25 | } 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/HttpApiProxyFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker; 2 | 3 | import com.github.dadiyang.httpinvoker.propertyresolver.MultiSourcePropertyResolver; 4 | import com.github.dadiyang.httpinvoker.propertyresolver.PropertyResolver; 5 | import com.github.dadiyang.httpinvoker.requestor.*; 6 | import com.github.dadiyang.httpinvoker.util.ObjectUtils; 7 | import org.junit.Test; 8 | import org.springframework.core.env.Environment; 9 | 10 | import java.io.IOException; 11 | import java.lang.reflect.InvocationHandler; 12 | import java.lang.reflect.Method; 13 | import java.lang.reflect.Proxy; 14 | import java.util.Properties; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertSame; 18 | 19 | /** 20 | * @author dadiyang 21 | * @since 2019/5/20 22 | */ 23 | public class HttpApiProxyFactoryTest { 24 | /** 25 | * 工厂类测试 26 | */ 27 | @Test 28 | public void testBuilder() { 29 | Requestor requestor = new Requestor() { 30 | @Override 31 | public HttpResponse sendRequest(HttpRequest request) throws IOException { 32 | return null; 33 | } 34 | }; 35 | RequestPreprocessor requestPreprocessor = new RequestPreprocessor() { 36 | @Override 37 | public void process(HttpRequest request) { 38 | } 39 | }; 40 | Properties properties = new Properties(); 41 | properties.setProperty("Pro1", "OK"); 42 | Environment environment = (Environment) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Environment.class}, new InvocationHandler() { 43 | @Override 44 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 45 | if (ObjectUtils.equals("getProperty", method.getName()) && args.length == 1) { 46 | if (ObjectUtils.equals("Env1", args[0])) { 47 | return "OK"; 48 | } 49 | } 50 | if (ObjectUtils.equals("containsProperty", method.getName()) 51 | && args.length == 1) { 52 | return ObjectUtils.equals("Env1", args[0]); 53 | } 54 | 55 | if (ObjectUtils.equals("hashCode", method.getName())) { 56 | return -1; 57 | } 58 | if (ObjectUtils.equals("equals", method.getName()) && args.length == 1) { 59 | return true; 60 | } 61 | return null; 62 | } 63 | }); 64 | PropertyResolver resolver = new PropertyResolver() { 65 | @Override 66 | public boolean containsProperty(String key) { 67 | return ObjectUtils.equals("PR1", key); 68 | } 69 | 70 | @Override 71 | public String getProperty(String key) { 72 | if (ObjectUtils.equals("PR1", key)) { 73 | return "OK"; 74 | } 75 | return null; 76 | } 77 | }; 78 | ResponseProcessor responseProcessor = new ResponseProcessor() { 79 | @Override 80 | public Object process(HttpResponse response, Method method) { 81 | return null; 82 | } 83 | }; 84 | HttpApiProxyFactory factory = new HttpApiProxyFactory.Builder() 85 | .setRequestor(requestor) 86 | .setRequestPreprocessor(requestPreprocessor) 87 | .setResponseProcessor(responseProcessor) 88 | .addProperties(properties) 89 | .addEnvironment(environment) 90 | .addPropertyResolver(resolver) 91 | .build(); 92 | assertSame(requestor, factory.getRequestor()); 93 | assertSame(requestPreprocessor, factory.getRequestPreprocessor()); 94 | assertSame(responseProcessor, factory.getResponseProcessor()); 95 | 96 | MultiSourcePropertyResolver resolvers = (MultiSourcePropertyResolver) factory.getPropertyResolver(); 97 | assertEquals("OK", resolvers.getProperty("Pro1")); 98 | assertEquals("OK", resolvers.getProperty("Env1")); 99 | assertEquals("OK", resolvers.getProperty("PR1")); 100 | } 101 | } -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker; 2 | 3 | import com.github.dadiyang.httpinvoker.annotation.HttpApiScan; 4 | import com.github.dadiyang.httpinvoker.mocker.MockRequestor; 5 | import com.github.dadiyang.httpinvoker.mocker.MockResponse; 6 | import com.github.dadiyang.httpinvoker.mocker.MockRule; 7 | import com.github.dadiyang.httpinvoker.requestor.HttpRequest; 8 | import com.github.dadiyang.httpinvoker.requestor.RequestPreprocessor; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.PropertySource; 12 | 13 | import java.util.Collections; 14 | 15 | @Configuration 16 | @HttpApiScan(configPaths = "classpath:conf.properties") 17 | @PropertySource("classpath:conf2.properties") 18 | public class TestApplication { 19 | 20 | @Bean 21 | public RequestPreprocessor requestPreprocessor() { 22 | return new RequestPreprocessor() { 23 | @Override 24 | public void process(HttpRequest request) { 25 | request.addHeader("testHeader", "OK"); 26 | request.addCookie("testCookie", "OK"); 27 | } 28 | }; 29 | } 30 | 31 | @Bean 32 | public MockRequestor requestor() { 33 | MockRequestor requestor = new MockRequestor(); 34 | MockRule rule = new MockRule("http://localhost:18888/city/getCityName", Collections.singletonMap("id", (Object) 1), new MockResponse(200, "北京")); 35 | requestor.addRule(rule); 36 | return requestor; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/entity/City.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.entity; 2 | 3 | 4 | /** 5 | * @author huangxuyang 6 | */ 7 | public class City { 8 | private Integer id; 9 | private String name; 10 | 11 | public City() { 12 | } 13 | 14 | public City(Integer id, String name) { 15 | this.id = id; 16 | this.name = name; 17 | } 18 | 19 | public Integer getId() { 20 | return id; 21 | } 22 | 23 | public void setId(Integer id) { 24 | this.id = id; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public void setName(String name) { 32 | this.name = name; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "City{" + 38 | "id=" + id + 39 | ", name='" + name + '\'' + 40 | '}'; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | City city = (City) o; 48 | if (id != null ? !id.equals(city.id) : city.id != null) return false; 49 | return name != null ? name.equals(city.name) : city.name == null; 50 | 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | int result = id != null ? id.hashCode() : 0; 56 | result = 31 * result + (name != null ? name.hashCode() : 0); 57 | return result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/entity/ComplicatedInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.entity; 2 | 3 | import java.util.List; 4 | public class ComplicatedInfo { 5 | private List cities; 6 | private String msg; 7 | private City city; 8 | 9 | public ComplicatedInfo(List cities, String msg, City city) { 10 | this.cities = cities; 11 | this.msg = msg; 12 | this.city = city; 13 | } 14 | 15 | public ComplicatedInfo() { 16 | } 17 | 18 | public List getCities() { 19 | return cities; 20 | } 21 | 22 | public void setCities(List cities) { 23 | this.cities = cities; 24 | } 25 | 26 | public String getMsg() { 27 | return msg; 28 | } 29 | 30 | public void setMsg(String msg) { 31 | this.msg = msg; 32 | } 33 | 34 | public City getCity() { 35 | return city; 36 | } 37 | 38 | public void setCity(City city) { 39 | this.city = city; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | 47 | ComplicatedInfo that = (ComplicatedInfo) o; 48 | 49 | if (cities != null ? !cities.equals(that.cities) : that.cities != null) return false; 50 | if (msg != null ? !msg.equals(that.msg) : that.msg != null) return false; 51 | return city != null ? city.equals(that.city) : that.city == null; 52 | 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | int result = cities != null ? cities.hashCode() : 0; 58 | result = 31 * result + (msg != null ? msg.hashCode() : 0); 59 | result = 31 * result + (city != null ? city.hashCode() : 0); 60 | return result; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "ComplicatedInfo{" + 66 | "cities=" + cities + 67 | ", msg='" + msg + '\'' + 68 | ", city=" + city + 69 | '}'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/entity/ResultBean.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.entity; 2 | 3 | public class ResultBean { 4 | private int code; 5 | private T data; 6 | private String msg; 7 | 8 | public ResultBean() { 9 | } 10 | 11 | public ResultBean(int code, T data) { 12 | this.code = code; 13 | this.data = data; 14 | } 15 | 16 | public ResultBean(String msg, int code) { 17 | this.code = code; 18 | this.msg = msg; 19 | } 20 | 21 | public int getCode() { 22 | return code; 23 | } 24 | 25 | public void setCode(int code) { 26 | this.code = code; 27 | } 28 | 29 | public T getData() { 30 | return data; 31 | } 32 | 33 | public void setData(T data) { 34 | this.data = data; 35 | } 36 | 37 | public String getMsg() { 38 | return msg; 39 | } 40 | 41 | public void setMsg(String msg) { 42 | this.msg = msg; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "ResultBean{" + 48 | "code=" + code + 49 | ", data=" + data + 50 | ", msg='" + msg + '\'' + 51 | '}'; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (o == null || getClass() != o.getClass()) return false; 58 | 59 | ResultBean that = (ResultBean) o; 60 | 61 | if (code != that.code) return false; 62 | if (data != null ? !data.equals(that.data) : that.data != null) return false; 63 | return msg != null ? msg.equals(that.msg) : that.msg == null; 64 | 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | int result = code; 70 | result = 31 * result + (data != null ? data.hashCode() : 0); 71 | result = 31 * result + (msg != null ? msg.hashCode() : 0); 72 | return result; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/entity/ResultBeanWithStatusAsCode.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.entity; 2 | 3 | public class ResultBeanWithStatusAsCode { 4 | private int status; 5 | private T data; 6 | private String msg; 7 | 8 | public ResultBeanWithStatusAsCode() { 9 | } 10 | 11 | public ResultBeanWithStatusAsCode(int status, T data) { 12 | this.status = status; 13 | this.data = data; 14 | } 15 | 16 | public ResultBeanWithStatusAsCode(String msg, int status) { 17 | this.status = status; 18 | this.msg = msg; 19 | } 20 | 21 | public int getStatus() { 22 | return status; 23 | } 24 | 25 | public void setStatus(int status) { 26 | this.status = status; 27 | } 28 | 29 | public T getData() { 30 | return data; 31 | } 32 | 33 | public void setData(T data) { 34 | this.data = data; 35 | } 36 | 37 | public String getMsg() { 38 | return msg; 39 | } 40 | 41 | public void setMsg(String msg) { 42 | this.msg = msg; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | 50 | ResultBeanWithStatusAsCode that = (ResultBeanWithStatusAsCode) o; 51 | 52 | if (status != that.status) return false; 53 | if (data != null ? !data.equals(that.data) : that.data != null) return false; 54 | return msg != null ? msg.equals(that.msg) : that.msg == null; 55 | 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | int result = status; 61 | result = 31 * result + (data != null ? data.hashCode() : 0); 62 | result = 31 * result + (msg != null ? msg.hashCode() : 0); 63 | return result; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "ResultBeanWithStatusAsCode{" + 69 | "status=" + status + 70 | ", data=" + data + 71 | ", msg='" + msg + '\'' + 72 | '}'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/interfaces/CityService.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.interfaces; 2 | 3 | 4 | import com.github.dadiyang.httpinvoker.annotation.*; 5 | import com.github.dadiyang.httpinvoker.entity.City; 6 | import com.github.dadiyang.httpinvoker.entity.ComplicatedInfo; 7 | import com.github.dadiyang.httpinvoker.entity.ResultBean; 8 | import com.github.dadiyang.httpinvoker.enumeration.ReqMethod; 9 | import com.github.dadiyang.httpinvoker.requestor.HttpResponse; 10 | import com.github.dadiyang.httpinvoker.requestor.MultiPart; 11 | import com.github.dadiyang.httpinvoker.requestor.Status; 12 | 13 | import java.io.InputStream; 14 | import java.util.Date; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * a example interface for testing 20 | * 21 | * @author huangxuyang 22 | * date 2018/11/1 23 | */ 24 | @HttpApi("${api.url.city.host}/city") 25 | @RetryPolicy 26 | @Headers(keys = {"globalHeader1", "globalHeader2", "h3"}, values = {"ok", "yes", "haha"}) 27 | @Cookies(keys = "globalCookie", values = "ok") 28 | @UserAgent("JUnit") 29 | public interface CityService { 30 | /** 31 | * 使用URI,会自动添加prefix指定的前缀 32 | */ 33 | @HttpReq("/allCities") 34 | @RetryPolicy(times = 2, fixedBackOffPeriod = 3000) 35 | List getAllCities(); 36 | 37 | /** 38 | * 使用Param注解指定方法参数对应的请求参数名称 39 | */ 40 | @HttpReq("${api.url.city.host2}/city/getById") 41 | City getCity(@Param("id") int id); 42 | 43 | /** 44 | * 如果是集合类或数组的参数数据会直接当成请求体直接发送 45 | */ 46 | @HttpReq(value = "/save", method = ReqMethod.POST) 47 | @RetryPolicy(times = 2, retryForStatus = Status.SERVER_ERROR) 48 | boolean saveCities(List cities); 49 | 50 | /** 51 | * 测试无需返回值的情况 52 | */ 53 | @HttpReq(value = "/{id}", method = ReqMethod.DELETE) 54 | void deleteCity(@Param("id") int id); 55 | 56 | /** 57 | * 默认使用GET方法,可以通过method指定请求方式 58 | */ 59 | @HttpReq(value = "/saveCity", method = ReqMethod.POST) 60 | boolean saveCity(@Param("id") Integer id, @Param("name") String name); 61 | 62 | /** 63 | * 使用完整的路径,不会添加前缀 64 | */ 65 | @HttpReq(value = "${api.url.city.host}/city/getCityByName") 66 | ResultBean getCityByName(@Param("name") String name); 67 | 68 | /** 69 | * 使用完整的路径,不会添加前缀 70 | */ 71 | @HttpReq(value = "/getCityByName?name=${name:北京}&id=${id:}") 72 | ResultBean getCityByNameWithConfigVariable(); 73 | 74 | /** 75 | * 支持路径参数 76 | */ 77 | @HttpReq("/getCityRest/{id}") 78 | City getCityRest(@Param("id") Integer id); 79 | 80 | /** 81 | * 支持路径参数 82 | */ 83 | @HttpReq("/getCityRest/{id:1}") 84 | City getCityRestWithDefaultPathVal(@Param("id") Integer id); 85 | 86 | /** 87 | * 获取请求体,可以拿到请求头和cookie等信息 88 | */ 89 | @HttpReq("/listCity") 90 | HttpResponse listCity(); 91 | 92 | /** 93 | * 带错误请求头的方法 94 | */ 95 | @HttpReq("/getCityRest/{id}") 96 | City getCityWithErrHeaders(@Param("id") int id, @Headers String headers); 97 | 98 | /** 99 | * 带正确请求头的方法 100 | */ 101 | @HttpReq("/getCityRest/{id}") 102 | City getCityWithHeaders(@Param("id") int id, @Headers Map headers); 103 | 104 | /** 105 | * 带cookie的方法 106 | */ 107 | @HttpReq("/getCityRest/{id}") 108 | City getCityWithCookies(@Param("id") int id, @Cookies Map cookies); 109 | 110 | @HttpReq("/getCityRest/{id}") 111 | City getCityWithCookiesAndHeaders(@Param("id") int id, @Cookies Map cookies, @Headers Map headers); 112 | 113 | /** 114 | * 判断给定的城市是否存在 115 | *

116 | * 用于测试复杂对象做为参数是否可以被解析 117 | */ 118 | @HttpReq("/getCity") 119 | boolean hasCity(City city); 120 | 121 | /** 122 | * 上传输入流 123 | * 124 | * @param fileName 文件名 125 | * @param in 输入流 126 | */ 127 | @HttpReq(value = "/picture/upload", method = "POST") 128 | @RetryPolicy(times = 0) 129 | String upload(@Param("fileName") String fileName, 130 | @Param(value = "media") InputStream in); 131 | 132 | /** 133 | * 提交 multipart/form-data 表单,实现多文件上传 134 | * 135 | * @param multiPart 表单 136 | */ 137 | @HttpReq(value = "/files/upload", method = "POST") 138 | @RetryPolicy(times = 0) 139 | String multiPartForm(MultiPart multiPart); 140 | 141 | /** 142 | * #{variable} 表示支持路径参数,且该路径参数不会在填充后被移除,而是在消息体中也带上该参数 143 | */ 144 | @HttpReq(value = "/#{id}", method = "PUT") 145 | boolean updateCity(@Param("id") int id, @Param("name") String name); 146 | 147 | /** 148 | * 模拟表单提交 application/x-www-form-urlencoded 149 | */ 150 | @Form 151 | @HttpReq(value = "/saveCity", method = "POST") 152 | boolean saveCityForm(City city); 153 | 154 | /** 155 | * 使用Param注解指定方法参数对应的请求参数名称 156 | */ 157 | @HttpReq("/getByIds") 158 | List getCities(@Param("id") List ids); 159 | 160 | /** 161 | * 获取城市 162 | *

163 | * 用于测试返回值为 resultBean 的场景,而且正确的 code = 1 164 | */ 165 | @ExpectedCode(1) 166 | @HttpReq("/getCityByName") 167 | City getCityWithResultBean(@Param("name") String name); 168 | 169 | /** 170 | * 测试返回值为 Object 的情况 171 | */ 172 | @HttpReq("/getCityObject") 173 | Object getCityObject(); 174 | 175 | /** 176 | * 测试返回值为 String 类型 177 | */ 178 | @HttpReq("/getCityName") 179 | @Headers(keys = {"happy", "h3"}, values = {"done", "nice"}) 180 | @Cookies(keys = {"globalCookie", "auth"}, values = {"bad", "ok"}) 181 | @UserAgent("cityAgent") 182 | @ContentType("text/plain") 183 | String getCityName(@Param("id") int id); 184 | 185 | /** 186 | * 测试 ResultBean 的成功标识为 status 的场景 187 | */ 188 | @HttpReq("/getCityByName") 189 | @ExpectedCode(value = 1, codeFieldName = "status") 190 | City getCityWithStatusCode(@Param("name") String name); 191 | 192 | /** 193 | * 测试复杂对象的提交 194 | */ 195 | @Form 196 | @HttpReq(value = "/getCityByComplicatedInfo", method = ReqMethod.POST) 197 | City getCityByComplicatedInfo(ComplicatedInfo info); 198 | 199 | @HttpReq(value = "/patchCity", method = ReqMethod.PATCH) 200 | boolean patchCity(City city); 201 | 202 | @HttpReq(value = "/head", method = ReqMethod.HEAD) 203 | void head(); 204 | 205 | @HttpReq(value = "/trace", method = ReqMethod.TRACE) 206 | boolean trace(); 207 | 208 | @HttpReq(value = "/options", method = ReqMethod.OPTIONS) 209 | boolean options(); 210 | 211 | @HttpReq(value = "/invalid", method = "xxx") 212 | void invalidMethod(); 213 | 214 | /** 215 | * 测试没有打 HttpReq 注解 216 | */ 217 | void invalidMethodWithoutHttpReq(); 218 | 219 | /** 220 | * 测试 date 类型参数及返回值 221 | */ 222 | @HttpReq(value = "/date", method = ReqMethod.POST) 223 | Date getDate(@Param("date") Date date); 224 | 225 | @NotResultBean 226 | @HttpReq(value = "/string") 227 | String getString(); 228 | } 229 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/interfaces/CityServiceErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.interfaces; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.dadiyang.httpinvoker.HttpApiProxyFactory; 5 | import com.github.dadiyang.httpinvoker.entity.City; 6 | import com.github.dadiyang.httpinvoker.entity.ResultBean; 7 | import com.github.dadiyang.httpinvoker.entity.ResultBeanWithStatusAsCode; 8 | import com.github.dadiyang.httpinvoker.requestor.ResultBeanResponseProcessor; 9 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 10 | import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | 15 | import java.io.IOException; 16 | import java.io.UnsupportedEncodingException; 17 | import java.net.URLEncoder; 18 | import java.util.List; 19 | 20 | import static com.github.dadiyang.httpinvoker.util.CityUtil.createCities; 21 | import static com.github.dadiyang.httpinvoker.util.CityUtil.createCity; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 23 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 24 | import static org.junit.Assert.*; 25 | 26 | /** 27 | * 对一些错误值进行测试 28 | * 29 | * @author dadiyang 30 | * date 2019/1/10 31 | */ 32 | public class CityServiceErrorTest { 33 | private static final int PORT = 18888; 34 | @Rule 35 | public WireMockRule wireMockRule = new WireMockRule(options().port(PORT)); 36 | private CityService cityService; 37 | 38 | @Before 39 | public void setUp() throws Exception { 40 | System.setProperty("api.url.city.host", "http://localhost:" + PORT); 41 | System.setProperty("api.url.city.host2", "http://localhost:" + PORT); 42 | HttpApiProxyFactory httpApiProxyFactory = new HttpApiProxyFactory(); 43 | cityService = httpApiProxyFactory.getProxy(CityService.class); 44 | } 45 | 46 | @Test(expected = IllegalArgumentException.class) 47 | public void testIllegalPathVariable() { 48 | cityService.getCityRest(null); 49 | } 50 | 51 | /** 52 | * 测试类上面加的重试注解 53 | */ 54 | @Test 55 | public void testClassAnnotatedRetry() { 56 | wireMockRule.stubFor(get(urlPathEqualTo("/city/getById")).willReturn(notFound())); 57 | try { 58 | cityService.getCity(1); 59 | fail("前面应该报异常"); 60 | } catch (Exception e) { 61 | e.printStackTrace(); 62 | assertEquals(e.getCause().getClass(), IOException.class); 63 | } 64 | // 前面报错后重试 3 次 65 | wireMockRule.verify(3, RequestPatternBuilder.allRequests().withUrl("/city/getById?id=1")); 66 | } 67 | 68 | /** 69 | * 测试方法上加的重试注解 70 | */ 71 | @Test 72 | public void testMethodAnnotatedRetry() { 73 | long start = System.currentTimeMillis(); 74 | wireMockRule.stubFor(get(urlPathEqualTo("/city/allCities")).willReturn(serverError())); 75 | try { 76 | cityService.getAllCities(); 77 | fail("前面应该报异常"); 78 | } catch (Exception e) { 79 | e.printStackTrace(); 80 | assertEquals(e.getCause().getClass(), IOException.class); 81 | } 82 | long timeConsume = System.currentTimeMillis() - start; 83 | assertTrue("重试之前会休眠3000秒,因此2次请求,需要重试1次,即需要在3-6秒完成测试", 84 | timeConsume > 3000 && timeConsume < 6000); 85 | // 前面报错后重试 2 次 86 | wireMockRule.verify(2, RequestPatternBuilder.allRequests().withUrl("/city/allCities")); 87 | } 88 | 89 | /** 90 | * 只在50x的时候重试 91 | */ 92 | @Test 93 | public void testOnlyRetry50x() { 94 | List mockCities = createCities(); 95 | String uri = "/city/save"; 96 | wireMockRule.stubFor(post(urlPathEqualTo(uri)).willReturn(serverError())); 97 | try { 98 | cityService.saveCities(mockCities); 99 | fail("前面应该报异常"); 100 | } catch (Exception e) { 101 | e.printStackTrace(); 102 | assertEquals(e.getCause().getClass(), IOException.class); 103 | } 104 | // 前面报只对 50x 的错误尝试 2 次 105 | wireMockRule.verify(2, RequestPatternBuilder.allRequests().withUrl(uri)); 106 | 107 | wireMockRule.resetAll(); 108 | wireMockRule.stubFor(post(urlPathEqualTo(uri)).willReturn(notFound())); 109 | try { 110 | cityService.saveCities(mockCities); 111 | fail("前面应该报异常"); 112 | } catch (Exception e) { 113 | assertEquals(e.getCause().getClass(), IOException.class); 114 | } 115 | // 前面报错后应该不重试 116 | wireMockRule.verify(1, RequestPatternBuilder.allRequests().withUrl(uri)); 117 | } 118 | 119 | @Test(expected = IllegalStateException.class) 120 | public void getAllCitiesWithResultBeanResponseProcessor() { 121 | List mockCities = createCities(); 122 | String uri = "/city/allCities"; 123 | wireMockRule.stubFor(get(urlEqualTo(uri)).willReturn(aResponse().withBody(JSON.toJSONString(new ResultBean>(1, mockCities))))); 124 | CityService cityServiceWithResultBeanResponseProcessor = HttpApiProxyFactory.newProxy(CityService.class, new ResultBeanResponseProcessor()); 125 | cityServiceWithResultBeanResponseProcessor.getAllCities(); 126 | } 127 | 128 | @Test(expected = IllegalStateException.class) 129 | public void getCityWithResultBean() throws UnsupportedEncodingException { 130 | String cityName = "北京"; 131 | String uri = "/city/getCityByName?name=" + URLEncoder.encode(cityName, "UTF-8"); 132 | City city = createCity(cityName); 133 | ResultBean mockCityResult = new ResultBean(0, city); 134 | wireMockRule.stubFor(get(urlEqualTo(uri)) 135 | .willReturn(aResponse().withBody(JSON.toJSONString(mockCityResult)))); 136 | CityService cityServiceWithResultBeanResponseProcessor = HttpApiProxyFactory.newProxy(CityService.class, new ResultBeanResponseProcessor()); 137 | cityServiceWithResultBeanResponseProcessor.getCityWithResultBean(cityName); 138 | } 139 | 140 | @Test(expected = IllegalStateException.class) 141 | public void getCityWithStatusCode() throws UnsupportedEncodingException { 142 | String cityName = "北京"; 143 | String uri = "/city/getCityByName?name=" + URLEncoder.encode(cityName, "UTF-8"); 144 | ResultBeanWithStatusAsCode mockCityResult = new ResultBeanWithStatusAsCode("出错啦~", 0); 145 | wireMockRule.stubFor(get(urlEqualTo(uri)) 146 | .willReturn(aResponse().withBody(JSON.toJSONString(mockCityResult)))); 147 | CityService cityServiceWithResultBeanResponseProcessor = HttpApiProxyFactory.newProxy(CityService.class, new ResultBeanResponseProcessor()); 148 | cityServiceWithResultBeanResponseProcessor.getCityWithStatusCode(cityName); 149 | } 150 | 151 | @Test(expected = IllegalArgumentException.class) 152 | public void invalidMethod() { 153 | cityService.invalidMethod(); 154 | } 155 | 156 | @Test 157 | public void invalidMethodWithoutHttpReq() { 158 | try { 159 | cityService.invalidMethodWithoutHttpReq(); 160 | fail("should throws an exception when invoke the method without annotated with @HttpReq"); 161 | } catch (IllegalStateException e) { 162 | assertEquals(e.getMessage(), "this proxy only implement those HttpReq-annotated method, please add a @HttpReq on it."); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/interfaces/CityServiceMockRequestorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.interfaces; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.dadiyang.httpinvoker.HttpApiProxyFactory; 5 | import com.github.dadiyang.httpinvoker.entity.City; 6 | import com.github.dadiyang.httpinvoker.mocker.MockRequestor; 7 | import com.github.dadiyang.httpinvoker.mocker.MockResponse; 8 | import com.github.dadiyang.httpinvoker.mocker.MockRule; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | import java.io.InputStream; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Random; 17 | 18 | import static com.github.dadiyang.httpinvoker.util.IoUtils.closeStream; 19 | import static org.junit.Assert.assertEquals; 20 | 21 | /** 22 | * MockRequestor 单测 23 | * 24 | * @author dadiyang 25 | * @since 2019-05-31 26 | */ 27 | public class CityServiceMockRequestorTest { 28 | private CityService cityService; 29 | private MockRequestor requestor; 30 | 31 | @Before 32 | public void setUp() throws Exception { 33 | requestor = new MockRequestor(); 34 | InputStream in = null; 35 | try { 36 | in = CityServiceMockRequestorTest.class.getClassLoader().getResourceAsStream("conf.properties"); 37 | // 通过 Builder 构建代理工厂,使用 MockRequestor 来接管发送请求的过程 38 | HttpApiProxyFactory factory = new HttpApiProxyFactory.Builder() 39 | .setRequestor(requestor) 40 | .addProperties(in) 41 | .build(); 42 | cityService = factory.getProxy(CityService.class); 43 | } finally { 44 | closeStream(in); 45 | } 46 | } 47 | 48 | @Test 49 | public void urlAndDataTest() { 50 | requestor.addRule(new MockRule("http://localhost:18888/city/getCityName", Collections.singletonMap("id", (Object) 1), new MockResponse(200, "北京"))); 51 | String name = cityService.getCityName(1); 52 | assertEquals("北京", name); 53 | } 54 | 55 | @Test 56 | public void urlRegTest() { 57 | requestor.addRule(new MockRule("http://localhost:18888/city/.*", Collections.singletonMap("id", (Object) 1), new MockResponse(200, "北京"))); 58 | String name = cityService.getCityName(1); 59 | assertEquals("北京", name); 60 | } 61 | 62 | @Test 63 | public void uriTest() { 64 | int id = nextInt(); 65 | City city = new City(id, "北京"); 66 | MockRule rule = new MockRule(); 67 | rule.setUriReg("/city/getCityRest/" + id); 68 | rule.setResponse(new MockResponse(200, JSON.toJSONString(city))); 69 | requestor.addRule(rule); 70 | City rs = cityService.getCityWithHeaders(id, genMap()); 71 | assertEquals(city, rs); 72 | } 73 | 74 | private int nextInt() { 75 | Random r = new Random(); 76 | return r.nextInt(); 77 | } 78 | 79 | @Test 80 | public void headerTest() { 81 | int id = nextInt(); 82 | City city = new City(id, "北京"); 83 | MockRule rule = new MockRule("http://localhost:18888/city/getCityRest/" + id, "GET", new MockResponse(200, JSON.toJSONString(city))); 84 | Map header = genMap(); 85 | rule.setHeaders(header); 86 | 87 | // 请求头里还有规则之外的东西,匹配器应该忽略这些 88 | Map requestHeader = new HashMap(); 89 | for (Map.Entry entry : header.entrySet()) { 90 | requestHeader.put(entry.getKey(), entry.getValue()); 91 | } 92 | requestHeader.put("xxx", "1234"); 93 | requestor.addRule(rule); 94 | City rs = cityService.getCityWithHeaders(id, requestHeader); 95 | assertEquals(city, rs); 96 | } 97 | 98 | @Test(expected = Exception.class) 99 | public void methodMismatch() { 100 | int id = nextInt(); 101 | City city = new City(id, "北京"); 102 | MockRule rule = new MockRule("http://localhost:18888/city/listCity" + id, "POST", new MockResponse(200, JSON.toJSONString(city))); 103 | requestor.addRule(rule); 104 | cityService.listCity(); 105 | } 106 | 107 | private Map genMap() { 108 | Map header = new HashMap(); 109 | header.put("ttt", String.valueOf(nextInt())); 110 | header.put("ttt2", String.valueOf(nextInt())); 111 | return header; 112 | } 113 | 114 | @Test 115 | public void cookieTest() { 116 | int id = nextInt(); 117 | City city = new City(id, "北京"); 118 | MockRule rule = new MockRule("http://localhost:18888/city/getCityRest/" + id, new MockResponse(200, JSON.toJSONString(city))); 119 | Map cookies = genMap(); 120 | rule.setCookies(cookies); 121 | requestor.addRule(rule); 122 | City rs = cityService.getCityWithCookies(id, cookies); 123 | assertEquals(city, rs); 124 | } 125 | 126 | @Test 127 | public void headerAndCookieTest() { 128 | int id = nextInt(); 129 | City city = new City(id, "北京"); 130 | MockRule rule = new MockRule("http://localhost:18888/city/getCityRest/" + id, new MockResponse(200, JSON.toJSONString(city))); 131 | Map cookies = genMap(); 132 | Map headers = genMap(); 133 | rule.setCookies(cookies); 134 | rule.setHeaders(headers); 135 | requestor.addRule(rule); 136 | City rs = cityService.getCityWithCookiesAndHeaders(id, cookies, headers); 137 | assertEquals(city, rs); 138 | } 139 | 140 | @Test(expected = IllegalStateException.class) 141 | public void getCityNameMultiMockRuleTest() { 142 | requestor.addRule(new MockRule("http://localhost:18888/city/getCityName", Collections.singletonMap("id", (Object) 1), new MockResponse(200, "北京"))); 143 | requestor.addRule(new MockRule("http://localhost:18888/city/getCityName", Collections.singletonMap("id", (Object) 1), new MockResponse(200, "北京"))); 144 | String name = cityService.getCityName(1); 145 | assertEquals("北京", name); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/interfaces/CityServiceSpringTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.interfaces; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.dadiyang.httpinvoker.TestApplication; 5 | import com.github.dadiyang.httpinvoker.entity.City; 6 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import static com.github.dadiyang.httpinvoker.util.CityUtil.createCity; 15 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 16 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 17 | import static org.junit.Assert.assertEquals; 18 | 19 | @RunWith(SpringRunner.class) 20 | @ContextConfiguration(classes = TestApplication.class) 21 | public class CityServiceSpringTest { 22 | private static final int PORT = 18888; 23 | @Rule 24 | public WireMockRule wireMockRule = new WireMockRule(options().port(PORT)); 25 | @Autowired 26 | private CityService cityService; 27 | 28 | @Test 29 | public void getCity() { 30 | System.out.println(cityService.toString()); 31 | int id = 1; 32 | String uri = "/city/getById?id=" + id; 33 | City mockCity = createCity(id); 34 | wireMockRule.stubFor(get(urlEqualTo(uri)) 35 | .withCookie("testCookie", equalTo("OK")) 36 | .withHeader("testHeader", equalTo("OK")) 37 | .willReturn(aResponse().withBody(JSON.toJSONString(mockCity)))); 38 | City city = cityService.getCity(id); 39 | assertEquals(mockCity, city); 40 | } 41 | 42 | @Test 43 | public void mock() { 44 | // 在 TestApplication 中添加了 mock 规则,因此这个请求不会被真正发出 45 | String name = cityService.getCityName(1); 46 | assertEquals("北京", name); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/requestor/DefaultHttpRequestorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.requestor; 2 | 3 | import com.github.dadiyang.httpinvoker.annotation.HttpReq; 4 | import com.github.dadiyang.httpinvoker.interfaces.CityService; 5 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | 11 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 12 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 13 | 14 | public class DefaultHttpRequestorTest { 15 | private DefaultHttpRequestor defaultHttpRequestor; 16 | private static final int PORT = 18888; 17 | @Rule 18 | public WireMockRule cityIoService = new WireMockRule(options().port(PORT)); 19 | 20 | @Before 21 | public void setUp() throws Exception { 22 | defaultHttpRequestor = new DefaultHttpRequestor(); 23 | } 24 | 25 | @Test 26 | public void sendRequest() throws Exception { 27 | String url = "http://localhost:" + PORT + "/getAllCities"; 28 | HttpReq anno = CityService.class.getMethod("getAllCities").getAnnotation(HttpReq.class); 29 | cityIoService.stubFor(get(urlEqualTo("/getAllCities")).willReturn(aResponse().withBody("abc"))); 30 | HttpRequest request = new HttpRequest(url); 31 | HttpResponse response = defaultHttpRequestor.sendRequest(request); 32 | Assert.assertEquals("abc", response.getBody()); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/serializer/JsonSerializerDeciderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class JsonSerializerDeciderTest { 8 | @Test 9 | public void getJsonSerializer() { 10 | JsonSerializer jsonSerializer = JsonSerializerDecider.getJsonSerializer(); 11 | assertTrue("默认使用FastJson的实现", jsonSerializer instanceof FastJsonJsonSerializer); 12 | 13 | JsonSerializerDecider.registerJsonSerializer("Gson", GsonJsonSerializer.getInstance()); 14 | JsonSerializerDecider.setJsonInstanceKey("Gson"); 15 | JsonSerializer gson = JsonSerializerDecider.getJsonSerializer(); 16 | assertTrue("指定使用Gson实现,则必须返回gson实现", gson instanceof GsonJsonSerializer); 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/serializer/JsonSerializerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.serializer; 2 | 3 | import com.github.dadiyang.httpinvoker.entity.City; 4 | import org.junit.Test; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.*; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | /** 12 | * 本单测主要尽最大可能让不同的序列化实现互相兼容 13 | * 14 | * @author huangxuyang 15 | * @since 2020/12/6 16 | */ 17 | public class JsonSerializerTest { 18 | private GsonJsonSerializer gsonJsonSerializer = new GsonJsonSerializer(); 19 | private FastJsonJsonSerializer fastJsonJsonSerializer = new FastJsonJsonSerializer(); 20 | 21 | @Test 22 | public void serialize() { 23 | Date now = new Date(); 24 | Map origMap = new LinkedHashMap(); 25 | origMap.put("date", now); 26 | origMap.put("double", 1.1); 27 | origMap.put("integer", 1); 28 | origMap.put("arr", Arrays.asList(1, 2, 3, 4, 5)); 29 | String gsonJson = gsonJsonSerializer.serialize(origMap); 30 | String fastJsonJson = fastJsonJsonSerializer.serialize(origMap); 31 | assertEquals("gson序列化结果应与fastJson序列化结果一致", gsonJson, fastJsonJson); 32 | String expectJson = "{\"date\":" + now.getTime() + ",\"double\":1.1,\"integer\":1,\"arr\":[1,2,3,4,5]}"; 33 | assertEquals("序列化结果应与期望的一致", expectJson, gsonJson); 34 | 35 | Map map = gsonJsonSerializer.toMap(gsonJson); 36 | Map fastJsonMap = fastJsonJsonSerializer.toMap(gsonJson); 37 | 38 | assertMapEquals(map, fastJsonMap); 39 | 40 | assertEquals("再序列化回来,然后再序列化,应该结果一致", expectJson, gsonJsonSerializer.serialize(map)); 41 | } 42 | 43 | private void assertMapEquals(Map m1, Map m2) { 44 | assertFalse("两个空map比对没有意义", m1.isEmpty()); 45 | assertFalse("两个空map比对没有意义", m2.isEmpty()); 46 | assertEquals("两个map的大小应该一致", m1.size(), m2.size()); 47 | assertTrue("两个map的key应互相全包含", m1.keySet().containsAll(m2.keySet())); 48 | assertTrue("两个map的key应互相全包含", m2.keySet().containsAll(m1.keySet())); 49 | 50 | for (Map.Entry entry : m1.entrySet()) { 51 | assertEquals(entry.getKey() + "应相等", entry.getValue(), m2.get(entry.getKey())); 52 | } 53 | } 54 | 55 | @Test 56 | public void parseObject() { 57 | City city = new City(); 58 | city.setId(1); 59 | city.setName("北京"); 60 | 61 | String gsonJson = gsonJsonSerializer.serialize(city); 62 | String fastJsonJson = fastJsonJsonSerializer.serialize(city); 63 | assertEquals("gson序列化结果应与fastJson序列化结果一致", gsonJson, fastJsonJson); 64 | 65 | City gsonCity = gsonJsonSerializer.parseObject(gsonJson, City.class); 66 | City fastCity = fastJsonJsonSerializer.parseObject(gsonJson, City.class); 67 | assertEquals("gson反序列化结果应与fastJson反序列化结果一致", gsonCity, fastCity); 68 | 69 | assertEquals("序列化后再反序列化回来应该相等", city, gsonCity); 70 | } 71 | 72 | @Test 73 | public void parseArray() { 74 | String arrJson = "[1.0,2,3,4,5]"; 75 | List gsonArr = gsonJsonSerializer.parseArray(arrJson); 76 | List fastJsonArr = fastJsonJsonSerializer.parseArray(arrJson); 77 | assertEquals(5, gsonArr.size()); 78 | assertEquals(5, fastJsonArr.size()); 79 | assertTrue("应包含指定的元素" + gsonArr, gsonArr.contains(new BigDecimal("1.0"))); 80 | assertTrue("应包含指定的元素" + gsonArr, gsonArr.contains(2)); 81 | assertTrue("应包含指定的元素" + gsonArr, gsonArr.contains(3)); 82 | assertTrue("应包含指定的元素" + gsonArr, gsonArr.contains(4)); 83 | assertTrue("应包含指定的元素" + gsonArr, gsonArr.contains(5)); 84 | 85 | assertTrue("应包含指定的元素" + fastJsonArr, fastJsonArr.contains(new BigDecimal("1.0"))); 86 | assertTrue("应包含指定的元素" + fastJsonArr, fastJsonArr.contains(2)); 87 | assertTrue("应包含指定的元素" + fastJsonArr, fastJsonArr.contains(3)); 88 | assertTrue("应包含指定的元素" + fastJsonArr, fastJsonArr.contains(4)); 89 | assertTrue("应包含指定的元素" + fastJsonArr, fastJsonArr.contains(5)); 90 | } 91 | } -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/util/CityUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | import com.github.dadiyang.httpinvoker.entity.City; 4 | 5 | import java.util.ArrayList; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | public class CityUtil { 10 | public static List createCities() { 11 | List cityList = new ArrayList(); 12 | cityList.add(new City(1, "北京")); 13 | cityList.add(new City(2, "上海")); 14 | cityList.add(new City(3, "广州")); 15 | cityList.add(new City(4, "深圳")); 16 | return cityList; 17 | } 18 | 19 | public static City createCity(int id) { 20 | List cities = createCities(); 21 | for (City city : cities) { 22 | if (city.getId() == id) { 23 | return city; 24 | } 25 | } 26 | throw new IllegalArgumentException("city not exists, id:" + id); 27 | } 28 | 29 | public static List getCities(List ids) { 30 | List cities = createCities(); 31 | List rs = new LinkedList(); 32 | for (City city : cities) { 33 | if (ids.contains(city.getId())) { 34 | rs.add(city); 35 | } 36 | } 37 | return rs; 38 | } 39 | 40 | public static City createCity(String name) { 41 | List cities = createCities(); 42 | for (City city : cities) { 43 | if (ObjectUtils.equals(city.getName(), name)) { 44 | return city; 45 | } 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/dadiyang/httpinvoker/util/ParamUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dadiyang.httpinvoker.util; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.dadiyang.httpinvoker.entity.ComplicatedInfo; 5 | import org.junit.Test; 6 | 7 | import java.util.Map; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | public class ParamUtilsTest { 12 | 13 | @Test 14 | public void toMapStringString() { 15 | ComplicatedInfo info = new ComplicatedInfo(CityUtil.createCities(), "123", CityUtil.createCity(1)); 16 | System.out.println(JSON.toJSONString(info, true)); 17 | Map rs = ParamUtils.toMapStringString(info, ""); 18 | System.out.println(rs); 19 | assertEquals("{msg=123, cities[3][id]=4, city[name]=北京, cities[2][id]=3, cities[1][id]=2, cities[2][name]=广州, city[id]=1, cities[0][name]=北京, cities[0][id]=1, cities[1][name]=上海, cities[3][name]=深圳}", rs.toString()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/resources/conf.properties: -------------------------------------------------------------------------------- 1 | api.url.city.host=http://localhost:18888 -------------------------------------------------------------------------------- /src/test/resources/conf2.properties: -------------------------------------------------------------------------------- 1 | api.url.city.host2=http://localhost:18888 -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=trace,console 2 | log4j.additivity.org.apache=true 3 | log4j.appender.console=org.apache.log4j.ConsoleAppender 4 | log4j.appender.console.Threshold=DEBUG 5 | log4j.appender.console.ImmediateFlush=true 6 | log4j.appender.console.Target=System.out 7 | log4j.appender.console.layout=org.apache.log4j.TTCCLayout --------------------------------------------------------------------------------