前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OpenFeign的定制

OpenFeign的定制

原创
作者头像
eeaters
修改2022-02-21 18:04:32
1.2K0
修改2022-02-21 18:04:32
举报
文章被收录于专栏:阿杰阿杰

feign定制使用

  • 项目背景
  • 基本模式和流程
  • 引入OpenFeign
  • Feign的定制
    • Encoder(加签)
    • RequestInterceptor(Header传递)
    • Decoder(统一解码)

项目背景

公司原先的模式是给客户提供统一的功能; 但是需求这种事情无法满足所有客户的需求; 因为各行各业都很卷,客户感觉自己没有被重视,客户会撂挑子不干的呀,因此公司换了一种玩法: 功能对外开放, 客户你不是觉得你提的需求简单嘛, you can you up, no can no bibi;

基本模式和流程

就是将现在的业务能力提供出去,对外暴露一个maven的依赖,客户的开发人员引入依赖就可以拥有默认的业务功能;

基本流程为: 前端调用(可以定制) → 客户服务(作为后端,主要面向一层) → 调用中台网关(新增的一层网关) → 内部服务

引入OpenFeign

  • 公司内部服务于服务之间的调用使用的是OpenFeign
  • 使用OpenFeign时代码足够简洁
  • 公司开发人员使用OpenFeign贼溜(虽然他们不知道contextId是什么,碰到两个服务名相同的FeignClient就束手无策)

可能是基于上面的原因,也可能是项目时间紧,拍板子的人随便拍一下脑袋就决定了,那么使用OpenFeign这个事情就愉快的定下来了;

Feign的定制

但是使用过程中碰到了一些问题,因为以前对OpenFeign也有一定的了解,所以解决了使用过程中碰到的一些OpenFeign的问题;简单记录一下;

Encoder(加签)

接口对外暴露那么肯定会加签, 对接第三方的时候都需要有密钥/token之类的; 都是尽可能的辨识出调用方,防止被人恶意攻击

使用目的

使用FeignClient时,参数在调用的时候需要包一层,直接那实体类来看

代码语言:javascript
复制
public class AppBaseRequest {
    private String ver;
    private String partnerId;
    private String appId;
    //你以为你只是传的对象实际上只是传递对象内的一个属性字段
    private String requestBody;
    private String sign;
}
解决方案

由于头一次碰到传参的时候把参数改的体无完肤的,因此debug时取了个巧,可能不是好的方案,但是运行正常

代码语言:javascript
复制
@Configuration
@EnableConfigurationProperties(SignatureProperties.class)
public class AppletAutoConfiguration {

    @Bean
    public EdenSignature edenSignature(SignatureProperties signatureProperties) {
        return new DefaultEdenSignature(signatureProperties);
    }
    
    //默认的SpringEncoder注入是有ConditionOnMissionBean; 有了这个默认的就没了
    @Bean
    public AppletBossEncoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters, EdenSignature edenSignature){
        return new AppletBossEncoder(messageConverters, edenSignature);
    }
}



public class AppletBossEncoder extends SpringEncoder {
​
    //加签抽了出来;如果RestTemplate调用也可以使用到
    private final EdenSignature edenSignature;
​
    public AppletBossEncoder(ObjectFactory<HttpMessageConverters> messageConverters,
                             EdenSignature edenSignature) {
        super(messageConverters);
        this.edenSignature = edenSignature;
    }
​
    @Override
    public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
        if (!AppBaseRequest.class.getName().equals(bodyType.getTypeName())) {
            //requestBody原先的类型是 T ; 加签后就一定成了AppBaseRequest
            requestBody = edenSignature.sign(requestBody);
            bodyType = requestBody.getClass();
        }
        super.encode(requestBody, bodyType, request);
    }
}

RequestInterceptor(Header传递)

由于原先前端很多信息是放到header里面传递的; 多了一层服务,那么header就可能丢失; 因为ServletRequest是有存放在一个本地线程变量中; 那么我们就取出来往后面传就行了

代码很简单; 但是有一个细节需要了解下: Feign在初始化的时候首先找自己的上下文的Config ; 如果有则使用自己的,如果没有就招parent上下文的(父上下文就是全局的配置) ; 那么下面的代码就一定要放在一个全局的配置中; 即: 这个Bean所在的类上需要标记@Configuration

代码语言:javascript
复制
    @Bean
    public RequestInterceptor headerPassInterceptor() {
        return requestTemplate -> {
            HttpServletRequest servletRequest = RequestUtils.currentServletRequest();
            //允许返回null ; 防止本次feign调用并不是前端传递过来的; 比如跑一个定时任务,根本就没有上下文的ServletRequest
            if (servletRequest != null) {
                Enumeration<String> headerNames = servletRequest.getHeaderNames();
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    if (!EXCLUDE_HEADER.contains(headerName)) {
                        Enumeration<String> headValue = servletRequest.getHeaders(headerName);
                        requestTemplate.header(headerName, Collections.list(headValue));
                    }
                }
            }
        };
    }

Decoder(统一解码)

这个是以前项目就碰到过的一个问题;我们对接了有十几个服务,他们的返回信息很多都不一样; 比如

  • 错误码有code,statusCode,status;
  • 返回实体类有result,data
  • 返回错误消息有message,有errorMessage

为此,

  • 老的项目每次执行远程调用就要在业务代码里面判断成功了嘛? 成功了再取出实体类,
  • 新的项目抽了一层,包名各有不同,就是为了处理这个判断,只返回成功的实体对象

叔可忍,婶婶不能忍; 刚好看过解码的代码,改一下试试:

希望的效果

对接了很多的服务; 但是我都可以使用一个Result<T>接受,使用起来岂不更舒服.(实际一写代码,发现还可以更牛逼一点)

统一的代码抽离

因为每一个FeignClient都可能有不同的返回值,因此一个Decoder走不遍填下

代码语言:javascript
复制
public abstract class AbstractCustomDecoder<T> extends Decoder.Default {
​
    protected static final String DEFAULT_SUCCESS_CODE = "200";
​
    private final Class<T> tClass;
​
    protected AbstractCustomDecoder() {
        tClass = retrieveAnnotationType();
        if (tClass == null || ResponseEntity.class.equals(tClass)) {
            throw new EdenAbstractException();
        }
    }
​
​
    @Override
    public Object decode(Response response, Type type) throws IOException {
        return customDecoder(response, type);
    }
​
    protected Object customDecoder(Response response, Type type) throws IOException {
        Class<?> rawClass = ResolvableType.forType(type).getRawClass();
        String body = Util.toString(response.body().asReader());
​
        T obj = JSONObject.parseObject(body, tClass);
        if (rawClass.equals(tClass)) {
            return obj;
        }
​
        Object data = getData(obj).get();
        if (type instanceof ParameterizedTypeImpl) {
            ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) type;
            if (parameterizedType.getActualTypeArguments() != null) {
                Type typeArgument = parameterizedType.getActualTypeArguments()[0];
                data = JSONObject.parseObject(JSONObject.toJSONString(getData(obj).get()), typeArgument);
            }
        }
        if (String.class.equals(type)) {
            return String.valueOf(data);
        }
​
        String code = getCode(obj).get();
        String message = getMessage(obj).get();
        Boolean status = getStatus(obj).get();
​
        return convertResult(rawClass, message, data, status, code);
​
    }
​
    protected Object convertResult(Class<?> rawClass, String message, Object data, Boolean status,String code) {
        if (Result.class.equals(rawClass)) {
            return new Result<>(status, message, data, code);
        }
        return JSONObject.parseObject(JSONObject.toJSONString(data), rawClass);
    }
​
​
​
    protected abstract Supplier<Object> getData(T obj);
    protected abstract Supplier<String> getCode(T obj);
    protected abstract Supplier<String> getMessage(T obj);
​
    protected Supplier<Boolean> getStatus(T obj) {
        return () -> {
            String code = getCode(obj).get();
            return DEFAULT_SUCCESS_CODE.equals(code);
        };
    }
​
    /**
     * 检索泛型对应的实际类型
     * @return
     */
    private Class<T> retrieveAnnotationType(){
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) {
                if (actualTypeArgument instanceof Class) {
                    return (Class) actualTypeArgument;
                }
            }
        }
        return null;
    }
​
}
Demo的config

一定要注意,这里的方法上有@Bean;但是类上并没有@Configuration; 这样子就谁用谁配置

代码语言:javascript
复制
public class CustomDecoderConfig {
​
​
    @Data
    public static class BaiduResult<T>{
        private String status;
        private T result;
    }
​
​
    @Bean
    public Decoder decoder() {
        return new AbstractCustomDecoder<BaiduResult>() {
            @Override
            protected Supplier<Object> getData(BaiduResult obj) {
                return obj::getResult;
            }
​
            @Override
            protected Supplier<String> getCode(BaiduResult obj) {
                return obj::getStatus;
            }
​
            @Override
            protected Supplier<String> getMessage(BaiduResult obj) {
                return () -> JSONObject.toJSONString(obj.getResult());
            }
​
            @Override
            protected Supplier<Boolean> getStatus(BaiduResult obj) {
                return () -> "OK".equals(obj.getStatus());
            }
​
            @Override
            protected Object convertResult(Class<?> rawClass, String message, Object data, Boolean status, String code) {
                if (BaiduResult.class.equals(rawClass)) {
                    BaiduResult cityResponse = new BaiduResult();
                    cityResponse.setStatus(code);
                    cityResponse.setResult(data);
                    return cityResponse;
                }
                return super.convertResult(rawClass, message, data, status, code);
            }
        };
    }
}
效果展示
代码语言:javascript
复制
@EnableFeignClients(clients = AlaBossDecoderExample.BaiduLbsClient.class)
public class AlaBossDecoderExample {
​
    @FeignClient(contextId = "demo.baiduLbs",
            name = "baiduLbs",
            url = "http://api.map.baidu.com",
            configuration = CustomDecoderConfig.class)
    public interface BaiduLbsClient {
        /**
         * 原生写法
         */
        @GetMapping("/geocoder")
        CustomDecoderConfig.BaiduResult<LocationInfo>  resultGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
​
        /**
         * String 接受
         */
        @GetMapping("/geocoder")
        String stringGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
​
        /**
         * 只接受Data内部需要的实体对象
         */
        @GetMapping("/geocoder")
        LocationInfo dataGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
​
        /**
         * 标准接受方式
         */
        @GetMapping("/geocoder")
        Result<LocationInfo> standardGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
    }
​
​
​
    @Test
    public void test() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AlaBossDecoderExample.class,
                FeignAutoConfiguration.class,
                HttpMessageConvertersAutoConfiguration.class
        );
        context.refresh();
​
        AlaBossDecoderExample.BaiduLbsClient lbsClient = context.getBean(AlaBossDecoderExample.BaiduLbsClient.class);
        String rest = lbsClient.stringGeocoder("39.983424,116.322987", "json");
        System.out.println("rest = " + rest);
​
        LocationInfo json = lbsClient.dataGeocoder("39.983424,116.322987", "json");
        System.out.println("json = " + json);
​
        CustomDecoderConfig.BaiduResult<LocationInfo> json1 = lbsClient.resultGeocoder("39.983424,116.322987", "json");
        System.out.println("json1 = " + json1);
​
        Result<LocationInfo> json2 = lbsClient.standardGeocoder("39.983424,116.322987", "json");
        System.out.println("json2 = " + ToStringBuilder.reflectionToString(json2));
​
        context.close();
    }
}
展示下效果

实际超出了预期,被玩出新玩法了,随便接受….

  • 可以实体接受
  • 原先的数据格式可以接受
  • 指定的数据格式可以寄售
  • String也可以接受
代码语言:javascript
复制
rest = {"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"near"}}
​
json = AlaBossDecoderExample.LocationInfo(location=AlaBossDecoderExample.LocationInfo.Location(lng=116.322987, lat=39.983424), formatted_address=北京市海淀区中关村大街27号1101-08室, business=中关村,人民大学,苏州街, cityCode=131, addressComponent=AlaBossDecoderExample.LocationInfo.AddressComponent(city=北京市, direction=附近, distance=7, district=海淀区, province=北京市, street=中关村大街, street_number=27号1101-08室))
​
json1 = CustomDecoderConfig.BaiduResult(status=OK, result={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"near"}})
​
json2 = com.freemud.eden.common.Result@35d6ca49[status=true,message={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"附近"}},result={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"附近"}},statusCode=OK]

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目背景
  • 基本模式和流程
  • 引入OpenFeign
  • Feign的定制
    • Encoder(加签)
      • 使用目的
      • 解决方案
    • RequestInterceptor(Header传递)
      • Decoder(统一解码)
        • 希望的效果
        • 统一的代码抽离
        • Demo的config
        • 效果展示
        • 展示下效果
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档