I have successfully created few routes overriding configure() method of RouteBuilder. A for loop is used to generate routes on application startup, for eg:
Route1: from("direct:Route1").to("netty-http:http://localhost:8080/route1)
Route2: from("direct:Route2").to("netty-http:http://localhost:8081/route2)
Route3: from("direct:Route3").to("netty-http:http://localhost:8082/route3)
Route4: from("direct:Route4").to("netty-http:http://localhost:8083/route4)
Route5: from("direct:Route5").to("netty-http:http://localhost:8084/route5)
for (endpoint in endpoints.iterator()) {
from("direct:" + endpoint.getEndpointRouteName())
.process(getProcessor(endpoint.getEndpointProcessor(), endpoint.getEndpointRouteName(), objectMapper))
.setHeader(Exchange.HTTP_METHOD, simple(endpoint.getEndpointRequestMethod()))
.setHeader(Exchange.CONTENT_TYPE, constant(endpoint.getEndpointContentType()))
.to("netty-http:" + endpoint.getEndpointUrl())
}
private fun getProcessor(processorClassName: String, name: String, objectMapper: ObjectMapper): Processor {
var processorClass = Class.forName("com.demo.camelpoc.processors.$name.$processorClassName")
return processorClass.getDeclaredConstructor(ObjectMapper::class.java).newInstance(objectMapper) as Processor
}
And there is a source endpoint which starts the workflow. For example the default workflow generated in runtime:
// Workflow
from("netty-http:$sourceUrl").process {
it.setProperty("Workflow", workflowMap)
}
.dynamicRouter(bean(DynamicRouteBean::class.java, "route(*, *, ${startEndpoint})"))
where workflowMap (Used in DynamicRouteBean) is a map of endpoint strings like Key: "direct:Route1 " Value : "direct:Route2", Key: "direct:Route2 " Value : "direct:Route3"... etc
Requirement: Retry sending to the same endpoint in the workflow when exception is thrown in that particular route
For eg:
Lets say, an exception occurs at direct:Route2, I want to retry sending to direct:Route2.
Here is my DynamicRouteBean class.
class DynamicRouteBean {
fun route(
#Header(Exchange.SLIP_ENDPOINT) previousRoute: String?,
exchange: Exchange,
startEndpoint: String
): String? {
if(checkException(exchange)) {
return exchange.getProperty(Exchange.SLIP_ENDPOINT) as String
}
if (exchange.getProperty(Properties.STOP_ROUTE) != null && exchange.getProperty(Properties.STOP_ROUTE) == true) {
return null
}
val workflow: MutableMap<String, Pair<String, String?>> =
exchange.getProperty("Workflow") as MutableMap<String, Pair<String, String?>>
return when (previousRoute) {
null ->
startEndpoint
else -> {
val message = exchange.getIn(NettyHttpMessage::class.java)
// Signifies last endpoint and thus means end of the route
if (!workflow.containsKey(previousRoute)) {
return null
}
if (message?.getHeader(previousRoute.substring(9)) != null) {
val isSuccess = message.getHeader(previousRoute.substring(9)) == true
if (isSuccess) {
"${workflow[previousRoute]?.first}"
} else if (workflow[previousRoute]?.second != null) {
"${workflow[previousRoute]?.second}"
} else {
null
}
} else {
null
}
}
}
}
When I return current Exchange.SLIP_ENDPOINT property as String on exception, it doesn't call that endpoint again but exception message is returned back to the consumer.
Whereas it works in normal cases when there is no exception.
Suggestions would be very helpful on handling this scenario.
Related
I have the following code where I call external APIs via webclient and return Mono.
I need to execute some logic when I receive data. And after all, requests are processed, execute one logic for all gathered data. I can collect all Monos and put them to flux and then execute some logic at the end. But I have serviceName filed which is accessible only in the loop, so I need to execute logic for mono in loop and here I'm stuck and don't know how to wait for all data to complete and do it in a reactive way.
#Scheduled(fixedDelay = 50000)
public void refreshSwaggerConfigurations() {
log.debug("Starting Service Definition Context refresh");
List<SwaggerServiceData> allServicesApi = new ArrayList<>();
swaggerProperties.getUrls().forEach((serviceName, serviceSwaggerUrl) -> {
log.debug("Attempting service definition refresh for Service : {} ", serviceName);
Mono<SwaggerServiceData> swaggerData = getSwaggerDefinitionForAPI(serviceName,
serviceSwaggerUrl);
swaggerData.subscribe(swaggerServiceData -> {
if (swaggerServiceData != null) {
allServicesApi.add(swaggerServiceData);
String content = getJSON(swaggerServiceData);
definitionContext.addServiceDefinition(serviceName, content);
} else {
log.error("Skipping service id : {} Error : Could not get Swagger definition from API ",
serviceName);
}
});
});
//I need to wait here for all monos to complete and after that proceed for All gathered data...
//Now it's empty And I know why, just don't know how to make it.
Optional<SwaggerServiceData> swaggerAllServicesData = getAllServicesApiSwagger(allServicesApi);
if (swaggerAllServicesData.isPresent()) {
String allApiContent = getJSON(swaggerAllServicesData.get());
definitionContext.addServiceDefinition("All", allApiContent);
}
}
private Mono<SwaggerServiceData> getSwaggerDefinitionForAPI(String serviceName, String url) {
log.debug("Accessing the SwaggerDefinition JSON for Service : {} : URL : {} ", serviceName,
url);
Mono<SwaggerServiceData> swaggerServiceDataMono = webClient.get()
.uri(url)
.exchangeToMono(clientResponse -> clientResponse.bodyToMono(SwaggerServiceData.class));
return swaggerServiceDataMono;
}
I would add a temporary class to group data and serivce name :
record SwaggerService(SwaggerServiceData swaggerServiceData, String serviceName) {
boolean hasData() {
return swaggerServiceData != null;
}
}
And then change your pipeline :
Flux.fromStream(swaggerProperties.getUrls().entrySet().stream())
.flatMap((e) -> {
Mono<SwaggerServiceData> swaggerDefinitionForAPI = getSwaggerDefinitionForAPI(e.getKey(),
e.getValue());
return swaggerDefinitionForAPI.map(swaggerServiceData -> new SwaggerService(swaggerServiceData, e.getKey()));
})
.filter(SwaggerService::hasData)
.map(swaggerService -> {
String content = getJSON(swaggerService.swaggerServiceData());
definitionContext.addServiceDefinition(swaggerService.serviceName(), content);
return swaggerService.swaggerServiceData();
})
// here we will collect all datas and they will be emmited as single Mono with list of SwaggerServiceData
.collectList()
.map(this::getAllServicesApiSwagger)
.filter(Optional::isPresent)
.map(Optional::get)
.subscribe(e -> {
String allApiContent = getJSON(e);
definitionContext.addServiceDefinition("All", allApiContent);
});
This does not deal with logging error when SwaggerServiceData is null but you can further change it if you want. Also I assume that DefinitionContext is thread safe.
Solution with error logging (using flatMap and Mono.empty()) :
Flux.fromStream(swaggerProperties.getUrls().entrySet().stream())
.flatMap((e) -> {
Mono<SwaggerServiceData> swaggerDefinitionForAPI = getSwaggerDefinitionForAPI(e.getKey(),
e.getValue());
return swaggerDefinitionForAPI
.flatMap(swaggerServiceData -> {
if(swaggerServiceData != null) {
return Mono.just(new SwaggerService(swaggerServiceData, e.getKey()));
} else {
log.error("Skipping service id : {} Error : Could not get Swagger definition from API ",
e.getKey());
return Mono.empty();
}
});
})
.map(swaggerService -> {
String content = getJSON(swaggerService.swaggerServiceData());
definitionContext.addServiceDefinition(swaggerService.serviceName(), content);
return swaggerService.swaggerServiceData();
}).collectList()
.map(this::getAllServicesApiSwagger)
.filter(Optional::isPresent)
.map(Optional::get)
.subscribe(e -> {
String allApiContent = getJSON(e);
definitionContext.addServiceDefinition("All", allApiContent);
});
You can also wrap those lambads into some meaningful methods to improve readibility.
Spring Boot Version: 2.5.1,
Spring Cloud Version: 2020.0.3
Hello guys !!!
I need your help ...
My question is that I can't modify the request body in spring gateway. Follow:
I have a MobileGatewayFilterFactory class that extends from AbstractGatewayFilterFactory where the apply method returns a custom filter: MobileGatewayFilter.
#Component
class MobileGatewayFilterFactory :
AbstractGatewayFilterFactory<MobileGatewayFilterFactory.Config>(Config::class.java),
Ordered {
override fun apply(config: Config): GatewayFilter {
logger.info { "Loading MobileGatewayFilter with config ${config.className}, ${config.execution}, ${config.custom}" }
return MobileGatewayFilter(config)
}
override fun getOrder(): Int {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1
}
data class Config(
val className: String,
val execution: String,
val custom: String?
)
}
So, inside the MobileGatewayFilter class I implement the business rules to determine which filter is running: PRE or POST filter. This is done in the filter method of the MobileGatewayFilter class where there is a condition to determine the type of decoration being executed, using reflection. If it is a request, the ServerHttpRequestDecorator is executed and a ServerHttpResponseDecorator otherwise.
class MobileGatewayFilter(private val config: MobileGatewayFilterFactory.Config) : GatewayFilter, Ordered {
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
return when (config.execution) {
"PRE" -> chain.filter(exchange.mutate().request(decoratorRequest(exchange)).build())
"POST" -> chain.filter(exchange.mutate().response(decoratorResponse(exchange)).build())
else -> chain.filter(exchange)
}
}
override fun getOrder(): Int {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1
}
private fun decoratorResponse(exchange: ServerWebExchange): ServerHttpResponse {
val aClass = Class.forName(config.className)
val obj = aClass.getConstructor(ServerHttpResponse::class.java, MobileGatewayFilterFactory.Config::class.java)
return obj.newInstance(exchange.response, config) as ServerHttpResponseDecorator
}
private fun decoratorRequest(exchange: ServerWebExchange): ServerHttpRequest {
val aClass = Class.forName(config.className)
val obj = aClass.getConstructor(ServerHttpRequest::class.java, MobileGatewayFilterFactory.Config::class.java)
return obj.newInstance(exchange.request, config) as ServerHttpRequestDecorator
}
}
Furthermore, I have a CustomerDataBodyDecorator that extends the ServerHttpRequestDecorator and overrides the getBody method. The getBody method is where the request body must be modified.
class CustomerDataBodyDecorator(
private val exchange: ServerHttpRequest,
private val config: MobileGatewayFilterFactory.Config
) : ServerHttpRequestDecorator(exchange) {
override fun getBody(): Flux<DataBuffer> {
logger.info { "getBody chamado ..." }
val body: Flux<DataBuffer> = exchange.body
var requestData = ""
body.subscribe {
val content = ByteArray(it.readableByteCount())
it.read(content)
DataBufferUtils.release(it)
requestData = String(content, Charset.forName("UTF-8"))
logger.info { "Request: $requestData" }
}
val factory = DefaultDataBufferFactory()
val buffer = factory.wrap(requestData.toByteArray())
return Flux.just(buffer)
}
}
However, the above code doesn't work because the return is executed first with empty requestData and after subscribe method is executed. I know that in Webflux the subscribe method is necessary to indicate to the publisher the information consumption needs
application.yml
id: opengw-mobile-simulation
uri: ${custom.resources.opengw}
predicates:
- Path=/opengw/v1/mobile/simulation
filters:
- name: Mobile
args:
className: br.com.decorator.CustomerDataBodyDecorator
execution: PRE
custom: ${custom.resources.customer}
- RewritePath=/opengw/v1/(?<segment>/?.*), /$\{segment}
I read several topics here but I couldn't find a solution that worked.
How can I read and then modify the request body of the Flux object in this scenario?
loadSingle return Single object, if it fails I want to call getObservable(rsList) which return Observable.
I am trying with onErrorResumeNext but it needs Single object.
How can I call getObservable(rsList) on failure of loadSingle() ?
Thanks in advance!!
repo.loadSingle()
.subscribeOn(Schedulers.io())
.onErrorResumeNext {
repo.getObservable(rsList)
}
.flatMapObservable {
if (it != null && it.status == Status.SUCCESS) {
upRsList(it.data)
}
repo.getObservable(rsList)
}
({ //observable success
}, {
//observable error
})
Api interface
interface HomeApi{
fun getSingel():Single<List<String>>
fun getObservable():Observable<HomeResponse>
}
Dependencies
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.6.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.6.2")
implementation "io.reactivex.rxjava3:rxjava:3.0.4"
implementation "io.reactivex.rxjava3:rxkotlin:3.0.0"
Required classes
internal interface Repo {
fun loadSingle(): Single<Result<List<String>>>
fun getObservable(list: List<String>): Observable<String>
}
internal class RepoImpl : Repo {
override fun loadSingle(): Single<Result<List<String>>> {
return Single.error(RuntimeException("fail"))
}
override fun getObservable(list: List<String>): Observable<String> {
if (list === emptyList<String>()) {
return Observable.just("42")
}
return Observable.just("success")
}
}
internal sealed class Result<T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure<T>(private val failure: Throwable) : Result<T>()
}
Test
Wrap the error via #onErrorReturn into a default value, and handle the result accordingly.
class So64751341 {
#Test
fun `64751341`() {
val repo: Repo = RepoImpl()
val testScheduler = TestScheduler()
val flatMapObservable = repo.loadSingle()
.subscribeOn(testScheduler)
.onErrorReturn { failure -> Result.Failure(failure) }
.flatMapObservable { result ->
when (result) {
is Result.Success -> repo.getObservable(result.value)
is Result.Failure -> repo.getObservable(emptyList())
}
}
val test = flatMapObservable.test()
testScheduler.triggerActions()
test // return default value 42 onError
.assertValue("42")
}
}
Repo#loadSingle() throws exception synchronously
internal class RepoExceptionImpl : Repo {
override fun loadSingle(): Single<Result<List<String>>> {
throw java.lang.RuntimeException("whatever")
}
override fun getObservable(list: List<String>): Observable<String> {
if (list === emptyList<String>()) {
return Observable.just("42")
}
return Observable.just("success")
}
}
Test
Repo#loadSingle must be wrapped with Single#defer. Single#defer will catch the exception and emit it as #onError to the subscriber, which in turn will be handled by #onErrorReturn
#Test
fun `64751341_exception`() {
val repo: Repo = RepoExceptionImpl()
val testScheduler = TestScheduler()
val flatMapObservable = Single.defer {
repo.loadSingle()
}
.subscribeOn(testScheduler)
.onErrorReturn { failure -> Result.Failure(failure) }
.flatMapObservable { result ->
when (result) {
is Result.Success -> repo.getObservable(result.value)
is Result.Failure -> repo.getObservable(emptyList())
}
}
val test = flatMapObservable.test()
testScheduler.triggerActions()
test // return default value 42 onError
.assertValue("42")
}
I am trying to log, method input/output using the aop approach in Web-flux. I was able to log request using following code ,but had trouble printing response and I see both request and response are printing as request,How can i print response.Is AOP is better solution for logging in reactive approach or filter/controller advice
#Aspect
#Component
class LogAspect(private val log: KLogger) {
#Around("#annotation(Loggable)")
#Throws(Throwable::class)
fun logAround(joinPoint: ProceedingJoinPoint): Any? {
val start = System.currentTimeMillis()
return when (val result: Any = joinPoint.proceed()) {
is Mono<*> -> {
val traceId = AtomicReference("")
result
.doOnSuccess { o ->
if (traceId.get().isNotEmpty()) {
MDC.put("correlationId", traceId.get())
}
var response: Any = ""
if (Objects.nonNull(o)) {
response = o.toString()
}
log.info(
"Enter: {}.{}() with argument[s] = {}",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args
)
log.info(
"Exit: {}.{}() had arguments = {}, with Response = {}, Execution time = {} ms",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args[0],
response, System.currentTimeMillis() - start
)
}
.subscriberContext { context ->
val contextTmp: Context = context as Context
if (contextTmp.hasKey("correlationId")) {
traceId.set(contextTmp.get("correlationId"))
MDC.put("correlationId", contextTmp.get("correlationId"))
}
context
}
}
else ->
{
log.warn(
"Body type is not Mono for {}.{}()",
joinPoint.signature.declaringTypeName,
joinPoint.signature.name
)
result
}
}
}
}
Currently, I use retrofit2 to call restful apis and get response. Because the response body can be multiple types, I wrote the code following.
//Interface
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<Any>
//Api Manager
fun postPayment(id: String): Observable<Any> {
return Observable.create {
subscriber ->
val callResponse = api.postPayment(id)
val response = callResponse.execute()
if (response.isSuccessful) {
if (response.body() is MyClass1) {
// never success...
} else if (response.body() is MyClass2) {
// never success...
}
subscriber.onNext(response.body())
subscriber.onCompleted()
} else {
subscriber.onError(Throwable(response.message()))
}
}
}
So I'm not able to cast response.body() to MyClass1 or MyClass2.
response.body() as MyClass1 occurs error too.
MyClass1 and MyClass2 are normal template classes.
class MyClass1( val id: String, val data: String)
Is there any smart way to cast response body to my custom classes?
Small update for MyClass2
class MyClass2( val token: String, val url: String, val quantity: Int)
As mentioned by #Miha_x64, Retrofit doesn't know about your classes (MyClass1 and MyClass2) because your Call uses the Any type. Therefore, Retrofit is not creating an instance of MyClass1 or MyClass2, instead it is just creating an instance of the Any class.
The simplest solution would just be to combine the two classes:
data class MyClass(
val id: String?,
val data: String?,
val token: String?,
val url: String?,
val quantity: Int
)
Then you can specify the response type in your interface:
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<MyClass>
In the case your response does not have an id or data element, they will just be null. Then you can check which type of response was received simply by checking which values are null:
if (response.body().id != null) {
// Handle type 1 response...
} else if (response.body().token != null) {
// Handle type 2 response...
}
A slightly more complex solution would be to write a wrapper for your two classes, and a type adapter to populate the wrapper. This would avoid the nullability of each of the fields, as well as keep your data structure separated.
This would differ based on the ConverterFactory you are using but if, for example, you are using Gson, it would look something like this:
data class ApiResponse(
val val1: MyClass1? = null,
val val2: MyClass2? = null
)
class ApiResponseAdapter : TypeAdapter<ApiResponse> {
#Throws(IOException::class)
override fun write(out: JsonWriter, value: ApiResponse?) {
if (value != null) {
out.beginObject()
value.val1?.id? let { out.name("id").value(it) }
value.val1?.data? let { out.name("data").value(it) }
value.val2?.token? let { out.name("token").value(it) }
value.val2?.url? let { out.name("url").value(it) }
value.val2?.quantity? let { out.name("quantity").value(it) }
out.endObject()
} else {
out.nullValue()
}
}
#Throws(IOException::class)
override fun read(in: JsonReader): ApiResponse {
reader.beginObject()
var id: String? = null
var data: String? = null
var token: String? = null
var url: String? = null
var quantity: Int = 0
while(in.hasNext()) {
val name = in.nextName()
if (name.equals("id", true)) {
id = in.nextString()
} else if (name.equals("data", true)) {
data = in.nextString()
} else if (name.equals("token", true)) {
token = in.nextString()
} else if (name.equals("url", true)) {
url = in.nextString()
} else if (name.equals("quantity", true)) {
quantity = in.nextInt()
}
}
reader.endObject()
if (id != null && data != null) {
return ApiResponse(MyClass1(id, data), null)
} else if (token != null && url != null) {
return ApiResponse(null, MyClass2(token, url, quantity))
} else {
return ApiResponse()
}
}
}
Then you can add this type adapter to your Gson instance:
val gson = GsonBuilder().registerTypeAdapter(ApiResponse::class.java, ApiResponseAdapter()).create()
Then replace the Call<Any> type with Call<ApiRepsone> and you can then check which response was received by checking which value is null:
if (response.body().val1 != null) {
// Handle MyClass1 response...
} else if (response.body().val2 != null) {
// Handle MyClass2 response...
}
First of all, thanks #Bryan for answer. Your answer was perfect but finally I did something tricky way.
...
if (response.isSuccessful) {
val jsonObject = JSONObject(response.body() as Map<*, *>)
val jsonString = jsonObject.toString()
if (jsonObject.has("id")) {
val myclass1Object = Gson().fromJson(jsonString, MyClass1::class.java)
...
} else {
val myclass2Object = Gson().fromJson(jsonString, MyClass2::class.java)
...
}
}
...