How to add a global filter in Spring Cloud Gateway - spring

I have this global filter :
`
#Component
public class CacheFilter implements GlobalFilter, Ordered
Another bean:
#Bean
public RouteLocator routes(
RouteLocatorBuilder builder,
CacheFilter cacheFilter) {
return builder.routes()
.route("service_route_java_config", r -> r.path("/service/**")
.filters(f ->
f.rewritePath("/service(?<segment>/?.*)", "$\\{segment}")
)
.uri("http://localhost:9050"))
.build();
}
`
How to add programaticaly this global filter to all routes?

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 .

Autowired parameter as bean declaration parameter

I have
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/employee/**")
.uri("http://localhost:8081/")
.id("employeeModule"))
.build()
But instead of http:://localhost:8081/ I want to have a dynamically derived value, like:
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder,
#Autowired val discoveryService: DiscoveryService) {
return builder.routes()
.route(r -> r.path("/employee/**")
.uri(discoveryService.getHost())
.id("employeeModule"))
Is this possible? How should I change the syntax to autowire the DiscoveryService?
Try out like this:
RouteLocatorBuilder.Builder routes = builder.routes();
discoveryClient.getServices().forEach(service->routes.route(r -> r.path("/"+service+"/**")
.uri("lb://"+service).id(service)));
return routes.build();
You can either do:
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder,
// note the absence of val
#Autowired discoveryService: DiscoveryService) {
return builder.routes()
.route(r -> r.path("/employee/**")
.uri(discoveryService.getHost())
.id("employeeModule"))
this will autowire the DiscoveryService (it also has to be declared as a #Bean) or you can call the method that declares the DiscoveryService directly since Spring will enhance that method with CGLIB so it will only ever create the DiscoveryService once:
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/employee/**")
// note the discoveryService() function call
.uri(discoveryService().getHost())
.id("employeeModule"))
// ...

How set SpringFox to show two (or more) versions of the Rest API using Spring Boot?

I'm trying to figure out how manage two (or more) version of my API endpoints using Spring Fox.
To version my APIs, I'm using the Versioning through content negotiation, also know as Versioning using Accept header. The versions of each endpoint are controlled individually using the header information. Per example, for the version one I use the attribute produces:
#Override
#PostMapping(
produces = "application/vnd.company.v1+json")
public ResponseEntity<User> createUser(
For version two, I use:
#Override
#PostMapping(
produces = "application/vnd.company.v2+json",
consumes = "application/vnd.company.v2+json")
public ResponseEntity<User> createUserVersion2(
I not use consumes for the first (v1) version, so if the client use only application/json on the call the first version will be called by default.
I would like to show the two version on the Swagger UI. How to do that?
It's very simple. Just create one Docket for each version.
Example, the first version:
#Bean
public Docket customImplementation(
#Value("${springfox.documentation.info.title}") String title,
#Value("${springfox.documentation.info.description}") String description) {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo(title, description, "1.0"))
.groupName("v1")
.useDefaultResponseMessages(false)
.securitySchemes(newArrayList(apiKey()))
.pathMapping("/api")
.securityContexts(newArrayList(securityContext())).select()
.apis(e -> Objects.requireNonNull(e).produces().parallelStream()
.anyMatch(p -> "application/vnd.company.v1+json".equals(p.toString())))
.paths(PathSelectors.any())
.build();
}
And for version two:
#Bean
public Docket customImplementationV2(
#Value("${springfox.documentation.info.title}") String title,
#Value("${springfox.documentation.info.description}") String description) {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo(title, description, "2.0"))
.groupName("v2")
.select()
.apis(e -> Objects.requireNonNull(e).produces()
.parallelStream()
.anyMatch(p -> "application/vnd.company.v2+json".equals(p.toString())))
.build();
}
The secret here is filter the available endpoints by the produces attribute.
The Swagger-UI will show the two versions on the combo:
This code needs to be on a class annotated with #Configuration. You also need to enable the Swagger with #EnableSwagger2.
As mentioned by Dherik you can create Docket for each version. But to filter here I have tried using Predicate and custom controller annotations.
Configuration class annotated with #Configuration and #EnableSwagger2
import com.google.common.base.Predicate;
#Bean
public Docket apiV30() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("v30")
.select()
.apis(selectorV30())
.paths(PathSelectors.any()).build().apiInfo(apiEndPointsInfo());
}
private Predicate<RequestHandler> selectorV30(){
return new Predicate<RequestHandler>() {
#Override
public boolean apply(RequestHandler input) {
return input.findControllerAnnotation(SwaggerDocV30.class).isPresent();
}
};
}
#Bean
public Docket apiV31() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("v31")
.select()
.apis(selectorV31())
.paths(PathSelectors.any()).build().apiInfo(apiEndPointsInfo());
}
private Predicate<RequestHandler> selectorV31(){
return new Predicate<RequestHandler>() {
#Override
public boolean apply(RequestHandler input) {
return input.findControllerAnnotation(SwaggerDocV31.class).isPresent();
}
};
}
Custom Annotation class : SwaggerDocV30
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface SwaggerDocV30 {
}
Custom Annotation class : SwaggerDocV31
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface SwaggerDocV31 {
}
Finally annotate your controllers with #SwaggerDocV30 or #SwaggerDocV31
#SwaggerDocV30
#Controller
public class MyController extends AbstractController {}
Or
#SwaggerDocV31
#Controller
public class MyController extends AbstractController {}]

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.

Configured ObjectMapper not used in spring-boot-webflux

I have mixins configured in my objectmapperbuilder config, using the regular spring web controller, the data outputted according to the mixins.
However using webflux, a controller with a method returning a Flow or Mono have the data serialized like if the objectmapper a default one.
How to get webflux to enforce an objectmapper configuration to be used ?
sample config:
#Bean
JavaTimeModule javatimeModule(){
return new JavaTimeModule();
}
#Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.mixIn(MyClass.class, MyClassMixin.class);
}
I actually found my solution by stepping through the init code:
#Configuration
public class Config {
#Bean
JavaTimeModule javatimeModule(){
return new JavaTimeModule();
}
#Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.mixIn(MyClass.class, MyClassMixin.class);
}
#Bean
Jackson2JsonEncoder jackson2JsonEncoder(ObjectMapper mapper){
return new Jackson2JsonEncoder(mapper);
}
#Bean
Jackson2JsonDecoder jackson2JsonDecoder(ObjectMapper mapper){
return new Jackson2JsonDecoder(mapper);
}
#Bean
WebFluxConfigurer webFluxConfigurer(Jackson2JsonEncoder encoder, Jackson2JsonDecoder decoder){
return new WebFluxConfigurer() {
#Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(encoder);
configurer.defaultCodecs().jackson2JsonDecoder(decoder);
}
};
}
}
I translated the solution of #Alberto Galiana to Java and injected the configured Objectmapper for convenience, so you avoid having to do multiple configurations:
#Configuration
#RequiredArgsConstructor
public class WebFluxConfig implements WebFluxConfigurer {
private final ObjectMapper objectMapper;
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(
new Jackson2JsonEncoder(objectMapper)
);
configurer.defaultCodecs().jackson2JsonDecoder(
new Jackson2JsonDecoder(objectMapper)
);
}
}
Just implement WebFluxConfigurer and override method configureHttpMessageCodecs
Sample code for Spring Boot 2 + Kotlin
#Configuration
#EnableWebFlux
class WebConfiguration : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)))
configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(ObjectMapper()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)))
}
}
Make sure all your data classes to be encoded/decoded have all its properties annotated with #JsonProperty even if property name is equal in class and json data
data class MyClass(
#NotNull
#JsonProperty("id")
val id: String,
#NotNull
#JsonProperty("my_name")
val name: String)
In my case, I was trying to use a customized ObjectMapper while inheriting all of the behavior from my app's default WebClient.
I found that I had to use WebClient.Builder.codecs. When I used WebClient.Builder.exchangeStrategies, the provided overrides were ignored. Not sure if this behavior is something specific to using WebClient.mutate, but this is the only solution I found that worked.
WebClient customizedWebClient = webClient.mutate()
.codecs(clientCodecConfigurer ->
clientCodecConfigurer.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(customObjectMapper)))
.build();
I have tried all the different solutions (#Primary #Bean for ObjectMapper, configureHttpMessageCodecs(), etc.). What worked for me at the end was specifying a MIME type. Here's an example:
#Configuration
class WebConfig: WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
val encoder = Jackson2JsonEncoder(objectMapper, MimeTypeUtils.APPLICATION_JSON)
val decoder = Jackson2JsonDecoder(objectMapper, MimeTypeUtils.APPLICATION_JSON)
configurer.defaultCodecs().jackson2JsonEncoder(encoder)
configurer.defaultCodecs().jackson2JsonDecoder(decoder)
}
}

Resources