102

Given a "standard" spring boot application with a @RestController, eg

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

Is there an annotation or technique that prevents the endpoint from starting at all if/unless a certain application property exists/doesn't exist.

Note: Testing a property inside the method and exploding is not a solution, because the endpoint will exist.

I don't care about the granularity: ie enabling/disabling just a method or the whole class are both fine.


Because a profile is not a property, control via profiles does not solve my problem.

Bohemian
  • 412,405
  • 93
  • 575
  • 722

4 Answers4

166

I found a simple solution using @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);
    }
}

With this annotation added, unless I have

my.controller.enabled=true

in my application.properties file, the controller won't start at all.

You can also use the more convenient:

@ConditionalOnProperty("my.property")

Which behaves exactly as above; if the property is present and "true", the component starts, otherwise it doesn't.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • 30
    You might want to consider `@ConditionalOnProperty` as it's slightly faster than SpEL evaluation. Try `@ConditionalOnProperty(prefix="my.controller", name="enabled")` – Phil Webb Apr 30 '15 at 17:35
  • Thanks, one additional clarification on what level this annotation can be applied: http://stackoverflow.com/questions/30065945/conditionalonproperty-conditionally-works – codesalsa Jan 13 '17 at 17:57
  • 5
    Using ConditionalOnProperty or ConditionalOnExpression after RestController is not working for me. Bean is being created URL's are still accessible getting following in logs for AdminController RestController : DozerInitializer - Dozer JMX MBean [org.dozer.jmx:type=DozerAdminController] auto registered with the Platform MBean Server any help ? – r.bhardwaj Aug 19 '17 at 16:43
  • 1
    The probem with this solution is that if you change the property, you will have to restart the server unless you are using spring cloud for configuration. – user666 Jun 30 '20 at 05:31
  • 2
    @user666 best practice has config as part of the (system tested) deployment bundle, so a restart is expected to be required if you are following best practice. This kind of control is generally a “feature toggle” anyway, so activation will be a planned change, not ad hoc. For ad hoc, you would probably control it through networking external to the application, eg via the load balancer. – Bohemian Jun 30 '20 at 05:51
  • @Bohemian is there a way this can be implemented on the method level? So if don't want a particular method of controller to be added in API, then how to do that. – Vikram Jain Dec 02 '21 at 13:00
  • @VikramkumarChhajer Yes: This approach works on methods too - just put the annotations on the method. Annotating the class is a convenience for applying to all methods. – Bohemian Dec 02 '21 at 18:36
  • @Bohemian, I have tried putting CondtionalOnProperty on the method of a controller class, but it was still accessible through swagger. So the reason I am looking for this is I have multiple endpoints enabled in a controller layer. I want to add another endpoint but would like to hide it for customers. – Vikram Jain Dec 06 '21 at 06:54
  • @VikramkumarChhajer swagger showing an endpoint has nothing to do with it being up or down - swagger works though static code analysis and has no idea of what properties are set. what happens if you try to hit the end point? I think you’ll find it isn’t there. – Bohemian Dec 06 '21 at 06:57
  • API is working fine even i added ConditionOnProperty annotation on method – Vikram Jain Dec 06 '21 at 11:01
  • @VikramkumarChhajer what is the name of the property (the parameter of `ConditionalOnProperty`)? What is the property’s value? Browse `/actuator/env` to confirm property value. – Bohemian Dec 06 '21 at 14:49
  • Property name is "feature.greeting" and value is false in application.properties while ConditionalOnProperty expects it to be true. – Vikram Jain Dec 06 '21 at 15:56
3

Adding to this question and another question here.

This is my answer:

I would actually used the @RefreshScope Bean and then when you want to stop the Rest Controller at runtime, you only need to change the property of said controller to false.

SO's link referencing to changing property at runtime.

Here are my snippets of working code:

@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
    }
}

And there are other alternatives of using 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)
    }
}

My Github explaining how to disable at runtime

2

I assume this question comes from the fact that you are using different application.properties files for your different enviroments. In this case you can use spring profiles and separate configurations into different files with profile name suffix for example:

application.properties:

spring.profiles.active=@activatedProperties@

application-local.properties:

 //some config

application-prod.properties:

//some config

then in your build paramethers you can specify which enviroment are you building by adding option:

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

then in your application you can enable/disable any spring bean by adding

@Profile("put here profile name")

for example:

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

//do some stuff

}

now my RestForTesting will be created only if im running a build created with

-Dspring.profiles.active=local

Akka Jaworek
  • 1,970
  • 4
  • 21
  • 47
  • 1
    No. This question has nothing to do with profiles, which is but one of many ways to manage properties. Rather, I wanted to deploy an endpoint to only non-production environments - I couldn’t have the endpoint exist in any form in production. – Bohemian Jan 17 '19 at 16:20
  • 2
    I've tried that before, adding a `@Profile` annotation to a controller does nothing. – Joseph Tinoco Feb 01 '19 at 14:06
2

In some case, the @ConditionalOnXXX cannot work, for example, depends on another bean instance to check condition. (XXXCondition class cannot invoke a bean).

In such case, register controller in Java configuration file.

See source code(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));
       }

Should add @RequestMapping annotation on type level for the controller bean. See:

@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;
    }
}
larsw
  • 3,790
  • 2
  • 25
  • 37
wangf
  • 895
  • 9
  • 12