DEV Community

Cover image for Clean code in Spring ecosystem
Artem Ptushkin
Artem Ptushkin

Posted on

Clean code in Spring ecosystem

I work a lot with Spring components including Spring Boot and Spring Cloud. There is a reason for it and I want to share it with you.

I pose a lot of questions to myself about the design and architecture of the code I write besides how to implement this solution. The reason for it is caring about future engineer work within a project.

Certainly, a lot of programmers have this kind of attitude. We ask questions regarding modularity like:

  • How to make changes easier in the future?
  • Should I make it reusable? How can I do it?
  • Is there any ready to use solution from our tech stack?
  • How to test future changes easier?
  • Is there any way to make existed code more consistently and uniformly?

I think the most important question is how to make it shareable with other developers who could use this solution. The way we solve it is by decomposing current code modules.

Spring framework provides an opportunity to make your design lose coupling and there is more material than requires about it online. But I'd like to show you the way you can reach it without academic knowledge.

Problem

I wrote a Medium story about how I'd been choosing a solution for images processing. It could help you to get the reasoning below.

Long story short, I developed a gateway server for images processing and by the way, a made proxy server on implementing Spring Cloud Gateway.
You can find the code at the GitHub repository.

Implementation

I've started writing a standalone gateway service on purpose to figure out how Spring Cloud Gateway works.
The only need here is to have this dependency at the classpath:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

I've been proceeding a curl requests and playing around with properties until I sorted out how to write custom filters. I made up a list of goals I want to achieve here from modularity.

  • It starts as a standalone jar file
  • It has an independent component to fetch inside any custom Spring Cloud Gateway Server. Obviously, there is a lot of it in Java community.
  • Functionality must be switched on/off by config

My first goal has been already reached by default because I had added the dependency above.
Spring Boot Starter represent a library with auto configuration of beans. It could be used as independent component for my second goal. You can read more about custom starter implementation.

Basically it has these requirements:

  • Your starter jar archive has a spring.factories file
org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.github.aptushkin.proxy.image.autoconfiguration.ProxyImageModificationAutoConfiguration
Enter fullscreen mode Exit fullscreen mode
  • Spring Configuration Context is configured inside classpath by Java class
@Configuration
@Import(ImageModificationConfiguration::class)
@ConditionalOnProperty(prefix = "proxy.image", name = ["enabled"], havingValue = "true", matchIfMissing = true)
class ProxyImageModificationAutoConfiguration
Enter fullscreen mode Exit fullscreen mode

You can see annotation @ConditionalOnProperty which gives me the third point from my goals. The switching off is enabled by changing the application property: proxy.image.enabled: true. It has a property matchIfMissing = true which means that this configuration is also turned on non-existed property case.

Thus I created an additional proxy-image-starter module inside this repository. The next step is to add it as a dependency inside my server pom.xml:

<dependency>
  <groupId>io.github.artemptushkin.proxy.image</groupId>
  <artifactId>proxy-image-starter</artifactId>
  <version>${project.version}</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Spring boot starter testing

Please pay attention that it's important to write unit-tests for all the application properties and Spring bean declarations you have written.

It is much more easier to understand your code if there is a test which speaks:

If you apply a property A than you'll have a bean B inside your Spring context. This test proves it

What I've got from this modularity?

I have an opportunity to write the starter code independently from server one. It is important because server dependency is redundant for unit-tests, it contains a lot of unnecessary functionality that can be tested separately.

All I need is to test what kind of beans I will get if I add this starter to some project.

Also, it is available to share with other developers now. They can fetch it and try it on their application side.

The actual functionality implementation

The first thing that comes up to a developer's mind is to write code right here - inside the starter library. It could be done, but there are consequences for project support in the future.

Your starter configuration classes will be growing up along with actual functionality. This seems okay until complexity reaches the threshold: at some point, it will be hard to understand what is actually happening at the module.

That's why I recommend creating an additional library (core, common etc.) for the functional classes. Specifically when you develop public libraries.

Thus I create a module proxy-image-common for all the classes that I need for my functionality. The next step is to add it to the starter pom.xml file:

<dependency>
  <groupId>io.github.artemptushkin.proxy.image</groupId>
  <artifactId>proxy-image-common</artifactId>
  <version>${project.version}</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

So I got this lose coupled dependency chain:
Alt Text

This means that the starter provides us some beans in some way and library clients don't know how it is done.
The common library, in turn, provides some classes which we can use inside our Spring application.

Heading to the code

I found out that there is a ModifyResponseBodyGatewayFilterFactory class that actually modifies the response from the downstream services by a rewriteFunction via Config class. The last one is configurable by Spring Properties this way:

spring:
  cloud:
    gateway:
      default-filters:
        - ProxyForward
      routes:
        - id: modify_image_router
          uri: no://op
          predicates:
            - Path=/**
          filters:
            - name: CropImage
              args:
                responseHeaderName: Content-Type
                regexp: image/.*
                defaultWidth: 600
                defaultHeight: 400
Enter fullscreen mode Exit fullscreen mode

Values under args are fields of Config class.

I discovered it by making no doubts about Spring classes reusability based on my experience and open source code.

It ought to be the way to change a class behavior on its client side by changing some interface implementation

I made up new goals regarding my code design now. I want to achieve reusability and keep code as simple as it could be for other developers in the feature:

  • Small classes that accept a few objects as a constructor parameter and same with the method parameter, better no more than one object
  • A common interface. The implementation of it has to modify an image

Abstractions first. If you want to achieve reusability and loose coupling - start on writing abstractions (interfaces)


The interface of response's body bytes modifications:

interface ByteArrayModifier {
    fun modify(byteArray: ByteArray): ByteArray
}
Enter fullscreen mode Exit fullscreen mode

My first implementation looked like:

val inputImage = ByteArrayInputStream(byteArray).use {
    return@use ImageIO.read(it)
}
//... some image processing logic
return ByteArrayOutputStream().use {
    ImageIO.write(scaledImage, format, it)
    return@use it.toByteArray()
}
Enter fullscreen mode Exit fullscreen mode

I noticed that a logic of byteArray -> BufferedImage and reverse transformation is the same for any current implementation. I extracted it to the abstract class and got only the requirements to implement #modifyImage(BufferedImage) conversion itself:

abstract class AbstractImageModifier(private val format: String) : ByteArrayModifier {

    override fun modify(byteArray: ByteArray): ByteArray {
        val inputImage = ByteArrayInputStream(byteArray).use {
            return@use ImageIO.read(it)
        }
        ...
    }

    protected abstract fun modifyImage(image: BufferedImage): BufferedImage
}
Enter fullscreen mode Exit fullscreen mode

Implementations became small and easy to test:

class CropImageModifier(private val cropRequest: CropRequest): AbstractImageModifier(cropRequest.format) {
    override fun modifyImage(image: BufferedImage): BufferedImage {
        val width = cropRequest.width
        val height = cropRequest.height
        val x = cropRequest.x
        val y = cropRequest.y
        if (x != null && y != null) {
            return Scalr.crop(image, x, y, width, height)
        }
        return Scalr.crop(image, width, height);
    }
}
Enter fullscreen mode Exit fullscreen mode

It is important to notice that such classes provide an opportunity to get a small amount of data (objects) in the constructor. It leads us to reuse those implementations at another place of code and keep code and tests easy to read.

As you can see I named the constructor input classes as ...Request because I think it can be used as a part of another starter for classic REST API applications and basically this DTO class represents query parameters i.e. request data.

More code

I have many small quality code examples of other approaches in this project. As you can see at the repository packages below, the modifier interface is not the only abstraction I've found for the project.
Alt Text

I have plans to write a starter for a classic Spring REST API server to enable filters inside those applications directly and I am open to this library customization.

Please, join to contribute!

Conclusions

I hope this article helps you to find new code design approaches and maybe inspires you to write more reusable solutions.

Summing up conclusion I'd like to give some advice:

  • Make strong requirements to your code's quality and the one you review
  • Abstract coding is much easier since you start to do it. Just write -> ask -> change it!
  • Share your code to make it better and more useful for other developers

Top comments (0)