/ SPRING MVC, CLEAN CODE, DESIGN

Common code in Spring MVC, where to put it?

During my journey coding an actuator for a non-Spring Boot application, I came upon an interesting problem regarding where to actually put a snippet of common code. This post tries to list all available options, and their respective pros and cons in a specific context.

As a concrete example, let’s use the REST endpoint returning the map of all JVM properties accessible through the /jvmprops sub-context. Furthermore, I wanted to offer the option to search not only for a single property e.g. /jvmprops/java.vm.vendor but also to allow for filtering for a subset of properties e.g. /jvmprops/java.vm.*.

The current situation

The code is designed around nothing but boring guidelines for a Spring application. The upper layer consists of controllers. They are annotated with @RestController and provide REST endpoints made available as @RequestMapping-annotated methods. In turn, those methods call the second layer implemented as services.

As seen above, the filter pattern itself is the last path segment. It’s mapped to a method parameter via the @PathVariable annotation.

@RestController class JvmPropsController(private val service: JvmPropsService) {
  @RequestMapping(path = arrayOf("/jvmprops/{filter}"), method = arrayOf(GET))
  fun readJvmProps(@PathVariable filter: String): Map<String, *> = service.getJvmProps()
}

To effectively implement filtering, the path segment allows star characters. In Java however, string matching is achieved via regular expression. It’s then mandatory to "translate" the simple calling pattern to a full-fledge regexp. Regarding the above example, not only the dot character needs to be escaped - from . but to \\., but the star character needs to be translated accordingly - from to .:

val regex = filter.replace(".", "\\.").replace("*", ".*")

Then, the associated service returns the filtered map, which is in turn returned by the controller. Spring Boot and Jackson take care of JSON serialization.

Straightforward alternatives

This is all fine and nice, until additional map-returning endpoints are required (for example, to get environment variables), and the above snippet ends up being copied-pasted in each of them.

There surely must be a better solution, so where factor this code?

In a controller parent class

The easiest hack is to create a parent class for all controllers, put the code there and call it explicitly.

abstract class ArtificialController() {
    fun toRegex(filter: String) = filter.replace(".", "\\.").replace("*", ".*")
}

@RestController class JvmProps(private val service: JvmPropsService): ArtificialController() {
  @RequestMapping(path = arrayOf("/jvmprops/{filter}"), method = arrayOf(GET))
  fun readJvmProps(@PathVariable filter: String): Map<String, *> {
    val regex = toRegex(filter)
    return service.getJvmProps(regex)
  }
}

This approach has three main disadvantages:

  1. It creates an artificial parent class just for the sake of sharing common code.
  2. It’s necessary for other controllers to inherit from this parent class.
  3. It requires an explicit call, putting the responsibility of the transformation in the client code. Chances are high that no developer but the one who created the method will ever use it.

In a service parent class

Instead of setting the code in a shared method of the controller layer, it can be set in the service layer.

The same disadvantages as above apply.

In a third-party dependency

Instead of an artificial class hierarchy, let’s introduce an unrelated dependency class. This translates into the following code.

class Regexer {
  fun toRegex(filter: String) = filter.replace(".", "\\.").replace("*", ".*")
}

@RestController class JvmProps(private val service: JvmPropsService,
                               private val regexer: Regexer) {
  @RequestMapping(path = arrayOf("/jvmprops/{filter}"), method = arrayOf(GET))
  fun readJvmProps(@PathVariable filter: String): Map<String, *> {
    val regex = regexer.toRegex(filter)
    return service.getJvmProps(regex)
  }
}

While favoring composition over inheritance, this approach still leaves out a big loophole: the client code is required to call the shared one.

In a Kotlin extension function

If one is allowed to use alternate languages on the JVM, it’s possible to benefit for Kotlin’s extension functions:

interface ArtificialController

fun ArtificialController.toRegex(filter: String) = filter.replace(".", "\\.").replace("*", ".*")

@RestController class JvmProps(private val service: JvmPropsService): ArtificialController {
  @RequestMapping(path = arrayOf("/jvmprops/{filter}"), method = arrayOf(GET))
  fun readJvmProps(@PathVariable filter: String): Map<String, *> {
    val regex = toRegex(filter)
    return service.getJvmProps(regex)
  }
}

Compared to putting the code in a parent controller, at least the code is localized to the file. But the same disadvantages still apply, so the gain is only marginal.

More refined alternatives

Refactorings described above work in every possible context. The following options apply specifically for (Spring Boot) web applications.

They all follow the same approach: instead of explicitly calling the shared code, let’s somehow wrap controllers in a single component where it will be executed.

In a servlet filter

In a web application, code that needs to be executed before/after different controllers are bound to take place in a servlet filter.

With Spring MVC, this is achieved through a filter registration bean:

@Bean
fun filterBean() = FilterRegistrationBean().apply {
  urlPatterns = arrayListOf("/jvmProps/*")
  filter = object : Filter {
    override fun destroy() {}
    override fun init(config: FilterConfig) {}
    override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) {
      chain.doFilter(httpServletReq, resp)
      val httpServletReq = req as HttpServletRequest
      val paths = request.pathInfo.split("/")
      if (paths.size > 2) {
        val subpaths = paths.subList(2, paths.size)
        val filter = subpaths.joinToString("")
        val regex = filter.replace(".", "\\.")
                          .replace("*", ".*")
        // Change the JSON here...
      }
    }
  }
}

The good point about the above code is it doesn’t require controllers to call the shared code explicitly. There’s a not-so-slight problem however: at this point, the map has already been serialized into JSON, and been processed into the response. It’s mandatory to wrap the initial respons in a response wrapper before proceeding with the filter chain and process the JSON instead of an in-memory data structure.

Not only is this way quite fragile, it has a huge impact on performance.

In a Spring MVC interceptor

Moving the above code from a filter in a Spring MVC interceptor unfortunately doesn’t improve anything.

In an aspect

The need of translating the string parameter and to filter the map are typical cross-cutting concerns. This is a typical use-case fore Aspect-Oriented Programming. Here’s what the code looks like:

@Aspect class FilterAspect {
  @Around("execution(Map ch.frankel.actuator.controller.*.*(..))")
  fun filter(joinPoint: ProceedingJoinPoint): Map<String, *> {
    val map = joinPoint.proceed() as Map<String, *>
    val filter = joinPoint.args[0] as String
    val regex = filter.replace(".", "\\.").replace("*", ".*")
    return map.filter { it.key.matches(regex.toRegex()) }
  }
}

Choosing this option works in the intended way. Plus, the aspect will be applied automatically to all methods of all classes in the configured package that return a map.

In a Spring MVC advice

There’s a nice gem hidden in Spring MVC: a specialized advice being executed just after the controller returns but before the returned value is serialized in JSON format (thanks to @Dr4K4n for the hint).

The class just needs to:

  1. Implement the ResponseBodyAdvice interface
  2. Be annotated with @ControllerAdvice to be scanned by Spring, and to control which package it will be applied to
@ControllerAdvice("ch.frankel.actuator.controller")
class TransformBodyAdvice(): ResponseBodyAdvice<Map<String, Any?>> {

  override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>) =
  returnType.method.returnType == Map::class.java

  override fun beforeBodyWrite(map: Map<String, Any?>, methodParameter: MethodParameter,
            mediaType: MediaType, clazz: Class<out HttpMessageConverter<*>>,
            serverHttpRequest: ServerHttpRequest, serverHttpResponse: ServerHttpResponse): Map<String, Any?>  {
    val request = (serverHttpRequest as ServletServerHttpRequest).servletRequest
    val filterPredicate = getFilterPredicate(request)
    return map.filter(filterPredicate)
  }

  private fun getFilterPredicate(request: HttpServletRequest): (Map.Entry<String, Any?>) -> Boolean {
    val paths = request.pathInfo.split("/")
    if (paths.size > 2) {
      val subpaths = paths.subList(2, paths.size)
      val filter = subpaths.joinToString("")
      val regex = filter.replace(".", "\\.")
                        .replace("*", ".*")
                        .toRegex()
      return { it.key.matches(regex) }
    }
    return { true }
  }
}

This code doesn’t require to be called explicitly, it will be applied to all controllers in the configured package. It also will only be applied if the return type of the method is of type Map (no generics check due to type erasure though).

Even better, it paves the way for future development involving further processing (ordering, paging, etc.).

Conclusion

There are several ways to share common code in a Spring MVC app, each having different pros and cons. In this post, for this specific use-case, the ResponseBodyAdvice has the most benefits.

The main taking here is that the more tools one has around one’s toolbelt, the better the final choice. Go explore some tools you don’t know already about: what about reading some documentation today?

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Common code in Spring MVC, where to put it?
Share this