0

I have three classes all of which needs to be enabled/disabled based on the @ConditionalOnExpression annotaion. All of these are in separate .java files.

EX:

@ConditionalOnExpression("#T(com.xxx.xxx.xxx.xxx.CxProperties).isAEnabled()}")
Class A{
}


@ConditionalOnExpression("#T(com.xxx.xxx.xxx.xxx.CxProperties).isBEnabled()}")
Class B{
}


@ConditionalOnExpression("#T(com.xxx.xxx.xxx.xxx.CxProperties).isCEnabled()}")
Class C{
}

Now i have an init funtion in a different class which gets executed first and i set the enable/disable value to the class CxProperties. Note: Assume all setters are static

class setvalues{

        public void init(){
             /*Read config values from a file*/
             CxProperties.setAEnabled(true/false);
             CxProperties.setBEnabled(true/false);
             CxProperties.setCEnabled(true/false);
        }
}

Now the evaluation of these conditions are happening at the beginning of the program (even before the execution of init) when the enable/disable is not set at all.

Is there any possible way in spring to order the evaluation of these conditions like say evaluate this after a certain point of execuation?

Any pointers are highly appreciated.

  • What about `@ConditionalOnProperty`? see https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-property-conditions – fateddy Jun 09 '17 at 16:49
  • I am not using any application.yml or property file. I read the config data from a customized file made within our project. So need to evaluate on a class object. But anyways, even in the case of @ConditionalOnProperty how do we delay/order the evaluation ? – Deepak Selvakumar Jun 09 '17 at 16:53
  • I believe it is not possible, at least in the way that you described. But it might be some workarounds. Can you explain what you are trying to achieve on the bigger scale? – Bohdan Levchenko Jun 09 '17 at 17:05
  • There are 3 kind of services which we offer as part of our project. Either of which can be enabled or disabled by the user from a customized UI which then creates a customized config file with these values. Our backend process is a daemon which will be restarted as soon as the config is changed. And when this process (the jar) is started the init method mentioned above has the logic to read the config file and it sets the values to CxProperties class. Now i want the three services (3 classes) to be conditionally enabled after the init method is called. – Deepak Selvakumar Jun 09 '17 at 17:16

1 Answers1

2

I would suggest you not to use @ConditionalOnExpression annotation for this.

Consider using @PreAuthorize instead. Yes, it's from spring-security.

With that you can protect each service from usage if it isn't enabled and dynamically toggle enabled/disabled state for it:

@SpringBootApplication
public class So44462763Application {

    public static void main(String[] args) {
        SpringApplication.run(So44462763Application.class, args);
    }

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)  // <-- this is required for PreAuthorize annotation to work
    public static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
        }
    }

    interface CxProperties {
        boolean isServiceAEnabled();
        boolean isServiceBEnabled();
        boolean isServiceCEnabled();

        boolean enableService(String service);
        boolean disableService(String service);
    }

    @Component("cx")
    public static class CxPropertiesImpl implements CxProperties {
        private static final ConcurrentHashMap<String, Boolean> services = new ConcurrentHashMap<>(); //could be database/redis/property file/etc

        @PostConstruct
        private void init() {
            //services.put("serviceA", true); //initial population from property file/network resource/whatever
        }

        public boolean isServiceAEnabled() {
            return services.getOrDefault("serviceA", false);
        }

        public boolean isServiceBEnabled() {
            return services.getOrDefault("serviceB", false);
        }

        public boolean isServiceCEnabled() {
            return services.getOrDefault("serviceC", false);
        }
        //just a sample how you can dynamically control availability for each service
        @Override
        public boolean enableService(String service) {
            services.put(service, true);
            return services.getOrDefault(service, false);
        }

        @Override
        public boolean disableService(String service) {
            services.put(service, false);
            return services.getOrDefault(service, false);
        }
    }

    interface BusinessService {
        String doSomething();
    }

    @Service("serviceA")
    @PreAuthorize("@cx.serviceAEnabled")
    public static class ServiceA implements BusinessService {

        @Override
        public String doSomething() {
            return this.getClass().getSimpleName() + " doing some work";
        }
    }

    @Service("serviceB")
    @PreAuthorize("@cx.serviceBEnabled")
    public static class ServiceB implements BusinessService {

        @Override
        public String doSomething() {
            return this.getClass().getSimpleName() + " doing some work";
        }
    }

    @Service("serviceC")
    @PreAuthorize("@cx.serviceCEnabled")
    public static class ServiceC implements BusinessService {

        @Override
        public String doSomething() {
            return this.getClass().getSimpleName() + " doing some work";
        }
    }

    @RestController
    @RequestMapping("/api/work")
    public static class WorkApi {
        private static final Logger log = LoggerFactory.getLogger(WorkApi.class);

        private final List<BusinessService> businessServices;

        @Autowired
        public WorkApi(final List<BusinessService> businessServices) {
            this.businessServices = businessServices;
        }

        @GetMapping
        public String doWork() {
            final StringJoiner joiner = new StringJoiner(",");
            for (BusinessService service : businessServices) {
                try {
                    joiner.add(service.doSomething());
                } catch (AccessDeniedException e) {
                    log.warn("Service {} is disabled.", service);
                }
            }
            return joiner.toString();
        }
    }

    @RestController
    @RequestMapping("/api/control")
    public static class ControlApi {

        private final CxProperties cxProperties;

        @Autowired
        public ControlApi(final CxProperties cxProperties) {
            this.cxProperties = cxProperties;
        }

        @PostMapping("{service}/enable")
        public boolean enable(@PathVariable("service") String serviceName) {
            return cxProperties.enableService(serviceName);
        }

        @PostMapping("{service}/disable")
        public boolean disable(@PathVariable("service") String serviceName) {
            return cxProperties.disableService(serviceName);
        }
    }
}

And here is a sample usage:

$ curl -u user:123 -XGET 'localhost:8080/api/work'
$ curl -u user:123 -XPOST 'localhost:8080/api/control/serviceC/enable'
true% 
$ curl -u user:123 -XGET 'localhost:8080/api/work'                    
ServiceC doing some work%
$ curl -u user:123 -XPOST 'localhost:8080/api/control/serviceA/enable'
true%
$ curl -u user:123 -XGET 'localhost:8080/api/work'                    
ServiceA doing some work,ServiceC doing some work%

Using this approach you can control accessibility of services even without restart.

And all of this can be done without spring-security as well but involves a little bit more of manual work and might slightly decrease overall readability of your code.

Bohdan Levchenko
  • 3,411
  • 2
  • 24
  • 28
  • The enable/disable config is read from a file (not yaml or properties) and at the start of the process. Its a daemon so once started it should read the value from the file and enable/disable the class and thats it It does not take other input after that in any form. – Deepak Selvakumar Jun 11 '17 at 10:59
  • toggling of services dynamically was a good thing to know though – Deepak Selvakumar Jun 11 '17 at 11:05
  • Is there any way to do it the other way around ? like calling the init method (Which reads the config file) even before the conditionalonexpression evaluation ???? – Deepak Selvakumar Jun 11 '17 at 11:14
  • Well, you can implement interface `ApplicationListener` in your CxProperties and then load properties in `public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent)` method. It is a latest possible state for spring app, this event occurs when entire application context gets fully initialized. – Bohdan Levchenko Jun 12 '17 at 06:59