Sometimes we want slightly different behaviour from our application depending on the situation at runtime. For example, maybe we want one instance of our app to connect to a host in London and another connecting to a host in New York. Or maybe we want a QA instance that connects to a local QA host.
In this post we’ll look at using Spring profiles as a way of providing this customisation at runtime, allowing us to use the same compiled code in each case. Under the bonnet, Spring will use these profiles to wire in different beans for the different use cases and thereby provide different behaviour.
To get started let’s look at a nice and simple application where we specify the behaviour ourselves, without the benefit of the Spring framework or autowiring. Then we can go through and see how we can modify this to get an autowired class provided by Spring, based decisions made at runtime.
The main class below simply creates a new Connector
and then calls the connect
method on it.
package com.scottlogic.profilepoc;
public class ProfilePocApplication {
public static void main(String[] args) {
Connector connector = new LondonProdConnector();
System.out.println(connector.connect());
}
}
The class Connector
is an abstract class:
public abstract class Connector {
public abstract String connect();
}
and is implemented in a class called LondonProdConnector
:
public class LondonProdConnector extends Connector {
@Override
public String connect() {
return "I connect to a host in London";
}
}
As the class name would suggest, this implementation is designed to run in prod and connect to a host in London.
Autowiring the connector
Now let’s look at what we need to do to make this a Spring Boot Application and allow Spring to autowire our Connector
for us. Here’s how we need to change our application to do this:
@SpringBootApplication
public class ProfilePocApplication {
@Autowired
Connector connector;
public static void main(String[] args) {
ApplicationContext context = new SpringApplication(ProfilePocApplication.class).run(args);
ProfilePocApplication app = context.getBean(ProfilePocApplication.class);
app.start();
}
private void start() {
System.out.println(connector.connect());
}
}
We’ve added the @SpringBootApplication
annotation to the class and given it an @Autowired
connector. However, we can’t autowire into a static context, so we’ve also moved the connector
out of the main method and into an instance method named start
. What the main method now does is get an instance of our application (the bean of type ProfilePocApplication
) which Spring can autowire, and this autowired connector will be called in the start
method.
We’ll also need to mark our implementation of Connector
with the @Component
annotation, so that the class is available for Spring to autowire in.
@Component
public class LondonProdConnector extends Connector {
@Override
public String connect() {
return "I connect to a host in London";
}
}
Adding profiles
So far so good. But what happens if we have more than one implementation of Connector
? In this example we’re also going to have a NewYorkProdConnector
for handling a connection to New York and a QAConnector
that will connect to a QA host.
@Component
public class NewYorkProdConnector extends Connector {
@Override
public String connect() {
return "I connect to a host in New York.";
}
}
@Component
public class QAConnector extends Connector {
@Override
public String connect() {
return "I connect to a QA host.";
}
}
With these extra components, Spring won’t know which Connector
to wire in, and we’ll see this message if we try and run our application:
Field connector in com.scottlogic.profilepoc.ProfilePocApplication required a single bean, but 3 were found:
- londonProdConnector: defined in file [...LondonProdConnector.class]
- newYorkProdConnector: defined in file [...NewYorkProdConnector.class]
- QAConnector: defined in file [...QAConnector.class]
That’s where the Spring profiles come into it. As we shall see in a moment, when we run our application we can define one or more active profiles. Profiles are linked to the beans by putting the @Profile
annotation on them. We decide which profiles we want the bean to be used with and pass them into the annotation as parameters. The bean will be registered for use if those profiles are active.
Hopefully this will be clearer with an example. We’ll go ahead and register the LondonProdConnector
bean for use if the profiles ldn
and prod
are both active:
@Profile(value="ldn & prod")
@Component
public class LondonProdConnector extends Connector {
@Override
public String connect() {
return "I connect to a host in London";
}
}
And similarly for our other implementations:
@Profile(value="nyc & prod")
@Component
public class NewYorkProdConnector extends Connector {
...
}
@Profile(value="test")
@Component
public class QAConnector extends Connector {
...
}
Then, at runtime, we specify which profiles are active. If we want to use the LondonProdConnector
like we were before then we provide prod
and ldn
as active profiles, matching the parameters in the @Profile
annotation for that class:
java -jar application.jar --spring.profiles.active=prod,ldn
(There are other ways of specifying the active profiles. Later on in the post we’ll look at how we can set profiles as active by default without having to pass them in at runtime.)
Any beans that don’t have the @Profile
annotation are always registered and are available no matter which profiles are active (and also available if there are no active profiles at all). So if there are multiple implementations of a class, which is the case for our Connector
, then we need to have @Profile
annotations on all of them if we’re going to specify active profiles. Otherwise we’ll hit the same runtime issue as before where Spring did not know which bean to use.
Testing
We’ll also need to specify which profile we want to use in integration tests, if we want our components to be autowired in for us.
Two things are needed for autowiring our test class. First, we have to to give the test a context which can provide our beans. In this case, we the test will use the context of the main application class by passing this as an argument into the @SpringBootTest
annotation. Second, we need to specify which are the active profiles when we run the test. In this case just specifying the test
profile is enough since, unlike the prod beans, there is just one QAConnector
and not one for each region.
@SpringBootTest(classes = ProfilePocApplication.class)
@ActiveProfiles("test")
class ProfilePocApplicationTests {
@Autowired
Connector connector;
With the active profile specified in the annotation, the test will know to use the QAConnector
:
@Test
void qaActiveProfileLoadsCorrectBean() {
String actual = connector.connect();
String expected = "I connect to a QA host.";
assertEquals(expected, actual);
}
Providing some defaults
It might be that we don’t want to have to tell Spring which profile to use every time we run our application. Maybe as a developer it’s getting a bit tiresome adding the @ActiveProfile
annotation to all of our tests so we want them to use that profile by default. So let’s remove the @ActiveProfiles("test")
from our test class. As it currently stands the test will fail as it won’t be able to find a bean to provide for the autowired Connector
.
Happily, there’s a way to fallback to a set of default active profiles. If we’re using a maven or gradle build then we can create a properties file in the following location and the build process will automatically put it on the classpath when we run our tests:
src/test/resources/application.yml
(And we would use src/main
rather than src/test
if we wanted the file on the classpath for our main build.)
Inside this yaml file we can specify default spring profiles. For our tests to work we just need the test
profile to be active by default:
spring:
profiles:
default: test
Since we are no longer specifying an active profile through the annotation, the test will fallback to using the default test
profile and this in turn will wire in the QAConnector
. Of course, if we do provide an active profile that will be used to determine the beans instead.
A note on syntax
I’ll go into a quick aside to show some of the different options we have available for us when we use the @Profile
annotation to determine which profiles we want as active. The functionality can be quite powerful, although we haven’t needed it in our example application.
We can pass in a single profile, as we do with qa:
@Profile(value="qa")
or multiple profiles, as we do with the prod connectors:
@Profile(value="ldn & prod")
we can set an “or” condition:
@Profile(value="ldn | prod")
which can also be passed as as a comma separated list:
@Profile(value={"ldn", "prod"})
We can also use more complex expressions for the parameters, for example register a bean if either the profiles nyc
or ldn
are active as well as the prod
profile:
@Profile(value="(nyc | ldn) & prod")
and if we have more complex requirements its possible to use regex matching to register beans.
Summary
Spring profiles provide a handy way to specify at runtime which configuration of beans we want to use. When we run the app, we specify one or more profiles as “active” and allow Spring to select which beans to use based on annotations we’ve put on the beans themselves. This can be used to change the behaviour of the app without having to recompile or redeploy anything.
We’ve also seen that we can use the properties file to specify a default profile if we’re not passing one in at runtime and we’ve seen how to set the active profile on a test class in order to use different beans in the application’s integration tests.