practically, fundamentally and in detail. Part 2

Dependencies

The Spring-Boot starter framework we created was quite good for understanding the operation of general mechanisms and getting to know the basic structure. But the development of the component requires a revision of its architecture. We are reorganizing factories: now they will be divided not by types of objects (animals and places of detention), but by the profile of each inhabitant. This will allow you to more precisely define the various conditions for creating objects. Let's go in order.

Let's name each factory by the name of its inhabitant and transfer the creation of a pet and enclosures for it there:

@AutoConfiguration("napoleonFactory")
public class NapoleonFactory {

   @Bean
   public Napoleon napoleon() {
       return new Napoleon();
   }

   @Bean
   public Lawn lawn(Napoleon napoleon) {
       return new Lawn(napoleon);
   }
}

It turned out that there is a condition: Napoleon is constantly chewing something, and in order for him to exist normally in the space zoo, there must be a garden with various vegetables on its territory. Let's create a class Garden:

@RequiredArgsConstructor
public class Lawn {

   private final Napoleon napoleon;

   public static class Garden {
//code
   }
}

And add it to the factory:

@AutoConfiguration("napoleonFactory")
public class NapoleonFactory {

   @Bean
   public Napoleon napoleon() {
       System.out.println("Napoleon created");
       return new Napoleon();
   }

   @Bean
   public Lawn.Garden garden() {
       System.out.println("Garden created");
       return new Lawn.Garden();
   }

   @Bean
   public Lawn lawn(Napoleon napoleon) {
       System.out.println("Lawn created");
       return new Lawn(napoleon);
   }
}

Let's write and run a test to check:

@SpringBootTest(classes = NapoleonFactory.class)
class NapoleonFactoryTest {

   @Autowired
   private ApplicationContext context;

   @ParameterizedTest
   @MethodSource("beanNames")
   void applicationContextContainsBean(String beanName) {
       Assertions.assertTrue(context.containsBean(beanName));
   }

   private static Stream<String> beanNames() {
       return Stream.of(
               "napoleonFactory",
               "napoleon",
               "garden",
               "lawn"
       );
   }
}

All beans have been created, but there is a problem – the console output shows the order in which they were created:

Napoleon created
Garden created
Lawn created

Before creating Napoleon, how can we be sure that there is already a vegetable garden for him? An annotation can help here. @DependsOn. As it says javadoc annotations @DependsOnit takes as a parameter an array of names of components on whose operation our bean. It also guarantees that:

  • the required components will be created before initialization bean

  • when the application is terminated, the application will be terminated first beanand then its dependencies

Let's indicate that the object “napoleon” depends on the object “garden”which means it must be created after it:

@Bean
@DependsOn("garden")
public Napoleon napoleon() {
   System.out.println("Napoleon created");
   return new Napoleon();
}

Let's run the test and look at the output to the console:

Garden created
Napoleon created
Lawn created

Now everything is correct. The dependency condition ensures that the required beans are created. Otherwise, if there is no component in the context “garden“, the application will crash with a NoSuchBeanDefinitionException error.

Conditions

Standard Annotations

Main difference @ConditionOn from @DependsOn The point is that failure to comply with the conditions will not result in an application startup error. A bean of a certain class will simply not be created. Also, the conditions are more flexible in configuration and work with different types of parameters. Let's create a simple example.

@ConditionalOnProperty

One of the conditions for creating objects can be predefined settings. With their help, you can change the behavior of the application, including or excluding components depending on the values ​​of properties in the configuration file. This is useful for creating a stub object in case a component is missing from the infrastructure or an external service is not available.

Let's see how it works. Let's return to our example. It turned out that during children's excursions to the zoo, some children are afraid of the tiger rat – it looks too ferocious. So it was decided not to show it on children's excursion days. Let's add a setting to our zoo options that will allow us to turn the creation of this animal on and off. To file application.properties let's add a parameter:

app.tigrokris.create=false

Let's create a factory and apply this annotation there:

@Bean
@ConditionalOnProperty(
       value = "app.tigrokris.create",
       havingValue = "true",
       matchIfMissing = false
)
public Tigrokris tigrokris() {
   return new Tigrokris();
}

Parameter value = “app.tigrokris.create” reports the name of the required parameter. havingValue = “true” is the expected value for the condition to be met. matchIfMissing = false is a definition of behavior in the absence of this parameter.

@ConditionalOnBean and @ConditionalOnMissingBean

annotation @ConditionalOnBean dictates that a bean will be created only if there is a component of a certain type or name in the application context. Applying @ConditionalOnMissingBean, we get the opposite condition: the bean will be created if the specified object does not exist. These annotations are useful when creating configurations for different runtime environments, and also for default implementation of interfaces that can be overridden by users.

The previous example described a situation where, under certain conditions, the bin class Tigrokris will not be created. But now the application may crash with an error – this object is required to create an enclosure. You can, of course, add here @ConditionalOnProperty with the same condition, but a more correct solution would be to focus on the presence of the desired bean in the context. That is, depending on the presence of a bin, a cage for a tiger rat will or will not be generated. This is what the entire factory looks like:

@AutoConfiguration("tigrokrisFactory")
public class TigrokrisFactory {

   @Bean
   @ConditionalOnProperty(
           value = "app.tigrokris.create",
           havingValue = "true",
           matchIfMissing = false
   )
   public Tigrokris tigrokris() {
       return new Tigrokris();
   }

   @Bean
   @ConditionalOnBean(name = "tigrokris")
   public ClosedEnclosure closedEnclosure(Tigrokris tigrokris) {
       return new ClosedEnclosure(tigrokris);
   }
}

Let's write and run the test:

@SpringBootTest(classes = TigrokrisFactory.class)
class TigrokrisFactoryTest {

   @Autowired
   private ApplicationContext context;

   @ParameterizedTest
   @MethodSource("beanNames")
   void applicationContextContainsBean(String beanName, boolean expected) {
       Assertions.assertEquals(expected, context.containsBean(beanName));
   }

   private static Stream<Arguments> beanNames() {
       return Stream.of(
               Arguments.of("tigrokrisFactory", true),
               Arguments.of("tigrokris", false),
               Arguments.of("closedEnclosure", false)
       );
   }
}

Let's immediately extend this condition to all enclosures.

@ConditionalOnResource

Checks the existence of the specified resource. This annotation can be used to define the logger that will be used in the application – depending on what settings file is located in the classpath (for example, logback.xml).

Let's apply this condition to Shusha. Shusha is a creative person. On moonlit nights he likes to write poetry. And there should always be a notepad nearby where he can write down what the muse told him. Let's create a file notebook.txt and place it in the resources folder. Factory for creating an object of type Shusha now it looks like this:

@AutoConfiguration("shushaFactory")
public class ShushaFactory {

   @Bean
   @ConditionalOnResource(resources = "classpath:notebook.txt")
   public Shusha shusha() {
       return new Shusha();
   }

   @Bean
   @ConditionalOnBean(name = "shusha")
   public Park park(Shusha shusha) {
       return new Park(shusha);
   }
}

Then let's add a test:

@SpringBootTest(classes = ShushaFactory.class)
class ShushaFactoryTest {

   @Autowired
   private ApplicationContext context;

   @ParameterizedTest
   @MethodSource("beanNames")
   void applicationContextContainsBean(String beanName) {
       Assertions.assertTrue(context.containsBean(beanName));
   }

   private static Stream<String> beanNames() {
       return Stream.of(
               "shushaFactory",
               "shusha",
               "park"
       );
   }
}

@ConditionalOnExpression

This annotation allows you to specify a condition as the result of evaluating a SpEL expression. For example, you can combine several configuration parameters, creating a stub object that will replace a necessary but unavailable in this environment web service with the data we need: “${app.web.stub.enabled} and ${app.web.mock.data}”.

Blue, like all reptiles, loves to bask on the sand in clear, warm weather. Let's add two parameters to the application settings:

app.sun.is-shining=true
app.weather=clear

Please note that one parameter has a boolean value, and the second has a string value. Now let's add calls to these parameters using Spring Expression Language:

@AutoConfiguration("siniiFactory")
public class SiniiFactory {

@Bean
@ConditionalOnExpression("${app.sun.is-shining} and '${app.weather}'.equals('clear')")
public Sinii sinii() {
   return new Sinii();
}

   @Bean
   @ConditionalOnBean(name = "sinii")
   public Swamp swamp(Sinii sinii) {
       return new Swamp(sinii);
   }
}

We have created all the conditions, and Blue should be happy. Next, let's check the creation of this object:

@SpringBootTest(classes = SiniiFactory.class)
class SiniiFactoryTest {

   @Autowired
   private ApplicationContext context;

   @ParameterizedTest
   @MethodSource("beanNames")
   void applicationContextContainsBean(String beanName) {
       Assertions.assertTrue(context.containsBean(beanName));
   }

   private static Stream<String> beanNames() {
       return Stream.of(
               "siniiFactory",
               "sinii",
               "swamp"
       );
   }
}

@ConditionalOnJava

An interesting annotation that allows you to adjust the created implementation according to the Java version. This condition is useful for maintaining backward compatibility.

Let's specify a special class that will be created when using java 1.7 and earlier versions. To do this we use the annotation @ConditionalOnJava specifying the java version and comparison rule in the parameters:

@ConditionalOnJava(value = JavaVersion.EIGHT, range = OLDER_THAN)
public class CosmoZooLegacy {

   @PostConstruct
   private void greeting() {
       System.out.println("Старая версия CosmoZoo");
   }
}

Don't forget to add it to spring.factories. And in the main class CosmoZoo we will need a condition @ConditionalOnMissingBean:

@ConditionalOnMissingBean(CosmoZooLegacy.class)
public class CosmoZoo {
	//code
}

Let's make changes to the test:

@SpringBootTest
class CosmoZooTest {

   @Autowired
   private ApplicationContext context;

   @ParameterizedTest
   @MethodSource("beanNames")
   void applicationContextContainsBean(String beanName, boolean expected) {
       Assertions.assertEquals(expected, context.containsBean(beanName));
   }

   private static Stream<Arguments> beanNames() {
       return Stream.of(
               Arguments.of("science.zoology.CosmoZoo", true),
               Arguments.of("science.zoology.CosmoZooLegacy", false)
       );
   }

   @SpringBootApplication
   public static class TestApplication {
       //no-op
   }
}

In addition to the annotations that we have reviewed, we can mention a few more that are ready to use out of the box. Let's go over them briefly:

  • @ConditionalOnClass And @ConditionalOnMissingClass — conditions that check the presence of the specified class in the classpath

  • @ConditionalOnSingleCandidate – this bean creation rule requires that only one bean of a certain type be available in the application context

  • @ConditionalOnWebApplication And @ConditionalOnNotWebApplication — help establish rules for creating an object depending on whether the web application is a program or not

There are several other annotations like @Conditional, proposed by the creators of Spring and ready to use. They can be found in the documentation.

Own conditions

Except ready conditions, you can create your own. With their help, we will set up conditions for robots caring for pets and the zoo. For example, a cleaning robot must maintain cleanliness throughout the day. Let's describe this condition – for this we will create a class that implements the interface Condition:

public class TimeCondition implements Condition {

   @Override
   public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
       String start = context.getEnvironment().getProperty("app.cleaning.start");
       String end = context.getEnvironment().getProperty("app.cleaning.end");

       int currentHour = LocalDateTime.now().getHour();
       return Integer.parseInt(start) <= currentHour && currentHour <= Integer.parseInt(end);
   }
}

Let's add to application.properties:

app.cleaning.start=10
app.cleaning.end=18

And of course, let’s create a nominal class for the cleaning robot itself:

public class Cleaner {

   public void doWork() {
       //code
   }
}

Let's add RobotFactory:

@AutoConfiguration("robotFactory")
public class RobotFactory {

   @Bean
   @Conditional(TimeCondition.class)
   public CleaningRobot cleaningRobot() {
       return new CleaningRobot();
   }
}

Implementing conditions via annotation

You can implement the condition more compactly by writing your own annotation. Let's add a robot magician who entertains visitors on weekends:

public class Magician {

   public void doWork() {
       //code
   }
}

Now let's add a new class that implements the Condition interface:

public class DayOfWeekCondition implements Condition {

   @Override
   public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
       int dayOfWeekNumber = LocalDateTime.now().get(DAY_OF_WEEK);

       return dayOfWeekNumber > 5;
   }
}

Let's move this condition into the annotation:

@Retention(RUNTIME)
@Conditional(DateCondition.class)
public @interface ConditionalOnDayOfWeek {
}

Let's add to the bean creation:

@Bean
@ConditionalOnDayOfWeek
public Magician magician() {
   return new Magician();
}

Connecting Multiple Conditions

AND

If you need to meet two or more conditions, just indicate them above the desired class:

@Bean
@Conditional(TimeCondition.class)
@ConditionalOnDayOfWeek
public MyBean myBean() {
 return new MyBean();
}

In this case, the object will be created when both conditions are met: it is a weekend and a time in the specified range. But if there are many conditions, it is more convenient to collect them into one. To do this, you need to create a new class, inherit it from AllNestedConditions and indicate in it all the necessary conditions:

public class TimeAndDayOfWeekConditions extends AllNestedConditions {

   public TimeAndDayOfWeekConditions() {
       super(ConfigurationPhase.REGISTER_BEAN);
   }

   @Conditional(TimeCondition.class)
   static class OnTimeCondition {
   }

   @ConditionalOnDayOfWeek
   static class OnDayOfWeekCondition {
   }
}

Then create a new annotation:

@Retention(RetentionPolicy.RUNTIME)
@Conditional(TimeAndDayOfWeekConditions.class)
public @interface ConditionalOnTimeAndDayOfWeek {
}

And apply it in our factory – let an automatic sweet shop operate during the daytime on weekends:

@Bean
@ConditionalOnTimeAndDayOfWeek
public CandyShop candyShop() {
   return new CandyShop();
}

OR

Similarly, you can describe several conditions connected by a logical “or”. To do this, create a class that extends the abstract class AnyNestedCondition:

public class TimeOrDayOfWeekConditions extends AnyNestedCondition {

   public TimeOrDayOfWeekConditions() {
       super(ConfigurationPhase.REGISTER_BEAN);
   }

   @Conditional(TimeCondition.class)
   static class OnTimeCondition {
   }

   @ConditionalOnDayOfWeek
   static class OnDayOfWeekCondition {
   }
}

The next steps are already familiar to you: you need to create an annotation, and then you can apply it inside the factory to any bean.

Obviously, if necessary, conditions can be complicated — for example, by including several “AND” conditions in one “OR”. AllNestedConditions and AnyNestedCondition can accept any conditions — both standard and self-developed. The options for adding conditions do not affect the result of the work — both a custom annotation and @Conditional with a parameter — a class implementing the Condition interface — are available.

ConfigurationPhase

Both classes, which combine nested class conditions, accept a ConfigurationPhase, enum parameter in their constructor. It allows you to choose one of two values:

  • PARSE_CONFIGURATION – used if the condition is applied over classes marked with the @Configuration annotation

  • REGISTER_BEAN – parameter used when describing a regular (not @Configuration) bean component

So we have mastered the skills of accurately configuring the context through various conditions for creating beans. Using conditional annotations allows you to create more flexible and customizable components that can adapt to different execution conditions. In addition, this allows you to avoid creating unnecessary beans that will not be used in a specific application configuration. If you have questions or want to share your experience, welcome to comment. I’ll read everything and definitely come back with feedback!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *