#ConditionalOnExpression enabling/disabling #RestController - java

I am trying to enable/disable controller depending on value in properties file.
My Controller looks like this:
#RestController
#ConditionalOnExpression("${properties.enabled}")
public class Controller{
public String getSomething() {
return "Something";
}
}
My properties file looks like this:
properties.enabled= false
And controller is always enabled (I can access method getSomething). I also tried combinations like this:
#ConditionalOnExpression("${properties.enabled:true}")
#ConditionalOnExpression("${properties.enabled}==true")
#ConditionalOnExpression("${properties.enabled}=='true'")
#ConditionalOnExpression("'${properties.enabled}'=='true'")
Edit:
Also tried different annotation:
#ConditionalOnProperty(prefix = "properties", name="enabled")

I finally found the problem. This Bean wasn't created by Spring but in WebConfiguration class so i had to also add annotation there
public class CommonWebConfiguration extends WebMvcConfigurationSupport {
#Bean
#ConditionalOnProperty(prefix="properties",name = "enabled")
public Controller controller() {
return new Controller ();
}
}
My Controller now looks like this:
#RestController
#ConditionalOnProperty(prefix="properties",name = "enabled")
public class Controller{
public String getSomething() {
return "Something";
}
}

Related

Testing conditional Bean creation with #ConditionalOnProperty and #FeignClient

I have a Rest Controller, a Service and a Feign Client being used inside the service.
Now, I need to conditionally create the controller bean based on an environment variable. I have been able to set up the configuration and it looks like it should work. So far so good. But I am having a hard time testing the conditional bean creation.
The REST Controller:
#RestController
#RequestMapping("/api/path")
public class AbcController {
private final AbcService abcService;
#Autowired
public AbcController(AbcService abcService) {
this.abcService = abcService;
}
...
}
The Service:
#Service
public class AbcService {
private final AbcClient abcClient;
#Autowired
public AbcService(AbcClient abcClient) {
this.abcClient = abcClient;
}
Feign Client:
#Component
#FeignClient(name = "abc", url = "${abc.url}", decode404 = true)
public interface AbcClient {
#RequestMapping(
value = "/match",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE
)
MatchResponse getSsoIds(RequestDto requestDto);
}
Configuration for conditionally loading the Controller class:
#Configuration
public class AbcConfiguration {
#Bean
#ConditionalOnProperty(
value="feature.enabled",
havingValue = "true",
matchIfMissing = true
)
public AbcController abcController(AbcService abcService) {
return new AbcController(abcService);
}
}
The Test class for testing the conditional bean creation:
public class AbcControllerTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AbcConfiguration.class));
#Test
public void loadsControllerBeanWhenPropertyIsEnabled() {
this.contextRunner
.withPropertyValues("feature.enabled=true")
.run(context -> context.assertThat().hasSingleBean(AbcController.class));
}
#Test
public void loadsControllerBeanWithoutDefinedProperty() {
this.contextRunner
.run(context -> context.assertThat().hasSingleBean(AbcController.class));
}
#Test
public void doesNotLoadControllerBeanWhenPropertyIsDisabled() {
this.contextRunner
.withPropertyValues("feature.enabled=false")
.run(context -> context.assertThat().doesNotHaveBean(AbcController.class));
}
}
I have tried to run the test but the first 2 tests always ends up complaining about missing or non-configurable beans. The last test runs as expected.
With the above test setup it complains about missing AbcService bean, which we can solve by adding
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = AbcService.class)
to the top of the test class, but then it complains No qualifying bean of type 'de.xxx.xxx.AbcClient' and we cannot create a bean of the feignClient AbcClient because it is an interface.
Help needed! How to make these tests pass?

How to create a spring object mid-method?

In the old pre spring version I had something like this:
public String jobRun(int projectID)
{
JobHelper jobHelper = new JobHelper(projectID);
jobHelper.this();
jobHelper.that();
}
How can I create the JobHelper object using spring? I made it a spring component, but it can't be global.
I guess I want something like this:
public String jobRun(int projectID)
{
#Autowired
#Scope("prototype")
JobHelper jobHelper;
jobHelper.this();
jobHelper.that();
}
However, that's not how spring works.
Thanks,
For that to work, you need to let Spring now that JobHelper is "Autowireable". It'd be like this for the class where you need JobHelper to do things:
public class YourClass{
#Autowired
JobHelper jobHelper;
public String jobRun(int projectID){
jobHelper.this();
jobHelper.that();
return "something";
}
}
And JobHelper itself would be like
#Component
#Scope("prototype")
public class JobHelper {
public void this(){}
public void that(){}
}
Injection is a broad topic, so I'd suggest you read guides/articles such like this on #Autowired or this on #Component from Baeldung for a start.

How to retrieve attribute from ControllerAdvice selector in ControllerAdvice class

I can define a Spring ControllerAdvice that is selectively used by a subset of controllers using a custom annotation:
#RestController
#UseAdviceA
#RequestMapping("/myapi")
class ApiController {
...
}
#ControllerAdvice(annotations = UseAdviceA.class)
class AdviceA {
...
}
But is it possible to pass in an attribute via the custom annotation where the advice class can pick up from the annotation? For e.g.:
#RestController
#UseAdviceA("my.value")
#RequestMapping("/myapi")
class ApiController {
...
}
#ControllerAdvice(annotations = UseAdviceA.class)
class AdviceA {
// Some way to get the string "myvalue" from the instance of UseAdviceA
...
}
Any other way to achieve the same outcome, which is to be able to define a custom configuration at the Controller method which can be passed to the ControllerAdvice would be much appreciated too.
Here is a solution.
Given
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface UseAdviceA {
public String myValue();
}
Controller
#RestController
#UseAdviceA(myValue = "ApiController")
#RequestMapping("/myapi")
class ApiController {
...
}
Your Controller Advice should be like
#ControllerAdvice(annotations = {UseAdviceA.class})
class AdviceA {
#ExceptionHandler({SomeException.class})
public ResponseEntity<String> handleSomeException(SomeException pe, HandlerMethod handlerMethod) {
String value = handlerMethod.getMethod().getDeclaringClass().getAnnotation(UseAdviceA.class).myValue();
//value will be ApiController
return new ResponseEntity<>("SomeString", HttpStatus.BAD_REQUEST);
}

Spring-Boot multi module project load property-file

I have a Spring-Boot-Application as a multimodule-Project in maven. The structure is as follows:
Parent-Project
|--MainApplication
|--Module1
|--ModuleN
In the MainApplication project there is the main() method class annotated with #SpringBootApplication and so on. This project has, as always, an application.properties file which is loaded automatically. So I can access the values with the #Value annotation
#Value("${myapp.api-key}")
private String apiKey;
Within my Module1 I want to use a properties file as well (called module1.properties), where the modules configuration is stored. This File will only be accessed and used in the module. But I cannot get it loaded. I tried it with #Configuration and #PropertySource but no luck.
#Configuration
#PropertySource(value = "classpath:module1.properties")
public class ConfigClass {
How can I load a properties file with Spring-Boot and access the values easily? Could not find a valid solution.
My Configuration
#Configuration
#PropertySource(value = "classpath:tmdb.properties")
public class TMDbConfig {
#Value("${moviedb.tmdb.api-key}")
private String apiKey;
public String getApiKey() {
return apiKey;
}
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Calling the Config
#Component
public class TMDbWarper {
#Autowired
private TMDbConfig tmdbConfig;
private TmdbApi tmdbApi;
public TMDbWarper(){
tmdbApi = new TmdbApi(tmdbConfig.getApiKey());
}
I'm getting an NullPointerException in the constructor when I autowire the warper.
For field injection:
Fields are injected right after construction of a bean, before any config methods are invoked. Such a config field does not have to be public. Refer Autowired annotation for complete usage. Use constructor injection in this case like below:
#Component
public class TMDbWarper {
private TMDbConfig tmdbConfig;
private TmdbApi tmdbApi;
#Autowired
public TMDbWarper(final TMDbConfig tmdbConfig){
this.tmdbConfig = tmdbConfig;
tmdbApi = new TmdbApi(tmdbConfig.getApiKey());
}
(or)
Use #PostConstruct to initialise like below:
#Component
public class TMDbWarper {
#Autowired
private TMDbConfig tmdbConfig;
private TmdbApi tmdbApi;
#PostConstruct
public void init() {
// any initialisation method
tmdbConfig.getConfig();
}
Autowiring is performed just after the creation of the object(after calling the constructor via reflection). So NullPointerException is expected in your constructor as tmdbConfig field would be null during invocation of constructor
You may fix this by using the #PostConstruct callback method as shown below:
#Component
public class TMDbWarper {
#Autowired
private TMDbConfig tmdbConfig;
private TmdbApi tmdbApi;
public TMDbWarper() {
}
#PostConstruct
public void init() {
tmdbApi = new TmdbApi(tmdbConfig.getApiKey());
}
public TmdbApi getTmdbApi() {
return this.tmdbApi;
}
}
Rest of your configuration seems correct to me.
Hope this helps.
Here is a Spring Boot multi-module example where you can get properties in different module.
Let's say I have main application module, dataparse-module, datasave-module.
StartApp.java in application module:
#SpringBootApplication
public class StartApp {
public static void main(String[] args) {
SpringApplication.run(StartApp.class, args);
}
}
Configuration in dataparse-module. ParseConfig.java:
#Configuration
public class ParseConfig {
#Bean
public XmlParseService xmlParseService() {
return new XmlParseService();
}
}
XmlParseService.java:
#Service
public class XmlParseService {...}
Configuration in datasave-module. SaveConfig.java:
#Configuration
#EnableConfigurationProperties(ServiceProperties.class)
#Import(ParseConfig.class)//get beans from dataparse-module - in this case XmlParseService
public class SaveConfig {
#Bean
public SaveXmlService saveXmlService() {
return new SaveXmlService();
}
}
ServiceProperties.java:
#ConfigurationProperties("datasave")
public class ServiceProperties {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
application.properties in datasave-module in resource/config folder:
datasave.message=Multi-module Maven project!
threads.xml.number=5
file.location.on.disk=D:\temp\registry
Then in datasave-module you can use all your properties either through #Value.
SaveXmlService.java:
#Service
public class SaveXmlService {
#Autowired
XmlParseService xmlParseService;
#Value("${file.location.on.disk: none}")
private String fileLocation;
#Value("${threads.xml.number: 3}")
private int numberOfXmlThreads;
...
}
Or through ServiceProperties:
Service.java:
#Component
public class Service {
#Autowired
ServiceProperties serviceProperties;
public String message() {
return serviceProperties.getMessage();
}
}
I had this situation before, I noticed that the properties file was not copied to the jar.
I made the following to get it working:
In the resources folder, I have created a unique package, then stored my application.properties file inside it. e.g: com/company/project
In the configuration file e.g: TMDBConfig.java I have referenced the full path of my .properties file:
#Configuration
#PropertySource("classpath:/com/company/project/application.properties")
public class AwsConfig
Build and run, it will work like magic.
You could autowire and use the Enviornment bean to read the property
#Configuration
#PropertySource(value = "classpath:tmdb.properties")
public class TMDbConfig {
#Autowired
private Environment env;
public String getApiKey() {
return env.getRequiredProperty("moviedb.tmdb.api-key");
}
}
This should guarantee that property is read from the context when you invoke the getApiKey() method regardless of when the #Value expression is resolved by PropertySourcesPlaceholderConfigurer.

Spring MVC multiple controllers of the same class

I want to have one controller class, but 4 instances of it, each of instance will have own datasource and controller path, everything else (methods, validations rules, views names) will be the same;
So i need something like this :
class MyController{
private MyService service;
#RequestMapping("somework")
public String handleRequest(){
........
}
....................
}
Configuration class :
#Configuration
#EnableWebMvc
public class AppConfiguration {
#Controller // assuming it exists to get the
#RequestMapping('con1') // desired result
MyController controller1(){
MyController con = new MyController();
con.setService(service1Bean);
return con;
}
#Controller // assuming it exists to get the
#RequestMapping('con2') // desired result
MyController controller2(){
MyController con = new MyController();
con.setService(service2Bean);
return con;
}
...............................
}
No, you can't do this.
First, annotations are a set in stone at compile time. They are constant meta data that you cannot modify. So even though, they are accessible at run time through reflection, you cannot modify them.
Second, the #Controller annotation call only be used to annotate types. You cannot use it on a method. There is no corresponding annotation in Spring MVC that does what you want in your example. (You could always write your own.)
Finally, the Spring MVC stack registers your #Controller beans' methods as handlers mapping them to the various URL patterns you provide. If it tries to register a pattern that has already been registered, it fails because duplicate mappings are not allowed.
Consider refactoring. Create a #Controller class for each path you want but move the logic to a #Service bean which you can customize to use whatever data source you need.
You may achieve what you want by implementing an abstract superclass of
your controller, with constructor parameters for your service.
Then you should write derive your controllers from the abstract superclass,
with a constructor, where you inject your concrete service implementation:
public abstract class MyBaseController {
private MyService service;
public MyBaseController(final MyService service) {
this.service = service;
}
...
#RequestMapping("method1")
public ... method1( ... ) {
...
}
}
#Controller
#RequestMapping("con1")
public MyController1 extends MyBaseController {
#Autowired
public MyController1(#Qualifier("con1") final MyService service) {
super(service);
}
}
#Controller
#RequestMapping("con2")
public MyController2 extends MyBaseController {
#Autowired
public MyController1(#Qualifier("con2") final MyService service) {
super(service);
}
}
#Configuration
public class MyConfiguration {
#Bean(name = "con1")
public MyService serviceCon1() {
return ...;
}
#Bean(name = "con2")
public MyService serviceCon2() {
return ...;
}
}

Categories

Resources