How do you add reactive interceptors to Spring Boot annotated controllers? - spring-boot

I've set up rsocket metrics using rsocket-micrometer on the CLIENT side, by configuring the RSocketConnector with interceptors, like this (Kotlin):
rSocketReqesterBuilder.rsocketConnector { configureConnector(it) }
// ...
private fun configureConnector(rSocketConnector: RSocketConnector) {
rSocketConnector.interceptors { iRegistry ->
// This gives us the rsocket.* counter metrics, like rsocket.frame
iRegistry.forResponder(MicrometerRSocketInterceptor(registry, *localTags.toArray()))
iRegistry.forRequester(MicrometerRSocketInterceptor(registry, *localTags.toArray()))
iRegistry.forConnection(MicrometerDuplexConnectionInterceptor(registry, *localTags.toArray()))
}
}
But on the SERVER side, I'm using an annotated (#MessageMapping) Spring Boot RSocket Controller, like this (Java):
#MessageMapping("replace-channel-controller")
public Flux<TransformResponse> replace(Flux<String> texts) ...
Here, I'm not explicitly in control of the connector.
How do I add interceptors on the server side?

#Configuration
public class RSocketConfig implements RSocketServerCustomizer {
private final MeterRegistry registry;
public RSocketConfig(MeterRegistry registry) {
this.registry = registry;
}
#Override
public void customize(RSocketServer rSocketServer) {
rSocketServer.interceptors(
iRegistry -> {
log.info("Adding RSocket interceptors...");
iRegistry.forResponder(new MicrometerRSocketInterceptor(registry, tags));
iRegistry.forRequester(new MicrometerRSocketInterceptor(registry, tags));
iRegistry.forConnection(new MicrometerDuplexConnectionInterceptor(registry, tags));
}
);
}
}

Related

Spring Boot actuator trigger method when endpoint called

I am using this spring boot actuator endpoint "actuator/health/readiness". How can I run some methods when this endpoint is hit?
If I create my own controller with the same endpoint naming what will happen?
Thanks
Works in spring 2.7.2
public class CustomReadinessStateHealthIndicator extends ReadinessStateHealthIndicator {
public CustomReadinessStateHealthIndicator(ApplicationAvailability availability) {
super(availability);
}
#Override
public Health getHealth(boolean includeDetails) {
//YOUR CODE
return super.getHealth(includeDetails);
}
}
#AutoConfiguration(before = {AvailabilityHealthContributorAutoConfiguration.class})
public class Configuration {
#Bean("readinessStateHealthIndicator")
#ConditionalOnMissingBean(name = "readinessStateHealthIndicator")
public ReadinessStateHealthIndicator customReadiness(ApplicationAvailability applicationAvailability) {
return new CustomReadinessStateHealthIndicator(applicationAvailability);
}
}

RSocket with Webflux router

I have an application with Spring boot webflux and exposing endpoints as below.
#Configuration
public class BusinessServiceConfiguration {
#Autowired
private final RequestHandler reqHandler;
#Bean
public RouterFunction<ServerResponse> createUser() {
return route(POST("/api/v1/user/create").and(contentType(APPLICATION_JSON)), reqHandler::createUser);
}
}
Came across RSocket and it is promising. Planning to move to RSocket down the line.
Exposing end point with RSocket,
#Controller
public class RSocketController {
#Autowired
private final RequestService reqService;
#MessageMapping("/api/v1/user/create")
public Mono<String> createuser() {
return reqService.createUser .....
}
}
Here, the way we expose API is totally different b/w Webflux and RSocket and it need some effort.
Is there anyway to just add RSocket without changing the way we expose end points?

Spring Boot RSocketRequester deal with server restart

I have a question about Springs RSocketRequester. I have a rsocket server and client. Client connects to this server and requests #MessageMapping endpoint. It works as expected.
But what if I restart the server. How to do automatic reconnect to rsocket server from client? Thanks
Server:
#Controller
class RSC {
#MessageMapping("pong")
public Mono<String> pong(String m) {
return Mono.just("PONG " + m);
}
}
Client:
#Bean
public RSocketRequester rSocketRequester() {
return RSocketRequester
.builder()
.connectTcp("localhost", 7000)
.block();
}
#RestController
class RST {
#Autowired
private RSocketRequester requester;
#GetMapping(path = "/ping")
public Mono<String> ping(){
return this.requester
.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println);
}
}
Updated for Spring Framework 5.2.6+
You could achieve it with io.rsocket.core.RSocketConnector#reconnect.
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder rSocketRequesterBuilder) {
return rSocketRequesterBuilder
.rsocketConnector(connector -> connector
.reconnect(Retry.fixedDelay(Integer.MAX_VALUE, Duration.ofSeconds(1))))
.connectTcp("localhost", 7000);
}
#RestController
public class RST {
#Autowired
private Mono<RSocketRequester> rSocketRequesterMono;
#GetMapping(path = "/ping")
public Mono<String> ping() {
return rSocketRequesterMono.flatMap(rSocketRequester ->
rSocketRequester.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println));
}
}
I don't think I would create a RSocketRequester bean in an application. Unlike WebClient (which has a pool of reusable connections), the RSocket requester wraps a single RSocket, i.e. a single network connection.
I think it's best to store a Mono<RSocketRequester> and subscribe to that to get an actual requester when needed. Because you don't want to create a new connection for each call, you can cache the result. Thanks to Mono retryXYZ operators, there are many ways you can refine the reconnection behavior.
You could try something like the following:
#Service
public class RSocketPingService {
private final Mono<RSocketRequester> requesterMono;
// Spring Boot is creating an auto-configured RSocketRequester.Builder bean
public RSocketPingService(RSocketRequester.Builder builder) {
this.requesterMono = builder
.dataMimeType(MediaType.APPLICATION_CBOR)
.connectTcp("localhost", 7000).retry(5).cache();
}
public Mono<String> ping() {
return this.requesterMono.flatMap(requester -> requester.route("pong")
.data("TEST")
.retrieveMono(String.class));
}
}
the answer here https://stackoverflow.com/a/58890649/2852528 is the right one. The only thing I would like to add is that reactor.util.retry.Retry has many options for configuring the logic of your retry including even logging.
So I would slightly improve the original answer, so we'd be increasing the time between the retry till riching the max value (16 sec) and before each retry log the failure - so we could monitor the activity of the connector:
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder builder) {
return builder.rsocketConnector(connector -> connector.reconnect(Retry.backoff(Integer.MAX_VALUE, Duration.ofSeconds(1L))
.maxBackoff(Duration.ofSeconds(16L))
.jitter(1.0D)
.doBeforeRetry((signal) -> log.error("connection error", signal.failure()))))
.connectTcp("localhost", 7000);
}

Spring Cloud Gateway pass bean to custom filter

We are attempting to use Spring Cloud Gateway to setup a microservice based architecture. Currently, we have defined a route programatically:
#ServletComponentScan
#SpringBootApplication
public class GatewayApplication {
// to be passed to and used by custom filter
#Autowired
RestTemplate restTemplate;
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("status", r -> r
.method(HttpMethod.GET)
.and()
.path("/status")
.filters(f -> f.rewritePath("/status", "/v2/status")
.filter(new AuthorizationFilter(restTemplate).apply(new Config(""))))
.uri("http://localhost:8081/"))
.build();
}
}
The above would route an incoming request /status via GET to another endpoint. We would like to apply a custom filter, which we have implemented in AuthorizationFilter. This filter, as the name implies, is another microservice which will either allow or deny an incoming request based on credentials and permissions.
Currently, the pattern we are following, which works, is to inject a Spring RestTemplate into the gateway class above, and then to pass this RestTemplate to the constructor of the filter.
However, how can this be done if we wanted to switch to using a YAML file for defining all the routes? Presumably in both cases Spring would be constructing a new filter for each incoming request. But in the case of YAML, how can we pass something in the construtor? If this cannot be done, is there any other way to inject a RestTemplate, or any other resource into a custom Spring gateway filter?
You can register your own custom GatewayFilterFactory. This allows you to provide a custom configuration, and within that configuration, you can use SpEL to reference a bean.
For example:
#Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
public AuthenticationGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
// TODO: Implement
}
public static class Config {
private RestTemplate restTemplate;
// TODO: Getters + Setters
}
}
Now you can use SpEL to properly reference a RestTemplate bean:
spring:
cloud:
gateway:
routes:
- id: status
uri: http://localhost:8081/
filters:
- name: Authentication
args:
restTemplate: "#{#nameOfRestTemplateBean}"
predicates:
- Path=/status
Alternatively, you could inject a RestTemplate bean within your gateway filter. For example:
#Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
private RestTemplate restTemplate;
public AuthenticationGatewayFilterFactory(RestTemplate restTemplate) {
super(Config.class);
this.restTemplate = restTemplate;
}
#Override
public GatewayFilter apply(Config config) {
// TODO: Implement
}
public static class Config {
// TODO: Implement
}
}
The code/configuration necessary to do the inject is less complex, but it also makes it more difficult if you ever decide to put AuthenticationGatewayFilterFactory in a separate library, as the "consumers" of this library won't have any control over which RestTemplate is being injected.

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.

Resources