Эх сурвалжийг харах

新增framework-feign-circuitbreaker模块

Woody 1 сар өмнө
parent
commit
804f4bf751

+ 44 - 0
framework-feign-circuitbreaker/pom.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.chelvc.framework</groupId>
+        <artifactId>framework-dependencies</artifactId>
+        <version>1.0.0-RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>framework-feign-circuitbreaker</artifactId>
+    <version>1.0.0-RELEASE</version>
+
+    <properties>
+        <framework-base.version>1.0.0-RELEASE</framework-base.version>
+        <resilience4j-spring-boot2.version>1.7.0</resilience4j-spring-boot2.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.chelvc.framework</groupId>
+            <artifactId>framework-base</artifactId>
+            <version>${framework-base.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-openfeign-core</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.cloud</groupId>
+                    <artifactId>spring-cloud-netflix-ribbon</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>io.github.resilience4j</groupId>
+            <artifactId>resilience4j-spring-boot2</artifactId>
+            <version>${resilience4j-spring-boot2.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 30 - 0
framework-feign-circuitbreaker/src/main/java/com/chelvc/framework/feign/circuitbreaker/CircuitBreakerInterceptor.java

@@ -0,0 +1,30 @@
+package com.chelvc.framework.feign.circuitbreaker;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import io.github.resilience4j.circuitbreaker.CircuitBreaker;
+import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspectExt;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.springframework.stereotype.Component;
+
+/**
+ * 熔断降级处理拦截器
+ *
+ * @author Woody
+ * @date 2025/4/15
+ */
+@Component
+public class CircuitBreakerInterceptor implements CircuitBreakerAspectExt {
+    @Override
+    public boolean canHandleReturnType(Class returnType) {
+        return true;
+    }
+
+    @Override
+    public Object handle(ProceedingJoinPoint point, CircuitBreaker circuitBreaker, String method) throws Throwable {
+        boolean enabled = ApplicationContextHolder.getProperty("circuit.breaker.enabled", boolean.class, true);
+        if (enabled) {
+            return circuitBreaker.executeCheckedSupplier(point::proceed);
+        }
+        return point.proceed();
+    }
+}

+ 25 - 0
framework-feign-circuitbreaker/src/main/java/com/chelvc/framework/feign/circuitbreaker/EnableFeignCircuitBreaker.java

@@ -0,0 +1,25 @@
+package com.chelvc.framework.feign.circuitbreaker;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Feign调用熔断启用注解
+ *
+ * @author Woody
+ * @date 2025/4/14
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnableFeignCircuitBreaker {
+    /**
+     * 获取启用熔断服务名称数组
+     *
+     * @return 服务名称数组
+     */
+    String[] servers() default {};
+}

+ 232 - 0
framework-feign-circuitbreaker/src/main/java/com/chelvc/framework/feign/circuitbreaker/FeignCircuitBreakerInitializer.java

@@ -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);
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 0
framework-feign-circuitbreaker/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.SpringApplicationRunListener=com.chelvc.framework.feign.circuitbreaker.FeignCircuitBreakerInitializer

+ 1 - 0
pom.xml

@@ -32,6 +32,7 @@
         <module>framework-download</module>
         <module>framework-kubernetes</module>
         <module>framework-elasticsearch</module>
+        <module>framework-feign-circuitbreaker</module>
         <module>framework-dependencies</module>
         <module>framework-cloud</module>
         <module>framework-cloud-nacos</module>