Stackify is now BMC. Read theBlog

How Spring Boot Can Level Up your Spring Application

By: Eugen
  |  March 11, 2024
How Spring Boot Can Level Up your Spring Application

The Spring Ecosystem

There are a two stable, mature stacks for building web applications in the Java ecosystem, and considering the popularity and strong adoption, the Spring Framework is certainly the primary solution.

Spring offers a quite powerful way to build a web app, with support for dependency injection, transaction management, polyglot persistence, application security, first-hand REST API support, an MVC framework and a lot more.

Traditionally, Spring applications have always required significant configuration and, for that reason, can sometimes build up a lot of complexity during development. That’s where Spring Boot comes in.

The Spring Boot project aims to make building web application with Spring much faster and easier. The guiding principle of Boot is convention over configuration.

Let’s have a look at some of the important features in Boot:

  • starter modules for simplifying dependency configuration
  • auto-configuration whenever possible
  • embedded, built-in Tomcat, Jetty or Undertow
  • stand-alone Spring applications
  • production-ready features such as metrics, health checks, and externalized configuration
  • no requirement for XML configuration

In the following sections, we’re going to take a closer look at the necessary steps to create a Boot application and highlight some of the features in the new framework in more detail.

Spring Boot Starters

Simply put, starters are dependency descriptors that reference a list of libraries.

To create a Spring Boot application, you first need to configure the spring-boot-starter-parent artifact in the parent section of the pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
    <relativePath />
</parent>

This way, you only need to specify the dependency version once for the parent. The value is then used to determine versions for most other dependencies – such as Spring Boot starters, Spring projects or common third-party libraries.

The advantage of this approach is that it eliminates potential errors related to incompatible library versions. When you need to update the Boot version, you only need to change a single, central version, and everything else gets implicitly updated.

Also note that there are more than 30 Spring Boot starters available, and the community is building more every day.

A good starting point is creating a basic web application. To get started, you can simply add the web starter to your pom:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

If you want to enable Spring Data JPA for database access, you can add the JPA starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Notice how we’re no longer specifying the version for either of these dependencies.

Before we dive into some of the functionality in the framework, let’s have a look at another way we can bootstrap a project quickly.

Spring Boot Initializr

Spring Boot is all about simplicity and speed, and that starts with bootstrapping a new application.

You can achieve that by using the Spring Boot Initializr page to download a pre-configured Spring Boot project, which you can then import into your IDE.

The Initializr lets you select whether you want to create a Maven or Gradle project, the Boot version you want to use and of course the dependencies for the project:

Spring Initializr

 

You can also select the “Switch to the full version” option, you can configure a lot more advanced options as well.

Spring Boot Auto-Configuration

Spring applications usually require a fair amount of configuration to enable features such as Spring MVC, Spring Security or Spring JPA. This configuration can take the form of XML but also Java classes annotated with @Configuration.

Spring Boot aims to simplify this process by providing a sensible default configuration, based on the dependencies on the classpath and loaded automatically behind the scenes.

This auto-configuration contains @Configuration annotated classes, intended to be non-invasive and only take effect if you have not defined them explicitly yourself.

The approach is driven by the @Conditional annotation – which determines what auto-configured beans are enabled based on the dependencies on the classpath, existing beans, resources or System properties.

It’s important to understand that, as soon as you define your configuration beans, then these will take precedence over the auto-configured ones.

Coming back to our example, based on the starters added in the previous section, Spring Boot will create an MVC configuration and a JPA configuration.

To work with Spring Data JPA, we also need to set up a database. Luckily, Boot provides auto-configuration for three types of in-memory databases: H2, HSQL, and Apache Derby.

All you need to do is add one of the dependencies to the project, and an in-memory database will be ready for use:

<dependency>
    <groupId>com.h2database</groupId> 
    <artifactId>h2</artifactId>
</dependency>

The framework also auto-configures Hibernate as the default JPA provider.

If you want to replace part of the auto-configuration for H2, the defaults are smart enough to gradually step back and allow you to do that while still preserving the beans you’re not explicitly defining yourself.

For example, if you want to add initial data to the database, you can create files with standard names such as schema.sql, data.sql or import.sql to be picked up automatically by Spring Boot auto-configuration, or you can define your DataSource bean to load a custom named SQL script manually:

@Configuration
public class PersistenceConfig {

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2)
          .addScript("mySchema.sql")
          .addScript("myData.sql")
          .build();
        return db;
    }
}

This has the effect of overriding the auto-configured DataSource bean, but not the rest of the default beans that make up the configuration of the persistence layer.

Before moving on, note that it’s also possible to define an entirely new custom auto-configuration that can then be reused in other projects as well.

The Entry Point in a Boot Application

The entry point for a Spring Boot application is the main class annotated with @SpringBootApplication:

@SpringBootApplication
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.class, args);
    }
}

This is all we need to have a running Boot application.

The shortcut @SpringBootApplication annotation is equivalent to using @Configuration, @EnableAutoConfiguration, and @ComponentScan and will pick up all config classes in or bellow the package where the class is defined.

Embedded Web Server

Out of the box, Spring Boot launches an embedded web server when you run your application.

If you use a Maven build, this will create a JAR that contains all the dependencies and the web server. This way, you can run the application by using only the JAR file, without the need for any extra setup or web server configuration.

By default, Spring Boot uses an embedded Apache Tomcat 7 server. You can change the version by specifying the tomcat.version property in your pom.xml:

<properties>
    <tomcat.version>8.0.43</tomcat.version>
</properties>

Not surprisingly, the other supported embedded servers are Jetty and Undertow. To use either of these, you first need to exclude the Tomcat starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Then, add the Jetty or the Undertow starters:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

Advanced Externalized Configuration

Another super convenient feature in Boot is the ability to easily configure the behavior of an application via external properties files, YAML files, environment variables and command-line arguments. These properties have standard names that will be automatically picked up by Boot and evaluated in a set order.

The advantage of this feature is that we get to run the same deployable-unit/application in different environments.

For example, you can use the application.properties file to configure an application’s port, context path, and logging level:

server.port=8081
server.contextPath=/springbootapp
logging.level.org.springframework.web: DEBUG

This can be a significant simplification in more traditional environments but is a must in virtualized and container environments such as Docker.

Of course, ready-to-go deployable units are a great first step, but the confidence you have in your deployment process is very much dependent on both the tooling you have around that process but also the practices within your organization.

Metrics

Beyond project setup improvements and operational features, Boot also brings in some highly useful functional features, such as internal metrics and health checks – all enabled via actuators.

To start using the actuators in the framework, you need to add only a single dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

The relevant information is available via endpoints that can be accessed out-of-the-box: /metrics and /health.

We also get access to other endpoints such as: /info which displays application information and /trace that shows the last few HTTP requests coming into the system.

Here are just some of the types of metrics we get access to by default:

  • system-level metrics – total system memory, free system memory, class load information, system uptime
  • DataSource metrics – for each DataSource defined in your application, you can check the number of active connections and the current usage of the connection pool
  • cache metrics – for each specified cache, you can view the size of the cache and the hit and miss ratio
  • Tomcat session metrics – the number of active and maximum sessions

You can also measure and track your own metrics, customize the default endpoints as well as add your own, entirely new endpoint.

Now, tracking and exposing metrics is quite useful until you get to production, but of course, once you do get to production, you need a more mature solution that’s able to go beyond simply displaying current metrics. That’s where Retrace is a natural next step to help you drill down into the details of the application runtime, but also keep track of this data over time.

Health Checks

One of the primary and most useful endpoints is, not surprisingly, /health. 

This will expose different information depending on the accessing user and on whether the enclosing application is secured.

By default, when accessed without authentication, the endpoint will only indicate whether the application is up or down. But, beyond the simple up or down status, the state of different components in the system can be displayed as well – such as the disk or database or other configured components like a mail server.

The point where /health goes beyond just useful is with the option to create your custom health indicator.

Let’s roll out a simple enhancement to the endpoint:

@Component
public class HealthCheck implements HealthIndicator {
  
    @Override
    public Health health() {
        int errorCode = check(); // perform some specific health check
        if (errorCode != 0) {
            return Health.down()
              .withDetail("Error Code", errorCode).build();
        }
        return Health.up().build();
    }
     
    public int check() {
        // Your logic to check health
        return 0;
    }
}

As you can see, this allows you to use your internal system checks and make those a part of /health.

For example, a standard check here would be to do a quick persistence-level read operation to ensure everything’s running and responding as expected.

Similarly to metrics, as you move towards production, you’ll definitely need a proper monitoring solution to keep track of the state of the application. Within Retrace, the People Metrics feature is a simple way you can define and watch these custom metrics.

A powerful step forward from just publishing metrics or health info on request is the more advanced Key Transactions feature in Retrace – which can be configured to actively monitor specific operations in the system and notify you when the metrics associated with that operation become problematic.

Example Application

After setting up the project, you can simply start creating controllers or customizing the configuration.

Let’s create a simple application that manages a list of employees.

First, let’s add an Employee entity and repository based on Spring Data:

@Entity
public class Employee {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long id;

    private String name;
    
    // standard constructor, getters, setters
}
public interface EmployeeRepository extends JpaRepository<Employee, Long>{ }

Let’s now create a controller to manipulate employee entities:

@RestController
public class EmployeeController {

    private EmployeeRepository employeeRepository;
    
    public EmployeeController(EmployeeRepository employeeRepository){
        this.employeeRepository = employeeRepository;
    }
    @PostMapping("/employees")
    @ResponseStatus(HttpStatus.CREATED)
    public void addEmployee(@RequestBody Employee employee){
        employeeRepository.save(employee);
    }
    
    @GetMapping("/employees")
    public List<Employee> getEmployees(){
        return employeeRepository.findAll();
    }
}

You also need to create the mySchema.sql and myData.sql files:

create table employee(id int identity primary key, name varchar(30));
insert into employee(name) values ('ana');

To avoid Spring Boot recreating the employee table and removing the data, you need to set the ddl-auto Hibernate property to update:

spring.jpa.hibernate.ddl-auto=update

Testing the Application

Spring Boot also provides excellent support for testing; all included in the test starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

This starter automatically adds commonly used dependencies for testing in Spring such as Spring Test, JUnit, Hamcrest, and Mockito.

As a result, you can create a test for the controller mappings, by using the @SpringBootTest annotation with the configuration classes as parameters.

Let’s add a JUnit test that creates an Employee record, then retrieves all the employees in the database and verifies that both the original record added and the one just created are present:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@WebAppConfiguration
public class EmployeeControllerTest {
    
    private static final String CONTENT_TYPE 
      = "application/json;charset=UTF-8";
    
    private MockMvc mockMvc;
    
    @Autowired
    private WebApplicationContext webApplicationContext;
    
    @Before
    public void setup() throws Exception {
         this.mockMvc = MockMvcBuilders
           .webAppContextSetup(webApplicationContext)
           .build();
    }
    
    @Test
    public void whenCreateEmployee_thenOk() throws Exception {
        String employeeJson = "{\"name\":\"john\"}";

        this.mockMvc.perform(post("/employees")
          .contentType(CONTENT_TYPE)
          .content(employeeJson))
          .andExpect(status().isCreated());
        
        this.mockMvc.perform(get("/employees"))
          .andExpect(status().isOk())
          .andExpect(content().contentType(CONTENT_TYPE))
          .andExpect(jsonPath("$", hasSize(2)))
          .andExpect(jsonPath("$[0].name", is("ana")))
          .andExpect(jsonPath("$[1].name", is("john")));      
    }
}

Simply put, @SpringBootTest allows us to run integration tests with Spring Boot. It uses the SpringBootContextLoader as the default ContextLoader and automatically searches for a @SpringBootConfiguration class if no specific classes or nested configuration are defined.

We also get a lot of additional and interesting support for testing:

  • @DataJpaTest annotation for running integration tests on the persistence layer
  • @WebMvcTest which configures the Spring MVC infrastructure for a test
  • @MockBean which can provide a mock implementation for a required dependency
  • @TestPropertySource used to set locations of property files specific to the test

Conclusions

Ever since Spring sidelined XML configuration and introduced its Java support, the core team has had simplicity and speed of development as primary goals. Boot was the next natural step in that direction, and it has certainly achieved this goal.

The adoption of Boot has been astounding over the last couple of years, and a 2.0 release will only accelerate that trend going forward.

And a large part of that success is the positive reaction of the community to the production-grade features that we explored here. Features that were traditionally built from the ground up by individual teams are now simply available by including a Boot starter. That is not only very useful, but also very cool.

The full source code of all the examples in the article is available here, as a ready to run Boot project.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]