侧边栏壁纸
博主头像
木易成

聊聊软件、聊聊金融、聊聊篮球

  • 累计撰写 29 篇文章
  • 累计创建 18 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

OpenFeign+Sentinel实现统一降级,标准化返回值

木易成
2022-11-07 / 0 评论 / 1 点赞 / 3,229 阅读 / 1,751 字

在使用OpenFeign时候总是要写很多Fallback,此文章可以通过扩展实现一个统一的降级,规范返回格式;

OpenFeign+Sentinel统一降级

OpenFeign使用方式及问题

见上一篇文章@FeignClient中的fallbackFactory,fallback不起作用的正确使用示例

实现统一降级

原理:通过自定义实现Feign.Builder,启动时候扫描所有带有@FeignClient注解的类在生成其代理类的同时配置fallbackFactory或fallback,为其注入返回参数;

  1. 重写com.alibaba.cloud.sentinel.feign.SentinelFeign,使其支持降级注入
package com.yangxc.feign.util;

import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import feign.Contract;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.hystrix.FallbackFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author yangxc
 * @version 1.0.0
 * @ClassName MySentinelFeign.java
 * @Description TODO
 * @createTime 2022年11月08日 16:00:00
 */
public final class MySentinelFeign {
    
    private MySentinelFeign() {
    
    }
    
    public static MySentinelFeign.Builder builder() {
        return new MySentinelFeign.Builder();
    }
    
    public static final class Builder extends Feign.Builder implements ApplicationContextAware {
        
        private Contract contract = new Contract.Default();
        
        private ApplicationContext applicationContext;
        
        private FeignContext feignContext;
        
        @Override
        public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
            throw new UnsupportedOperationException();
        }
        
        @Override
        public MySentinelFeign.Builder contract(Contract contract) {
            this.contract = contract;
            return this;
        }
        
        @Override
        public Feign build() {
            super.invocationHandlerFactory(new InvocationHandlerFactory() {
                @Override
                public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
                    // 查找所有标记FeignClient
                    FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class);
                    Class fallback = feignClient.fallback();
                    Class fallbackFactory = feignClient.fallbackFactory();
                    
                    String beanName = feignClient.contextId();
                    if (!StringUtils.hasText(beanName)) {
                        beanName = feignClient.name();
                    }
                    
                    Object fallbackInstance;
                    FallbackFactory fallbackFactoryInstance;
                    // check fallback and fallbackFactory properties
                    if (void.class != fallback) {
                        fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
                        return new MySentinelInvocationHandler(target, dispatch,
                                new FallbackFactory.Default(fallbackInstance));
                    }
                    if (void.class != fallbackFactory) {
                        fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory",
                                fallbackFactory, FallbackFactory.class);
                        return new MySentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
                    }
                    return new MySentinelInvocationHandler(target, dispatch);
                }
                
                private Object getFromContext(String name, String type, Class fallbackType, Class targetType) {
                    Object fallbackInstance = feignContext.getInstance(name, fallbackType);
                    if (fallbackInstance == null) {
                        throw new IllegalStateException(String.format(
                                "No %s instance of type %s found for feign client %s", type, fallbackType, name));
                    }
                    
                    if (!targetType.isAssignableFrom(fallbackType)) {
                        throw new IllegalStateException(String.format(
                                "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
                                type, fallbackType, targetType, name));
                    }
                    return fallbackInstance;
                }
            });
            
            super.contract(new SentinelContractHolder(contract));
            return super.build();
        }
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
            feignContext = this.applicationContext.getBean(FeignContext.class);
        }
        
    }
}
  1. 重写为标有@FeignClient注解的接口生成代理类的类(com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler)
package com.yangxc.feign.util;

import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.MethodMetadata;
import feign.Target;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;

import static feign.Util.checkNotNull;

/**
 * @author yangxc
 * @version 1.0.0
 * @ClassName MySentinelInvocationHandler.java
 * @Description TODO
 * @createTime 2022年11月08日 16:07:00
 */
@Slf4j
public class MySentinelInvocationHandler implements InvocationHandler {
    
    public static final String EQUALS = "equals";
    
    public static final String HASH_CODE = "hashCode";
    
    public static final String TO_STRING = "toString";
    
    private final Target<?> target;
    
    private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
    
    private FallbackFactory fallbackFactory;
    
    private Map<Method, Method> fallbackMethodMap;
    
    MySentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
            FallbackFactory fallbackFactory) {
        this.target = checkNotNull(target, "target");
        this.dispatch = checkNotNull(dispatch, "dispatch");
        this.fallbackFactory = fallbackFactory;
        this.fallbackMethodMap = toFallbackMethod(dispatch);
    }
    
    MySentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
        this.target = checkNotNull(target, "target");
        this.dispatch = checkNotNull(dispatch, "dispatch");
    }
    
    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        if (EQUALS.equals(method.getName())) {
            try {
                Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                return equals(otherHandler);
            }
            catch (IllegalArgumentException e) {
                return false;
            }
        }
        else if (HASH_CODE.equals(method.getName())) {
            return hashCode();
        }
        else if (TO_STRING.equals(method.getName())) {
            return toString();
        }
        
        Object result;
        InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
        // only handle by HardCodedTarget
        if (target instanceof Target.HardCodedTarget) {
            Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
            MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
                    .get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method));
            // resource default is HttpMethod:protocol://url
            if (methodMetadata == null) {
                result = methodHandler.invoke(args);
            }
            else {
                String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url()
                        + methodMetadata.template().path();
                Entry entry = null;
                try {
                    ContextUtil.enter(resourceName);
                    entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
                    result = methodHandler.invoke(args);
                }
                catch (Throwable ex) {
                    // fallback handle
                    if (!BlockException.isBlockException(ex)) {
                        Tracer.trace(ex);
                    }
                    if (fallbackFactory != null) {
                        try {
                            Object fallbackResult = fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex),
                                    args);
                            return fallbackResult;
                        }
                        catch (IllegalAccessException e) {
                            // shouldn't happen as method is public due to being an
                            // interface
                            throw new AssertionError(e);
                        }
                        catch (InvocationTargetException e) {
                            throw new AssertionError(e.getCause());
                        }
                    }
                    else {
                        // 注入重点逻辑
                        //如果未自定义过sentinel限流后的返回值类型,此处需要修改为sentinel默认的返回类型,详情见com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler
                        if (R.class == method.getReturnType()) {
                            log.error("feign 服务间调用异常", ex);
                            return R.failed(ex.getLocalizedMessage());
                        }
                        else {
                            throw ex;
                        }
                    }
                }
                finally {
                    if (entry != null) {
                        entry.exit(1, args);
                    }
                    ContextUtil.exit();
                }
            }
        }
        else {
            // other target type using default strategy
            result = methodHandler.invoke(args);
        }
        
        return result;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof SentinelInvocationHandler) {
            MySentinelInvocationHandler other = (MySentinelInvocationHandler) obj;
            return target.equals(other.target);
        }
        return false;
    }
    
    @Override
    public int hashCode() {
        return target.hashCode();
    }
    
    @Override
    public String toString() {
        return target.toString();
    }
    
    static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
        Map<Method, Method> result = new LinkedHashMap<>();
        for (Method method : dispatch.keySet()) {
            method.setAccessible(true);
            result.put(method, method);
        }
        return result;
    }
    
}
  1. 修改sentinel限流后的返回值(com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler)
package com.sunacwy.frame.common.sentinel.handle;

import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.sunacwy.frame.common.core.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class MyUrlBlockHandler implements BlockExceptionHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
		log.error("sentinel 降级 资源名称{}", e.getRule().getResource(), e);
		response.setContentType(ContentType.JSON.toString());
		response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
		response.getWriter().print(JSONUtil.toJsonStr(R.failed(e.getMessage())));
	}
}
  1. 自定义的返回值
package com.yangxc.feign.util;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * @author yangxc
 * @version 1.0.0
 * @ClassName R.java
 * @Description TODO
 * @createTime 2022年11月08日 16:09:00
 */
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    @Getter
    @Setter
    private int code;
    
    @Getter
    @Setter
    private String msg;
    
    @Getter
    @Setter
    private T data;
    
    public static <T> R<T> ok() {
        return restResult(null, 0, null);
    }
    
    public static <T> R<T> ok(T data) {
        return restResult(data, 0, null);
    }
    
    public static <T> R<T> ok(T data, String msg) {
        return restResult(data, 0, msg);
    }
    
    public static <T> R<T> failed() {
        return restResult(null, -1, null);
    }
    
    public static <T> R<T> failed(String msg) {
        return restResult(null, -1, msg);
    }
    
    
    public static <T> R<T> failed(T data) {
        return restResult(data, -1, null);
    }
    
    public static <T> R<T> failed(T data, String msg) {
        return restResult(data, -1, msg);
    } 
}

  1. 编写自动注入配置类(替换容器中默认注入的 Feign.Builder)
package com.yangxc.feign.config;

import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration;
import com.yangxc.feign.util.MySentinelFeign;
import feign.Feign;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * @author yangxc
 * @version 1.0.0
 * @ClassName FeignConfig.java
 * @Description TODO
 * @createTime 2022年11月08日 16:34:00
 */
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class)
public class FeignConfig {
    
    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "feign.sentinel.enabled")
    public Feign.Builder feignSentinelBuilder() {
        return MySentinelFeign.builder();
    }
}

总结

OpenFeign+Sentinel目前已成为远程调用和限流的标配,但OpenFeign默认提供的fallback需要写大量的方法,不太符合程序员追求极简的代码风格。此方式将大大减少代码量不需要关注限流后的返回值,同时可以标准化返回值。

1

评论区