The Danger of magic


Code magic is incredibly helpful for developers—it simplifies tasks and often just works. However, it can sometimes lead to dangerous issues that many developers may not be aware of. In this article, I will demonstrate how a seemingly small change can break a perfectly functioning application and introduce a security risk.

As lead developers, one of our objectives is to create an application that handles a traceId. A traceId is a unique identifier that persists throughout a request. It helps to separate log messages and store session data in a database. Importantly, this traceId should change with each request to ensure that requests don’t interfere with each other.

We’ll start by implementing this functionality in a simple Spring Boot application. Here’s how we can do it.

We’ll start by defining a basic Spring Boot application. This is the entry point of our app:

@SpringBootApplication
public class TraceAppApplication {

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

Next, we need the TraceContext class, which stores the unique traceId for each request. If the traceId isn’t initialized, a new random number will be generated. This is a simple class:

public class TraceContext {

    private Integer traceId;
    
    public int getTraceId() {
        if (traceId == null) {
            traceId = new Random().nextInt();
        }

        return traceId;
    }
}

To ensure the trace context is created for every request, we define a configuration class:

@Configuration
public class TraceAppConfig {

    @Bean
    @RequestScope
    TraceContext traceContext() {
        return new TraceContext();
    }
}

Finally, let’s create a REST controller to display the traceId to the user:

@org.springframework.web.bind.annotation.RestController
public class RestController {

    private final TraceContext traceContext;

    public RestController(TraceContext traceContext) {
        this.traceContext = traceContext;
    }

    @GetMapping("/")
    public String getTrace() {
        return "TraceId: " + traceContext.getTraceId();
    }

}

Let the Magic Begin

With just a few lines of code, we have a fully functional web application that generates a unique traceId for every request:

$ curl http://localhost:8080
TraceId: -1144476760
$ curl http://localhost:8080
TraceId: 1205007174
$ curl http://localhost:8080
TraceId: -1133329310

Imagine you build upon this foundation in a larger application where requests generate multiple lines of log data. Thanks to the traceId, you can separate logs and link them to individual requests, making troubleshooting much easier:

19:10:01 INFO  1827893 Buy article #23
19:10:10 INFO 1827893 Send order to payment system
19:10:01 INFO 3398001 Buy article #754
19:10:10 INFO 3398001 Send order to payment system
19:11:23 INFO 1827893 Order successful
19:11:54 ERROR 3398001 Order failed

Here, we can clearly see that the order for article #754 failed.

Lets improve it

While everything seems to be working fine, we realize there’s room for improvement. The getTraceId() method can be marked as final because it doesn’t need to be overridden. Let’s make this change and push it to production:

public class TraceContext {

    private Integer traceId;
    
    public final int getTraceId() {
        if (traceId == null) {
            traceId = new Random().nextInt();
        }

        return traceId;
    }
}

After this simple change, we notice something unexpected. The traceId no longer changes with each request – it’s always the same.

$ curl http://localhost:8080
TraceId: -211272353
$ curl http://localhost:8080
TraceId: -211272353
$ curl http://localhost:8080
TraceId: -211272353

What happened here? To answer that, we need to understand the magic happening behind the scenes in Spring’s dependency injection. Here lies the danger: developers often don’t fully understand the underlying mechanics of the frameworks they use. Spring makes many things work automatically, but this convenience can sometimes lead to issues.. There is a reason why it is among the most used frameworks in the world. But sometimes it doesn’t work, and that’s why a developer should know the pitfalls.

Understanding the Issue

The problem lies in how Spring handles bean scopes and method interception. Spring uses dynamic proxies or CGLIB to manage beans. If a class has an interface, Spring will use a dynamic proxy to forward method calls. If there’s no interface, it uses CGLIB, which extends the class and overrides its methods, as described in the documentation.

However, if a method is marked final, CGLIB cannot override it, and it will simply use the original method implementation. In our case, marking getTraceId() as final prevents Spring from creating a proper call to the request-scoped class for the TraceContext. Instead, every request shares the same instance of TraceContext from the CGLIB proxy, resulting in the same traceId being returned for all requests.

The Lesson: Understanding Frameworks

This is a classic example of why developers should take the time to understand how frameworks like Spring work under the hood. It’s easy to rely on „magic,“ but when something breaks, you’ll need to understand what’s happening at a deeper level.

I recommend spending a few hours each month exploring the frameworks you’re using. You might uncover useful features you didn’t know existed, or you might find potential pitfalls, just like in this example.

Remember: there is no real magic in coding – just well-understood principles that, when used properly, make our lives easier.