读了上一篇文章(【翻译】怎么自定义feign的重试机制)的同学多少了解一些了。这篇文章,我们从头到尾编写一个feign configuration。

1 编写FeignConfiguration

编写FeignConfiguration,实现RequestInterceptor接口:

@Component
public class MyFeignConfiguration implements RequestInterceptor {

    public static final String TOKEN_STR = "token";
    public static final String SEC_STR = "sec_str";
    // salt
    public static final String key = "prepared_salt";

    @Bean
    public Logger.Level logLevel() {
        return Logger.Level.BASIC;
    }

    /**
     * 持续3秒 间隔1000毫秒,重试 3 次
     *
     * @return
     */
    @Bean
    public Retryer retryer() {
        return new MyRetryer(1000L, 3000L, 3);
    }

    @Bean
    public Request.Options options() {
        return new Request.Options(5000, 5000);
    }

    /**
     * 特定错误重试
     *
     * @return
     */
    @Bean
    public CloudErrorDecoder cloudErrorDecoder() {
        return new MyErrorDecoder();
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            // 加密请求路径
            String method = request.getRequestURL().toString();
            String token = AESUtil.encryptData(key, method);
            requestTemplate.header(TOKEN_STR, token);
            requestTemplate.header(METHOD_URL, method);
            
        }
    }
}

实现,特定错误重试;制定重试次数,对请求进行加密,防止被刷。

PS:这里加密信息可以自定义,这里只是举个例子。

2 创建重试类MyRetryer

创建重试类MyRetryer,需要实现接口Retryer,指定最大重试次数、两次重试间隔时间、最大持续时间等参数,然后在continueOrPropagate方法中,编写具体的重试逻辑等定制化内容:

public class MyRetryer implements Retryer {

    private Logger logger = LoggerFactory.getLogger(CloudRetryer.class);
    // 最大重试次数
    private final int maxAttempts;
    // 两次重试间隔时间
    private final long period;
    // 最大持续时间
    private final long maxPeriod;

    public CloudRetryer() {
        // 默认参数,1秒内重试最多5次,每次间隔100毫秒
        this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
    }

    public CloudRetryer(long period, long maxPeriod, int maxAttempts) {
        this.period = period;
        this.maxPeriod = maxPeriod;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        // 打印重试日志
        logger.info("Feign/retry/attempt/[{}]/due/to/][{}] ", attempt, e.getMessage());
        // 如果超出重试次数,抛出异常,停止重试;否则继续休眠period时间
        if(attempt++ == maxAttempts){
            throw e;
        }
        try {
            Thread.sleep(period);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public Retryer clone() {
        return new CloudRetryer(this.period, this.maxPeriod, this.maxAttempts);
    }
}

3 定制编码器MyErrorDecoder

定制编码器MyErrorDecoder,需要实现接口ErrorDecoder,实现特定错误重试:

public class MyErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String s, Response response) {
        Exception exception = defaultErrorDecoder.decode(s, response);

        if(exception instanceof RetryableException){
            return exception;
        }
        // 自定义一些异常,进行重试
        if(504 == response.status()){
            return new RetryableException("504 资源未找到", response.request().httpMethod(),
                    new Date(System.currentTimeMillis()) );
        }

        return exception;
    }
}

如果判断是RetryableException,继续返回该异常,会继续重试,否则判断是否是需要重试的异常,如果是,返回RetryableException异常。

4 服务解密

增加拦截器进行解密

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor());
    }
}

拦截器代码,在 preHandle 方法中解密header中的token信息,判断解密后的字符串和加密前的字符串是否相等,如果相等,则放行。如果请求为白名单请求,则放行,其他,进行拦截。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

public class MyInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(MyInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj){
        // 解密
        String token = AESUtil.decryptData(MyFeignConfiguration.key, request.getHeader(MyFeignConfiguration.TOKEN_STR));
        // 经过加密的请求或者白名单请求放行,否则拦截
        if(Objects.equals(qsc_multi_token, request.getHeader(FeignCloudConfiguration.SEC_STR)) 
        || whiteList().contanis(request.getRequestURI()) {
            return true;
        }
        logger.info("request/invalid/url/[{}]", request.getRequestURL());
        return false;
    }

  private String whiteList() {
    StringBuilder whilteStr = new StringBuilder();
    whilteStr.append("/user/getUserInfo");
  }
}

5 调用例子

在url中填入域名或者ip:port,在 path 中指明需要访问的路径。

这里可以通过继承服务的接口定义接口,也可以直接在接口内添加对应的请求方法,使用PostMappint或者GetMapping添加具体访问路径。

@FeignClient(name = "user-service", path = "user-service", url = "127.0.0.1:8081", configuration = MyFeignConfiguration.class)
public interface UserFeign extends UserServiceI {

}

所有发生在我们身上的事件都是一个经过仔细包装的礼物。只要我们愿意面对它有时候有点丑恶的包装,带着耐心和勇气一点一点的拆开包装的话,我们会惊喜的看到里面珍藏的礼物。 ----遇见未知的自己