Hello I am new to Webflux I follow a tutorial for building reactive microservices. In my project I faced the following problem.
I want to create a crud api for the product service and the following is the Create method
#Override
public Product createProduct(Product product) {
Optional<ProductEntity> productEntity = Optional.ofNullable(repository.findByProductId(product.getProductId()).block());
productEntity.ifPresent((prod -> {
throw new InvalidInputException("Duplicate key, Product Id: " + product.getProductId());
}));
ProductEntity entity = mapper.apiToEntity(product);
Mono<Product> newProduct = repository.save(entity)
.log()
.map(mapper::entityToApi);
return newProduct.block();
}
The problem is that when I call this method from postman I get the error
"block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3" but when I use a StreamListener this call works ok. The stream Listener gets events from a rabbit-mq channel
StreamListener
#EnableBinding(Sink.class)
public class MessageProcessor {
private final ProductService productService;
public MessageProcessor(ProductService productService) {
this.productService = productService;
}
#StreamListener(target = Sink.INPUT)
public void process(Event<Integer, Product> event) {
switch (event.getEventType()) {
case CREATE:
Product product = event.getData();
LOG.info("Create product with ID: {}", product.getProductId());
productService.createProduct(product);
break;
default:
String errorMessage = "Incorrect event type: " + event.getEventType() + ", expected a CREATE or DELETE event";
LOG.warn(errorMessage);
throw new EventProcessingException(errorMessage);
}
}
}
I Have two questions.
Why this works with The StreamListener and not with a simple request?
Is there a proper way in webflux to return the object of the Mono or we always have to return a Mono?
Your create method would want to look more like this and you would want to return a Mono<Product> from your controller rather than the object alone.
public Mono<Product> createProduct(Product product) {
return repository.findByProductId(product.getProductId())
.switchIfEmpty(Mono.just(mapper.apiToEntity(product)))
.flatMap(repository::save)
.map(mapper::entityToApi);
}
As #Thomas commented you are breaking some of the fundamentals of reactive coding and not getting the benefits by using block() and should read up on it more. For example the reactive mongo repository you are using will be returning a Mono which has its own methods for handling if it is empty without needing to use an Optional as shown above.
EDIT to map to error if entity already exists otherwise save
public Mono<Product> createProduct(Product product) {
return repository.findByProductId(product.getProductId())
.hasElement()
.filter(exists -> exists)
.flatMap(exists -> Mono.error(new Exception("my exception")))
.then(Mono.just(mapper.apiToEntity(product)))
.flatMap(repository::save)
.map(mapper::entityToApi);
}
Related
I am developing a Microservice application in SpringBoot. I am using Spring Cloud gateway there,now since Spring Cloud Gateway uses WebFlux module so,I want to extract username and password inside ServerAuthenticationConverter. But unfortunately flow is getting stuck on subscribe() method.
#Component
public class MyConverter implements ServerAuthenticationConverter {
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
String token = exchange.getRequest().getHeaders().getFirst("token");
Map<String,String> credentialMap = new HashMap<>();
if(StringUtils.containsIgnoreCase(exchange.getRequest().getPath().toString(),"/login")){
exchange.getFormData().subscribe(data -> {
for(Map.Entry<String,List<String>> mapEntry : data.entrySet()) {
for (String value : mapEntry.getValue()) {
credentialMap.put(mapEntry.getKey(),value);
log.info("key=" + mapEntry.getKey() + "|value=" + mapEntry.getValue());
}
}
});
User user = new User(credentialMap.get("username"),credentialMap.get("password"));
return Mono.justOrEmpty(new UsernamePasswordAuthenticationToken(user,credentialMap.get("password"), List.of(new SimpleGrantedAuthority("ADMIN"))));
}
else{
if(StringUtils.isNotBlank(token)){
if(StringUtils.contains(token,"Bearer")){
return Mono.justOrEmpty(new MyToken(AuthorityUtils.NO_AUTHORITIES,token.substring(7)));
}else{
return Mono.justOrEmpty(new MyToken(AuthorityUtils.NO_AUTHORITIES,token));
}
}
}
}
throw new IllegalArgumentException("Invalid Access");
}
}
But after printing log statement within subscribe method program flow is getting halted,no exception.
I think subscribe() method is causing some thread level issue.Can someone figureout the problem????
In my Spring Webflux API gateway I am receiving a Flux from a microservice via REST:
public Flux<MyObject> getMyObjects(String id) {
Flux<MyObject> myObjects = webClient.get().uri(nextServerUrl + "/myobject" + issueId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(MyObject.class);
return myObjects;
}
I have to rearrange the information received by the microservice in the API gateway for the response to the client. I tried to do it in two ways:
Use the Flux as far as possible:
private Rearranged createClientResponse(String id) {
Rearranged rearranged = new Rearranged();
Flux<MyObject> myObjects = myObjectService.getMyObjects(id);
rearranged.setMyObjects(myObjects);
myObjects.map(myObject -> {
rearranged.setInfo(myObject.getInfo());
//set more
return myObjects;
});
return rearranged;
}
public class Rearranged {
private Flux<MyObject> myObjects;
//more attributes
}
Result: Following empty object:
{
"information": null,
"myObjects": {
"scanAvailable": true,
"prefetch": -1
}
}
Block the Flux and work with synchronous objects
private Rearranged createClientResponse(String id) {
Rearranged rearranged = new Rearranged();
List<MyObject> myObjects = myObjectService.getMyObjects(id).collectList().block();
rearranged.setMyObjects(myObjects);
rearranged.setInfo(myObjects.get(0).getInfo());
return rearranged;
}
public class Rearranged {
private List<MyObject> myObjects;
//more attributes
}
Result: receiving the exception block()/blockFirst()/blockLast() are blocking which is not supported in thread
What would be the right way to achieve the possibility of rearranging the information from the microservice response to respond to the client?
How would I be able to block for the Flux to complete? I understand that a block is possible when I am returning a "synchronous" object (like I am doing but still getting the exception)?
First of all, your model should not countains reactive stream. Use plain object or list.
public class Rearranged {
private MyObject myObject;
}
Or
public class Rearranged {
private List<MyObject> myObjects;
}
If you block the thread, reactor threads will exhausted in a moments. If your getMyObjects method only receives one object (if not, look at the end of the comment), then you should handle it as a Mono.
Then in the createClientResponse, you have to return with Mono<Rearranged>
Now you can easily map from one Mono to another using the .map method.
private Mono<Rearranged> createClientResponse(String id) {
Mono<MyObject> myObjects = myObjectService.getMyObjects(id);
return myObjects.map(myObject -> {
retrun new Rearranged(myObject)
//create the proper object here
});
}
If you need more object, you can use the same method, for example, the collectList() collect the elements from the Flux<> into Mono<List<>>, then the same method can be accepted.
I have a strage(for me) question to ask. I have created synchronized Service which is called by Controller:
#Controller
public class WebAppApiController {
private final WebAppService webApService;
#Autowired
WebAppApiController(WebAppService webApService){
this.webApService= webApService;
}
#Transactional
#PreAuthorize("hasAuthority('ROLE_API')")
#PostMapping(value = "/api/webapp/{projectId}")
public ResponseEntity<Status> getWebApp(#PathVariable(value = "projectId") Long id, #RequestBody WebAppRequestModel req) {
return webApService.processWebAppRequest(id, req);
}
}
Service layer is just checking if there is no duplicate in request and store it in database. Because client which is using this endpoint is making MANY requests continously it happened that before one request was validated agnist duplicate other the same was put in database - that is why I am trying to do synchronized block.
#Service
public class WebAppService {
private final static String UUID_PATTERN_TO = "[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}";
private final WebAppRepository waRepository;
#Autowired
public WebAppService(WebAppRepository waRepository){
this.waRepository= waRepository;
}
#Transactional(rollbackOn = Exception.class)
public ResponseEntity<Status> processScanWebAppRequest(Long id, WebAppScanModel webAppScanModel){
try{
synchronized (this){
Optional<WebApp> webApp=verifyForDuplicates(webAppScanModel);
if(!webApp.isPresent()){
WebApp webApp=new WebApp(webAppScanModel.getUrl())
webApp=waRepository.save(webApp);
processpropertiesOfWebApp(webApp);
return new ResonseEntity<>(HttpStatus.CREATED);
}
return new ResonseEntity<>(HttpStatus.CONFLICT);
}
} catch (NonUniqueResultException ex){
return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
} catch (IncorrectResultSizeDataAccessException ex){
return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
}
}
}
Optional<WebApp> verifyForDuplicates(WebAppScanModel webAppScanModel){
return waRepository.getWebAppByRegex(webAppScanModel.getUrl().replaceAll(UUID_PATTERN_TO,UUID_PATTERN_TO)+"$");
}
And JPA method:
#Query(value="select * from webapp wa where wa.url ~ :url", nativeQuery = true)
Optional<WebApp> getWebAppByRegex(#Param("url") String url);
processpropertiesOfWebApp method is doing further processing for given webapp which at this point should be unique.
Intended behaviour is:
when client post request contains multiple urls like:
https://testdomain.com/user/7e1c44e4-821b-4d05-bdc3-ebd43dfeae5f
https://testdomain.com/user/d398316e-fd60-45a3-b036-6d55049b44d8
https://testdomain.com/user/c604b551-101f-44c4-9eeb-d9adca2b2fe9
Only first one will be stored within database but at this moment this is not what is happening. Select from my database:
select inserted,url from webapp where url ~ 'https://testdomain.com/users/[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$';
2019-11-07 08:53:05 | https://testdomain.com/users/d398316e-fd60-45a3-b036-6d55049b44d8
2019-11-07 08:53:05 | https://testdomain.com/users/d398316e-fd60-45a3-b036-6d55049b44d8
2019-11-07 08:53:05 | https://testdomain.com/users/d398316e-fd60-45a3-b036-6d55049b44d8
(3 rows)
I will try to add unique constraint on url column but I can't imagine this will solve the problem while when UUID changes new url will be unique
Could anyone give me a hint what I am doing wrong?
Question is related with the one I asked before but not found proper solution, so I simplified my method but still no success
My First Question:
In my StateMachineConfiguration.class.
#Bean
public StateMachineListener<CompanyStatus, CompanyEvents> listener() {
return new StateMachineListenerAdapter<CompanyStatus, CompanyEvents>() {
#Override
public void transition(Transition<CompanyStatus, CompanyEvents> transition) {
if(transition.getTarget().getId() == CompanyStatus.COMPANY_CREATED) {
logger.info("公司创建,发送消息到用户服务和菜单服务");
// how to get stateContext in there?
StateContext stateContext;
Message message = new Message.Builder<String>().messageType(CompanyStatus.COMPANY_CREATED.toString()).build();
messageSender.sendToUaa(message);
messageSender.sendToRes(message);
}
}
};
}
In my service.
log.debug("Request to save Company : {}", companyDTO);
Company company = companyMapper.toCmpy(companyDTO);
company = companyRepository.save(company);
stateMachine.sendEvent(MessageBuilder
.withPayload(CompanyEvents.COMPANY_CREATE)
.setHeader("companyId", company.getId())
.build());
return companyMapper.toCmpyDTO(company);
How I can get message header[companyId] in listener?
My Second Question:
statechart
In StateMachineListener you could use its stateContext method which gives you access to StateContext. StateContext then have access to message headers via its getMessageHeaders.
Original listener interface didn't expose that much so we had to add new method which exposes context which were introduced to machine later than listener interface were created. This because we need not to break things and we generally like to be backward compatibility.
We are building an API for our service and we would like to leverage Spring Data Rest as much as possible.
This API and the new model underneath will substitute a legacy API (and it's old model) that we still need to support.
Our idea is to build an "adapter" web app that replicates the structure of the old api and serve the old model using some internal transformations.
Also the old api is using Spring Data Rest, so here the idea:
build a repository implementation that instead of querying a database will query our brand new API, retrieve the new model, apply some transformations, and return the old model.
Unfortunately, even if I'm annotating the repository implementation with the #Repository annotation, Spring is not exposing the repository in the API.
I'm not sure if this is actually something possible to do or is just a matter of me not implementing some core functionalities.
What I would like to avoid is reimplement all spring data rest methods manually in a controller.
Here my Repository class
// Method are not implemented, this is just the backbone
#Repository
public class SampleRespositoryImpl implements ReadOnlyRepository<OldSample, String> {
NewApiClient client;
public SampleRespositoryImpl(NewApiClient client) {
this.client = client;
}
#Override
public OldSample findOne(String accession) {
NewSample newSample = client.fetch(accession)
OldSample oldSample = //apply transformation to newSample
return oldSample;
}
#Override
public boolean exists(String accession) {
return client.fetch(accession) != null;
}
#Override
public Iterable<OldSample> findAll() {
return new ArrayList<>();
}
#Override
public Iterable<OldSample> findAll(Iterable<String> var1) {
return new ArrayList<>();
}
#Override
public long count() {
return 0;
}
#Override
public Iterable<OldSample> findAll(Sort var1) {
return new ArrayList<>();
}
#Override
public Page<OldSample> findAll(Pageable var1) {
List<OldSample> OldSampleList = new ArrayList<>();
Page<OldSample> page = new PageImpl<>(OldSampleList);
return page;
}
}
Here what I would like to get back when I hit the api root (http://localhost:8080/)
{
"_links": {
"samples": {
"href": "http://localhost:8080/samples{?page,size,sort}
}
}
}
Someone else linked me to another answer in StackOverflow available here as possible duplication.
Reading through that answer, I decided that is too much effort to follow this path for our needs, so I'm more oriented to create a custom controller to expose necessary methods.
This solution was reported by Kevin as answer to Implementing methods of Spring Data repository and exposing them through REST