Quite often during implementation of a REST client, it is nice to have a handful and flexible REST server,
and sometimes just one port per app is not enough.

An example of such situation: your application is designed to send requests to different hosts simultaneously.
So a multi-port web server, which could be easily and quickly set up could help with that.
However, even if we have a multi-port web server, what should be the port configuration, to have it really “easy and quick” ?

So here is the essentials of a REST server, which is:

  • multi-port
  • configuration based
  • implemented on Kotlin

This quick note does not contain the whole project code.
The idea: to focus on the conception. The particular implementation
today is just a matter of some hours or even minutes.

So, we need an extendable controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@IgnoredBean
@RestController
class Controller {

private val logger = getLogger()

/* use of the RequestMapping
is one of the key point here.
The annotation is a kind of pointer
to a function, which should be taken into account.
*/
@RequestMapping("/api/endpoint-0", method = [RequestMethod.POST])
fun postEndpoint0(
@RequestBody
request: String
): Response {
logger.info("POST, got the request: $request")
return Response(request)
}

companion object {
/* the collection contains all endpoint handlers
the controller is designed to expose.
*/
val HANDLER_METHODS =
getMethodsListWithAnnotation(Controller::class.java, RequestMapping::class.java).map { handlerMethod ->
handlerMethod.getAnnotation(RequestMapping::class.java).method.map {
EndpointHandler(
handlerMethod.getAnnotation(RequestMapping::class.java).value[0],
it,
handlerMethod
)
}
}.flatten()
}

@ExceptionHandler
fun onError(exception: Exception): ResponseEntity<ErrorMessage> {
logger.warn("Failed to handle a request: ${exception.message}")
return ResponseEntity.status(400).body(ErrorMessage(400, exception.message, Instant.now()))
}
}

data class Request(
val parameter: String
)

data class Response(
val result: String
)

class ErrorMessage(
val status: Int,
val message: String?,
val `when`: Instant
)

The main part of the conception is a service, which builds new port listeners:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@Component
class DynamicControllerServiceSupplier(
private val controllerService: DynamicControllerService,
private val endpointConfigurations: List<Endpoint>,
private val endpointHandlers: List<EndpointHandler> = Controller.HANDLER_METHODS
) {
@PostConstruct
fun initPortMapping() = controllerService.buildEndpoints(endpointConfigurations, endpointHandlers)
}

@Service
class DynamicControllerService(
private val requestHandlerMapper: RequestMappingHandlerMapping,
private val tomcat: Tomcat
) {

private val logger = getLogger()
private val mappingByPort: MutableMap<Int, RequestMappingInfo> = ConcurrentHashMap()

fun buildEndpoints(
endpointConfigurations: List<Endpoint>,
handlerMethods: List<EndpointHandler>
) {
endpointConfigurations.forEach { endpointConfiguration ->
addMappedEndpoint(endpointConfiguration,
handlerMethods.single {
it.path == endpointConfiguration.path
}.handlerMethod
)
}
}

private fun addMappedEndpoint(
configuration: Endpoint,
handlerMethod: Method
) {
val connector = Connector(Http11NioProtocol())
connector.throwOnFailure = true
connector.port = configuration.port

try {
tomcat.connector = connector
} catch (exception: IllegalArgumentException) {

tomcat.service.removeConnector(connector)
val rootCause = exception.cause

throw IllegalArgumentException(rootCause)
}

val port = connector.port
val mapping = RequestMappingInfo
.paths(configuration.path)
.methods(configuration.type)
.customCondition(PortRequestCondition(port))
.build()

val controller = Controller()
requestHandlerMapper.registerMapping(
mapping,
controller,
handlerMethod
)

mappingByPort[port] = mapping
logger.info("Added request mapping ${configuration.type} ${configuration.path} on port ${port}")
}
}

internal class PortRequestCondition(
private vararg val ports: Int
) : RequestCondition<PortRequestCondition> {

override fun combine(other: PortRequestCondition): PortRequestCondition {
return PortRequestCondition(*(ports + other.ports))
}

override fun getMatchingCondition(request: HttpServletRequest): PortRequestCondition? {
return if (ports.contains(request.localPort)) this else null
}

override fun compareTo(other: PortRequestCondition, request: HttpServletRequest): Int {
return 0
}
}

data class EndpointHandler(
val path: String,
val requestMethod: RequestMethod,
val handlerMethod: Method
)

The config file, where the ports are described.

Here we use the path from the @RequestMapping described in the controller.
All other members of the configuration are pretty obvious:

1
2
3
4
5
# Configuration of endpoints. Contains a list of endpoint descriptors
endpoints:
- path: /api/endpoint-0
port: 8089
type: POST

And the final part of the puzzle: a config file handler:

1
2
3
4
5
6
7
8
9
10
data class Config(
val endpoints: List<Endpoint>
) {

data class Endpoint(
val path: String,
val port: Int,
val type: RequestMethod
)
}

So actually that’s it.
The workflow is simple, just two steps:

  1. in the Controller add new handler with @RequestMapping
  2. in the config file add appropriate mapping <the port> -> <the handler path>

Hope, this quick note will be useful to you and save some your time.