Looking for the Spring profile in a component class always returns null

Viewed 1444

For some context:

The component needs to do logic based on the current environment. Since there is no way to set the environment manually, a lot of the @Properties annotations won't work - setting the properties file is just not flexible enough for our needs. However, when I try to pull environment details into my component class, nothing seems to work! Examples make it look so easy but mine always returns null. Could I be missing something below the hood of Spring that would cause this? Here is what I've tried:

Using Resource, and it's logs:

@Component
@Slf4j
public class compClass {
    @Resource
    private Environment env;

    public void map(DtoObj dto){
        log.info("RUN PROFILE IS: {}", env.getActiveProfiles());
    }
}

java.lang.NullPointerException: null at com.demo.Com.map(CompClass.java:22) ~[classes/:na]

Using Autowire, and it's logs:

@Component
@Slf4j
public class CompClass {
    @Autowired
    private Environment env;

    public void map(DtoObj dto){
        log.info("RUN PROFILE IS: {}", env.getActiveProfiles());
    }
}

java.lang.NullPointerException: null at com.demo.Com.map(CompClass.java:22) ~[classes/:na]

@Value with environment and it's logs:

@Component
@Slf4j
public class CompClass {
    @Value("#{environment.activeProfiles}")
    private String[] profiles;

    public void map(DtoObj dto){
        log.info("RUN PROFILE IS: {}", profiles);
    }
}

RUN PROFILE IS: null

@Value with properties path and it's logs

@Component
@Slf4j
public class CompClass {
    @Value("${spring.profiles.active}")
    private String[] profiles;

    public void map(DtoObj dto){
        log.info("RUN PROFILE IS: {}", profiles);
    }
}

RUN PROFILE IS: null

here is what I can share of properties under java/main/resources:

spring:
  application:
    name: namnam
  profiles:
    active: dev

I am runing this on IntelliJ using this VM:

-Dspring.profiles.active=dev

I have my properties file in the java/main/resources directory, and we're able to use it's other properties in the code. What about this have I messed up?

2 Answers

First off, let me clarify something: I do not suggest placing @Configuration on your business objects (beans). classes annotated with @Configuration are meant to be a tool for definitions of other beans. They usually look like:

@Configuration
public class SampleConfiguration {
   @Bean
   public SomeBean someBean() {
      return new SomeBean();
   }

      @Bean
   public AnotherBean anotherBean() {
      return new AnotherBean();
   }
}

In this example, SomeBean and AnotherBean are "business" objects. This works as an alternative to putting @Service/@Component on these classes, and in any case you never should put @Configuration on them.

Now regarding the question and proposed solutions:

In a nutshell, you can inject the environment like this:

@Service
public class EnvChecker {


    private final Environment env;

    public EnvChecker(Environment env) {
        this.env = env;
    }

    @PostConstruct
    public void init() {
        System.out.println(env); // this is not null
    }
}

If you want to activate the profile, use --spring.profiles.active=dev as a "Program Argument" text field in intelliJ (-D won't work)

Although this answers the question, since its real project, let me express some thoughts that may influence the actual code you'll write at the end.

IMO: its not a good idea to make your business code (I mean at the level of coding) dependent on different environments, what if tomorrow you'll add yet another environment (test, qa, integration, production 1, production 2 - you name it). This logic will become unmaintainable.

To address this, spring traditionally used profiles.

Instead of using if-condition in the code depending on the injected environment you can create two beans and activate with different profiles:

Example:

You probably have something like this:

public class MyClass {
    private  Environment env; // lets assume it works right
    
    public void doSomething() {

      if(env.getActiveProfiles() contains "dev") { // its a pseudo code, not real java, but still...
         doFoo(); // some code
      }
      else {
        doBar(); // another piece of code
      }
    }    
}

So The first suggestion using profiles is:


@Service
@Profile("dev")
public class DevBean {
   public void doSomething() {
     doFoo();
   }
}


@Service
@Profile("prod")
public class ProdBean {
   public void doSomething() {
     doBar();
   }
}

This approach is better because the business code will be easier to read and maintain, on the other hand, there is still a code that depends on profile state. However there is another solution that I like even better and can recommend:

The idea is "treat" a profile as a series of business features that can be enabled or disabled. This makes sense because profiles by themselves usually denote the environment but the project is always comprised of business features.

So, lets assume that your code implements the feature X that deals with database connectivity (for the sake of example). You want to disable the database in the development environment and would like to work "in-memory".

So in the terms of feature that the system provides you can define the following:

feature.X.mode=database / in-memory

Then you can configure the dev profile with "in-memory" property and the default value (the rest of profiles) can be "database":

application-dev.yml:

feature:
  X: 
    mode: "in-memory"
application.yml:
feature:
  X: 
    mode: "database" # or you can even omit the definition, leaving the default to be the "database" 

Now the beans can be defined in a similar manner to the previous technique but now you won't depend anymore on the profile, and instead will depend on the availability of the business feature:

@Service
@ConditionalOnProperty(name = "feature.X.mode", havingValue="in-memory")
public class DevBean {
   public void doSomething() {
     doFoo();
   }
}


@Service
@ConditionalOnProperty(name = "feature.X.mode", havingValue="database", matchIfMissing=true) // the last parameter guarantees that the bean will be loaded even if there is no configuration at all
public class ProdBean {
   public void doSomething() {
     doBar();
   }
}

Now finally you don't have the "environment dependent code".

The last small tip is what if you have several beans like this (service/components). In this case you can define the "stereotype annotation" (@FeatureXService) or use Java Configuration (class annotated with @Configuration) and place that @ConditionalOnProperty annotation only once.

This piece of code is working fine with me I hope it will be useful to you.

@Configuration
public class CompClass {

private final Environment env;

public CompClass (Environment env) {
    this.env = env;
}

public void map(DtoObj dto){
    log.info("RUN PROFILE IS: {}", env.getActiveProfiles());
}

public void doSomethingBasedOnSpeceficProfile() {
 if (env.acceptsProfiles(Constants.SPRING_PROFILE_PRODUCTION)) {
    // do something if the profile is what you want
  } else {
        // do something else
  }
}}
Related