DEV Community

Cover image for Annotation-free Spring
Nicolas Frankel
Nicolas Frankel

Posted on • Originally published at blog.frankel.ch

Annotation-free Spring

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

Discussion (7)

Collapse
siy profile image
Sergiy Yevtushenko

Once you remove annotation-based "magic" from Spring, it immediately looses it's appeal. The "magic" often used to justify Spring bad performance and huge resource consumption. Funny enough is that Micronaut disproves this justification and makes techniques described in the article unnecessary.

Collapse
thumbone profile image
Bernd Wechner

Wow, that sure is written to a target audience! Have you considered maybe explaining what Spring and Kotlin are? This is a pretty general feed and I love learning, and that is made possible when focus articles include even a weeny little springboard for noobs at the top to put them into context. Just a thought.

Collapse
nfrankel profile image
Nicolas Frankel Author

I find your comment a bit puzzling. When you're reading a paper, do you complain to the paper's author that you lack the foundational knowledge to understand it properly? If you're genuinely interested, you can easily search on the Web. If not, you can safely skip it.

Comparison aside (this is obviously not a paper), you can still apply the same approach here.

Collapse
thumbone profile image
Bernd Wechner

Nicolas, a paper appears in a journal and is pitched at a very small audience that is assumed to be in the same area or doing research into it.

A newspaper by comparison is pitched at the general public.

In all contexts there is an audience, and a write consciously (or not) makes call on what language is accessible the audience and what not. The line is not objective and not stationary and not clear, but it exists and good writing is conscious of it.

I'd be very curious what portion of the dev.to audience even understands the title of this one.

As to my specifics, you're spot on, and I did look them up. Kotlin is easy and I know what it is now. Spring is more challenging, try it ... I have a lose idea of what it is, but very loose, it's not even clearly explained by its own website ;-). A Java framework that makes you more productive is about all I got.

But therein lies the source of your puzzlement. I was not complaining, and I'm fully on top of my own options, yes. I was drawn in by curiosity and offered an accessibility tip. Accessibility has been a theme on my dev.to feed and I've been learning a bit myself. Take the tip or leave it.

There's an irony here, one the internet is full of, and I actually like it, it's a sweet one. Because you're right. As to my options. But not just with regard to tose, also with regard to yours. You can read up on accessible writing or pass my comment over ;-).

Note, I am not actually dropping that suggestion, not at all. I am very open to a conversation on it.

I would ask perhaps in reflection, whether you'd concur (or not) that on your average dev.to feed (and I'm only able to guess at the demography there based on mine which I won't claim is universal as I have no idea what "algorithms" - I quote that as it's a buzz word in social media and Facebook contexts regarding selection of items for populating feeds - dev.to uses) it would draw even more readers in or save them time or generally improve the quality of such feeds if your article kick started in a dev.to audience pitch akin to - and this is just a sampler not a prescription:

Avoiding Annotations in the Java Spring Framework
Spring is a very popular Java framework and I've seen a lot of posts critical of it. They seem mostly rooted in complaints about annotations, and blissfully unaware of the options to avoid them and still use Spring ....

My point being you have a much broader audience. Of course you're entitled not to care, and pitch at that readership that knows what you mean by this title, and your intro. And that in fact is something I'd find natural and expected maybe on a Java forum. On dev.to it surprises me a little, as its a very broad forum with one feed that sees on my end mostly Web development, Javascript, Node, python stuff float by and some industry topics and professional development stuff.

I mean I'm not griping, nor complaining and in fact thank you for the opportunity to learn a little more about Java (which I know little of clearly). Was just offering some constructive feedback on a more accessible approach in this general forum

I mean I see from your footer it was originally published at A Java Geek. Case in point, an article in Java geek context moved out of it into a general developers forum ...

Thread Thread
nfrankel profile image
Nicolas Frankel Author

Thanks for your detailed explanation.

The gist of my answer is: my point here is not to appeal to a broader audience. If you happen to be interested in the post, great, if not, great too 🙂

In all cases, I'm happy it made you look a bit more into the concepts I mention.

Thread Thread
thumbone profile image
Bernd Wechner

Thanks. And no worries. Stil, I'm curious, why publish on dev.to if no aiming at a more general developer audience? Not least stuff already published ... what's the draw to republishing on dev.to? I'm just curious, and keen to understand. That's all. Not critical.

Thread Thread
mjamsek profile image
Miha Jamsek

He is aiming at advanced Spring developers as target audience, dev.to is as good of a place for this as any other platform. Majority of posts here are for some specific target audience and not for general developer audience (apart from posts about architecture, most of posts are not for general audience). Tags are for denoting what kind of audience is this written for (java, kotlin, spring)