├── .gitattributes ├── .gitignore ├── src └── main │ └── java │ └── vip │ └── justlive │ └── easyhttp │ ├── annotation │ ├── HttpClient.java │ └── HttpClientScan.java │ ├── factory │ ├── HttpClientFactoryBean.java │ ├── HttpClientProxy.java │ └── HttpClientMethod.java │ └── scanner │ ├── HttpClientRegistrar.java │ └── HttpClientScanner.java ├── README.md ├── pom.xml └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .project 3 | .DS_Store 4 | .classpath 5 | .settings 6 | *.log 7 | *.log.* 8 | 9 | .idea 10 | .gradle 11 | build 12 | bin 13 | out 14 | *.iml 15 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/annotation/HttpClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.annotation; 16 | 17 | import java.lang.annotation.Documented; 18 | import java.lang.annotation.ElementType; 19 | import java.lang.annotation.Retention; 20 | import java.lang.annotation.RetentionPolicy; 21 | import java.lang.annotation.Target; 22 | 23 | /** 24 | * mark an interface as httpclient 25 | * 26 | * @author wubo 27 | */ 28 | @Documented 29 | @Target(ElementType.TYPE) 30 | @Retention(RetentionPolicy.RUNTIME) 31 | public @interface HttpClient { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/annotation/HttpClientScan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.annotation; 16 | 17 | import java.lang.annotation.Documented; 18 | import java.lang.annotation.ElementType; 19 | import java.lang.annotation.Retention; 20 | import java.lang.annotation.RetentionPolicy; 21 | import java.lang.annotation.Target; 22 | import org.springframework.context.annotation.Import; 23 | import vip.justlive.easyhttp.scanner.HttpClientRegistrar; 24 | 25 | /** 26 | * 扫描注解 27 | * 28 | * @author wubo 29 | */ 30 | @Retention(RetentionPolicy.RUNTIME) 31 | @Target(ElementType.TYPE) 32 | @Documented 33 | @Import(HttpClientRegistrar.class) 34 | public @interface HttpClientScan { 35 | 36 | /** 37 | * 扫描路径 38 | * 39 | * @return pkgs 40 | */ 41 | String[] value() default {}; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/factory/HttpClientFactoryBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.factory; 16 | 17 | import java.lang.reflect.Proxy; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import org.springframework.beans.factory.FactoryBean; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.context.EnvironmentAware; 23 | import org.springframework.core.env.Environment; 24 | import vip.justlive.oxygen.core.util.net.http.HttpRequestExecution; 25 | import vip.justlive.oxygen.core.util.net.http.HttpRequestInterceptor; 26 | 27 | /** 28 | * factory bean of httpclient 29 | * 30 | * @param 泛型 31 | * @author wubo 32 | */ 33 | public class HttpClientFactoryBean implements FactoryBean, EnvironmentAware { 34 | 35 | private final Class clientInterface; 36 | private Environment environment; 37 | private HttpRequestExecution requestExecution; 38 | private List interceptors; 39 | 40 | public HttpClientFactoryBean(Class clientInterface) { 41 | this.clientInterface = clientInterface; 42 | } 43 | 44 | @Autowired(required = false) 45 | public void setRequestExecution(HttpRequestExecution requestExecution) { 46 | this.requestExecution = requestExecution; 47 | } 48 | 49 | @Autowired(required = false) 50 | public void setInterceptors(List interceptors) { 51 | this.interceptors = interceptors; 52 | if (this.interceptors != null) { 53 | Collections.sort(this.interceptors); 54 | } 55 | } 56 | 57 | @Override 58 | public T getObject() { 59 | return clientInterface.cast(Proxy 60 | .newProxyInstance(clientInterface.getClassLoader(), new Class[]{clientInterface}, 61 | new HttpClientProxy<>(clientInterface, environment, requestExecution, interceptors))); 62 | } 63 | 64 | @Override 65 | public Class getObjectType() { 66 | return clientInterface; 67 | } 68 | 69 | @Override 70 | public boolean isSingleton() { 71 | return true; 72 | } 73 | 74 | @Override 75 | public void setEnvironment(Environment environment) { 76 | this.environment = environment; 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/scanner/HttpClientRegistrar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.scanner; 16 | 17 | import java.util.Arrays; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 21 | import org.springframework.context.ResourceLoaderAware; 22 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 23 | import org.springframework.core.annotation.AnnotationAttributes; 24 | import org.springframework.core.io.ResourceLoader; 25 | import org.springframework.core.type.AnnotationMetadata; 26 | import vip.justlive.easyhttp.annotation.HttpClientScan; 27 | import vip.justlive.oxygen.core.util.base.Strings; 28 | 29 | /** 30 | * httpclient registrar 31 | * 32 | * @author wubo 33 | */ 34 | public class HttpClientRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { 35 | 36 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientRegistrar.class); 37 | 38 | private ResourceLoader resourceLoader; 39 | 40 | @Override 41 | public void setResourceLoader(ResourceLoader resourceLoader) { 42 | this.resourceLoader = resourceLoader; 43 | } 44 | 45 | @Override 46 | public void registerBeanDefinitions(AnnotationMetadata metadata, 47 | BeanDefinitionRegistry registry) { 48 | 49 | AnnotationAttributes attributes = AnnotationAttributes 50 | .fromMap(metadata.getAnnotationAttributes(HttpClientScan.class.getName())); 51 | if (attributes != null) { 52 | String[] basePackages = attributes.getStringArray("value"); 53 | String basePackage = metadata.getClassName() 54 | .substring(0, metadata.getClassName().lastIndexOf(Strings.DOT)); 55 | if (basePackages.length == 0) { 56 | basePackages = new String[]{basePackage}; 57 | } else { 58 | basePackages = Arrays.copyOf(basePackages, basePackages.length + 1); 59 | basePackages[basePackages.length - 1] = basePackage; 60 | } 61 | HttpClientScanner scanner = new HttpClientScanner(registry); 62 | scanner.setResourceLoader(resourceLoader); 63 | scanner.scan(basePackages); 64 | return; 65 | } 66 | LOGGER.warn("not found @HttpClientScan or has no value"); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy-http 2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/vip.justlive/easy-http/badge.svg)](https://maven-badges.herokuapp.com/maven-central/vip.justlive/easy-http/) 3 | [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 4 | 5 | easy to contact http rest api 6 | 7 | 8 | ## 介绍 9 | 10 | - 基于Spring mvc`@RequestMapping`、`@GetMapping`、`@PostMapping`、`@RequestParam`、`RequestHeader`、`PathVariable`等注解扩展 11 | - 自动扫描接口实例化并托管至Spring,参考自Mybatis自动扫描Mapper 12 | - 需要声明的接口和Server端保持一致即可完美对接 13 | 14 | ## 快速开始 15 | 16 | ### 引入 17 | 18 | 创建`Maven`项目 19 | 20 | ```xml 21 | 22 | vip.justlive 23 | easy-http 24 | ${lastVersion} 25 | 26 | ``` 27 | 28 | 或`Gradle` 29 | 30 | ``` 31 | compile 'vip.justlive:easy-http:$lastVersion' 32 | ``` 33 | 34 | ### 使用方式 35 | 36 | ```java 37 | 38 | // 创建接口 39 | 40 | @HttpClient 41 | @RequestMapping("https://api.github.com") 42 | public interface GithubApi { 43 | 44 | @RequestMapping(method = RequestMethod.GET) 45 | String root(); 46 | 47 | 48 | @GetMapping("/repos/{owner}/{repo}") 49 | Repository repos(@PathVariable String owner, @PathVariable String repo); 50 | 51 | @Data 52 | class Repository { 53 | 54 | private Long id; 55 | private String name; 56 | private int forks; 57 | private int watchers; 58 | } 59 | } 60 | 61 | // 使用${xx}获取配置 62 | @HttpClient 63 | @RequestMapping("${thirdpart.api.url}") 64 | public interface ThirdpartApi { 65 | 66 | @PostMapping("/api/token") 67 | RespVo token(@RequestParam String appKey, @RequestParam String appSecret); 68 | 69 | @PostMapping("/api/account") 70 | RespVo account(@RequestHeader String accessToken, @RequestBody Account account); 71 | 72 | @Data 73 | @Accessors(chain = true) 74 | class AccessToken { 75 | 76 | private String accessToken; 77 | private Long expiresIn; 78 | } 79 | 80 | @Data 81 | @Accessors(chain = true) 82 | class Account { 83 | 84 | private String name; 85 | private BigDecimal balance; 86 | } 87 | } 88 | 89 | // 增加扫描(默认为当前类所处包) 90 | 91 | @HttpClientScan("com.xxx") 92 | @Configuration 93 | public class HttpClientAutoConfiguration { 94 | 95 | } 96 | 97 | // 使用 98 | 99 | @Slf4j 100 | @Component 101 | public class Demo { 102 | 103 | @Autowired 104 | private GithubApi githubApi; 105 | 106 | @Autowired 107 | private ThirdpartApi thirdpartApi; 108 | 109 | @PostConstruct 110 | private void init() { 111 | 112 | String result = githubApi.root(); 113 | log.info("githubApi root {}", result); 114 | 115 | Repository repository = githubApi.repos("justlive1", "easy-http"); 116 | log.info("githubApi repos {}", repository); 117 | 118 | AccessToken token = thirdpartApi.token("key", "secret").getData(); 119 | log.info("thirdpartApi token {}", token); 120 | 121 | Account account = new Account().setName("aa").setBalance(new BigDecimal("12.3")); 122 | account = thirdpartApi.account(token.getAccessToken(), account).getData(); 123 | log.info("thirdpartApi account {}", account); 124 | } 125 | } 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/scanner/HttpClientScanner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.scanner; 16 | 17 | import java.util.Set; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; 21 | import org.springframework.beans.factory.config.BeanDefinitionHolder; 22 | import org.springframework.beans.factory.support.AbstractBeanDefinition; 23 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 24 | import org.springframework.beans.factory.support.GenericBeanDefinition; 25 | import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; 26 | import org.springframework.core.type.filter.AnnotationTypeFilter; 27 | import vip.justlive.easyhttp.annotation.HttpClient; 28 | import vip.justlive.easyhttp.factory.HttpClientFactoryBean; 29 | 30 | /** 31 | * scan interfaces marked with @HttpClient 32 | * 33 | * @author wubo 34 | */ 35 | public class HttpClientScanner extends ClassPathBeanDefinitionScanner { 36 | 37 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientScanner.class); 38 | 39 | HttpClientScanner(BeanDefinitionRegistry registry) { 40 | super(registry, false); 41 | addIncludeFilter(new AnnotationTypeFilter(HttpClient.class)); 42 | } 43 | 44 | @Override 45 | protected Set doScan(String... basePackages) { 46 | Set beanDefinitions = super.doScan(basePackages); 47 | if (beanDefinitions.isEmpty()) { 48 | LOGGER.warn("No HttpClient was found in '{}' package. Please check your configuration.", 49 | (Object) basePackages); 50 | } else { 51 | processBeanDefinitions(beanDefinitions); 52 | } 53 | return beanDefinitions; 54 | } 55 | 56 | @Override 57 | protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { 58 | return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata() 59 | .isIndependent(); 60 | } 61 | 62 | private void processBeanDefinitions(Set beanDefinitions) { 63 | GenericBeanDefinition definition; 64 | 65 | for (BeanDefinitionHolder holder : beanDefinitions) { 66 | definition = (GenericBeanDefinition) holder.getBeanDefinition(); 67 | 68 | String beanClassName = definition.getBeanClassName(); 69 | if (beanClassName == null) { 70 | continue; 71 | } 72 | if (LOGGER.isDebugEnabled()) { 73 | LOGGER.debug("creating HttpClientFactoryBean with name '{}' and '{}' interface", 74 | holder.getBeanName(), beanClassName); 75 | } 76 | 77 | definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 78 | definition.setBeanClass(HttpClientFactoryBean.class); 79 | definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); 80 | 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/factory/HttpClientProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.factory; 16 | 17 | import java.lang.invoke.MethodHandles; 18 | import java.lang.invoke.MethodHandles.Lookup; 19 | import java.lang.reflect.Constructor; 20 | import java.lang.reflect.InvocationHandler; 21 | import java.lang.reflect.Method; 22 | import java.lang.reflect.Modifier; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.concurrent.ConcurrentHashMap; 26 | import org.springframework.core.env.Environment; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | import vip.justlive.oxygen.core.util.net.http.HttpRequestExecution; 29 | import vip.justlive.oxygen.core.util.net.http.HttpRequestInterceptor; 30 | 31 | /** 32 | * proxy 33 | * 34 | * @param 泛型 35 | * @author wubo 36 | */ 37 | public class HttpClientProxy implements InvocationHandler { 38 | 39 | private final Class clientInterface; 40 | private final Environment environment; 41 | private final HttpRequestExecution requestExecution; 42 | private final List interceptors; 43 | private final Map cache = new ConcurrentHashMap<>(4); 44 | private String root; 45 | 46 | HttpClientProxy(Class clientInterface, Environment environment, 47 | HttpRequestExecution requestExecution, List interceptors) { 48 | this.clientInterface = clientInterface; 49 | this.environment = environment; 50 | this.requestExecution = requestExecution; 51 | this.interceptors = interceptors; 52 | this.init(); 53 | } 54 | 55 | private void init() { 56 | RequestMapping req = this.clientInterface.getAnnotation(RequestMapping.class); 57 | if (req != null && req.value().length > 0) { 58 | root = this.environment.resolvePlaceholders(req.value()[0]); 59 | } 60 | } 61 | 62 | @Override 63 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 64 | 65 | if (Object.class.equals(method.getDeclaringClass())) { 66 | return method.invoke(this, args); 67 | } 68 | 69 | if (isDefaultMethod(method)) { 70 | return invokeDefaultMethod(proxy, method, args); 71 | } 72 | 73 | return cache.computeIfAbsent(method, 74 | k -> new HttpClientMethod(root, method, environment, requestExecution, interceptors)) 75 | .execute(args); 76 | } 77 | 78 | private boolean isDefaultMethod(Method method) { 79 | return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) 80 | == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); 81 | } 82 | 83 | private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { 84 | final Constructor constructor = MethodHandles.Lookup.class 85 | .getDeclaredConstructor(Class.class, int.class); 86 | if (!constructor.isAccessible()) { 87 | constructor.setAccessible(true); 88 | } 89 | final Class declaringClass = method.getDeclaringClass(); 90 | return constructor.newInstance(declaringClass, 91 | MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE 92 | | MethodHandles.Lookup.PUBLIC).unreflectSpecial(method, declaringClass).bindTo(proxy) 93 | .invokeWithArguments(args); 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | vip.justlive 8 | easy-http 9 | 1.1.0 10 | jar 11 | 12 | easy-http 13 | http://www.justlive.vip 14 | easy to contact http rest api 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 20 | 1.2.78 21 | 5.8.1 22 | 1.18.22 23 | 3.0.7.2 24 | 1.7.32 25 | 5.3.13 26 | 27 | ${java.version} 28 | ${java.version} 29 | 3.0.1 30 | 3.1.0 31 | 3.1.0 32 | 2.7 33 | 34 | 35 | 36 | 37 | Apache 2 38 | http://www.apache.org/licenses/LICENSE-2.0.txt 39 | 40 | 41 | 42 | 43 | 44 | justlive1 45 | qq11419041@163.com 46 | +8 47 | 48 | 49 | 50 | 51 | master 52 | https://github.com/justlive1/easy-http 53 | https://github.com/justlive1/easy-http.git 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-source-plugin 62 | ${maven-source-plugin.version} 63 | 64 | 65 | package 66 | 67 | jar-no-fork 68 | 69 | 70 | 71 | 72 | 73 | org.codehaus.mojo 74 | versions-maven-plugin 75 | ${maven-version-plugin.version} 76 | 77 | false 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | release 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-javadoc-plugin 92 | ${maven-javadoc-plugin.version} 93 | 94 | 95 | package 96 | 97 | jar 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-gpg-plugin 106 | ${maven-gpg-plugin.version} 107 | 108 | 109 | verify 110 | 111 | sign 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | oss 121 | https://oss.sonatype.org/content/repositories/snapshots/ 122 | 123 | 124 | oss 125 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 126 | 127 | 128 | 129 | 130 | doc 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-javadoc-plugin 137 | ${maven-javadoc-plugin.version} 138 | 139 | 140 | package 141 | 142 | jar 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | org.springframework 156 | spring-web 157 | ${spring.version} 158 | 159 | 160 | 161 | org.springframework 162 | spring-context 163 | ${spring.version} 164 | 165 | 166 | 167 | com.alibaba 168 | fastjson 169 | ${fastjson.version} 170 | 171 | 172 | 173 | vip.justlive 174 | oxygen-core 175 | ${oxygen.version} 176 | 177 | 178 | 179 | 180 | org.slf4j 181 | slf4j-api 182 | ${slf4j.version} 183 | 184 | 185 | 186 | org.projectlombok 187 | lombok 188 | ${lombok.version} 189 | provided 190 | 191 | 192 | 193 | org.junit.jupiter 194 | junit-jupiter 195 | ${junit.version} 196 | test 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "{}" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2019 justlive1 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/vip/justlive/easyhttp/factory/HttpClientMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 justlive1 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package vip.justlive.easyhttp.factory; 16 | 17 | import com.alibaba.fastjson.JSON; 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.lang.reflect.Method; 21 | import java.lang.reflect.Parameter; 22 | import java.lang.reflect.Type; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | import org.slf4j.MDC; 29 | import org.springframework.core.env.Environment; 30 | import org.springframework.util.StringUtils; 31 | import org.springframework.web.bind.annotation.DeleteMapping; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.PatchMapping; 34 | import org.springframework.web.bind.annotation.PathVariable; 35 | import org.springframework.web.bind.annotation.PostMapping; 36 | import org.springframework.web.bind.annotation.PutMapping; 37 | import org.springframework.web.bind.annotation.RequestBody; 38 | import org.springframework.web.bind.annotation.RequestHeader; 39 | import org.springframework.web.bind.annotation.RequestMapping; 40 | import org.springframework.web.bind.annotation.RequestMethod; 41 | import org.springframework.web.bind.annotation.RequestParam; 42 | import org.springframework.web.bind.annotation.RequestPart; 43 | import org.springframework.web.multipart.MultipartFile; 44 | import vip.justlive.oxygen.core.exception.Exceptions; 45 | import vip.justlive.oxygen.core.util.base.HttpHeaders; 46 | import vip.justlive.oxygen.core.util.base.MoreObjects; 47 | import vip.justlive.oxygen.core.util.base.SnowflakeId; 48 | import vip.justlive.oxygen.core.util.base.Strings; 49 | import vip.justlive.oxygen.core.util.io.FileCleaner; 50 | import vip.justlive.oxygen.core.util.io.FileUtils; 51 | import vip.justlive.oxygen.core.util.net.http.HttpMethod; 52 | import vip.justlive.oxygen.core.util.net.http.HttpRequest; 53 | import vip.justlive.oxygen.core.util.net.http.HttpRequestExecution; 54 | import vip.justlive.oxygen.core.util.net.http.HttpRequestInterceptor; 55 | import vip.justlive.oxygen.core.util.net.http.HttpResponse; 56 | 57 | /** 58 | * method wrapper 59 | * 60 | * @author wubo 61 | */ 62 | public class HttpClientMethod { 63 | 64 | public static final String TRACE_ID = "trace-id"; 65 | private static final Pattern REGEX_PATH_GROUP = Pattern.compile("\\{(\\w+)[}]"); 66 | 67 | private final String root; 68 | private final Method method; 69 | private final Environment environment; 70 | private final HttpRequestExecution requestExecution; 71 | private final List interceptors; 72 | private String url; 73 | private Type responseType; 74 | private RequestMethod httpMethod = RequestMethod.GET; 75 | private final Map headers = new HashMap<>(2); 76 | private final Map query = new HashMap<>(2); 77 | private boolean multipart = false; 78 | private int bodyIndex = -1; 79 | private boolean isHttpclient; 80 | private Map pathVars; 81 | private final Map baseHeaders = new HashMap<>(2); 82 | 83 | HttpClientMethod(String root, Method method, Environment environment, 84 | HttpRequestExecution requestExecution, List interceptors) { 85 | this.root = root; 86 | this.method = method; 87 | this.environment = environment; 88 | this.requestExecution = requestExecution; 89 | this.interceptors = interceptors; 90 | parse(); 91 | } 92 | 93 | Object execute(Object... args) throws IOException { 94 | if (!isHttpclient) { 95 | return null; 96 | } 97 | 98 | try (FileCleaner cleaner = new FileCleaner(); 99 | HttpResponse response = buildRequest(cleaner, args).execute()) { 100 | if (responseType == Void.TYPE) { 101 | return null; 102 | } 103 | if (responseType == String.class) { 104 | return response.bodyAsString(); 105 | } 106 | return JSON.parseObject(response.bodyAsString(), responseType); 107 | } 108 | } 109 | 110 | private void parse() { 111 | if (parseMapping() || parseGet() || parsePost() || parsePut() || parseDelete() 112 | || parsePatch()) { 113 | if (url == null) { 114 | return; 115 | } 116 | if (StringUtils.hasText(root)) { 117 | url = concatUrl(root, url); 118 | } 119 | this.isHttpclient = true; 120 | parsePathVars(); 121 | parseParam(); 122 | } 123 | } 124 | 125 | private String pathVarSeat(String name) { 126 | return String.format(" %s ", name); 127 | } 128 | 129 | private void parsePathVars() { 130 | Matcher matcher = REGEX_PATH_GROUP.matcher(url); 131 | int start = 0; 132 | if (matcher.find(start)) { 133 | pathVars = new HashMap<>(2); 134 | do { 135 | String name = matcher.group(1); 136 | url = url.replace(matcher.group(0), pathVarSeat(name)); 137 | start = matcher.end(); 138 | } while (matcher.find(start)); 139 | } 140 | } 141 | 142 | private boolean parseMapping() { 143 | RequestMapping req = method.getAnnotation(RequestMapping.class); 144 | if (req != null) { 145 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 146 | if (req.method().length > 0) { 147 | httpMethod = req.method()[0]; 148 | } 149 | return true; 150 | } 151 | return false; 152 | } 153 | 154 | private boolean parseGet() { 155 | GetMapping req = method.getAnnotation(GetMapping.class); 156 | if (req != null) { 157 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 158 | httpMethod = RequestMethod.GET; 159 | return true; 160 | } 161 | return false; 162 | } 163 | 164 | private boolean parsePost() { 165 | PostMapping req = method.getAnnotation(PostMapping.class); 166 | if (req != null) { 167 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 168 | httpMethod = RequestMethod.POST; 169 | return true; 170 | } 171 | return false; 172 | } 173 | 174 | private boolean parsePut() { 175 | PutMapping req = method.getAnnotation(PutMapping.class); 176 | if (req != null) { 177 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 178 | httpMethod = RequestMethod.PUT; 179 | return true; 180 | } 181 | return false; 182 | } 183 | 184 | private boolean parseDelete() { 185 | DeleteMapping req = method.getAnnotation(DeleteMapping.class); 186 | if (req != null) { 187 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 188 | httpMethod = RequestMethod.DELETE; 189 | return true; 190 | } 191 | return false; 192 | } 193 | 194 | private boolean parsePatch() { 195 | PatchMapping req = method.getAnnotation(PatchMapping.class); 196 | if (req != null) { 197 | parseMappingValue(req.value(), req.consumes(), req.produces(), req.headers()); 198 | httpMethod = RequestMethod.PATCH; 199 | return true; 200 | } 201 | return false; 202 | } 203 | 204 | private void parseMappingValue(String[] values, String[] consumes, String[] produces, 205 | String[] headers) { 206 | if (values.length > 0) { 207 | url = environment.resolvePlaceholders(values[0]); 208 | } else { 209 | url = Strings.EMPTY; 210 | } 211 | if (consumes.length > 0) { 212 | baseHeaders.put(HttpHeaders.CONTENT_TYPE, consumes[0]); 213 | } 214 | if (produces.length > 0) { 215 | baseHeaders.put(HttpHeaders.ACCEPT, 216 | StringUtils.arrayToDelimitedString(produces, Strings.COMMA)); 217 | } 218 | if (headers.length > 0) { 219 | for (String header : headers) { 220 | String[] qs = header.split(Strings.EQUAL); 221 | baseHeaders.put(qs[0], qs[1]); 222 | } 223 | } 224 | } 225 | 226 | private void parseParam() { 227 | responseType = method.getGenericReturnType(); 228 | Parameter[] parameters = method.getParameters(); 229 | for (int index = 0, len = parameters.length; index < len; index++) { 230 | handlerParamAnnotations(parameters[index], index); 231 | } 232 | } 233 | 234 | private void handlerParamAnnotations(Parameter parameter, int index) { 235 | RequestParam requestParam = parameter.getAnnotation(RequestParam.class); 236 | if (requestParam != null) { 237 | query.put(Strings.firstNonNull(requestParam.value(), parameter.getName()), index); 238 | return; 239 | } 240 | RequestHeader requestHeader = parameter.getAnnotation(RequestHeader.class); 241 | if (requestHeader != null) { 242 | headers.put(Strings.firstNonNull(requestHeader.value(), parameter.getName()), index); 243 | return; 244 | } 245 | if (parameter.isAnnotationPresent(RequestBody.class)) { 246 | bodyIndex = index; 247 | return; 248 | } 249 | PathVariable pathVariable = parameter.getAnnotation(PathVariable.class); 250 | if (pathVariable != null) { 251 | pathVars.put(Strings.firstNonNull(pathVariable.value(), parameter.getName()), index); 252 | return; 253 | } 254 | RequestPart requestPart = parameter.getAnnotation(RequestPart.class); 255 | if (requestPart != null) { 256 | multipart = true; 257 | query.put(Strings.firstNonNull(requestPart.value(), parameter.getName()), index); 258 | } 259 | } 260 | 261 | private HttpRequest buildRequest(FileCleaner cleaner, Object... args) { 262 | HttpRequest request = HttpRequest.url(buildRequestUrl(args)) 263 | .method(HttpMethod.valueOf(httpMethod.name())).httpRequestExecution(requestExecution) 264 | .interceptors(interceptors); 265 | 266 | baseHeaders.forEach(request::addHeader); 267 | headers.forEach((k, v) -> request.addHeader(k, MoreObjects.safeToString(args[v]))); 268 | String traceId = MDC.get(TRACE_ID); 269 | if (traceId != null) { 270 | request.addHeader(TRACE_ID, traceId); 271 | } 272 | 273 | Map qv = new HashMap<>(4); 274 | for (Map.Entry entry : query.entrySet()) { 275 | Object value = args[entry.getValue()]; 276 | if (value instanceof Map) { 277 | ((Map) value).forEach((k, v) -> qv.put(k.toString(), v)); 278 | } else { 279 | qv.put(entry.getKey(), value); 280 | } 281 | } 282 | 283 | buildRequestQuery(qv, cleaner, request); 284 | 285 | if (bodyIndex > -1) { 286 | Object body = args[bodyIndex]; 287 | if (body == null) { 288 | request.jsonBody("{}"); 289 | } else { 290 | request.jsonBody(JSON.toJSONString(body)); 291 | } 292 | } 293 | return request; 294 | } 295 | 296 | private String buildRequestUrl(Object... args) { 297 | String realUrl = url; 298 | if (pathVars != null) { 299 | for (Map.Entry entry : pathVars.entrySet()) { 300 | realUrl = realUrl 301 | .replace(pathVarSeat(entry.getKey()), MoreObjects.safeToString(args[entry.getValue()])); 302 | } 303 | } 304 | return realUrl; 305 | } 306 | 307 | private void buildRequestQuery(Map qv, FileCleaner cleaner, HttpRequest request) { 308 | if (qv.isEmpty()) { 309 | return; 310 | } 311 | if (multipart) { 312 | qv.forEach((k, v) -> this.handleMultipart(cleaner, request, k, v)); 313 | } else { 314 | Map qvs = new HashMap<>(4); 315 | qv.forEach((k, v) -> qvs.put(k, MoreObjects.safeToString(v))); 316 | if (request.getMethod() == HttpMethod.GET) { 317 | request.queryParam(qvs); 318 | } else { 319 | request.formBody(qvs); 320 | } 321 | } 322 | } 323 | 324 | private void handleMultipart(FileCleaner cleaner, HttpRequest request, String key, Object value) { 325 | if (value instanceof MultipartFile) { 326 | MultipartFile mf = (MultipartFile) value; 327 | File tempFile = new File(FileUtils.tempBaseDir(), 328 | SnowflakeId.defaultNextId() + mf.getOriginalFilename()); 329 | FileUtils.mkdirsForFile(tempFile); 330 | cleaner.track(tempFile); 331 | try { 332 | mf.transferTo(tempFile); 333 | } catch (IOException e) { 334 | throw Exceptions.wrap(e); 335 | } 336 | request.multipart(key, tempFile, mf.getOriginalFilename()); 337 | } else if (value instanceof File) { 338 | request.multipart(key, (File) value); 339 | } else { 340 | request.multipart(key, MoreObjects.safeToString(value)); 341 | } 342 | } 343 | 344 | private static String concatUrl(String parent, String child) { 345 | StringBuilder sb = new StringBuilder(); 346 | boolean endWithSlash = false; 347 | if (parent != null && parent.length() > 0) { 348 | sb.append(parent); 349 | if (!parent.endsWith(Strings.SLASH)) { 350 | sb.append(Strings.SLASH); 351 | } 352 | endWithSlash = true; 353 | } 354 | if (child != null) { 355 | if (endWithSlash && child.startsWith(Strings.SLASH)) { 356 | sb.append(child.substring(1)); 357 | } else { 358 | sb.append(child); 359 | } 360 | } 361 | return sb.toString(); 362 | } 363 | 364 | } --------------------------------------------------------------------------------