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 extends Throwable>[] 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 extends FactoryBean> factoryBean;
29 | private Class extends Annotation> 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