I have this situation:
abstract class BaseWebClient(baseUrl: String) {
abstract val defaultHeaders: HttpHeaders
val client = WebClient
.builder()
.baseUrl(baseUrl)
.defaultHeaders { it.addAll(defaultHeaders) }
.build()
}
class AnimalsPortalClient(val config: Config, baseUrl: String) : BaseWebClient(baseUrl) {
override val defaultHeaders: HttpHeaders
get() {
val headers = HttpHeaders()
headers.add("app-name", config.appName)
headers.add("app-version", config.appVersion)
return headers
}
fun getAnimals(): String {
return client.get( // ... etc
}
}
This solution doesn't work, because - when defaultHeaders are attempted to be retrieved from overriding property in derived class - the variable config is null.
A possible solution is to pass the config object to the base class' constructor:
abstract class BaseWebClient(val config: Config, baseUrl: String) {
abstract val defaultHeaders: HttpHeaders
// ... etc
}
class AnimalsPortalClient(localConfig: Config, baseUrl: String) : BaseWebClient(localConfig, baseUrl) {
override val defaultHeaders: HttpHeaders
get() {
val headers = HttpHeaders()
headers.add("app-name", config.appName)
headers.add("app-version", config.appVersion)
return headers
}
// ... etc
}
But this solution has a drawback: not all extending classes need a config object. In most of derived class I have empty default headers. Like this:
class SoccerPortalClient(baseUrl: String) : BaseWebClient(baseUrl) {
override val defaultHeaders: HttpHeaders
get() = HttpHeaders()
Using the solution I proposed, I would be forced to always have a config object to pass to the base class, even if there is no need for it.
So basically:
I'm a bit puzzled about the behavior: why the variable is null? Is it a matter of visibility, or...?
What's the correct implementation to get around this problem?
Thank you!
The problem is that your client field in the base class is initialized before anything else. You can initialize it lazily. Like so:
val client by lazy {
WebClient
.builder()
.baseUrl(baseUrl)
.defaultHeaders { it.addAll(defaultHeaders) }
.build()
}
You are trying to access defaultHeaders property at the moment when derived class is not yet initialized.
Consided either convert client property initializer to getter:
val client
get() = WebClient
.builder()
.baseUrl(baseUrl)
.defaultHeaders { it.addAll(defaultHeaders) }
.build()
or use lazy delegate:
val client by lazy {
WebClient
.builder()
.baseUrl(baseUrl)
.defaultHeaders { it.addAll(defaultHeaders) }
.build()
}
Related
Currently, the ktor client logging implementation is as below, and it works as intended but not what I wanted to have.
public class Logging(
public val logger: Logger,
public var level: LogLevel,
public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList()
)
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
if (level.info) {
logger.log("REQUEST: ${Url(request.url)}")
logger.log("METHOD: ${request.method}")
}
val content = request.body as OutgoingContent
if (level.headers) {
logger.log("COMMON HEADERS")
logHeaders(request.headers.entries())
logger.log("CONTENT HEADERS")
logHeaders(content.headers.entries())
}
return if (level.body) {
logRequestBody(content)
} else null
}
Above creates a nightmare while looking at the logs because it's logging in each line. Since I'm a beginner in Kotlin and Ktor, I'd love to know the way to change the behaviour of this. Since in Kotlin, all classes are final unless opened specifically, I don't know how to approach on modifying the logRequest function behaviour. What I ideally wanted to achieve is something like below for an example.
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
...
if (level.body) {
val content = request.body as OutgoingContent
return logger.log(value("url", Url(request.url)),
value("method", request.method),
value("body", content))
}
Any help would be appreciative
No way to actually override a private method in a non-open class, but if you just want your logging to work differently, you're better off with a custom interceptor of the same stage in the pipeline:
val client = HttpClient(CIO) {
install("RequestLogging") {
sendPipeline.intercept(HttpSendPipeline.Monitoring) {
logger.info(
"Request: {} {} {} {}",
context.method,
Url(context.url),
context.headers.entries(),
context.body
)
}
}
}
runBlocking {
client.get<String>("https://google.com")
}
This will produce the logging you want. Of course, to properly log POST you will need to do some extra work.
Maybe this will be useful for someone:
HttpClient() {
install("RequestLogging") {
responsePipeline.intercept(HttpResponsePipeline.After) {
val request = context.request
val response = context.response
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}"
}
GlobalScope.launch(Dispatchers.Unconfined) {
val responseBody =
response.content.tryReadText(response.contentType()?.charset() ?: Charsets.UTF_8)
?: "[response body omitted]"
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}\nBODY START" +
"\n$responseBody" +
"\nBODY END"
}
}
}
}
}
You also need to add a method from the Ktor Logger.kt class to your calss with HttpClient:
internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
readRemaining().readText(charset = charset)
} catch (cause: Throwable) {
null
}
I need some params from the headers inside my coroutine in my controller, and log them as a corelation id for my requests.
Is it possible to use webflux / kotlin coroutines in controller AND to do contextual logging with the params in the header ?
I know Webflux can use WebFilter to intercept headers and log them or modify them, but can it be sent to the coroutine it will trigger ?
#RestController
class ItemController(private val itemRepository: ItemRepository) {
#GetMapping("/")
suspend fun findAllItems(): List<Item> =
// do stuff
logger.log("Corelation id is : " + myCorelationIdHeaderParam) // that's the param i need
return itemService.findAll()
}
Any context you set in the webfilter can be accessed by using subscriberContext down the line in your controllers/services.
Below is an example using Java. You can use similar logic in your Kotlin code:
Your filter: (Here you are setting the header value "someHeaderval" in your "myContext" )
public class MyFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String someHeaderval = request.getHeaders().get("someHeader").get(0);
return chain.filter(exchange).subscriberContext(context -> {
return context.put("myContext",someHeaderval);
});;
}
}
Now you can use this context anywhere:
#GetMapping(value = "/myGetApi")
public Mono<String> sampleGet() {
return Mono.subscriberContext()
.flatMap(context -> {
String myHeaderVal = (String)context.get("myContext");
//do logging with this header value
return someService.doSomething(myHeaderVal);
});
}
It turns out you can access the ReactorContext from the CoroutineContext with
coroutineContext[ReactorContext]
Here is my code :
#Component
class MyWebFilter : WebFilter {
val headerKey = "correlation-token-key"
val contextKey = "correlationId"
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers: HttpHeaders = exchange.request.headers
return chain.filter(exchange)
.subscriberContext(Context.of(contextKey, headers[headerKey] ?: "unidentified"))
}
}
and the controller part (most important for Kotlin users) :
#RestController
class ItemController(private val itemRepository: ItemRepository) {
#GetMapping("/")
suspend fun findAllItems(): List<Item> =
// do stuff
logger.log("Correlation id of request is : " + coroutineContext[ReactorContext]?.context?.get<List<String>>("correlationId")?.firstOrNull())
return itemService.findAll()
}
Note - I'm a Java+Spring guy trying out Kotlin+Micronaut.
I'm trying to use the TestPropertyProvider to set properties after my embedded service starts.
It works ok, as long as there are no constructor parameters in my test class.
I can add the RxHttpClient as a constructor parameter and it gets injected fine.
But, I'd like to inject the RxHttpClient from Micronaut and also implement TestPropertyProvider.
I tried adding #Inject to the RxHttpClient but get the error This annotation is not applicable to target 'local variable' [because the test body is a lambda passed to the superclass]
Without the #Inject I get the error lateinit property client has not been initialized
My base class has the TestPropertyProvider implementation .
abstract class ZeebeSpecification(body: AbstractStringSpec.() -> Unit): StringSpec(body), TestPropertyProvider {
override fun getProperties(): MutableMap<String, String> {
return mutableMapOf("orchestrator.management.client.brokerContactPoint" to IntegrationTestHarness.instance.getBroker())
}
}
TestPropertyProvider works, but RxHttpClient not injected
#MicronautTest
class ZeebeBroker1Test() : ZeebeSpecification({
#Client("/") lateinit var client: RxHttpClient;
...tests
}) {}
RxHttpClient injected, but TestPropertyProvider not evaluated
#MicronautTest
class ZeebeBroker1Test(#Client("/" val client: RxHttpClient) : ZeebeSpecification({
...tests
}) {}
I removed the base class from the equation and made my test directly implement the TestPropertyProvider but it still fails.
#MicronautTest
class ZeebeBroker1Test(#Client("/") var client: HttpClient) : BehaviorSpec(), TestPropertyProvider {
init {
...tests
}
private fun getBroker(): String {
return IntegrationTestHarness.instance.getBroker()
}
override fun getProperties(): MutableMap<String, String> {
return mutableMapOf("orchestrator.management.client.brokerContactPoint" to getBroker())
}
}
Seems like it's the same issue as this, but I'm already using v1.1.2
https://github.com/micronaut-projects/micronaut-test/issues/82
If I tried to use #Inject #Client("/") client: RxHttpClient it would throw the error message: Missing bean argument [LoadBalancer loadBalancer] for type: io.micronaut.http.client.DefaultHttpClient. Required arguments: LoadBalancer
How do I use both TestPropertyProvider and injected RxHttpClient?
I resolved the issue by moving the body of the spec into the init, and injecting the RxHttpClient as a field.
#MicronautTest
class ZeebeBroker1Test() : ZeebeSpecification() {
#Inject #field:Client("/") lateinit var client: RxHttpClient
private val log: Logger = LoggerFactory.getLogger(ZeebeBroker1Test::class.java)
init {
"test case 1" {
...
}
"test case 2" {
...
}
}
}
And I let the base class implement the TestPropertyProvider interface .
abstract class ZeebeSpecification(): StringSpec(), TestPropertyProvider {
open fun getBroker(): String {
return IntegrationTestHarness.instance.getBroker()
}
override fun getProperties(): MutableMap<String, String> {
return mutableMapOf("orchestrator.management.client.brokerContactPoint" to getBroker())
}
}
I am trying to understand how to hide a base constructor parameter in a subclass in kotlin. How do you put a facade over a base constructor? This doesn't work:
import com.android.volley.Request
import com.android.volley.Response
class MyCustomRequest(url: String)
: Request<String>(Request.Method.POST, url, hiddenListener) {
private fun hiddenListener() = Response.ErrorListener {
/* super secret listener */
}
...
}
I think I understand the problem:
During construction of a new instance of a derived class, the base
class initialization is done as the first step (preceded only by
evaluation of the arguments for the base class constructor) and thus
happens before the initialization logic of the derived class is run.
I'm trying to solve this problem for Volley, where I need my custom request to be be a Request so that it can be passed into a RequestQueue. It would be easier of RequestQueue took in some kind of interface but since it doesn't I have to subclass. There are other ways I can hide these complexities from the caller, but this limitation has come up for me other times in Kotlin and I'm not sure how to solve it.
I am not familiar with volley but I tried to come up with an example that should give you some insight how to solve your problem. What you can do is use a companion object:
interface MyListener {
fun handleEvent()
}
open class Base<T>(anything: Any, val listener: MyListener) { // this would be your Request class
fun onSomeEvent() {
listener.handleEvent()
}
}
class Derived(anything: Any) : Base<Any>(anything, hiddenListener) { // this would be your MyCustomRequest class
private companion object {
private val hiddenListener = object : MyListener {
override fun handleEvent() {
// do secret stuff here
}
}
}
}
So if you apply this to your problem, the result should look something like this:
class MyCustomRequest(url: String)
: Request<String>(Request.Method.POST, url, hiddenListener) {
private companion object {
private val hiddenListener = Response.ErrorListener {
/* super secret listener */
}
}
...
}
A different way would be to use a decorator, create your Request withing that decorator and just delegate the calls to it:
class Decorator(anything: Any) {
private var inner: Base<Any>
private val hiddenListener: MyListener = object : MyListener {
override fun handleEvent() { }
}
init {
inner = Base(anything, hiddenListener)
}
}
And once again for your example that would look like this:
class MyCustomRequest(url: String) {
private var inner: Request<String>
private val hiddenListener = Response.ErrorListener {
/* super secret listener */
}
init {
inner = Request<String>(Request.Method.POST, url, hiddenListener)
}
...
}
I need to supply shared secret attribute to XML, so I decided to add it without exposing it to my API.
#JacksonXmlRootElement(localName = "Request")
data class TestRequest(#JacksonXmlText val request: String)
Here is example POJO, after serializer it looks like
<Request>text</Request>
I need to add attribute to it, like
<Request secret="foobar">text</Request>
I looked to Jackson API and it looks like I need to create custom serializer for root, so
class SessionModule: SimpleModule("Test serializer", PackageVersion.VERSION) {
override fun setupModule(context: SetupContext) {
context.addBeanSerializerModifier(object : XmlBeanSerializerModifier() {
override fun modifySerializer(config: SerializationConfig, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> {
val modifiedSerializer = super.modifySerializer(config, beanDesc, serializer)
if (modifiedSerializer is XmlBeanSerializer) {
println("Registering custom serializer")
return SessionFieldSerializer(modifiedSerializer)
}
return modifiedSerializer
}
})
}
}
And my custom serializer that do nothing
class SessionFieldSerializer: XmlBeanSerializer {
constructor(src: BeanSerializerBase?) : super(src)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?, filterId: Any?) : super(src, objectIdWriter, filterId)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?) : super(src, objectIdWriter)
constructor(src: XmlBeanSerializerBase?, toIgnore: MutableSet<String>?) : super(src, toIgnore)
override fun serialize(bean: Any?, g: JsonGenerator?, provider: SerializerProvider?) {
TODO()
}
}
So, all it do is throw not-implemented exception, however even if SessionFieldSerializer() getting instantiated ( I see "Registering custom serializer" message), serialize function is not called.
Test code:
val mapper = XmlMapper()
mapper.registerModule(KotlinModule())
mapper.registerModule(SessionModule())
val request = TestRequest("Foobar")
val test = mapper.writeValueAsString(request)
println(test)
Am I missing something?