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:
- It creates an artificial parent class just for the sake of sharing common code.
- It’s necessary for other controllers to inherit from this parent class.
- 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:
- Implement the
ResponseBodyAdvice
interface - 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?