Spring Core in Action

The first thing we do in a new application is to give control of creating our services to the Spring Container. The Container initializes the services and also injects dependencies, i.e.- our SimpleSpamDetector into the control flow.

A service just declares that it needs a service that implements the SpamDetector interface and the Spring Container provides it. The only requirement now is that everything must be supervised and controlled by Spring.

In the center of Spring is the Spring Container also referred to as context or ApplicationContext (the interface). It knows every registered class and how they connect to each other.

A class made available in the context is called a bean. Beans are usually plain old Java objects (POJO) and often implement certain interfaces. In the context, each bean is identified by its id (aka name) and its type (the class). The identifier is unique across the context, and if you provide it multiple times, a previously defined one will be replaced by the new one. By default, all beans in the context are handled as singletons. You can change that, but we do not cover it in the course (search for scope and prototype).

Before we switch to Spring now, let’s first move the control flow from the main method into its own class like:

public class ControlFlow {

    public void run(String[] args)
    throws IOException {
        SpamDetector spamDetector 
          = SpamDetectorFactory.getInstance(args);
        System.out.println(
          spamDetector.containsSpam(args[0])
        );
    }
}

And the main application is reduced to:

public class SpamCheckerApplication {

    public static void main(String[] args)
    throws Exception {
        new ControlFlow().run(args);
    }

}

Now, we are ready to use Spring.

First, we add the dependency to our project:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.3.10.RELEASE</version>
</dependency>

Next, we create the context.

public class SpamCheckerApplication {

    public static void main(String[] args)
    throws Exception {
        ApplicationContext context 
          = new AnnotationConfigApplicationContext(
              SpamAppConfig.class
            );
        context.getBean(
          ControlFlow.class
        ).run(args);
    }

}

The context is created by the line:

ApplicationContext context 
  = new AnnotationConfigApplicationContext(
      SpamAppConfig.class
    );

AnnotationConfigApplicationContext will start the Spring Container and load the config from a class named SpamAppConfig. SpamAppConfig is a regular POJO with a particular annotation on it, so Spring accepts it as a configuration and populates the context. More on that in a minute.

The ApplicationContext comes in two styles. The first one is by defining all our beans in an XML document following various Spring XML schema and the second one is to use the Java based configuration with annotations. We will use the latter as it is the norm now and will spare you some brain damage by not using XML.

To mark our SpamAppConfig as a configuration source, we annotate it with @Configuration. In it, we define our beans:

@Configuration
public class SpamAppConfig {

    @Bean
    public SpamDetector simpleSpamDetector(
      @Value("${sbb.spamwords.filename}")
      String filename
    ) throws IOException {
        List<String> spamWords 
          = new ArrayList<String>();
        spamWords = Files.readAllLines(
          Paths.get(filename)
        );
        return new SimpleSpamDetector(
          spamWords
        );
    }

    @Bean
    public ControlFlow controlFlow(
      SpamDetector spamDetector
    ) {
        return new ControlFlow(
          spamDetector
        );
    }
}

In a @Configuration, we provide the beans by adding methods creating an instance and annotate these with the @Bean annotation.

Those methods can have parameters, and Spring tries to resolve these by other provided beans in the context. In this case, it will expect to have one and exact one bean in the context. It is a shortcut for the @Autowired annotation; more on that soon. Spring will load the beans in order to resolve dependencies. It will use the method name as the bean identifier in the context, except we declare a different one with the @Bean annotation like:

@Bean("myBean")

Or:

@Bean(name="myBean")

For referencing the filename, we use the @Value annotation. Here you can define an expression in the Spring Expression Language (SpEL) how to resolve the value. In the case above, we look up the value in a property source by the key sbb.spamwords.filename. This property source is populated by provided property files and the system environment. For this example, you can provide it at runtime with a JVM parameter like:

-Dsbb.spamwords.filename=words.spam

Now, we have populated the context and can use our ControlFlow in the main application by:

context.getBean(ControlFlow.class).run(args);

getBean will look up a bean in the context of type ControlFlow and return it. It expects to find exactly one match, as in our case now and throws exceptions if not.

On the return ControlFlow, we just call our run method as before.

As you see, with just a few annotations we replaced the factory with a solution we can reuse. Plus, it is more flexible in adding common features like transaction handling. As everything is connected by Spring, it can plug in particular features during runtime. It is possible because Spring creates a proxy for our class. This proxy enables Spring to add features without the need that we must change our classes. It is transparent for us.

However, the proxy has its limits. When a class calls a method on itself, it is not going through the proxy. It only goes through the proxy, when one service calls another service. Of course, both must be maintained by Spring.

Most of the time you will not notice this limit, but if you ever do, you can overcome it by using AspectJ during compile time. Just be aware, this way has its own pros and cons.

The usual way to enable a feature is to set up the module like Spring Data, and then you can use it through various annotations. And with Spring Boot it will become even easier to enable a new feature.

In the next section we are making the configuration even smaller and introducing common annotation you will encounter all the time. But before we continue, let’s recap the used annotations so far:

  • @Configuration marks a class as a config class
  • @Bean marks a method so that its return value is added as a bean to the context
  • @Autowired references a bean by type and it is expected to have exact one in the context. It can be omitted like in the version above.
  • @Value is a way to inject values hard coded or by using the Spring Expression Language (SpEL), the version we used above will look up the value in the environment

Complete and Continue  
Discussion

0 comments