好记忆不如烂笔头,能记下点东西,就记下点,有时间拿出来看看,也会发觉不一样的感受.
在 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)可实现实时判断是否放行,支持方法级细粒度控制,无需重启即可生效。
(二)样例代码
-
自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureToggle {
String featureName(); // 功能名称(用于关联配置)
String defaultError() default "服务不可用"; // 关闭时的提示信息
}
-
定义切面
@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();
}
}
-
使用示例
@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...");
}
}
-
配置文件
pack:
features:
user:
enabled: false
user.query:
enabled: true
(三)优点
-
解耦性好,支持动态刷新,注解配置灵活
-
适合灰度发布、熔断降级等场景
(四)缺点
-
若结合配置中心,会增加一定的复杂度和依赖
三、基于拦截器实现
(一)原理
通过实现 HandlerInterceptor 前置拦截请求,结合配置动态控制接口访问,在请求进入 Controller 前进行开关判断。
(二)样例代码
-
定义配置属性类
@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;
}
}
-
定义拦截器
@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;
}
}
-
注册拦截器
@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 路由层拦截。
(二)样例代码
-
自定义 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;
}
}
-
注册处理器
@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. 缺点
-
引入了消息队列组件,增加了系统的复杂度和运维成本
-
实现相对复杂,需要对消息队列的使用有一定的了解
六、注意事项
-
配置文件的规范性:在使用基于配置文件的控制方式时,要确保配置文件的格式正确,避免因配置错误导致接口控制失效。
-
动态配置的实时性:当使用配置中心(如 Nacos)时,要注意配置的实时刷新问题,确保客户端能够及时获取到最新的配置信息。
-
异常处理:在接口被关闭时,要提供友好的错误提示信息,方便用户了解当前接口不可用的原因。
-
性能影响:不同的控制方式对性能的影响不同,在选择时要根据实际业务场景进行权衡。
-
安全性:在控制接口访问时,要注意安全性问题,防止未经授权的访问。
-
测试覆盖:在实现接口控制功能后,要进行全面的测试,确保在各种情况下接口控制都能正常工作。
七、总结
Spring Boot 提供了多种控制 Controller 接口的方式,包括基于@ConditionalOnProperty、AOP、拦截器、自定义 HandlerMapping 等。每种方式都有其特点和适用场景,在实际开发中应根据具体需求选择合适的方案。同时,还可以结合其他技术(如 Spring Cloud Gateway、消息队列等)来实现更复杂的接口控制逻辑。在实现过程中,要注意配置的规范性、性能影响、异常处理等问题,确保系统的稳定性和可靠性。
相知不迷路,来者皆是兄弟,搜索微信公众号 :“codingba” or “码出精彩” 交朋友,有更多资源