|
@@ -0,0 +1,232 @@
|
|
|
+package com.chelvc.framework.feign.circuitbreaker;
|
|
|
+
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Enumeration;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+import com.chelvc.framework.common.util.ObjectUtils;
|
|
|
+import com.chelvc.framework.common.util.StringUtils;
|
|
|
+import com.google.common.collect.Sets;
|
|
|
+import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
|
|
+import javassist.ClassPool;
|
|
|
+import javassist.CtClass;
|
|
|
+import javassist.CtMethod;
|
|
|
+import javassist.CtNewMethod;
|
|
|
+import javassist.LoaderClassPath;
|
|
|
+import javassist.Modifier;
|
|
|
+import javassist.bytecode.AnnotationsAttribute;
|
|
|
+import javassist.bytecode.ConstPool;
|
|
|
+import javassist.bytecode.MethodInfo;
|
|
|
+import javassist.bytecode.annotation.Annotation;
|
|
|
+import javassist.bytecode.annotation.StringMemberValue;
|
|
|
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
|
|
+import org.springframework.boot.SpringApplication;
|
|
|
+import org.springframework.boot.SpringApplicationRunListener;
|
|
|
+import org.springframework.cloud.openfeign.EnableFeignClients;
|
|
|
+import org.springframework.context.ConfigurableApplicationContext;
|
|
|
+import org.springframework.core.type.AnnotationMetadata;
|
|
|
+import org.springframework.util.ClassUtils;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Feign调用熔断初始化处理器
|
|
|
+ *
|
|
|
+ * @author Woody
|
|
|
+ * @date 2025/4/14
|
|
|
+ */
|
|
|
+public class FeignCircuitBreakerInitializer implements SpringApplicationRunListener {
|
|
|
+ private static volatile boolean INITIALIZED;
|
|
|
+ private static volatile Set<String> SERVERS;
|
|
|
+
|
|
|
+ private final boolean enabled;
|
|
|
+
|
|
|
+ public FeignCircuitBreakerInitializer(SpringApplication application, String[] args) {
|
|
|
+ Class<?> main = application.getMainApplicationClass();
|
|
|
+ EnableFeignCircuitBreaker configuration = main.getAnnotation(EnableFeignCircuitBreaker.class);
|
|
|
+ this.enabled = configuration != null && main.isAnnotationPresent(EnableFeignClients.class);
|
|
|
+ if (this.enabled && !INITIALIZED) {
|
|
|
+ SERVERS = ObjectUtils.ifEmpty(configuration.servers(), Sets::newHashSet, Collections::emptySet);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化Feign客户端注册监听器
|
|
|
+ *
|
|
|
+ * @param pool 对象池
|
|
|
+ * @throws Exception 处理异常
|
|
|
+ */
|
|
|
+ private void initializeFeignClientRegisterListener(ClassPool pool) throws Exception {
|
|
|
+ CtClass clazz = pool.get("org.springframework.cloud.openfeign.FeignClientsRegistrar");
|
|
|
+ CtMethod method = clazz.getDeclaredMethod("registerFeignClient");
|
|
|
+ CtMethod doing = CtNewMethod.copy(method, clazz, null);
|
|
|
+ doing.setName("doRegisterFeignClient");
|
|
|
+ clazz.removeMethod(method);
|
|
|
+ clazz.addMethod(doing);
|
|
|
+ String body = String.format(
|
|
|
+ "private void registerFeignClient(%s registry, %s metadata, java.util.Map attributes) {\n" +
|
|
|
+ "%s.initializeFeignCircuitBreaker(metadata, attributes);\n" +
|
|
|
+ "this.doRegisterFeignClient(registry, metadata, attributes);\n" +
|
|
|
+ "}", BeanDefinitionRegistry.class.getName(), AnnotationMetadata.class.getName(),
|
|
|
+ FeignCircuitBreakerInitializer.class.getName()
|
|
|
+ );
|
|
|
+ clazz.addMethod(CtNewMethod.make(body, clazz));
|
|
|
+ clazz.toClass();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将对象类型转换成方法返回信息
|
|
|
+ *
|
|
|
+ * @param type 对象类型
|
|
|
+ * @return 方法返回信息
|
|
|
+ */
|
|
|
+ private static String type2return(Class<?> type) {
|
|
|
+ if (type == void.class) {
|
|
|
+ return StringUtils.EMPTY;
|
|
|
+ } else if (type == boolean.class) {
|
|
|
+ return "return false;";
|
|
|
+ } else if (type == Boolean.class) {
|
|
|
+ return "return java.lang.Boolean.FALSE;";
|
|
|
+ } else if (type.isPrimitive()) {
|
|
|
+ return "return 0;";
|
|
|
+ } else if (type.isArray()) {
|
|
|
+ return "return new " + type.getComponentType().getName() + "[0];";
|
|
|
+ } else if (Map.class.isAssignableFrom(type)) {
|
|
|
+ return "return java.util.Collections.emptyMap();";
|
|
|
+ } else if (Set.class.isAssignableFrom(type)) {
|
|
|
+ return "return java.util.Collections.emptySet();";
|
|
|
+ } else if (Collection.class.isAssignableFrom(type)) {
|
|
|
+ return "return java.util.Collections.emptyList();";
|
|
|
+ } else if (Iterator.class.isAssignableFrom(type)) {
|
|
|
+ return "return java.util.Collections.emptyIterator();";
|
|
|
+ } else if (Enumeration.class.isAssignableFrom(type)) {
|
|
|
+ return "return java.util.Collections.emptyEnumeration();";
|
|
|
+ }
|
|
|
+ return "return null;";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取Feign客户端名称
|
|
|
+ *
|
|
|
+ * @param attributes @FeignClient注解属性名/值映射表
|
|
|
+ * @return 客户端名称
|
|
|
+ */
|
|
|
+ private static String getFeignClientName(Map<String, Object> attributes) {
|
|
|
+ String name = StringUtils.trim((String) attributes.get("contextId"));
|
|
|
+ if (StringUtils.isEmpty(name)) {
|
|
|
+ name = StringUtils.trim((String) attributes.get("value"));
|
|
|
+ }
|
|
|
+ if (StringUtils.isEmpty(name)) {
|
|
|
+ name = StringUtils.trim((String) attributes.get("name"));
|
|
|
+ }
|
|
|
+ if (StringUtils.isEmpty(name)) {
|
|
|
+ name = StringUtils.trim((String) attributes.get("serviceId"));
|
|
|
+ }
|
|
|
+ return name;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建熔断降级方法
|
|
|
+ *
|
|
|
+ * @param method 原方法对象
|
|
|
+ * @return 降级方法对象
|
|
|
+ * @throws Exception 处理异常
|
|
|
+ */
|
|
|
+ private static CtMethod buildFallbackMethod(CtMethod method) throws Exception {
|
|
|
+ CtClass clazz = method.getDeclaringClass();
|
|
|
+ String returnType = method.getReturnType().getName();
|
|
|
+ String returns = type2return(ClassUtils.resolveClassName(returnType, null));
|
|
|
+ String body = String.format("public %s fallback_%s(Throwable t) {\n" +
|
|
|
+ "org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(%s.class);\n" +
|
|
|
+ "logger.error(\"'%s' invoke fallback: {}\", t.getMessage());\n" +
|
|
|
+ "%s\n}", returnType, StringUtils.uuid(), clazz.getName(), method.getName(), returns);
|
|
|
+ return CtNewMethod.make(body, clazz);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化Feign调用方法@CircuitBreaker注解
|
|
|
+ *
|
|
|
+ * @param name 熔断器名称
|
|
|
+ * @param method 目标方法
|
|
|
+ * @throws Exception 处理异常
|
|
|
+ */
|
|
|
+ private static void initializeFeignCircuitBreaker(String name, CtMethod method) throws Exception {
|
|
|
+ // 初始化熔断降级方法
|
|
|
+ CtMethod fallback = buildFallbackMethod(method);
|
|
|
+ method.getDeclaringClass().addMethod(fallback);
|
|
|
+
|
|
|
+ // 绑定方法@CircuitBreaker注解
|
|
|
+ MethodInfo info = method.getMethodInfo();
|
|
|
+ ConstPool pool = info.getConstPool();
|
|
|
+ AnnotationsAttribute attribute = (AnnotationsAttribute) info.getAttribute(AnnotationsAttribute.visibleTag);
|
|
|
+ if (attribute == null) {
|
|
|
+ attribute = new AnnotationsAttribute(pool, AnnotationsAttribute.visibleTag);
|
|
|
+ }
|
|
|
+ Annotation annotation = new Annotation(CircuitBreaker.class.getName(), pool);
|
|
|
+ annotation.addMemberValue("name", new StringMemberValue(name, pool));
|
|
|
+ annotation.addMemberValue("fallbackMethod", new StringMemberValue(fallback.getName(), pool));
|
|
|
+ attribute.addAnnotation(annotation);
|
|
|
+ info.addAttribute(attribute);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化Feign调用熔断器
|
|
|
+ *
|
|
|
+ * @param metadata Feign客户端注解元信息
|
|
|
+ * @param attributes @FeignClient注解属性名/值映射表
|
|
|
+ */
|
|
|
+ public static void initializeFeignCircuitBreaker(AnnotationMetadata metadata, Map<String, Object> attributes) {
|
|
|
+ // 忽略已配置@CircuitBreaker注解客户端
|
|
|
+ if (metadata.hasAnnotation(CircuitBreaker.class.getCanonicalName())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 忽略不需要熔断服务调用
|
|
|
+ String server = getFeignClientName(attributes);
|
|
|
+ if (StringUtils.isEmpty(server) || (ObjectUtils.notEmpty(SERVERS) && !SERVERS.contains(server))) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动态绑定Feign调用方法@CircuitBreaker注解
|
|
|
+ boolean bound = false;
|
|
|
+ ClassPool pool = ClassPool.getDefault();
|
|
|
+ try {
|
|
|
+ CtClass clazz = pool.get(metadata.getClassName());
|
|
|
+ CtMethod[] methods = clazz.getDeclaredMethods();
|
|
|
+ if (ObjectUtils.notEmpty(methods)) {
|
|
|
+ for (CtMethod method : methods) {
|
|
|
+ if (Modifier.isAbstract(method.getModifiers()) && !method.hasAnnotation(CircuitBreaker.class)) {
|
|
|
+ initializeFeignCircuitBreaker(server, method);
|
|
|
+ bound = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (bound) {
|
|
|
+ clazz.toClass();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void contextLoaded(ConfigurableApplicationContext context) {
|
|
|
+ if (this.enabled && !INITIALIZED) {
|
|
|
+ synchronized (FeignCircuitBreakerInitializer.class) {
|
|
|
+ if (!INITIALIZED) {
|
|
|
+ INITIALIZED = true;
|
|
|
+
|
|
|
+ ClassPool pool = ClassPool.getDefault();
|
|
|
+ pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.initializeFeignClientRegisterListener(pool);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|