OpenFeign的定制

2022-02-21 18:04:32 浏览数 (1)

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]

0 人点赞