I have a Spring project that runs locally during the development/debugging phase,
while on production it will be loaded on a PaaS.
My problem is that there are certain instruction that must be executed depending on the platform!
Currently I check a boolean (using #ConfigurationProperties) that I read from the application.properties, but I'm wondering if there's a smarter way because I have also to change the boolean when I push in production.
You should use Spring profiles and implement your check a little bit mor object oriented:
I assume your code looks something like this, and Logic is a spring managed bean:
#Component
public class Logic {
public void doIt() {
doMoreLogic();
if (yourProperty == true) {
your();
certain();
instructions();
}
doWhateverYouWant();
}
}
If you extract the certain logic to a class, then you can do it more the object oriented way:
public interface PlatformDependentLogic {
void platformInstructions();
}
#Component #Profile("dev")
public class DevLogic implements PlatformDependentLogic {
public void platformInstructions() {
your();
certain();
instructions();
}
}
#Component #Profile("!dev")
public class NoopLogic implements PlatformDependentLogic {
public void platformInstructions() {
// noop
}
}
Now you can reference the logic by doing this in your Logic bean:
#Component
public class Logic {
private #Autowired PlatformDependentLogic platformLogic;
public void doIt() {
doMoreLogic();
platformLogic.platformInstructions();
doWhateverYouWant();
}
}
Of course you can utilize the spring boot specific #ConditionalOnProperty instead of the #Profile annotation like this:
#ConditionalOnProperty(name="your.property", hasValue="dev")
To get a better understanding of this annotation and how it workds you should read the official documentation of #ConditionalOnProperty
May I suggest using Gradle product flavors for your local/Paas environments, something similar to this:
https://code.tutsplus.com/tutorials/using-gradle-build-variants--cms-25005
Related
I have a spring-boot app that now needs to support multiple Object stores and selectively use the desired store based on the environment. Essentially what i have done is create an interface that each store repository then implements.
I have simplified the code for the examples.
I have created 2 beans for each store type based on the spring profile determining the env:
#Profile("env1")
#Bean
public store1Sdk buildClientStore1() {
return new store1sdk();
}
#Profile("env2")
#Bean
public store2Sdk buildClientStore2() {
return new store2sdk();
}
in the service layer I have autowired the interface and then in the repositories i have used #Profile to specify which instance of the interface to use.
public interface ObjectStore {
String download(String fileObjectKey);
...
}
#Service
public class ObjectHandlerService {
#Autowired
private ObjectStore objectStore;
public String getObject(String fileObjectKey) {
return objectStore.download(fileObjectKey);
}
...
}
#Repository
#Profile("env1")
public class Store1Repository implements ObjectStore {
#Autowired
private Store1Sdk store1client;
public String download(String fileObjectKey) {
return store1client.getObject(storeName, fileObjectKey);
}
}
When I start the application with the configured "env" this actually runs as expected. however when running the test I get the "no qualifying bean of type ObjectStore. expected at least 1 bean which qualifies as autowire candidate."
#ExtendWith({ SpringExtension.class })
#SpringBootTest(classes = Application.class)
#ActiveProfiles("env1,test")
public class ComposerServiceTest {
#Autowired
private ObjectHandlerService service;
#Test
void download_success() {
String response = service.getObject("testKey");
...
}
}
As noted in the #ActiveProfile on the test class there are some other environments e.g. dev,test,prod. I have tried playing around with Component scan, having impl and interface in the same package, etc, to no success. I feel like I am missing something obvious with the test setup. But could be something with my overall application config? my main aim with the solution is to avoid having something a long the lines of
if (store1Sdk != null) {
store1Sdk.download(fileObjectKey);
}
if (store2Sdk != null) {
store2Sdk.download(fileObjectKey);
}
Try #ActiveProfiles({"env1", "test"}).
Activate multiple profiles using #ActiveProfiles and specify profiles as an array.
this probrom because Store1Repository use #Profile("env1"), when you use #test,this class not invoke. try delete #Profile("env1") of Store1Repository.
if you use #test, both of store1Sdk/store2Sdk don't instanse, try add default instanse.eg:
#Bean
public store2Sdk buildClientStoreDefault() {
return new store2sdk();
}
I am in the process of developing a common java library with reusable logic to interact with some AWS services, that will in turn be used by several consumer applications. For reasons outlined here, and the fact that Spring Boot seems to provide a lot of boilerplate free code for things like SQS integration, I have decided to implement this common library as a custom spring boot starter with auto configuration.
I am also completely new to the Spring framework and as a result, have run into a problem where my auto-configured class's instance variables are not getting initialized via the AutoWired annotation.
To better explain this, here is a very simplified version of my common dependency.
CommonCore.java
#Component
public class CommonCore {
#AutoWired
ReadProperties readProperties;
#AutoWired
SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging
public CommonCore() {
Properties props = readProperties.loadCoreProperties();
//initialize stuff
}
processEvents(){
// starts processing events from a kinesis stream.
}
}
ReadProperties.java
#Component
public class ReadProperties {
#Value("${some.property.from.application.properties}")
private String someProperty;
public Properties loadCoreProperties() {
Properties properties = new Properties();
properties.setProperty("some.property", someProperty);
return properties;
}
}
CoreAutoConfiguration.java
#Configuration
public class CommonCoreAutoConfiguration {
#Bean
public CommonCore getCommonCore() {
return new CommonCore();
}
}
The common dependency will be used by other applications like so:
#SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class SampleConsumerApp implements ApplicationRunner {
#Autowired
CommonCore commonCore;
public SampleConsumerApp() {
}
public static void main(String[] args) {
SpringApplication.run(SampleConsumerApp.class, args);
}
#Override
public void run(ApplicationArguments args) {
try {
commonCore.processEvents();
} catch (Exception e) {
e.printStackTrace();
}
}
}
The main problem I have like I mentioned, is the AutoWired objects in the CommonCore instance are not getting initialized as expected. However, I think the actual problems are more deeply rooted; but due to my lack of understanding of the Spring framework, I am finding it difficult to debug this on my own.
I am hoping for a few pointers along these points
Does this approach of developing a custom starter make sense for my use case?
What is the reason for the AutoWired dependencies to not get initialized with this approach?
Wild guess, but I think it's because of the order of how things are constructed. I am talking about this class:
#Component
public class CommonCore {
#AutoWired
ReadProperties readProperties;
#AutoWired
SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging
public CommonCore() {
Properties props = readProperties.loadCoreProperties();
//initialize stuff
}
processEvents(){
// starts processing events from a kinesis stream.
}
}
You are trying to use a Spring injected component in a constructor, but constructor is called before Spring can do its #Autowire magic.
So one option is to autowire as a constructor argument
Something like this (untested):
#Component
public class CommonCore {
private final ReadProperties readProperties;
private final SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging
#AutoWired
public CommonCore(SqsListener sqsListener, ReadProperties readProperties) {
this.readProperties = readPropertis;
this.sqsListener = sqsListener;
Properties props = readProperties.loadCoreProperties();
//initialize stuff
}
processEvents(){
// starts processing events from a kinesis stream.
}
}
Sidenote: I prefer to use dependency injection via constructor arguments always, wherever possible. This also makes unit testing a lot easier without any Spring specific testing libraries.
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?
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
}
}}
I have a Spring Boot application, using Spring 4, in this app I have an interface and an implementing class:
interface Sorter{
void start();
List<MailingListMessage> getMessages();
}
and
#Service
public class SorterImpl implements Sorter {
List<MailingListMessage> messages;
#Override
public void start() {
}
#Override
public List<MailingListMessage> getMessages() {
return null;
}
}
What I want to to is test the implementation directly but still as a bean with autowiring and everything.
I want to do that because when I need to assert something that for example relates to messages, I have to make the method public as you can see in the Impl class.
Is there a way in Spring to properly write tests for implementations directly and still have them work as beans?
Thanks.
I'm trying to write tests for an application that uses #RefreshScope. I would like to add a test that actually changes out properties and asserts that the application responds correctly. I have figured out how to trigger the refresh (autowiring in RefreshScope and calling refresh(...)), but I haven't figured out a way to modify the properties. If possible, I'd like to write directly to the properties source (rather than having to work with files), but I'm not sure where to look.
Update
Here's an example of what I'm looking for:
public class SomeClassWithAProperty {
#Value{"my.property"}
private String myProperty;
public String getMyProperty() { ... }
}
public class SomeOtherBean {
public SomeOtherBean(SomeClassWithAProperty classWithProp) { ... }
public String getGreeting() {
return "Hello " + classWithProp.getMyProperty() + "!";
}
}
#Configuration
public class ConfigClass {
#Bean
#RefreshScope
SomeClassWithAProperty someClassWithAProperty() { ...}
#Bean
SomeOtherBean someOtherBean() {
return new SomeOtherBean(someClassWithAProperty());
}
}
public class MyAppIT {
private static final DEFAULT_MY_PROP_VALUE = "World";
#Autowired
public SomeOtherBean otherBean;
#Autowired
public RefreshScope refreshScope;
#Test
public void testRefresh() {
assertEquals("Hello World!", otherBean.getGreeting());
[DO SOMETHING HERE TO CHANGE my.property TO "Mars"]
refreshScope.refreshAll();
assertEquals("Hello Mars!", otherBean.getGreeting());
}
}
You could do this (I assume you mistakenly omitted the JUnit annotations at the top of your sample, so I'll add them back for you):
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class MyAppIT {
#Autowired
public ConfigurableEnvironment environment;
#Autowired
public SomeOtherBean otherBean;
#Autowired
public RefreshScope refreshScope;
#Test
public void testRefresh() {
assertEquals("Hello World!", otherBean.getGreeting());
EnvironmentTestUtils.addEnvironment(environment, "my.property=Mars");
refreshScope.refreshAll();
assertEquals("Hello Mars!", otherBean.getGreeting());
}
}
But you aren't really testing your code, only the refresh scope features of Spring Cloud (which are already tested extensively for this kind of behaviour).
I'm pretty sure you could have got this from the existing tests for refresh scope as well.
Properties used in the application must be variables annotated with #Value. These variables must belong to a class that is managed by Spring, like in a class with the #Component annotation.
If you want to change the value of the properties file, you can set up different profiles and have various .properties files for each profile.
We should note that these files are meant to be static and loaded once, so changing them programmatically is sort of out of the scope of ther intended use. However, you could set up a simple REST endpoint in a spring boot app that modifies the file on the host's file system (most likely in the jar file you are deploying) and then calls Refresh on the original spring boot app.