Mono flatmap body runs twice - java

I have a java service.
Service receives a message, enriches it, stores it in a database, and sends it to kafka.
The service is made of reactive streams, DAO layer made of jdbc.
Controller:
#Api #Slf4j #Validated
#RestController
#ParametersAreNonnullByDefault
#RequiredArgsConstructor
public class IncomingMessageController {
private final IncomingMessageProcessingService processingService;
#PostMapping(value = "/incoming")
public Mono<ResponseEntity<String>> handleIncomingMessages(
#RequestBody VkIncomingMessage message
) {
return processingService.processMessage(message)
.then(Mono.just(ResponseEntity
.status(HttpStatus.OK)
.body("ok")));
}
}
Processing Service:
#Slf4j
#Service
#ParametersAreNonnullByDefault
public class IncomingMessageProcessingService {
private final List<IncomingMessageProcessor> incomingMessageProcessors;
public IncomingMessageProcessingService(
Optional<LoggingIncomingMessageProcessor> loggingIncomingMessageProcessor,
Optional<SendToKafkaIncomingMessageProcessor> kafkaIncomingMessageProcessor
) {
incomingMessageProcessors = new ArrayList<>();
loggingIncomingMessageProcessor.ifPresent(this.incomingMessageProcessors::add);
kafkaIncomingMessageProcessor.ifPresent(this.incomingMessageProcessors::add);
}
public Mono<VkIncomingDto> processMessage(VkIncomingMessage vkIncomingMessage) {
var vkIncomingDto = MessageMappingUtils.toVkIncomingDto(vkIncomingMessage);
var vkIncomingDtoMono = Mono.just(vkIncomingDto);
for (var processor : incomingMessageProcessors) {
vkIncomingDtoMono = processor.processIncomingMessage(vkIncomingDtoMono);
}
return vkIncomingDtoMono;
}
}
LoggingIncomingMessageProcessor:
#Slf4j
#Service
#RequiredArgsConstructor
#ParametersAreNonnullByDefault
#ConditionalOnProperty(
name = {"toggle.logging-incoming-processor"},
havingValue = "true",
matchIfMissing = true)
public class LoggingIncomingMessageProcessor
implements IncomingMessageProcessor {
private final IncomingMessagesDao incomingMessagesDao;
private final MessagesDao messagesDao;
private final IdGenerator idGenerator;
#Override
public Mono<VkIncomingDto> processIncomingMessage(Mono<VkIncomingDto> vkIncomingDtoMono) {
return vkIncomingDtoMono
.flatMap(vkIncomingDto -> {
var originalMsgOutLogin = getOriginalMsgOutLogin(Long.parseLong(vkIncomingDto.vkIncoming.getObject()
.getMessage()
.getMessageTag()));
var enrichedIncomingMessage = enrichVkIncomingMessages(vkIncomingDto, originalMsgOutLogin);
incomingMessagesDao.insert(enrichedIncomingMessage);
return Mono.just(enrichedIncomingMessage);
});
}
private VkIncomingDto enrichVkIncomingMessages(VkIncomingDto vkIncomingDto, String login) {
return vkIncomingDto.toBuilder()
.id(idGenerator.getNext())
.vkMessageId(Long.parseLong(Objects.requireNonNull(
vkIncomingDto.vkIncoming.getObject().getMessage().getMessageTag())))
.login(login)
.build();
}
private String getOriginalMsgOutLogin(Long originalMsgOutId) {
return messagesDao.getLoginToIdMap(Set.of(originalMsgOutId)).get(originalMsgOutId);
}
}
SendToKafkaIncomingMessageProcessor:
#Slf4j
#RequiredArgsConstructor
#Repository
#ParametersAreNonnullByDefault
#ConditionalOnProperty(
name = {"toggle.kafka-incoming-processor", "server.providermode"},
havingValue = "true",
matchIfMissing = true)
public class SendToKafkaIncomingMessageProcessor implements IncomingMessageProcessor {
private final VkIncomingMessageProducer messageProducer;
#Override
public Mono<VkIncomingDto> processIncomingMessage(Mono<VkIncomingDto> vkIncomingDtoMono) {
return vkIncomingDtoMono
.flatMap(vkIncomingDto -> messageProducer.sendMessage(MessageMappingUtils.toIncomingMessage(vkIncomingDto)))
.then(vkIncomingDtoMono);
}
}
I need to return an enriched message from LoggingIncomingMessageProcessor in order to send this information to Kafka.
The problem is that the code inside flatMap is run twice, resulting in 2 records being saved to the database and 1 message being sent to kafka.

Related

Unit Test for Redis cache in Java

Note: I already look at and tried some approaches on SO e.g. How to test Spring's declarative caching support on Spring Data repositories?, but as most of them old, I cannot make them work properly and I need a solution with the latest library versions. So, I would be appreciated if you have a look at the question and help.
#Service
#EnableCaching
#RequiredArgsConstructor
public class DemoServiceImpl implements DemoService {
private static final String CACHE_NAME = "demoCache";
private final LabelRepository labelRepository;
private final LabelTranslatableRepository translatableRepository;
private final LanguageService languageService;
#Override
public LabelDTO findByUuid(UUID uuid) {
final Label label = labelRepository.findByUuid(uuid)
.orElseThrow(() -> new EntityNotFoundException("Not found."));
final List<LabelTranslatable> translatableList = translatableRepository.findAllByEntityUuid(uuid);
return new LabelDTO(Pair.of(label.getUuid(), label.getKey()), translatableList);
}
}
I created the following Unit Test to test caching for the nethod above:
#EnableCaching
#ImportAutoConfiguration(classes = {
CacheAutoConfiguration.class,
RedisAutoConfiguration.class
})
#ExtendWith(MockitoExtension.class)
class TextLabelServiceImpl_deneme_Test {
#Autowired
private CacheManager cacheManager;
#InjectMocks
private LabelService labelService;
#Mock
private LabelRepository labelRepository;
#Mock
private LabelTranslatableRepository translatableRepository;
#Test
void test_Cache() {
UUID uuid = UUID.randomUUID();
final TextLabel textLabel = new TextLabel();
textLabel.setId(1);
textLabel.setKey("key1");
TextLabelTranslatable textLabelTranslatable = new TextLabelTranslatable();
textLabelTranslatable.setEntityUuid(uuid);
textLabelTranslatable.setLanguage(SupportedLanguage.fr);
textLabelTranslatable.setValue("value1");
final List<TextLabelTranslatable> translatableList = new ArrayList<>();
translatableList.add(textLabelTranslatable);
when(labelRepository.findByUuid(uuid)).thenReturn(Optional.of(textLabel));
when(translatableRepository.findAllByEntityUuid(uuid)).thenReturn(translatableList);
TextLabelDTO result1 = labelService.findByUuid(uuid);
TextLabelDTO result2 = labelService.findByUuid(uuid);
assertEquals(result1, result2);
Mockito.verify(translatableRepository, Mockito.times(1)).findAllByEntityUuid(uuid);
}
I am not sure if there is a missing part in my test, but at the last line (Mockito.verify()), it returns 2 instead of 1 that means caching not works. But it is working properly and there is a problem in my test I think. How should I complete the unit test to check the caching properly?
You need to annotate the service class method with #Cacheable. Try to follow the code in this tutorial. The following test code works as expected
#Import({CacheConfig.class, DemoServiceImpl.class})
#ExtendWith(SpringExtension.class)
#EnableCaching
#ImportAutoConfiguration(classes = {
CacheAutoConfiguration.class,
RedisAutoConfiguration.class
})
class DemoServiceImplTest {
#MockBean
private LabelRepository labelRepository;
#Autowired
private DemoServiceImpl demoService;
#Autowired
private CacheManager cacheManager;
#TestConfiguration
static class EmbeddedRedisConfiguration {
private final RedisServer redisServer;
public EmbeddedRedisConfiguration() {
this.redisServer = new RedisServer();
}
#PostConstruct
public void startRedis() {
redisServer.start();
}
#PreDestroy
public void stopRedis() {
this.redisServer.stop();
}
}
#Test
void givenRedisCaching_whenFindItemById_thenItemReturnedFromCache() {
UUID id = UUID.randomUUID();
Label aLabel = new Label(id, "label");
Mockito.when(labelRepository.findById(id)).thenReturn(Optional.of(aLabel));
Label labelCacheMiss = demoService.findByUuid(id);
Label labelCacheHit = demoService.findByUuid(id);
Mockito.verify(labelRepository, Mockito.times(1)).findById(id);
}
}
With this service class code:
#Service
#RequiredArgsConstructor
#EnableCaching
public class DemoServiceImpl {
public static final String CACHE_NAME = "demoCache";
private final LabelRepository labelRepository;
#Cacheable(value = CACHE_NAME)
public Label findByUuid(UUID uuid) {
return labelRepository.findById(uuid)
.orElseThrow(() -> new EntityNotFoundException("Not found."));
}
}
And this CacheConfig:
#Configuration
public class CacheConfig {
#Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return (builder) -> builder
.withCacheConfiguration(DemoServiceImpl.CACHE_NAME,
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
}
#Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}
}

Architecture pattern for "microservice" with hard logic (Spring boot)

i've got a microservice which implements some optimization function by calling many times another microservice (the second one calculates so called target function value and the first micriservice changes paramters of this tagrget function)
It leads to necessity of writing some logic in Rest Controller layer. To be clear some simplified code will be represented below
#RestController
public class OptimizerController {
private OptimizationService service;
private RestTemplate restTemplate;
#GetMapping("/run_opt")
public DailyOptResponse doOpt(){
Data iniData = service.prepareData(null);
Result r = restTemplate.postForObject(http://calc-service/plain_calc", iniData, Result.class);
double dt = service.assessResult(r);
while(dt > 0.1){
Data newData = service.preapreData(r);
r = restTemplate.postForObject(http://calc-service/plain_calc", newData , Result.class);
dt = service.assessResult(r);
}
return service.prepareResponce(r);
}
As i saw in examples all people are striving to keep rest controller as simple as possible and move all logic to service layer. But what if i have to call some other microservices from service layer? Should i keep logic of data formin in service layer and return it to controller layer, use RestTemplate object in service layer or something else?
Thank you for your help
It is straightforward.
The whole logic is in the service layer (including other services).
Simple example:
Controller:
#RestController
#RequestMapping("/api/users")
public class UserController {
private final UserManager userManager;
#Autowired
public UserController(UserManager userManager) {
super();
this.userManager = userManager;
}
#GetMapping()
public List<UserResource> getUsers() {
return userManager.getUsers();
}
#GetMapping("/{userId}")
public UserResource getUser(#PathVariable Integer userId) {
return userManager.getUser(userId);
}
#PutMapping
public void updateUser(#RequestBody UserResource resource) {
userManager.updateUser(resource);
}
}
Service:
#Service
public class UserManager {
private static final Logger log = LoggerFactory.getLogger(UserManager.class);
private final UserRepository userRepository;
private final UserResourceAssembler userResourceAssembler;
private final PictureManager pictureManager;
#Autowired
public UserManager(
UserRepository userRepository,
UserResourceAssembler userResourceAssembler,
PictureManager pictureManager
) {
super();
this.userRepository = userRepository;
this.userResourceAssembler = userResourceAssembler;
this.pictureManager= pictureManager;
}
public UserResource getUser(Integer userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User with ID " + userId + " not found!"));
return userResourceAssembler.toResource(user);
}
public List<UserResource> getUsers() {
return userResourceAssembler.toResources(userRepository.findAll());
}
public void updateUser(UserResource resource) {
User user = userRepository.findById(resource.getId()).orElseThrow(() -> new NotFoundException("User with ID " + resource.getId() + " not found!"));
PictureResource pictureResource = pictureManager.savePicture(user);
user = userResourceAssembler.fromResource(user, resource);
user = userRepository.save(user);
log.debug("User {} updated.", user);
}
}
Service 2:
#Service
public class PictureManager {
private static final Logger log = LoggerFactory.getLogger(PictureManager.class);
private final RestTemplate restTemplate;
#Autowired
public PictureManager(RestTemplate restTemplate) {
super();
this.restTemplate = restTemplate;
}
public PictureResource savePicture(User user) {
//do some logic with user
ResponseEntity<PictureResource> response = restTemplate.exchange(
"url",
HttpMethod.POST,
requestEntity,
PictureResource.class);
return response.getBody();
}
}
Repository:
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}

How to test #RestController when all of the mapping return with ResponseEntity?

I need to do unit testing on a #RestController where every method returns with a ResponseEntity.
I have a CRUD repository to use but I don't know how can I test it with the ResponseEntities.
#RestController
#RequestMapping("/events")
public class EventController {
#Autowired
private EventRepository eventRepository;
#GetMapping("")
public ResponseEntity<Iterable<Event>> getAll() {
return ResponseEntity.ok(eventRepository.findAll());
}
#GetMapping("/{id}")
public ResponseEntity<Event> get(#PathVariable Integer id) {
Optional<Event> event= eventRepository.findById(id);
if (event.isPresent()) {
return ResponseEntity.ok(event.get());
} else {
return ResponseEntity.notFound().build();
}
}
#PostMapping("")
public ResponseEntity<Event> post(#RequestBody Event event) {
EventsavedEvent = eventRepository.save(event);
return ResponseEntity.ok(savedEvent);
}
.
.
.
So far so good , I can help you .
First of all, you must add unit test dependency.
After that you must examine below code.
Below code only consist for create.
Good luck.
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
#ActiveProfiles("dev")
public class EventControllerTests {
#Autowired
private TestRestTemplate testRestTemplate;
#Test
public void testCreateEvent() {
Event event = new Event(); // Your entity
event.setEventName("Test"); // Your entity attributes
URI location = testRestTemplate.postForLocation("http://localhost:8080/events", event);
Event event2 = testRestTemplate.getForObject(location, Event.class);
MatcherAssert.assertThat(event2.getEventName(), Matchers.equalTo(event.getEventName()));
}
}

Aspect for Rabbit MQ listener

My RMQ listener:
#RabbitListener(
bindings = #QueueBinding(
value = #Queue,
exchange = #Exchange(
value = "${rabbitmq.exchange.sales-exchange.name}",
type = "${rabbitmq.exchange.sales-exchange.type}",
ignoreDeclarationExceptions = "true"
),
key = "${rabbitmq.exchange.sales-exchange.sales-bk}"
)
)
public void listenSalesOrderCreatedMessage(
#Headers Map<String, Object> headers,
#Payload SalesDTO payload
)
{
log.info("Message Received!");
}
salesDTO:
package org.laptop.sale.event.subscriber.dto;
#JsonInclude(JsonInclude.Include.NON_NULL)
#Getter
#Setter
public class SalesOrderCreatedEventDTO implements Serializable {
#JsonProperty("vendorDetails")
#Valid
private VendorDetails vendorDetails;
#JsonInclude(JsonInclude.Include.NON_NULL)
#Getter
#Setter
public static class VendorDetails implements Serializable {
#JsonProperty("name")
private String name;
#JsonProperty("vendorCommission")
private Double vendorCommission;
}
}
My #Around Aspect to be executed before and after the message call:
package org.laptop.sale.event.subscriber;
#Component
#Aspect
#Slf4j
public class SubscriberAdvice {
#Around("execution(* org.laptop.sale.event.subscriber..*(..)))")
public void payloadValidationFailed(ProceedingJoinPoint joinPoint) {
try {
joinPoint.proceed();
} catch (Throwable throwable) {
log.error("Exception in the process execution {}", throwable);
}
}
}
Invalid Message payload:
{
"vendorDetails": {
"name": "Vendor Name",
"vendorCommission": "wrongData"
}
}
Valid Message payload:
{
"vendorDetails": {
"name": "Vendor Name",
"vendorCommission": 8000.1
}
}
Here the the program flow is entering the aspect only incase of Valid payload. It's not entering the Aspect incase of Invalid Payload. I tried with #AfterThrowing as well. that did not work either

#SpringBootTest runing with test-properties (test database) file

I had a project on "spring boot 2" and I want to test it.
Table:
#Entity
#Table(name = "Contract")
public class Contract extends ADBObjectWithID<ContractBean>
{
#NotBlank
#Size(max = 512)
private String name;
#Size(max = 2056)
private String comment;
#Override
public ContractBean toBean()
{
return new ContractBean(getId(), getName(), getComment());
}
}
Repository is CrudRepository<Contract, Long>:
Service:
#Service
public class ContractServiceImpl implements ContractService
{
private ContractRepository contractRepository;
public ContractServiceImpl(ContractRepository contractRepository)
{
this.contractRepository = contractRepository;
}
#Override
#Transactional
public Contract saveObject(ContractBean contractBean)
{
Contract contract;
if (contractBean.getId() == null)
{
contract = new Contract();
}
else
{
contract = findById(contractBean.getId()).orElseThrow(() -> new NullPointerException("Contract not found"));
}
contract.setName(contractBean.getName());
contract.setComment(contractBean.getComment());
return contractRepository.save(contract);
}
#Override
#Transactional
public void deleteObject(ContractBean contractBean)
{
}
#Override
public Optional<Contract> findById(Long id)
{
return contractRepository.findById(id);
}
}
I wanting to test "Service" layer and testing it in the test database. Parameters of the test database available in the "application-test.properties", but I running test, "SpringBoot" used the real database from "application.properties".
Test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class ContractTest
{
#Autowired
private ContractService contractService;
#Test
public void createContract()
{
String name = "Contract name";
String comment = "Contract comment";
ContractBean contractBean = new ContractBean();
contractBean.setName(name);
contractBean.setComment(comment);
Contract contract = contractService.saveObject(contractBean);
Assert.assertEquals(name, contract.getName());
Assert.assertEquals(comment, contract.getComment());
contractBean = contract.toBean();
Assert.assertEquals(name, contractBean.getName());
Assert.assertEquals(comment, contractBean.getComment());
}
}
Pls, tell me, how do I switch to the test base? I trying #PropertySource("classpath:application-test.properties") and #TestPropertySource("classpath:application-test.properties"), but not work
Run with Spring Profile test.
-Dspring.profiles.active=test
You can add the default profile as test into your application.yml to pick it automatically.
spring:
profiles.active: test

Categories