How can we create a multi-tenant application in spring webflux using Mongodb-reactive repository?
I cannot find any complete resources on the web for reactive applications. all the resources available are for non-reactive applications.
UPDATE:
In a non-reactive application, we used to store contextual data in ThreadLocal but this cannot be done with reactive applications as there is thread switching. There is a way to store contextual info in reactor Context inside a WebFilter, But I don't how get hold of that data in ReactiveMongoDatabaseFactory class.
Thanks.
I was able to Implement Multi-Tenancy in Spring Reactive application using mangodb. Main classes responsible for realizing were: Custom MongoDbFactory class, WebFilter class (instead of Servlet Filter) for capturing tenant info and a ThreadLocal class for storing tenant info. Flow is very simple:
Capture Tenant related info from the request in WebFilter and set it in ThreadLocal. Here I am sending Tenant info using header: X-Tenant
Implement Custom MondoDbFactory class and override getMongoDatabase() method to return database based on current tenant available in ThreadLocal class.
Source code is:
CurrentTenantHolder.java
package com.jazasoft.demo;
public class CurrentTenantHolder {
private static final ThreadLocal<String> currentTenant = new InheritableThreadLocal<>();
public static String get() {
return currentTenant.get();
}
public static void set(String tenant) {
currentTenant.set(tenant);
}
public static String remove() {
synchronized (currentTenant) {
String tenant = currentTenant.get();
currentTenant.remove();
return tenant;
}
}
}
TenantContextWebFilter.java
package com.example.demo;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Component
public class TenantContextWebFilter implements WebFilter {
public static final String TENANT_HTTP_HEADER = "X-Tenant";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().containsKey(TENANT_HTTP_HEADER)) {
String tenant = request.getHeaders().getFirst(TENANT_HTTP_HEADER);
CurrentTenantHolder.set(tenant);
}
return chain.filter(exchange).doOnSuccessOrError((Void v, Throwable throwable) -> CurrentTenantHolder.remove());
}
}
MultiTenantMongoDbFactory.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoDatabase;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
public class MultiTenantMongoDbFactory extends SimpleReactiveMongoDatabaseFactory {
private final String defaultDatabase;
public MultiTenantMongoDbFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDatabase = databaseName;
}
#Override
public MongoDatabase getMongoDatabase() throws DataAccessException {
final String tlName = CurrentTenantHolder.get();
final String dbToUse = (tlName != null ? tlName : this.defaultDatabase);
return super.getMongoDatabase(dbToUse);
}
}
MongoDbConfig.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoClientFactoryBean;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
#Configuration
public class MongoDbConfig {
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate(MultiTenantMongoDbFactory multiTenantMongoDbFactory) {
return new ReactiveMongoTemplate(multiTenantMongoDbFactory);
}
#Bean
public MultiTenantMongoDbFactory multiTenantMangoDbFactory(MongoClient mongoClient) {
return new MultiTenantMongoDbFactory(mongoClient, "test1");
}
#Bean
public ReactiveMongoClientFactoryBean mongoClient() {
ReactiveMongoClientFactoryBean clientFactory = new ReactiveMongoClientFactoryBean();
clientFactory.setHost("localhost");
return clientFactory;
}
}
UPDATE:
In reactive-stream we cannot store contextual information in ThreadLocal any more as the request is not tied to a single thread, So, This is not the correct solution.
However, Contextual information can be stored reactor Context in WebFilter like this. chain.filter(exchange).subscriberContext(context -> context.put("tenant", tenant));. Problem is how do get hold of this contextual info in ReactiveMongoDatabaseFactory implementation class.
Here is my very rough working solution for Spring WebFlux - they have since updated the ReactiveMongoDatabaseFactory - getMongoDatabase to return a Mono
Create web filter
public class TenantContextFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantContextFilter.class);
#Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
ServerHttpRequest request = swe.getRequest();
HttpHeaders headers = request.getHeaders();
if(headers.getFirst("X-TENANT-ID") == null){
LOGGER.info(String.format("Missing X-TENANT-ID header"));
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String tenantId = headers.getFirst("X-TENANT-ID");
LOGGER.info(String.format("Processing request with tenant identifier [%s]", tenantId));
return wfc.filter(swe)
.contextWrite(TenantContextHolder.setTenantId(tenantId));
}
}
Create class to get context (credit to somewhere I found this)
public class TenantContextHolder {
public static final String TENANT_ID = TenantContextHolder.class.getName() + ".TENANT_ID";
public static Context setTenantId(String id) {
return Context.of(TENANT_ID, Mono.just(id));
}
public static Mono<String> getTenantId() {
return Mono.deferContextual(contextView -> {
if (contextView.hasKey(TENANT_ID)) {
return contextView.get(TENANT_ID);
}
return Mono.empty();
}
);
}
public static Function<Context, Context> clearContext() {
return (context) -> context.delete(TENANT_ID);
}
}
My spring security setup (all requests allowed for testing)
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain WebFilterChain(ServerHttpSecurity http) {
return http
.formLogin(it -> it.disable())
.cors(it -> it.disable()) //fix this
.httpBasic(it -> it.disable())
.csrf(it -> it.disable())
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange(it -> it.anyExchange().permitAll()) //allow anonymous
.addFilterAt(new TenantContextFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
.build();
}
}
Create Tenant Mongo DB Factory
I still have some clean-up work for defaults etc...
public class MultiTenantMongoDBFactory extends SimpleReactiveMongoDatabaseFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(MultiTenantMongoDBFactory.class);
private final String defaultDb;
public MultiTenantMongoDBFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDb = databaseName;
}
#Override
public Mono<MongoDatabase> getMongoDatabase() throws DataAccessException {
return TenantContextHolder.getTenantId()
.map(id -> {
LOGGER.info(String.format("Database trying to retrieved is [%s]", id));
return super.getMongoDatabase(id);
})
.flatMap(db -> {
return db;
})
.log();
}
}
Configuration Class
#Configuration
#EnableReactiveMongoAuditing
#EnableReactiveMongoRepositories(basePackages = {"com.order.repository"})
class MongoDbConfiguration {
#Bean
public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
return new MultiTenantMongoDBFactory(MongoClients.create("mongodb://user:password#localhost:27017"), "tenant_catalog");
}
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate() {
ReactiveMongoTemplate template = new ReactiveMongoTemplate(reactiveMongoDatabaseFactory());
template.setWriteResultChecking(WriteResultChecking.EXCEPTION);
return template;
}
}
Entity Class
#Document(collection = "order")
//getters
//setters
Testing
Create two mongo db's with same collection, put different documents in both
In Postman I just did a get request with the "X-TENANT-ID" header and database name as the value (e.g. tenant-12343 or tenant-34383) and good to go!
Related
I have a interface Event which internally calls the checkEvent() of each implementation and gives EventResponse as object. So the final output using all implementation gives me List
As these are not synchronized, try to execute in async way to get the output fast.
I am trying to achieve the same thing using Spring Web Flux.
However not able to acheive the final result i.e.List
My event interface is like:
public interface IEventProvider {
EventResponse check(EventRequest request);
}
I have 3 implementation of this which are as below:
MumbaiEventProvider
import org.springframework.stereotype.Component;
#Component
public class MumbaiEventProvider implements IEventProvider {
#Override
public EventResponse check(EventRequest request) {
return EventResponse.builder().location("Mumbai").build();
}
}
DelhiEventProvider
import org.springframework.stereotype.Component;
#Component
public class DelhiEventProvider implements IEventProvider {
#Override
public EventResponse check(EventRequest request) {
return EventResponse.builder().location("Delhi").build();
}
}
ChennaiEventProvider
import org.springframework.stereotype.Component;
#Component
public class ChennaiEventProvider implements IEventProvider {
#Override
public EventResponse check(EventRequest request) {
return EventResponse.builder().location("Chennai").build();
}
}
I have tried using below code but not helping as final output is not as expected:
#Service
public class EventProviderHandler {
private final List<IEventProvider> providers;
#Autowired
public EligibilityHandler(List<IEventProvider> providers) {
this.providers = providers;
}
public Mono<ServerResponse> getEligibilities(ServerRequest request) {
List<EventResponse> eventList = new ArrayList<>();
Mono<EventRequest> eventRequestMono = request.bodyToMono(EventRequest.class);
List<EventResponse> events = Flux.fromIterable(providers).parallel().flatMap(eventProvider -> {
eventList.add(eventProvider.check(eventRequestMono));
return eventList;
}).collectList();
}
}
What is expected is to return:
List<EligibilityResponse> eligibilities = Flux.fromIterable(providers).parallel().flatMap(eligibilityProvider -> {
eligibilities.add(eligibilityProvider.check(eligibilityRequestMono));
return eligibilities;
})
The application is based on the following stack:
Quarkus 1.5.0
Extensions: vertx-web, reactive-pgclient
The complete codes is here.
I created a Router by #Observes Router.
#ApplicationScoped
public class RoutesObserver {
#Inject PostsHandlers handlers;
public void route(#Observes Router router) {
router.get("/posts").produces("application/json").handler(handlers::getAll);
router.post("/posts").consumes("application/json").handler(handlers::save);
router.get("/posts/:id").produces("application/json").handler(handlers::get);
router.put("/posts/:id").consumes("application/json").handler(handlers::update);
router.delete("/posts/:id").handler(handlers::delete);
router.get("/hello").handler(rc -> rc.response().end("Hello from my route"));
}
}
And extracted the handlers into a standalone bean.
#ApplicationScoped
class PostsHandlers {
private static final Logger LOGGER = Logger.getLogger(PostsHandlers.class.getSimpleName());
PostRepository posts;
ObjectMapper objectMapper;
#Inject
public PostsHandlers(PostRepository posts, ObjectMapper objectMapper) {
this.posts = posts;
this.objectMapper = objectMapper;
}
public void getAll(RoutingContext rc) {
this.posts.findAll().thenAccept(
data -> rc.response()
.write(toJson(data))
.end()
);
}
//... other methods.
}
And the PostRepository used the Java 8 CompletionStage API.
#ApplicationScoped
public class PostRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(PostRepository.class);
private final PgPool client;
#Inject
public PostRepository(PgPool _client) {
this.client = _client;
}
public CompletionStage<List<Post>> findAll() {
return client.query("SELECT * FROM posts ORDER BY id ASC")
.execute()
.thenApply(rs -> StreamSupport.stream(rs.spliterator(), false)
.map(this::from)
.collect(Collectors.toList())
);
}
And when I ran this application and tried to access the /posts. It is frozen and no response printed.
When using the write method, you need to set (beforehand) the content-length header.
There are a several approaches to handle this:
You can use .end(toJson(data)) instead of write(...).end() - it will computed the length automatically
You can use .putHeader("transfer-encoding", "chunked") and you write(...).end() - if you plan to retrieve multiple results, it's interesting as it writes each chunk to the client one by one, avoiding sending a large payload in one go
you can set the content-length as in:
String result = toJson(data);
rc.response()
.putHeader("content-length", Long.toString(result.length()))
.write(result)
.end();
Guice module integration issue with REST
I have define one AOP guice based module, but when I tried to integrate with REST code, methodInvocation.proceed retun null.
What might be best way to solve this issue.
Define AOP Guice based module as below
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
#interface NotOnWeekends {}
public class WeekendBlocker implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Calendar today = new GregorianCalendar();
if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {
throw new IllegalStateException(
invocation.getMethod().getName() + " not allowed on weekends!");
}
return invocation.proceed();
}
}
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
new WeekendBlocker());
}
}
But I tried to Integrate this with my REST API
public class WorkerBean implements Worker {
#Autowired
private WorkerRepository workerRepository;
#Override
#NotOnWeekends
public Collection<Worker> findAll() {
Collection<Worker> workers = workerRepository.findAll();
return workers;
}
#RestController
public class WorkerController {
#RequestMapping(
value = "/api/workers",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Collection<Worker>> getWorkers() {
Worker worker = Guice.createInjector(new NotOnWeekendsModule()).getInstance(Worker.class);
Collection<Worker> worker = worker.findAll(); // return null
....
}
The goal is to attach some data from security context using RequestInterceptor, but the problem, that the calling SecurityContextHolder.getContext().getAuthentication() always returns null even though it is not null (I am sure 100%).
As I understand that's because the Interceptor is created and is being run in other thread.
How could I solve this problem and get actual data from security context?
My service:
#FeignClient(value = "api", configuration = { FeignConfig.class })
public interface DocumentService {
#RequestMapping(value = "/list", method = RequestMethod.GET)
DocumentListOperation list();
}
My FeignConfig class:
#Bean
public RequestInterceptor requestInterceptor() {
return new HeaderInterceptor(userService);
}
public class HeaderInterceptor implements RequestInterceptor {
private UserService userService;
public HeaderInterceptor(UserService userService) {
this.userService = userService;
}
#Override
public void apply(RequestTemplate requestTemplate) {
Authentication a = SecurityContextHolder.getContext().getAuthentication()
requestTemplate.header("authentication", a.toString());
}
}
I managed to figure it out, thanks to the article I found here
Firstly you need to initiliaze HystrixRequestContext HystrixRequestContext.initializeContext();.
You have to create your own Context in which you will store information you need to pass to Hystrix child threads.
Here is example:
public class UserHystrixRequestContext {
private static final HystrixRequestVariableDefault<User> userContextVariable = new HystrixRequestVariableDefault<>();
private UserHystrixRequestContext() {}
public static HystrixRequestVariableDefault<User> getInstance() {
return userContextVariable;
}
}
You have to register new concurrency strategy that would wrap Callable interface
#Component
public class CustomHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
public CustomHystrixConcurrencyStrategy() {
HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
}
#Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return new HystrixContextWrapper<T>(callable);
}
public static class HystrixContextWrapper<V> implements Callable<V> {
private HystrixRequestContext hystrixRequestContext;
private Callable<V> delegate;
public HystrixContextWrapper(Callable<V> delegate) {
this.hystrixRequestContext = HystrixRequestContext.getContextForCurrentThread();
this.delegate = delegate;
}
#Override
public V call() throws Exception {
HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();
try {
HystrixRequestContext.setContextOnCurrentThread(this.hystrixRequestContext);
return this.delegate.call();
} finally {
HystrixRequestContext.setContextOnCurrentThread(existingState);
}
}
}
}
So before calling Callable object we set new thread's Context to parent's context.
After that is done you should be able to access your new defined context inside Hystrix child threads
User = UserHystrixRequestContext.getInstance().get();
Hope that will help someone.
I have defined two convertors like this using Spring Java config. I always get a XML response unless I specified the 'Accept=applicaiton/json' in the HTTP header. Is there a way to set the default convertor to be JSON instead of XML convertor.
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = {"foo.bar"})
public class WebMvcConfig extends WebMvcConfigurerAdapter {
...
#Bean
public MappingJackson2HttpMessageConverter jsonConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
#Bean
public MappingJackson2XmlHttpMessageConverter xmlConverter() {
MappingJackson2XmlHttpMessageConverter xmlConverter = new MappingJackson2XmlHttpMessageConverter();
return xmlConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConverter());
converters.add(xmlConverter());
super.configureMessageConverters(converters);
}
Here is my controller.
#RequestMapping(value = "/product")
public
#ResponseBody
BSONObject getProducts(#RequestParam String ids,
#RequestParam(required = false) String types) {
List<BSONObject> products = commonDataService.getData(ids, types);
return products;
}
Try the following configuration, it sets up the default Content negotiation strategy(based on article here):
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Another option will be to provide other ways of specifying the content format, if Accept header is not feasible, an option could be to specify an extension /myuri/sample.json which would be returned as a json.