概述
虽然当前很多项目都以微服务为主,但也会存在调用外部接口的情况,如公司项目调用外部公司提供的接口。这种情况下,一般我们会使用httpclient进行远程接口调用,但能不能对httpclient进行封装,像Feign那样访问远程外部接口呢?本文就是模拟Feign,实现远程外部接口的访问。
本文和前面的《springboot(4.4)集成mybatis原理分析》有些类似,可以一起进行阅读参考,但本文功能更完善,可以直接复制代码进行修改后用于生产。
feignclient修饰的接口如下:
@FeignClient(name = "userService", url = "${user.service.url:}", path = "/user")
public interface UserService {
@PostMapping("getUser")
ApiResponse<UserVO> getUser(@RequestBody UserVO uservo);
}
要实现这个功能,我们需要准备如下几点:
- 扫描接口注解:扫描某些包下的类或接口,类似mybatis的MapperScan注解
- 定义接口注解:类似FeignClient注解
- 定义接口方法注解:类似PostMapping注解
- 用于访问外部远程接口的service,类似上述接口UserService
- 将自定义的service注入到spring中
扫描接口注解
扫描某些包下的类或接口,一般在main方法所属的类上进行注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HttpClientScannerRegistrar.class)
public @interface HttpClientScan {
String[] value() default {};
}
定义接口注解
类似FeignClient注解,表示这个接口将作为外部接口访问的service
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface HttpClient {
//定义远程访问接口的IP:端口
String url();
//其他信息,如私钥、token等,根据需要新增属性
String appKey();
String secret();
String name() defaul "defaul";
}
定义接口方法注解
类似PostMapping注解,修饰接口里的具体方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface HttpClientConfig {
//接口URI
String url();
//其他信息,比如请求方式post、get等,根据需要新增属性
}
用于访问外部远程接口的service
@HttpClient(url = "${user.service.url:}", appKey = "123456",secret = "123456")
public interface UserService {
@HttpClientConfig("/getUser")
ApiResponse<UserVO> getUser(@RequestBody UserVO uservo);
}
将自定义的service注入到spring中
这个步骤比较复杂,这里主要是模拟的Feign中的主要步骤实现的。
定义register类
/**
* 实现ImportBeanDefinitionRegistrar:向spring中手动注册bean,如本例中所有HttpClientScan扫描到的且被HttpClient修饰的接口
* 实现ResourceLoaderAware:获取资源加载器,加载相关资源
* 实现EnvironmentAware:配置项加载,使用environment可以手动获取配置中的具体数据,类似@Value注解一样
*/
public class HttpClientScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware,EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取HttpClientScan注解类的属性值
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(HttpClientScan.class.getName()));
List<String> basePackages = new ArrayList<String>();
for (String pkg : annoAttrs.getStringArray("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
try{
//扫描路径下的所有类和接口
Set<String> scan = scan(basePackages);
for(String s:scan){
//为service接口生成BeanDefinition,并将BeanDefinition保存到spring的beanDefinitionMap中,
// 这样在bean实例化的时候,会根据BeanDefinition实例化对应的bean
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HttpClientFactoryBean.class.getName());
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
Class clazz = Class.forName(s);
HttpClient annotation = (HttpClient)clazz.getAnnotation(HttpClient.class);
//过滤掉没有用HttpClient注解修饰的类或接口
if(annotation==null){
continue;
}
String appKey = annotation.appKey();
//获取appkey的值,并判断是否需要从配置文件中获取配置项,如果需要,则使用environment获取具体的配置值
if(StringUtils.hasText(appKey)&&appKey.indexOf("${")>=0&&appKey.indexOf("}")>=0){
appKey=this.environment.resolvePlaceholders(appKey);
}
String secret = annotation.secret();
//获取secret的值,并判断是否需要从配置文件中获取配置项,如果需要,则使用environment获取具体的配置值
if(StringUtils.hasText(secret)&&secret.indexOf("${")>=0&&secret.indexOf("}")>=0){
secret=this.environment.resolvePlaceholders(secret);
}
String url = annotation.url();
//获取url的值,并判断是否需要从配置文件中获取配置项,如果需要,则使用environment获取具体的配置值
if(StringUtils.hasText(url)&&url.indexOf("${")>=0&&url.indexOf("}")>=0){
url=this.environment.resolvePlaceholders(url);
}
builder.addPropertyValue("appKey", appKey);
builder.addPropertyValue("secret", secret);
builder.addPropertyValue("url", url);
builder.addPropertyValue("name", annotation.name());
//为构造函数指定参数
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz);
//向beanDefinitionMap注册beanDefinition
registry.registerBeanDefinition(clazz.getSimpleName().substring(0,1).toLowerCase()+clazz.getSimpleName().substring(1), beanDefinition);
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 扫描某些路径下的所有类或接口
* @param bases
* @return
* @throws Exception
*/
public Set<String> scan(List<String> bases) throws Exception{
Set<String> set = new HashSet<>();
MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
for(String base:bases) {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath(new StandardEnvironment().resolveRequiredPlaceholders(base)) + "/**.class";
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
for (Resource r : resources) {
MetadataReader reader = metaReader.getMetadataReader(r);
set.add(reader.getClassMetadata().getClassName());
}
}
return set;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
自定义factorybean
/**
* FactoryBean在spring中是用来生成bean的,每调用一次getObject方法,就会生成一个对象,并将其注入到spring中
*/
public class HttpClientFactoryBean implements FactoryBean {
private Class target;
private String url;
private String appKey;
private String secret;
private String name;
private HttpClientFactoryBean(Class target){
this.target = target;
}
@Override
public Object getObject() throws Exception {
HttpClientProxy myProxy = new HttpClientProxy(target,url,appKey,secret,name);
return myProxy.getObject();
}
@Override
public Class<?> getObjectType() {
return target;
}
@Override
public boolean isSingleton() {
return true;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getAppKey() {
return appKey;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
动态代理
我们自定义的service都是接口,接口是必须要有实现类的,否则就没有意义,而我们并没有为service类写实现类,那是因为我们需要一个动态代理类来做所有的事,具体代码如下:
/**
* 利用jdk提供的动态代理来实现相关功能
*/
public class HttpClientProxy implements InvocationHandler {
private Class target;
private String url;
private String appKey;
private String secret;
private String name;
public HttpClientProxy(Class target,String url,String appKey,String secret,String name){
this.target= target;
this.url= url;
this.appKey= appKey;
this.secret= secret;
this.name= name;
}
/**
* 方便获取代理对象
* @return
*/
public Object getObject(){
return Proxy.newProxyInstance(target.getClassLoader(),new Class[]{target},this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取HttpClientConfig,如果没有这个注解,表示这个方法不是用于远程接口访问的
HttpClientConfig httpClientConfig = method.getAnnotation(HttpClientConfig.class);
if(httpClientConfig==null){
return method.invoke(target.newInstance(),args);
}
//根据名称获取对应的远程处理器
RemoteHandler remoteHandler = AbstractRemoteHandler.remoteHandlers.get(this.name);
if(remoteHandler==null){
throw new RuntimeException("没有对应的处理器");
}
//调远程处理器的处理方法
return remoteHandler.remoteHandler(proxy, method, args, this, httpClientConfig);
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getAppKey() {
return appKey;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
远程调用处理器
要调用远程接口,一般来说需要如下几步:
- 组装请求参数
- 组装请求header(非必须)
- 发送远程请求
- 处理返回结果
因此我们定义一个接口来完事上述几件事,RemoteHandler
public interface RemoteHandler {
/**
* 处理远程接口
*/
Object remoteHandler(Object proxy, Method method, Object[] args,HttpClientProxy httpClientProxy,HttpClientConfig httpClientConfig);
/**
* 准备请求参数
*/
String preRequest(Object proxy, Method method, Object[] args,HttpClientProxy httpClientProxy,HttpClientConfig httpClientConfig);
/**
* 处理返回接口
*/
Object postRequest(String remoteResult,Object proxy, Method method, Object[] args,HttpClientProxy httpClientProxy,HttpClientConfig httpClientConfig);
/**
* 处理请求header
*/
Map<String,String> preRequestHeader(Object proxy, Method method, Object[] args,HttpClientProxy httpClientProxy,HttpClientConfig httpClientConfig);
/**
* 外部接口名称,因为不同公司的外部接口 对请求参数和返回接口的编解码都是不一样的
*/
String getName();
}
为接口写一个抽象的类AbstractRemoteHandler:
/**
* SmartInitializingSingleton:在spring所有bean的注入完成后才回调这个接口提供的方法,即将本类注册到remoteHandlers中,供HttpClientProxy调用
*/
public abstract class AbstractRemoteHandler implements RemoteHandler,SmartInitializingSingleton {
public static final Map<String,RemoteHandler> remoteHandlers = new ConcurrentHashMap<>();
/**
* 模板方法,剩余的钩子方法让子类实现
*/
public Object remoteHandler(Object proxy, Method method, Object[] args, HttpClientProxy httpClientProxy, HttpClientConfig httpClientConfig) {
String remoteRequest = preRequest(proxy, method, args, httpClientProxy, httpClientConfig);
Map<String,String> headers = preRequestHeader(proxy, method, args, httpClientProxy, httpClientConfig);
String remoteResult = MyHttpClientUtil.post(httpClientProxy.getUrl()+httpClientConfig.url(), remoteRequest,headers);
Object result = postRequest(remoteResult, proxy, method, args, httpClientProxy, httpClientConfig);
return result;
}
@Override
public void afterSingletonsInstantiated() {
remoteHandlers.put(getName(), this);
}
}
为抽象类写一个默认的实现类DefaulRemoteHandler:
@Component("defaulRemoteHandler")
public class DefaulRemoteHandler extends AbstractRemoteHandler {
/**
* 请求前准备参数 假设远程接口的请求参数为json
*/
public String preRequest(Object proxy, Method method, Object[] args, HttpClientProxy httpClientProxy, HttpClientConfig httpClientConfig) {
String remoteRequest = null;
if(args.length==1){
remoteRequest = JSONObject.toJSONString(args[0]);
}else{
remoteRequest = JSONObject.toJSONString(args);
}
return remoteRequest;
}
/**
* 处理返回结果 假设远程接口的返回参数为json
*/
@Override
public Object postRequest(String remoteResult,Object proxy, Method method, Object[] args, HttpClientProxy httpClientProxy, HttpClientConfig httpClientConfig) {
Object result = JSONObject.parseObject(remoteResult, method.getReturnType());
return result;
}
/**
* 处理请求header 假设header需要appkey
*/
@Override
public Map<String, String> preRequestHeader(Object proxy, Method method, Object[] args, HttpClientProxy httpClientProxy, HttpClientConfig httpClientConfig) {
Map<String,String> headers = new HashMap<>();
headers.put("appKey", httpClientProxy.getAppKey());
return headers;
}
@Override
public String getName() {
return "defaul";
}
}
编写httpclient util进行远程接口访问
public class MyHttpClientUtil {
private static RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(100000)
.setConnectionRequestTimeout(100000).setSocketTimeout(100000).build();
public static String post(String url, String stringParam,Map<String,String> headers) {
HttpPost httpPost = new HttpPost(url);
StringEntity entity = new StringEntity(stringParam, "utf-8");
entity.setContentEncoding("utf-8");
entity.setContentType("application/json;charset=UTF-8");
httpPost.setEntity(entity);
if(headers != null){
Iterator<String> iterator = headers.keySet().iterator();
while(iterator.hasNext()){
String key = iterator.next();
String value = headers.get(key);
httpPost.addHeader(key,value);
}
}
return send(httpPost);
}
public static String send(HttpRequestBase request) {
request.setConfig(requestConfig);
request.addHeader("Accept", "application/json");
try {
CloseableHttpResponse response = acceptsUntrustedCertsHttpClient().execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode==200){
HttpEntity httpEntity = response.getEntity();
String responseBody = EntityUtils.toString(httpEntity, "utf-8");
return responseBody;
}else {
throw new RuntimeException("调用接口"+request+"失败,返回码为:"+statusCode);
}
}catch (Exception e) {
throw new RuntimeException("调用接口"+request+"失败",e);
}
}
public static CloseableHttpClient acceptsUntrustedCertsHttpClient()
throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
HttpClientBuilder b = HttpClientBuilder.create();
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
b.setSSLContext(sslContext);
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslSocketFactory)
.build();
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
connMgr.setMaxTotal(200);
connMgr.setDefaultMaxPerRoute(100);
b.setConnectionManager(connMgr);
CloseableHttpClient client = b.build();
return client;
}
}
启动类如下:
@SpringCloudApplication
@ComponentScan(basePackages = {"com.xx"})
@EnableFeignClients(basePackages = {"com.xx.api"})
@HttpClientScan(value = {"com.xx.service"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
总结
按照上述步骤,我们就利用httpclient实现了一个远程访问接口的仿feign框架,当然这只是一个基础版,可以在此基础上进行无线扩展,以满足需求。