Spring Cloud API Gateway Passing Dynamic Parameter value - spring

I'm learning how to build an API Gateway using Spring Cloud. I've scoured through the documentation on how to pass a parameter and all examples seem to show them as hardcoded in. But what if I have a dynamic value?
For example I have this type of request: http://localhost:8080/people/lookup?searchKey=jdoe,
How do I pass in the "jdoe" part?
I tried the following code and it works only if I hardcode the value in the code.
i.e., .filters(f -> f.addRequestParameter("searchKey", "jdoe") .
That test also proves that my discovery server (Eureka) is working.
I'm not sure how to access the value using the provided builder methods. It's such a simple scenario but I'm surprised to find out there's not a lot of example or documentation for it so it must be just me.
#SpringBootApplication
#EnableEurekaClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("people-service", r -> r.path("/people/active-associates")
.uri("lb://people-service"))
.route(r -> r.path("/people/lookup")
.filters(f -> f.addRequestParameter("searchKey", howDoIPassDynamicValueHere))
.uri("lb://people-service")
.id("addrequestparameter_route"))
.build();
}
This obviously worked when I call the service directly because my microservice controller handles it like this using the #RequestParam...pretty straightforward:
#RestController
#RequestMapping(value = "/people")
public class PersonController {
#Autowired
private PersonService personService;
/**
* Searches by FirstName, Lastname or NetworkId.
*
* #param searchKey
* #return ResponseEntity<List<Person>>
*/
#GetMapping(value = "/lookup")
public ResponseEntity<List<Person>> findPersonsBySearchKey(#RequestParam(name = "searchKey") String searchKey) {
List<Person> people = personService.findActivePersonsByFirstLastNetworkId(searchKey.trim().toLowerCase());
return new ResponseEntity<List<Person>>(people, HttpStatus.OK);
}
}

Thanks to the comments, it started making sense to me. I guess I did overthink when I read the documentation about the filter's addRequestParameter() method. I thought that I would need to use that method if my requests have parameters. Been scratching my head for a day and I can't believe it's that simple. So I got it working by just removing that filter:
#SpringBootApplication
#EnableEurekaClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("people-service", r -> r.path("/people/active-associates")
.uri("lb://people-service"))
.route(r -> r.path("/people/lookup")
.uri("lb://people-service"))
.build();
}
}

Related

How to add Pre Filter in Spring cloud gateway

I am using spring cloud gateway to route request to my downstream application
I have the router defined something like below
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/test/**")
.uri("http://localhost:8081/test")
.id("testModule"))
.build();
}
}
Routing works fine, now I need to add a prefilter which can do some pre-condition and get routing path. but not getting how to change uri dynamically .uri("http://localhost:8081/test")
Below is the code I am trying for out in preFilter.
#Component
public class testPreFilter extends AbstractGatewayFilterFactory {
#Override
public GatewayFilter apply(Config config) {
System.out.println("inside testPreFilter.apply method");
return (exchange, chain) -> {
//get headers and do lookup for URI in mapping DB
**//If contains return modify the uri**
return chain.filter(exchange.mutate().request(request).build());
//else 401
};
}
}
so I need to forward from incoming path /user/test/** to http://localhost:8081/test1 or http://localhost:8081/test2 based on db lookup return in my custom filter
You are basically changing the path I believe , so you can do that in this fashion .
Based on the value you get from the database , set the path .

Apache camel dynamic routing

I have following Apache camel rest service(/sales) that internally calls another rest service(/getOrders) and get list of objects. Am able to print JSON response in the processor but getting java objects in response while trying from postman. Could anyone pls help me to resolve the issue. Attaching the response log for ref..
#Component
public class ApplicationResource extends RouteBuilder {
#Autowired
private OrderService service;
#BeanInject
private OrderProcessor processor;
#Override
public void configure() throws Exception {
restConfiguration().component("servlet").port(9090).host("localhost");
rest().get("/getOrders").produces(MediaType.APPLICATION_JSON_VALUE).route().setBody(() -> service.getOrders());
rest().get("/sales").produces(MediaType.APPLICATION_JSON_VALUE).route()
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
.toD("http://localhost:9090/getOrders?bridgeEndpoint=true").convertBodyTo(String.class).marshal()
.json(JsonLibrary.Jackson, Order.class).to("log:foo?showHeaders=true");;
;
}
}
You should remove the last .endRest() on "direct:bye" route.
I think you get the rest response before calling your Processor.
This works for me.
First, I needed to set the bindingMode as RestBindingMode.json in the restConfiguration.
Secondly, instead of marshal(), you need to use unmarshal().
Third, since you are returning a list of orders, .json(JsonLibrary.Jackson, Order.class) will not be sufficient to unmarshal the list of orders. You need to use a custom format which will be able to unmarshal the list of orders into a json array. This you need to do using JacksonDataFormat format = new ListJacksonDataFormat(Order.class);
#Override
public void configure() {
JacksonDataFormat format = new ListJacksonDataFormat(Order.class);
restConfiguration().component("servlet").port(9090).host(localhost).bindingMode(RestBindingMode.json);
rest()
.get("/getOrders")
.produces(MediaType.APPLICATION_JSON_VALUE)
.route()
.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
exchange.getMessage().setBody(service.getOrders());
}})
.to("log:getOrders?showHeaders=true&showBody=true");
rest()
.get("/sales")
.produces(MediaType.APPLICATION_JSON_VALUE)
.route()
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
.toD("http://localhost:9090/getOrders?bridgeEndpoint=true")
.unmarshal(format)
.to("log:sales?showHeaders=true&showBody=true");
}
Solvedddd !!! i did two things as follows,May be use full for some one
1,bindingMode(RestBindingMode.auto) - RestBindingMode changes to auto
from json
2, Added this in the main
service(/getOrders).marshal().json(JsonLibrary.Jackson);
#Component
public class ApplicationResource extends RouteBuilder {
#Autowired
private OrderService service;
#BeanInject
private OrderProcessor processor;
#Override
public void configure() throws Exception {
restConfiguration().component("servlet").port(9090).host("localhost").bindingMode(RestBindingMode.auto);
rest().get("/getOrders").produces(MediaType.APPLICATION_JSON_VALUE).route().setBody(() -> service.getOrders())
.marshal().json(JsonLibrary.Jackson);
rest().get("/sales").produces(MediaType.APPLICATION_JSON_VALUE).route()
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
.toD("http://localhost:9090/getOrders?bridgeEndpoint=true").convertBodyTo(String.class)
.log("body = ${body}");
;
;
}
}

Persisting Spring Cloud Gateway Routes in Database

I am currently using the spring cloud gateway project to build simple api gateway, the plan was to persist the route in mongodb, then refresh, so that the new route can be available. I have done something simple like this to get my route from mongo.
#Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder){
List<CreateAPIRequest> apiRequestList = repository.findAll();
RouteLocatorBuilder.Builder routeLocator = builder.routes();
for (CreateAPIRequest request: apiRequestList) {
routeLocator
.route(r-> {
r.path("/"+request.getProxy().getListenPath())
.filters(f->f.stripPrefix(1))
.uri(request.getProxy().getTargetUrl())
});
}
return routeLocator.build();
}
I was able to create new route in the db, but I am unable to refresh on the fly.
I need to understand how to refresh the routes on the fly.
Thanks
Whenever you wish to update the routes dynamically send a RefreshRoutesEvent. The following component implements the event sending functionality.
#Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
publisher = applicationEventPublisher;
}
public void refreshRoutes() {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
Here is a sample showing how to use the component above:
#Autowired
GatewayRoutesRefresher gatewayRoutesRefresher;
...
public void buildRoutes() {
// build your routes basing on your db entries then refresh the routes in gateway
...
gatewayRoutesRefresher.refreshRoutes();
}
You can find a more complete picture of the concept by looking into the following project code: https://github.com/botorabi/HomieCenter
SCG(Spring Cloud Gateway) has been provided RouteDefinitionRepository, you can write your own RouteDefinitionRepository, and implements RouteDefinitionRepository to override getRouteDefinitions method.
You can refer to this class: InMemoryRouteDefinitionRepository
For example:
#Service
public class MongodbDefinitionRepository implements RouteDefinitionRepository {
#Autowired
private RouteConfigDao routeConfigDao;
#Override
public Flux<RouteDefinition> getRouteDefinitions() {
// todo
List<RouteDefinition> routeConfigs = routeConfigDao.findAll();
return Flux.fromIterable(routeConfigs);
}
#Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
// todo
return Mono.empty();
});
}
#Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
// todo
int delete = routeConfigDao.delete(routeId);
if (delete > 0) {
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new Exception("delete route definition error, routeId:" + routeId)));
});
}
}
How to refresh the routes on the fly
Enable actuator
place this in your application.yml
management:
endpoints:
web:
exposure:
include: gateway
POST http://ip:port/actuator/gateway/refresh
Publish RefreshRoutesEvent
#Service
public class MyPublishBiz implements ApplicationEventPublisherAware {
protected ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public Mono<Void> refresh() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Mono.empty();
}
}
I went quickly to the repo and the open issues.
And it seems that at the moment the only way to refresh the routes is from Actuator via:
/actuator/gateway/refresh
You can check the discussion here: https://github.com/spring-cloud/spring-cloud-gateway/issues/43
Can you use Consul for persisting your route definitions instead of mongo. Then a simple POST call to the actuator's refresh will reload your route definitions on the fly.

Spring cloud gateway: How to create a filter

I'm new to spring cloud gateway.
I've been watching some of the youtube videos from the SpringDeveloper channel and am working on the following example:
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/get")
.addRequestHeader("X-SpringOne", "Awesome")
.uri("http://httpbin.org:80"))
.build();
}
Prior to looking at spring cloud gateway, i've also looked at Spring Netflix Zuul. I understand that in Netflix Zuul, you can create filters by creating a class that extends ZuulFilter and define it as a pre, post, route, etc.
However I was wondering how one can create a PRE/ POST filter using Spring cloud gateway?
Any help/ advice is much appreciated.
Thanks.
For a pre filter here is AddRequestHeader (code is executed before chain.filter() call):
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
#Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
For a 'post' filter, here is SetStatus (code is run in lambda in chain.filter(exchange).then()):
public class SetStatusGatewayFilterFactory extends AbstractGatewayFilterFactory<SetStatusGatewayFilterFactory.Config> {
#Override
public GatewayFilter apply(Config config) {
final HttpStatus status = ServerWebExchangeUtils.parse(config.status);
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// check not really needed, since it is guarded in setStatusCode,
// but it's a good example
if (!exchange.getResponse().isCommitted()) {
setResponseStatus(exchange, status);
}
}));
};
}
}
Here is a simple example in Kotlin: the URI http://.../customers is mapped to the URI obtained from the discovery service (lb = load balanced) for the service named customer and appended with "/". Furthermore, the forwarded request is enhanced with an additional header entry. Hope this helps.
#SpringBootApplication
class Application {
#Bean
fun routes(builder: RouteLocatorBuilder) = builder.routes {
route {
path("/customers")
filters {
setPath("/")
addRequestHeader("aKey", "aValue")
}
uri("lb://customer")
}
}
}
I am not sure this is the correct way to do it because I am also trying to achieve this behavior, I am thinking if this is something that needs to be done:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomFilter implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//code for PRE filter
Mono<Void> v = chain.filter(exchange);
//code for POST filter
return v;
}
}
Let me know if that works for you or if you found another solution.

Adding programmatically new route to zuul proxy

I am using a spring boot application with #EnableZuulProxy annotation. But I would like to add custom routes during runtime. How is this possible?
Existing documentation only shows static examples, in which routes are defined in the application.yml. Could you point me to code snippets of my use case.
In the ZuulConfiguration I found a possibility to add routes routeLocator().getRoutes().add(route); but they are not applied to the runtime. What am I missing?
Thanks a lot. Cheers
Gerardo
What I did was subclass the SimpleRouteLocator class with my own RouteLocator class. Here is sample of what I did:
public class RouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
#Autowired
private ZuulHandlerMapping zuulHandlerMapping;
private Map<String, ZuulRoute> routes = new ConcurrentHashMap<>();
public RouteLocator(TaskExecutor executor, String servletPath, ZuulProperties properties) {
super(servletPath, properties);
executor.execute(new ServiceWatcher());
}
#Override
public Map<String, ZuulRoute> locateRoutes() {
return this.routes;
}
#Override void refresh() {
this.doRefresh();
}
private class ServiceWatcher implements Runnable {
#Override
public void run(){
// Add your routes to this.routes here.
ZuulRoute route1 = new ZuulRoute("/somePath", "http://someResourceUrl:8080");
ZuulRoute route2 = new ZuulRoute("/someOtherPath", "some-service-id");
routes.put("/somePath", route1);
routes.put("/someOtherPath", route2);
zuulHandlerMapping.setDirty(true);
}
}
}
I'm not exactly sure when the ServiceWatcher gets called since in my actual code the ServiceWatcher wraps around a Kubernetes Watcher (since I am running Zuul in an OpenShift environment), but this should provide the gist of how to get started.

Resources