zoukankan      html  css  js  c++  java
  • 如何通过HTTP优雅调用第三方-Feign

    Java常用HTTP客户端

    1. Java原生HttpURLConnection

    2. Apache HttpClient

    3. OkHttp

    4. Spring RestTemplate

    示例

    public interface Client {
        /**
         * 
         * @param body
         * @return
         */
        Response post(Object body) throws Exception;
    }
    
    public class ApacheHttpClient implements Client {
    
        private static final String DEFAULT_CONTENT_TYPE_VALUE = "application/json";
    
        private static final String DEFAULT_CHARSET_NAME = "UTF-8";
    
        private String url;
    
        private final HttpClient client;
    
        private JsonSerializer jsonSerializer;
    
        /**
         * 
         * @param url
         */
        public ApacheHttpClient(String url) {
            this(url, HttpClientBuilder.create().build());
        }
    
        /**
         * 
         * @param url
         * @param client
         */
        public ApacheHttpClient(String url, HttpClient client) {
            this(url, client, new JacksonJsonSerializer());
    
        }
    
        /**
         * 
         * @param url
         * @param client
         * @param jsonSerializer
         */
        public ApacheHttpClient(String url, HttpClient client, JsonSerializer jsonSerializer) {
            if (StringUtils.isBlank(url)) {
                throw new IllegalArgumentException("Url can't be null!");
            }
            if (Objects.isNull(client)) {
                throw new IllegalArgumentException("client can't be null!");
            }
            if (Objects.isNull(jsonSerializer)) {
                throw new IllegalArgumentException("jsonSerializer can't be null!");
            }
            this.url = url;
            this.client = client;
            this.jsonSerializer = jsonSerializer;
        }
    
        @Override
        public Response post(Object body) throws Exception {
            if (Objects.isNull(body)) {
                return Response.NULL_BODY_RESPONSE;
            }
            if (body instanceof Collection && ((Collection<?>) body).size() <= 0) {
                return Response.EMPTY_BODY_RESPONSE;
            }
            HttpPost post = new HttpPost(url);
            String jsonString = jsonSerializer.serialize(body);
            StringEntity entity = new StringEntity(jsonString, DEFAULT_CHARSET_NAME);
            entity.setContentType(DEFAULT_CONTENT_TYPE_VALUE);
            post.setEntity(entity);
            HttpResponse httpResponse = client.execute(post);
            HttpEntity httpEntity = httpResponse.getEntity();
            String result = EntityUtils.toString(httpEntity, "UTF-8");
            Response response = new Response(httpResponse.getStatusLine().getStatusCode(),
                    httpResponse.getStatusLine().getReasonPhrase(), result);
            httpResponse.getEntity().getContent().close();
            return response;
        }
    
    }
    

    存在问题

    • 面向方法
    • 耦合HTTP客户端
    • 手动处理请求响应
    • 无法通用,重复编码

    面向接口实现HTTP调用

    HTTP请求和响应的组成

    • 请求:请求行、请求头、请求体

    • 响应:响应行、响应头、响应体

    从访问百度主页开始

    声明接口

    package com.zby.service;
    
    public interface BaiduClient {
    
        @RequestLine(url = "http://www.baidu.com", method = "GET")
        String index();
    
    }
    

    声明注解

    package com.zby.annotation;
    
    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RequestLine {
    
        String method();
    
        String url();
    
    }
    
    
    • 如果我能通过注入BaiduClient接口的方式调用第三方,并且新增第三方时,只需要新增类似接口,与面向过程的方式相比,是否更优雅?

    • 要实现注入BaiduClient接口,那么必须解析RequestLine注解,并且生成BaiduClient对象

    HOW?

    • 代理模式
    • 模版方法模式
    • 工厂模式

    声明工厂类

    package com.zby.proxy;
    
    /**
     * @author zby
     * @title HTTP代理工厂
     * @date 2020年6月16日
     * @description
     */
    public interface HttpProxyFactory {
        /**
         * 获取HTTP代理对象
         * 
         * @param clazz
         * @return
         */
        <T> T getProxy(Class<T> clazz);
    
    }
    
    

    抽象工厂,处理注解

    package com.zby.proxy;
    
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    import com.zby.annotation.RequestLine;
    
    /**
     * @author zby
     * @title 抽象代理工厂
     * @date 2020年6月16日
     * @description
     */
    public abstract class AbstractHttpProxyFactory implements HttpProxyFactory {
    
        private Map<Class<?>, Map<Method, RequestLine>> classMethodCacheMap = new ConcurrentHashMap<>();
    
        @Override
        public <T> T getProxy(Class<T> clazz) {
            if (!clazz.isInterface()) {
                throw new IllegalArgumentException();
            }
            Map<Method, RequestLine> methodCacheMap = classMethodCacheMap.get(clazz);
            if (methodCacheMap == null) {
                methodCacheMap = new HashMap<>();
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods) {
                    RequestLine requestLine = method.getDeclaredAnnotation(RequestLine.class);
                    if (requestLine == null) {
                        throw new IllegalArgumentException(method.getName());
                    }
                    methodCacheMap.put(method, requestLine);
                }
                classMethodCacheMap.putIfAbsent(clazz, methodCacheMap);
            }
            return createProxy(clazz, methodCacheMap);
        }
    
        /**
         * 创建代理对象
         * 
         * @param clazz
         * @param methodCacheMap
         * @return
         */
        protected abstract <T> T createProxy(Class<T> clazz, Map<Method, RequestLine> methodCacheMap);
    
    
    }
    
    

    基于代理和HttpURLConnection实现

    package com.zby.proxy;
    
    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.net.URLConnection;
    import java.util.Map;
    
    import com.zby.annotation.RequestLine;
    
    /**
     * @author zby
     * @title HttpURLConnectionProxyFactory
     * @date 2020年6月16日
     * @description
     */
    public class HttpURLConnectionProxyFactory extends AbstractHttpProxyFactory {
    
        @Override
        @SuppressWarnings("unchecked")
        protected <T> T createProxy(Class<T> clazz, Map<Method, RequestLine> methodCacheMap) {
            return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, new InvocationHandler() {
    
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    RequestLine requestLine = methodCacheMap.get(method);
                    if (requestLine == null) {
                        throw new IllegalArgumentException();
                    }
                    URL url = new URL(requestLine.url());
                    URLConnection rulConnection = url.openConnection();
                    HttpURLConnection httpUrlConnection = (HttpURLConnection) rulConnection;
                    httpUrlConnection.setRequestMethod(requestLine.method());
                    httpUrlConnection.connect();
                    InputStream inputStream = httpUrlConnection.getInputStream();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    StringBuilder stringBuilder = new StringBuilder();
                    String newLine = null;
                    while ((newLine = bufferedReader.readLine()) != null) {
                        stringBuilder.append(newLine);
    
                    }
                    return stringBuilder.toString();
                }
            });
        }
    }
    
    

    拉出来溜溜

    package com.zby;
    
    import com.zby.proxy.HttpProxyFactory;
    import com.zby.proxy.HttpURLConnectionProxyFactory;
    import com.zby.service.BaiduClient;
    
    /**
     * @author zby
     * @title HttpProxyDemo
     * @date 2020年6月16日
     * @description
     */
    public class HttpProxyDemo {
    
        public static void main(String[] args) {
            HttpProxyFactory httpProxyFactory = new HttpURLConnectionProxyFactory();
            BaiduClient baiduClient = httpProxyFactory.getProxy(BaiduClient.class);
            System.out.println(baiduClient.index());
        }
    
    }
    
    
    <!DOCTYPE html><!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
    

    面向接口的好处

    • 新增第三方不再是增加方法,而是增加接口
    • 面向接口编程而不是面向方法编程
    • 无需每次自己处理请求响应
    • 随时更换具体实现HttpURLConnectionProxyFactory而不影响原有代码

    不要重复造轮子?

    • NO!是不要重复造低级轮子。
    • 如果你的轮子比别人好,那么还是可以尝试一下干掉别人的轮子。
    • 比如SpringMVC干掉了Strust,Mybatis干掉了Hibernate

    优化

    • 架构设计
    • 插件式设计
    • 客户端抽象
    • 编解码抽象
    • 注解支持
    • 重试支持
    • 第三方适配
    • Spring整合
    • SpringMVC注解支持
    • 负载均衡支持
    • 等等等等

    轮子篇-Feign

    • Feign github地址

    • Feign makes writing java http clients easier

    • 简单来说,就是一个基于接口的HTTP代理生成器

    先睹为快

    		<dependency>
    			<groupId>io.github.openfeign</groupId>
    			<artifactId>feign-core</artifactId>
    			<version>10.2.0</version>
    		</dependency>
    
    package com.zby.feign;
    
    import feign.Feign;
    import feign.Logger;
    import feign.Logger.ErrorLogger;
    import feign.RequestLine;
    
    public class FeignMain {
    
        public static void main(String[] args) {
            Baidu baidu = Feign.builder()
                    .logger(new ErrorLogger())
                    .logLevel(Logger.Level.FULL)
                    .target(Baidu.class, "http://www.baidu.com");
            System.out.println(baidu.index());
        }
    
    }
    
    
    interface Baidu {
    
        @RequestLine(value = "GET /")
        String index();
    
    }
    
    
    [Baidu#index] ---> GET http://www.baidu.com/ HTTP/1.1
    [Baidu#index] ---> END HTTP (0-byte body)
    [Baidu#index] <--- HTTP/1.1 200 OK (168ms)
    [Baidu#index] content-length: 2381
    [Baidu#index] content-type: text/html
    [Baidu#index] date: Tue, 16 Jun 2020 06:50:21 GMT
    [Baidu#index] server: bfe
    [Baidu#index] 
    [Baidu#index] 【省略内容。。。。。。】
    
    [Baidu#index] <--- END HTTP (2381-byte body)
    【省略内容。。。。。。】
    
    
    

    插件式设计

    • 默认使用HttpURLConnection作为HTTP客户端,如果需要修改为HttpClient需要干啥?
    		<dependency>
    			<groupId>io.github.openfeign</groupId>
    			<artifactId>feign-httpclient</artifactId>
    			<version>10.2.0</version>
    		</dependency>
    
    package com.zby.feign;
    
    import feign.Feign;
    import feign.Logger;
    import feign.Logger.ErrorLogger;
    import feign.RequestLine;
    import feign.httpclient.ApacheHttpClient;
    
    public class FeignMain {
    
        public static void main(String[] args) {
            Baidu baidu = Feign.builder()
                    .logger(new ErrorLogger())
                    .logLevel(Logger.Level.FULL)
                    .client(new ApacheHttpClient())
                    .target(Baidu.class, "http://www.baidu.com");
            System.out.println(baidu.index());
        }
    
    }
    
    
    interface Baidu {
    
        @RequestLine(value = "GET /")
        String index();
    
    }
    
    
    • So Easy

    其他组件

    • feign.Feign.Builder
        private final List<RequestInterceptor> requestInterceptors =
            new ArrayList<RequestInterceptor>();
        private Logger.Level logLevel = Logger.Level.NONE;
        private Contract contract = new Contract.Default();
        private Client client = new Client.Default(null, null);
        private Retryer retryer = new Retryer.Default();
        private Logger logger = new NoOpLogger();
        private Encoder encoder = new Encoder.Default();
        private Decoder decoder = new Decoder.Default();
        private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default();
        private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
        private Options options = new Options();
        private InvocationHandlerFactory invocationHandlerFactory =
            new InvocationHandlerFactory.Default();
        private boolean decode404;
        private boolean closeAfterDecode = true;
        private ExceptionPropagationPolicy propagationPolicy = NONE;
    

    注解解析:契约

    实战篇-对接GIO

    一、定义接口

    /**
     * 获取事件分析数据,文档地址:
     * https://docs.growingio.com/docs/developer-manual/api-reference/statistics-api/definition/getevent
     * 
     * @author zby
     * @Date 2020年4月20日
     *
     */
    public interface EventAnalysisClient {
        /**
         * 获取事件分析数据
         * 
         * @param projectUid 项目ID
         * @param chartId 图表ID
         * @param eventAnalysisRequest 参数
         * @return
         */
        @RequestLine("GET /v2/projects/{project_uid}/charts/{chart_id}.json")
        EventAnalysisResponse getEvent(@Param("project_uid") String projectUid, @Param("chart_id") String chartId,
                @QueryMap EventAnalysisRequest eventAnalysisRequest);
    
    }
    
    

    二、Feign抽象配置

    /**
     * GIOFeign抽象配置
     * 
     * @author zby
     * @Date 2020年4月21日
     *
     */
    public class AbstractGIOFeignConfiguration {
        /**
         * GIO官网地址
         */
        @Value("${gio.url}")
        private String gioUrl;
        /**
         * 认证信息
         */
        @Value("${gio.authorization}")
        private String authorization;
    
        /**
         * 创建Feign代理对象
         * 
         * @param clazz
         * @return
         */
        protected <T> T newInstance(Class<T> clazz) {
            return feign().newInstance(new Target.HardCodedTarget<T>(clazz, gioUrl));
        }
    
        /**
         * 公用的Feign对象
         * 
         * @return
         */
        @SuppressWarnings("deprecation")
        private Feign feign() {
            return Feign.builder()
                    .logger(new Slf4jLogger())
                    .logLevel(Level.BASIC)
                    .encoder(new JacksonEncoder())
                    .decoder(new JacksonDecoder())
                    .requestInterceptor(new AuthorizationInterceptor())
                    // 处理null
                    .queryMapEncoder(new QueryMapEncoder.Default() {
                        @Override
                        public Map<String, Object> encode(Object object) throws EncodeException {
                            if (object == null) {
                                return new HashMap<String, Object>();
                            }
                            return super.encode(object);
                        }
                    })
                    .build();
        }
    
        /**
         * GIO认证信息拦截器
         * 
         * @author zby
         * @Date 2020年4月20日
         *
         */
        class AuthorizationInterceptor implements RequestInterceptor {
            @Override
            public void apply(RequestTemplate template) {
                template.header("Authorization", authorization);
            }
        }
    
    }
    
    

    三、注入IOC容器

    /**
     * GIO的feig客户端配置类
     * 
     * @author zby
     * @Date 2020年4月21日
     *
     */
    @Configuration
    public class GIOFeignConfiguration extends AbstractGIOFeignConfiguration {
    
        /**
         * 创建事件分析Feign客户端,代优化:定义注解FeignClient直接扫描创建实例放到spring容器
         * 
         * @return
         */
        @Bean
        public EventAnalysisClient eventAnalysisClient() {
            return newInstance(EventAnalysisClient.class);
        }
    
    }
    
    

    四、使用

    /**
     * 事件分析GIO客户端实现
     * 
     * @author zby
     * @Date 2020年4月21日
     *
     */
    @Service
    public class EventAnalysisGIOClientImpl implements EventAnalysisGIOClient {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(EventAnalysisGIOClientImpl.class);
    
        @Autowired
        private EventAnalysisClient eventAnalysisClient;
    
        @Override
        public List<NewsListEventAnalysisDto> getNewsListData(EventAnalysisRequest eventAnalysisRequest) {
            List<NewsListEventAnalysisDto> list;
            try {
                LOGGER.info("获取GIO新闻列表数据参数:eventAnalysisRequest={}", eventAnalysisRequest);
                EventAnalysisResponse eventAnalysisResponse = eventAnalysisClient.getEvent(GIOConstant.PROJECT_UID,
                        EventAnalysisConstant.CHART_ID_NEWS_LIST, eventAnalysisRequest);
                LOGGER.info("获取GIO新闻列表数据结果:{}", eventAnalysisResponse);
                list = EventAnalysisUtil.parse(eventAnalysisResponse,
                        new AbstractGIOFieldObjectFactory<NewsListEventAnalysisDto>() {});
            } catch (Exception e) {
                LOGGER.error(String.format("获取GIO新闻列表数据出错,请求参数:%s", eventAnalysisRequest), e);
                return null;
            }
            return list;
        }
    }
    

    赠送篇-结果封装

    • GIO数据结果通用格式
    • 基于注解、反射、工厂模式、反省、BeanWrapper等封装通用数据格式为业务对象
    /**
     * 事件分析响应实体
     * 
     * @author zby
     * @Date 2020年4月20日
     *
     */
    @Data
    public class EventAnalysisResponse {
        /**
         * Chart Uid
         */
    	private String id;
        /**
         * Chart Name
         */
    	private String name;
        /**
         * 同参数
         */
    	private Long startTime;
        /**
         * 同参数
         */
    	private Long endTime;
        /**
         * 同参数
         */
    	private Long interval;
        /**
         * 表格头元数据
         */
    	private Meta[] meta;
        /**
         * 表格数据
         */
    	private String[][] data;
    
        /**
         * 元数据,标题列
         * 
         * @author zby
         * @Date 2020年4月21日
         *
         */
    	@Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Meta {
            /**
             * 名称
             */
    		private String name;
            /**
             * 方面
             */
    		private boolean dimension;
            /**
             * 统计数据
             */
            private boolean metric;
    	}
    }
    

    Q&A

  • 相关阅读:
    软件测试人员的年终绩效考核怎么应对
    收藏
    顶踩组件 前后两版
    订阅组件
    hdu 1963 Investment 完全背包
    hdu 4939 Stupid Tower Defense 动态规划
    hdu 4405 Aeroplane chess 动态规划
    cf 414B Mashmokh and ACM 动态规划
    BUPT 202 Chocolate Machine 动态规划
    hdu 3853 LOOPS 动态规划
  • 原文地址:https://www.cnblogs.com/zby9527/p/13141381.html
Copyright © 2011-2022 走看看