Annotation-free Spring

Nicolas Fränkel - Sep 12 '21 - - Dev Community

Some, if not most, of our judgments regarding technology stacks come either from third-party opinions or previous experiences. Yet, we seem to be adamant about them. For a long time (and sometimes even now), I've seen posts that detailed how Spring is bad because it uses XML for its configuration. Unfortunately, they blissfully ignore the fact that annotation-based configuration has been available for ages. Probably because of the same reason I recently read that Spring is bad... because of annotations. If you belong to this crowd, I've news for you: you can get rid of most annotations, and even more so if you're using Kotlin. In this post, I'd like to show you how to remove annotations for different features that Spring provides.

Annotation-free beans

The first place where we tend to set annotations is to register beans. Let's see how to move away from them. It involves several steps. We shall start from the following code:

@Service
public class MyService {}
Enter fullscreen mode Exit fullscreen mode

The @Service stereotype annotation serves two functions:

  • It marks the MyService class as belonging to the service layer
  • It lets the framework know about the class so that it will instantiate a new object and make it available in the context

The first step is to move the annotation away from the class to a dedicated configuration class.

public class MyService {}

@Configuration
public class MyConfiguration {

    @Bean
    public MyService service() {
        return new MyService();
    }
}

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

Because @SpringBootApplication is itself annotated with @Configuration, we can simplify the code further:

public class MyService {}

@SpringBootApplication
public class MyApplication {

    @Bean
    public MyService service() {
        return new MyService();
    }

    // Run the app
}
Enter fullscreen mode Exit fullscreen mode

At this point, the MyService class is free of annotations. For me, that would be enough. However, my earlier promise was to remove annotations altogether.

For this, Kotlin offers the Beans DSL. You can refactor the above snippet like this:

class MyService

fun beans() = beans {
    bean<MyService>()                        // 1
}

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args) {
        addInitializers(beans())
    }
}

@SpringBootApplication                       // 2
class MyApplication
Enter fullscreen mode Exit fullscreen mode
  1. Create a new bean without annotation
  2. Single annotation to start the Spring Boot application; see below for how to remove it

Controllers to routes

Our next feature focuses on web endpoints. The traditional Spring way to provide them is via the @Controller annotation:

@Controller                                                        // 1
public class MyController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)  // 2
    @ResponseBody                                                  // 3
    public String hello() {
        return "Hello";
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Register the class as a controller
  2. Register the method as a request handler
  3. Return the result directly without involving a view

For REST controllers, like the snippet above, Spring makes it simpler by providing compound annotations. We can refactor the code as:

@RestController                                                    // 1
public class MyController {

    @GetMapping("/hello")                                          // 2
    public String hello() {
        return "Hello";
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Compound @Controller and @ResponseBody
  2. @RequestMapping with the method attribute set to GET

Refactoring doesn't fulfill the "no annotation" promise. Yet, since Spring Web MVC v5.0, the framework offers an alternative to controllers called routes. Let's use them to refactor the previous code:

@Bean
RouterFunction<ServerResponse> hello() {
    return route(GET("/hello"),
                 req -> ServerResponse.ok().body("Hello"));
}
Enter fullscreen mode Exit fullscreen mode

You could object that there's still one annotation - @Bean but we handled this case in the previous paragraph with the help of Kotlin. Spring also provides a dedicated DSL for routes. By using both the above Beans DSL and the Routes DSL, we can rid of all annotations:

bean {
    router {
        GET("/") { ok().body("Hello") }
    }
}
Enter fullscreen mode Exit fullscreen mode

Cross-cutting concerns

A lot (all?) of Spring cross-cutting concerns are configurable with annotations. Such concerns include transaction management and caching. In this paragraph, I'll use caching as an example, but all related features are similar.

@Cacheable("things")
public Thing getAddress(String key) {
    // Get the relevant Thing from the data store
}
Enter fullscreen mode Exit fullscreen mode

Spring wraps methods annotated with @Cacheable in a proxy. When you call the proxied method, it first checks whether the object is in the cache:

  1. If it is, it returns the cached entity, bypassing the datastore-fetching logic
  2. If not, it does call it and puts the value in the cache.

Nothing prevents you from eschewing annotations and implementing the above logic yourself.

public class ThingRepository {

    private final Cache cache;

    public ThingRepository(Cache cache) {
        this.cache = cache;
    }

    public Thing getAddress(String key) {
        var value = cache.get(key, Thing.class);
        if (value == null) {
            // Get Thing and return it
        }
        return value;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you're a Functional Programming fan, you can refactor the above code to something more suitable to your tastes:

public class ThingRepository {

    private final Cache cache;

    public ThingRepository(Cache cache) {
        this.cache = cache;
    }

    public Thing getAddress(String key) {
        return Optional.ofNullable(cache.get(key, Thing.class))
                       .orElse(/* Get Thing */);
    }
}
Enter fullscreen mode Exit fullscreen mode

Error handling

Spring provides a rich error handling mechanism to ease developers' life via annotations. It makes no sense to paraphrase the documentation as it's pretty well documented:

Here's an example of using @ExceptionHandler in a controller:

@RestController
public class MyController {

    private final MyService service;

    public MyController(MyService service) {
        this.service = service;
    }

    @GetMapping("/hello")
    public String hello() {
        service.hello();                                       // 1
    }

    @GetMapping("/world")
    public String world() {
        service.world();                                       // 1
    }

    @ErrorHandler
    public ResponseEntity<String> handle(ServiceException e) { // 2
        return ResponseEntity(e.getMessage(),
            HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. May throw an unchecked ServiceException
  2. Spring calls this method if a ServiceException class is thrown in one of the above methods

However, nothing prevents you from handling the error in your code. Here's how you can do it:

@RestController
public class MyController {

    private final MyService service;

    public MyController(MyService service) {
        this.service = service;
    }

    @GetMapping("/hello")
    public ResponseEntity<String> hello() {
        try {
            return ResponseEntity(service.hello(), HttpStatus.OK);
        } catch (ServiceException e) {
            return handle(e);
        }
    }

    @GetMapping("/world")
    public String world() {
        try {
            return ResponseEntity(service.world(), HttpStatus.OK);
        } catch (ServiceException e) {
            return handle(e);
        }
    }

    private ResponseEntity<String> handle(ServiceException e) {
        return ResponseEntity(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
Enter fullscreen mode Exit fullscreen mode

I consider it a bit noisy. Of course, we can also use routes:

@Bean
public RouterFunction<ServerResponse> hello(MyService service) {
    return route(GET("/hello"),
        req -> {
            try {
                return ServerResponse.ok().body(service.hello());
            } catch (ServiceException e) {
                return handle(e);
            }
        }).andRoute(GET("/world"),
        req -> {
            try {
                return ServerResponse.ok().body(service.world());
            } catch (ServiceException e) {
                return handle(e);
            }
        });
}

private ServerResponse handle(ServiceException e) {
    return ServerResponse.status(500).body(e.getMessage());
}
Enter fullscreen mode Exit fullscreen mode

But I don't think the above snippet is a significant improvement. Kotlin Router DSL doesn't help much either:

router {
    fun handle(e: ServiceException) = status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.message)
    GET("/hello") {
        try {
            ok().body(ref<MyService>().hello())
        } catch (e: ServiceException) {
            handle(e)
        }
    }
    GET("/world") {
        try {
            ok().body(ref<MyService>().world())
        } catch (e: ServiceException) {
            handle(e)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We don't have any annotations, but IMHO, it's not much more readable than the initial snippet.

We can redesign MyService to replace exception throwing with a functional approach to improve the code. The easiest path is to use Kotlin's Result type from the stdlib. It contains either the requested value or an Exception type. Alternative types include Arrow or Vavr Either type.

class MyService {
    fun hello(): Result<String> = // compute hello
    fun world(): Result<String> = // compute world
}

var routes = router {
    fun handle(e: ServiceException) = status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.message)
    GET("/hello") {
        ref<MyService>().hello().fold(
            { ok().body(it) },
            { handle(it as ServiceException) }
        )
    }
    GET("/world") {
        ref<MyService>().world().fold(
            { ok().body(it) },
            { handle(it as ServiceException) }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Starting the application

So far, we have been able to remove every annotation, but the main one: @SpringBootApplication compounds @SpringBootConfiguration, @EnableAutoConfiguration, and @ComponentScan. If you dislike annotations, it's a nightmare come true as it does a lot of "magic" under the cover.

It's possible to remove it anyway, provided you accept to use APIs considered experimental. The solution is Spring Fu, with "Fu" standing for functional. It's available in two flavors, one for Java and one for Kotlin, respectively named JaFu and KoFu.

Here's a snippet from the GitHub repo:

val app = webApplication {                   // 1
    messageSource {
        basename = "messages/messages"
    }
    webMvc {
        thymeleaf()
        converters {
            string()
            resource()
            jackson {
                indentOutput = true
            }
        }
        router {
            resources("/webjars/**", ClassPathResource("META-INF/resources/webjars/"))
        }
    }
    jdbc(DataSourceType.Generic) {
        schema = listOf("classpath*:db/h2/schema.sql")
        data = listOf("classpath*:db/h2/data.sql")
    }
    enable(systemConfig)
    enable(vetConfig)
    enable(ownerConfig)
    enable(visitConfig)
    enable(petConfig)
}

fun main() {
    app.run()                                // 2
}
Enter fullscreen mode Exit fullscreen mode
  1. Configure the context
  2. Start the application with no annotations

Conclusion

In this post, I've shown you how to move away from annotations in Java and Kotlin, using stable and experimental APIs.

On a more general note, I believe in Darwinism for libraries and frameworks. I'm pretty interested in Quarkus and Micronaut, and I think that their birth made Spring better.

However, things move fast in our industry. Critic or not, I'd suggest that every developer regularly check if their knowledge is still relevant when they express an opinion - and reassess it regularly.

Originally published at A Java Geek on September 12th, 2021

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .