一个Spring Boot的@RestController可以使用属性进行启用/禁用吗?

105

假设有一个使用@RestController注解的“标准”Spring Boot应用程序,例如:

@RestController
@RequestMapping(value = "foo", produces = "application/json;charset=UTF-8")
public class MyController {
    @RequestMapping(value = "bar")
    public ResponseEntity<String> bar(
        return new ResponseEntity<>("Hello world", HttpStatus.OK);
    }
}

是否有一种注释或技术可以防止端点在某个应用程序属性存在/不存在的情况下完全启动。

注意:在方法内部测试属性并抛出异常不是解决方案,因为端点将会存在。

我不关心粒度:即启用/禁用单个方法或整个类都可以。


由于配置文件不是属性,因此通过配置文件无法解决我的问题。

4个回答

169

我发现使用@ConditionalOnExpression可以找到一个简单的解决方案:

@RestController
@ConditionalOnExpression("${my.controller.enabled:false}")
@RequestMapping(value = "foo", produces = "application/json;charset=UTF-8")
public class MyController {
    @RequestMapping(value = "bar")
    public ResponseEntity<String> bar(
        return new ResponseEntity<>("Hello world", HttpStatus.OK);
    }
}

有了这个注释,除非我有

my.controller.enabled=true

在我的application.properties文件中,控制器根本不会启动。

您还可以使用更方便的方式:

@ConditionalOnProperty("my.property")

如果属性存在且值为"true",该组件将会启动,否则不会启动,其表现与上述完全相同。


31
你可以考虑使用 @ConditionalOnProperty,因为它比 SpEL 表达式评估稍微快一点。尝试使用 @ConditionalOnProperty(prefix="my.controller", name="enabled") - Phil Webb
5
在RestController之后使用ConditionalOnProperty或ConditionalOnExpression对我没有作用。Bean被创建了,URL仍然可访问,针对AdminController RestController,日志中出现以下内容:DozerInitializer - Dozer JMX MBean [org.dozer.jmx:type=DozerAdminController]自动注册到平台MBean服务器。需要帮助吗? - r.bhardwaj
1
这个解决方案的问题在于,如果您更改属性,除非您使用Spring Cloud进行配置,否则必须重新启动服务器。 - user666
2
@user666 最佳实践是将配置作为(系统测试过的)部署包的一部分,因此如果您遵循最佳实践,则预计需要重新启动。这种控制通常是“功能切换”,因此激活将是计划更改,而不是临时更改。对于临时更改,您可能会通过应用程序外部的网络控制它,例如通过负载均衡器。 - Bohemian
@Bohemian,这个方法能否在方法级别上实现?如果不想将控制器的特定方法添加到API中,该怎么做呢? - Vikram kumar Chhajer
显示剩余7条评论

3

针对这个问题和另一个问题 在这里。

这是我的答案:

我会使用@RefreshScope Bean,然后当你想要在运行时停止Rest Controller时,你只需要将该控制器的属性更改为false。

SO的链接引用了在运行时更改属性的方法。

这是我的工作代码片段:

@RefreshScope
@RestController
class MessageRestController(
    @Value("\${message.get.enabled}") val getEnabled: Boolean,
    @Value("\${message:Hello default}") val message: String
) {
    @GetMapping("/message")
    fun get(): String {
        if (!getEnabled) {
            throw NoHandlerFoundException("GET", "/message", null)
        }
        return message
    }
}

还有其他使用Filter的替代方案:

@Component
class EndpointsAvailabilityFilter @Autowired constructor(
    private val env: Environment
): OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val requestURI = request.requestURI
        val requestMethod = request.method
        val property = "${requestURI.substring(1).replace("/", ".")}." +
                "${requestMethod.toLowerCase()}.enabled"
        val enabled = env.getProperty(property, "true")
        if (!enabled.toBoolean()) {
            throw NoHandlerFoundException(requestMethod, requestURI, ServletServerHttpRequest(request).headers)
        }
        filterChain.doFilter(request, response)
    }
}

我的Github页面,解释如何在运行时禁用


路径中如果包含变量怎么办? - user666

2
在某些情况下,@ConditionalOnXXX可能无法工作,例如依赖于另一个bean实例来检查条件(XXXCondition类无法调用bean)。
在这种情况下,在Java配置文件中注册控制器。
参见源代码(Spring webmvc 5.1.6):
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.isHandler(Class<?>)
 
       @Override
       protected boolean isHandler(Class<?> beanType) {
              return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                           AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
       }

应该在控制器Bean的类型级别上添加@RequestMapping注释。请参见:
@RequestMapping // Make Spring treat the bean as request handler
public class MyControllerA implements IMyController {
    @RequestMapping(path = { "/path1" })
    public .. restMethod1(...) {
  ........
    }
}

@RequestMapping // Make Spring treat the bean as request handler
public class MyControllerB implements IMyController {
    @RequestMapping(path = { "/path1" })
    public .. restMethod1(...) {
  ........
    }
}

@Configuration
public class ControllerConfiguration {

    /**
     *
     * Programmatically register Controller based on certain condition.
     *
     */
    @Bean
    public IMyController myController() {
        IMyController controller;
        if (conditionA) {
            controller = new MyControllerA();
        } else {
            controller = new MyControllerB();
        }
        return controller;
    }
}

只是注意到,这个方法在Spring WebMVC 6.x版本中不再适用。isHandler方法只检查@Controller注解,所以如果没有@Controller注解,它将无法工作。 - undefined

2

我猜这个问题是因为你在不同的环境中使用了不同的application.properties文件。在这种情况下,你可以使用Spring配置文件,并将配置分成不同的文件,以配置文件名后缀来区分不同的配置文件,例如:

application.properties:

spring.profiles.active=@activatedProperties@

application-local.properties:

 //some config

application-prod.properties:

//some config

然后在您的构建参数中,您可以通过添加选项来指定正在构建的环境:

-Dspring.profiles.active= //<-put here profile local or prod

然后在您的应用程序中,您可以通过添加代码来启用/禁用任何Spring Bean:

@Profile("put here profile name")

比如说:

@RestController
@Profile("local")
@RequestMapping("/testApi")
public class RestForTesting{

//do some stuff

}

现在我的 RestForTesting 只会在我运行使用以下构建创建的时候才会被创建

-Dspring.profiles.active=local


1
不。这个问题与配置文件无关,配置文件只是管理属性的众多方式之一。相反,我想要部署一个端点到非生产环境 - 我不能让端点以任何形式存在于生产环境中。 - Bohemian
2
我以前尝试过,在控制器上添加@Profile注释没有任何作用。 - Joseph Tinoco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接