SB 控制 Controller 接口的多种方式,你都知道哪些?

好记忆不如烂笔头,能记下点东西,就记下点,有时间拿出来看看,也会发觉不一样的感受.

 

在 Spring Boot 开发中,动态控制 Controller 接口的可用性是常见的需求场景,比如灰度发布、功能降级、紧急熔断等。以下是多种控制 Controller 接口的方式,包括各自的特点、适用场景、样例代码以及注意事项。

一、基于@ConditionalOnProperty

(一)原理

利用 Spring Boot 的@ConditionalOnProperty条件装配机制,在应用启动时根据配置文件(如 application.yml)决定是否注册 Controller。

(二)样例代码

@RestController
@ConditionalOnProperty(name = "pack.features.users.enabled", havingValue = "true")
public class UserController {}

当配置文件中的 pack.features.users.enabled 配置为 true 后,Spring 才会注册 UserController 接口。

(三)优点

  • 零编码,直接依赖 Spring 原生支持

  • 启动时确定,避免运行时检查开销

(四)缺点

  • 需重启生效,无法动态调整

  • 粒度较粗,仅支持类/Bean 级别控制

二、基于AOP实现

(一)原理

通过自定义注解(如@FeatureToggle)标记需控制的接口,结合 AOP 动态拦截请求,结合配置中心(如 Nacos)可实现实时判断是否放行,支持方法级细粒度控制,无需重启即可生效。

(二)样例代码

  1. 自定义注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureToggle {
    String featureName(); // 功能名称(用于关联配置)
    String defaultError() default "服务不可用"; // 关闭时的提示信息
}
  1. 定义切面

@Aspect
@Component
public class FeatureToggleAspect {
    private static final String PREFIX = "pack.features.";
    private static final String SUFFIX = ".enabled";
    private final Environment env;

    public FeatureToggleAspect(Environment env) {
        this.env = env;
    }

    @Pointcut(value = "@within(toggle) || @annotation(toggle)")
    public void matchAnnotatedClassOrMethod(FeatureToggle toggle) {}

    @Around("matchAnnotatedClassOrMethod(toggle)")
    public Object checkFeature(ProceedingJoinPoint pjp, FeatureToggle toggle) throws Throwable {
        String propertyKey = null;
        if (toggle == null) {
            toggle = AopUtils.getTargetClass(pjp.getTarget()).getAnnotation(FeatureToggle.class);
        }
        if (toggle == null) {
            return pjp.proceed();
        }
        propertyKey = PREFIX + toggle.featureName() + SUFFIX;
        boolean isEnabled = Boolean.parseBoolean(env.getProperty(propertyKey, "false"));
        if (!isEnabled) {
            throw new RuntimeException(toggle.defaultError());
        }
        return pjp.proceed();
    }
}
  1. 使用示例

@RestController
@RequestMapping("/users")
@FeatureToggle(featureName = "user")
public class UserController {
    @FeatureToggle(featureName = "user.query")
    @GetMapping("/query")
    public ResponseEntity<?> query() {
        return ResponseEntity.ok("query...");
    }

    @GetMapping("/create")
    public ResponseEntity<?> create() {
        return ResponseEntity.ok("create...");
    }
}
  1. 配置文件

pack:
  features:
    user:
      enabled: false
    user.query:
      enabled: true

(三)优点

  • 解耦性好,支持动态刷新,注解配置灵活

  • 适合灰度发布、熔断降级等场景

(四)缺点

  • 若结合配置中心,会增加一定的复杂度和依赖

三、基于拦截器实现

(一)原理

通过实现 HandlerInterceptor 前置拦截请求,结合配置动态控制接口访问,在请求进入 Controller 前进行开关判断。

(二)样例代码

  1. 定义配置属性类

@Component
@ConfigurationProperties(prefix = "pack.features")
public class FeatureProperties {
    // 存储所有功能开关状态
    private final Map<String, State> toggle = new ConcurrentHashMap<>();

    // 检查功能是否启用
    public boolean isEnabled(String featureName) {
        State state = toggle.get(featureName);
        if (state == null) {
            return false;
        }
        return state.enabled();
    }

    public static record State(Boolean enabled) {}

    public Map<String, State> getToggle() {
        return toggle;
    }
}
  1. 定义拦截器

@Component
public class FeatureToggleInterceptor implements HandlerInterceptor {
    private final FeatureProperties featureProperties;

    public FeatureToggleInterceptor(FeatureProperties featureProperties) {
        this.featureProperties = featureProperties;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            // 检查方法或类上的注解
            FeatureToggle toggle = hm.getMethodAnnotation(FeatureToggle.class);
            if (toggle == null) {
                toggle = hm.getBeanType().getAnnotation(FeatureToggle.class);
            }
            if (toggle != null && !featureProperties.isEnabled(toggle.featureName())) {
                throw new RuntimeException(toggle.defaultError());
            }
        }
        return true;
    }
}
  1. 注册拦截器

@Component
public class WebConfig implements WebMvcConfigurer {
    private final FeatureToggleInterceptor toggleInterceptor;

    public WebConfig(FeatureToggleInterceptor toggleInterceptor) {
        this.toggleInterceptor = toggleInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(toggleInterceptor).addPathPatterns("/**");
    }
}

(三)优点

  • 避免 AOP 开销,适合 URL 级管控

  • 对请求的拦截较早,可以进行一些前置处理

(四)缺点

  • 对于复杂的业务逻辑处理可能不如 AOP 方便

四、自定义 HandlerMapping

(一)原理

通过自定义 RequestMappingHandlerMapping 在路由匹配阶段动态控制请求,根据配置决定是否返回处理器,相比拦截器更底层,直接在 Spring MVC 路由层拦截。

(二)样例代码

  1. 自定义 HandlerMapping

public class PackHandlerMapping extends RequestMappingHandlerMapping {
    private final FeatureProperties featureProperties;

    public PackHandlerMapping(FeatureProperties featureProperties) {
        this.featureProperties = featureProperties;
    }

    @Override
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        HandlerMethod handler = super.getHandlerInternal(request);
        if (handler != null) {
            FeatureToggle toggle = handler.getMethodAnnotation(FeatureToggle.class);
            if (toggle == null) {
                toggle = handler.getBeanType().getAnnotation(FeatureToggle.class);
            }
            if (toggle != null && !featureProperties.isEnabled(toggle.featureName())) {
                return null;
            }
        }
        return handler;
    }
}
  1. 注册处理器

@Component
public class HandlerConfig implements WebMvcRegistrations {
    private final FeatureProperties featureProperties;

    public HandlerConfig(FeatureProperties featureProperties) {
        this.featureProperties = featureProperties;
    }

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new PackHandlerMapping(featureProperties);
    }
}

(三)优点

  • 在路由匹配阶段控制,性能较好

  • 能够更精细地控制请求的路由

(四)缺点

  • 实现相对复杂,需要对 Spring MVC 的内部机制有一定了解

五、其他控制方式探讨

除了上述四种方式外,还可以考虑以下控制方式:

(一)基于 Spring Cloud Gateway 的路由控制

在微服务架构中,可以利用 Spring Cloud Gateway 作为网关,通过路由配置来控制后端 Controller 接口的访问。例如,根据请求头、查询参数等条件来决定是否放行请求到特定的服务。

1. 样例代码(Spring Cloud Gateway 配置)
spring:
  cloud:
    gateway:
      routes:
        - id: user_service_route
          uri: lb://user-service
          predicates:
            - Path=/users/**
          filters:
            - name: RequestHeader
              args:
                name: X-Feature-Enabled
                value: true

在这种配置下,只有当请求头中包含 X-Feature-Enabled 且值为 true 时,才会将请求路由到用户服务的 /users/** 接口。

2. 优点
  • 适合在微服务架构中进行统一的流量控制

  • 可以灵活地根据各种条件进行路由决策

3. 缺点
  • 需要引入 Spring Cloud Gateway,增加了系统的复杂度

  • 对于单体应用不太适用

(二)基于消息队列的异步控制

在某些场景下,可以利用消息队列来异步控制接口的访问。例如,当一个接口需要进行限流时,可以将请求放入消息队列,然后由消费者按照一定的速率处理请求。

1. 样例代码(使用 RabbitMQ 进行限流)
// 消息生产者
@Component
public class MessageSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendRequest(String requestId, String interfaceName) {
        Map<String, String> message = new HashMap<>();
        message.put("requestId", requestId);
        message.put("interfaceName", interfaceName);
        rabbitTemplate.convertAndSend("interface-exchange", "interface.key", message);
    }
}

// 消息消费者
@Component
public class MessageReceiver {
    @RabbitListener(queues = "interface-queue")
    public void receiveMessage(Map<String, String> message) {
        String requestId = message.get("requestId");
        String interfaceName = message.get("interfaceName");
        // 模拟接口处理
        System.out.println("Processing request: " + requestId + " for interface: " + interfaceName);
        // 这里可以调用实际的 Controller 接口或其对应的服务
    }
}

在使用这种方式时,可以通过控制消息队列的消费速率来间接控制接口的访问频率。

2. 优点
  • 可以实现异步处理,提高系统的吞吐量

  • 适合需要对接口进行限流或削峰的场景

3. 缺点
  • 引入了消息队列组件,增加了系统的复杂度和运维成本

  • 实现相对复杂,需要对消息队列的使用有一定的了解

六、注意事项

  1. 配置文件的规范性:在使用基于配置文件的控制方式时,要确保配置文件的格式正确,避免因配置错误导致接口控制失效。

  2. 动态配置的实时性:当使用配置中心(如 Nacos)时,要注意配置的实时刷新问题,确保客户端能够及时获取到最新的配置信息。

  3. 异常处理:在接口被关闭时,要提供友好的错误提示信息,方便用户了解当前接口不可用的原因。

  4. 性能影响:不同的控制方式对性能的影响不同,在选择时要根据实际业务场景进行权衡。

  5. 安全性:在控制接口访问时,要注意安全性问题,防止未经授权的访问。

  6. 测试覆盖:在实现接口控制功能后,要进行全面的测试,确保在各种情况下接口控制都能正常工作。

七、总结

Spring Boot 提供了多种控制 Controller 接口的方式,包括基于@ConditionalOnProperty、AOP、拦截器、自定义 HandlerMapping 等。每种方式都有其特点和适用场景,在实际开发中应根据具体需求选择合适的方案。同时,还可以结合其他技术(如 Spring Cloud Gateway、消息队列等)来实现更复杂的接口控制逻辑。在实现过程中,要注意配置的规范性、性能影响、异常处理等问题,确保系统的稳定性和可靠性。

 相知不迷路,来者皆是兄弟,搜索微信公众号 :“codingba” or “码出精彩” 交朋友,有更多资源​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值