I am trying to build a micro service using web-flux which will send/publish some data based on an event for a particular subscriber.
With the below implementation (Another Stackflow Issue) I am able to create one publisher and all who are subscribed will receive the data automatically when we trigger the event by calling "/send" API
#SpringBootApplication
#RestController
public class DemoApplication {
final FluxProcessor processor;
final FluxSink sink;
final AtomicLong counter;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
public DemoApplication() {
this.processor = DirectProcessor.create().serialize();
this.sink = processor.sink();
this.counter = new AtomicLong();
}
#GetMapping("/send/{userId}")
public void test(#PathVariable("userId") String userId) {
sink.next("Hello World #" + counter.getAndIncrement());
}
#RequestMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent> sse() {
return processor.map(e -> ServerSentEvent.builder(e).build());
}
}
Problem statement - My app is having user based access and for each user there will be some notifications which I want to push only based on an event. Here the events will be stored in the DB with user id's and when we hit the "send" end point from another API along with "userId" as a path variable, it should only send the data related to that user only if it is registered as a subscriber and still listening on the channel.
This is not an accurate or actual solution, but this thing works:
First the SSE end-point :
#RestController
public class SSEController {
private String value = "";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> getWords() {
System.out.println("sse ?");
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> getValue());
}
}
And from any Service or wherever you can autowire, just autowire it:
#Service
public class SomeService {
#Autowired
private SSEController sseController;
...
void something(){
....
sseController.setValue("some value");
....
}
This is the way I'm using.
Related
In my Spring Boot app, I want to read data from the following APIs using Spring WebClient as shown below (I have no prior experience and after making several search on many pages and articles, I concluded to use Spring WebClient):
The endpoint URLs are:
service:
private static final String BASE_URL = "https://demo-api/v1";
private WebClient webClient = WebClient.create(BASE_URL);
public Mono fetchMergedData(String city) {
Mono<EduData> edu = getEduData(city);
Mono<GeoData> geo = getGeoData(city);
return Mono.zip(edu, geo, MergedData::new);
}
public Mono<EduData> getEduData(String city) {
return webClient.get()
.uri("/edu/{city}", city)
.retrieve()
.bodyToMono(EduData.class);
}
public Mono<GeoData> getGeoData(String city) {
return webClient.get()
.uri("/geo/{city}", city)
.retrieve()
.bodyToMono(GeoData.class);
}
Here are the models:
models:
#Getter
public class EduData {
private int institution;
}
#Getter
public class GeoData {
private int population;
}
#Getter
public class MergedData {
private int institution;
private int population;
public MergedData(EduData edu, GeoData geo) {
this.institution = edu.getInstitution();
this.population = get.getPopulation();
}
}
Although there is no error and all the endpoints return data when I test using Postman, I cannot see any data in neither edu, geo variables, nor the return of fetchMergedData() method. So, where is the problem?
To get data out of a Mono, you can either block() (turning this into a blocking operation), or subscribe() and pass it a Consumer or Subscriber.
Simply blocking the call would give you the results, but if you want to do this in a reactive manner then you'll need to subscribe to the Mono.
// Using a block
this.institution = edu.getInstitution().block();
// Using a subscription, when available, EduData can be accessed via response.get
AtomicReference<EduData> response = new AtomicReference<>();
edu.getInstitution().subscribe(response::set);
Here's a simple subscription that provides a Consumer.
public class SomeClass {
EduData eduData;
public void setEduData(EduData eduData) {
this.eduData = eduData;
}
public void fetchData(String city) {
webClient.get()
.uri("/edu/{city}", city)
.retrieve()
.bodyToMono(EduData.class).subscribe(this::setEduData);
}
}
When the response is available, the setEduData method will be called with the result.
I searched for a while but have no clue what I am missing.
I want to implement the observer pattern in Spring, to update documents in MongoDB which will create a notification in the front-end.
I implemented a Service class for the notifications (here are more than one, I show you just two of them), using #EventListener annotation
#Service
#RequiredArgsConstructor
public class NotificationService {
#EventListener ({ContextStartedEvent.class})
public void updateNotificationForIdea(String ideaId) {
//some logic inside the Service classes
}
}
#EventListener ({ContextStartedEvent.class})
public void createNotificationForNewComment(String ideaId, String creatorId) {
//some logic inside the Service classes
}
}
and I tried to implement the service, from where I want to send the notification
#Service
#RequiredArgsConstructor
public class CommentService {
private final ApplicationEventPublisher eventPublisher;
public CommentServiceResult createComment(Comment comment, String creatorId) {
// some logic
eventPublisher.publishEvent(); //here is where I miss something
return CommentServiceResult.builder()
.comment(createdComment)
.resultState(state)
.build();
}
I read that I need something like an "EventHelperClass" and tried to build this:
public class NotificationEvents extends ApplicationEvent{
public NotificationEvents(Object source) {
super(source);
}
private void updateNotificationForIdea(String ideaId){
}
private void createNotificationForNewComment(String ideaId, String creatorId) {
}
}
but I really don't know how to get the notifications from the NotificationsService in here (because they are void and are not a simple String message), to start the event from within CommentService...
I shortened the dependencies from the services and removed the internal logik for readability...
eventPublisher.publishEvent(...)
fires an event, you'll need to pass an event object as parameter, e.g.
public class CustomSpringEvent extends ApplicationEvent {
private String message;
public CustomSpringEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
Methods annotated with #EventListener ({CustomSpringEvent.class}) will catch it.
In your case:
CommentService fires an CommentEvent (fields: Comment comment, String creatorId).
Any Bean with a method
#EventListener ({CommentEvent.class})
void onCommentEvent(CommentEvent event) {
// ....
}
will catch it and get the event data.
https://www.baeldung.com/spring-events
I am new to Server Sent Event concepts. Recently I added SSEemitter in my existing Spring Boot application, taking idea from internet.
In-memory SseEmitter repository class is as follow:
#Repository
public class InMemorySseEmitterRepository implements SseEmitterRepository {
private static final Logger log = LoggerFactory.getLogger(InMemorySseEmitterRepository.class);
private Map<String, SseEmitter> userEmitterMap = new ConcurrentHashMap<>();
#Override
public void addEmitter(String memberId, SseEmitter emitter) {
userEmitterMap.put(memberId, emitter);
}
#Override
public void remove(String memberId) {
if (userEmitterMap != null && userEmitterMap.containsKey(memberId)) {
log.info("Removing emitter for member: {}", memberId);
userEmitterMap.remove(memberId);
} else {
log.info("No emitter to remove for member: {}", memberId);
}
}
#Override
public Optional<SseEmitter> get(String memberId) {
return Optional.ofNullable(userEmitterMap.get(memberId));
}
}
SseEmitter service class is as follows:
#Service
public class SseEmitterServiceImpl implements SseEmitterService {
private static final Logger log = LoggerFactory.getLogger(SseEmitterServiceImpl.class);
private final SseEmitterRepository repository;
public SseEmitterServiceImpl(#Value(SseEmitterRepository repository) {
this.repository = repository;
}
#Override
public SseEmitter subscribe(String entityId, EventType eventType) {
SseEmitter emitter = new SseEmitter(-1L); //SO THAT IT NEVER TIMES OUT
String memberId = entityId + "_" + eventType; //MEMBER ID HAS BEEN INTRODUCED BECAUSE FOR DIFFERENT ENTITIES, FOR DIFFERENT EVENTS WE CAN SEND EVENTS FROM SERVER TO CLIENT
emitter.onCompletion(() -> repository.remove(memberId));
emitter.onTimeout(() -> repository.remove(memberId));
emitter.onError(e -> {
log.error("Create SseEmitter exception", e);
repository.remove(memberId);
});
repository.addEmitter(memberId, emitter);
return emitter;
}
}
Associated resource class is as follows:
#RestController
#RequestMapping("/api")
public class SseEmitterResource {
private static final Logger log = LoggerFactory.getLogger(SseEmitterResource.class);
private final SseEmitterService emitterService;
public SseEmitterResource(SseEmitterService emitterService) {
this.emitterService = emitterService;
}
#GetMapping("/subscribe/{eventType}")
public SseEmitter subscribeToEvents(
#PathVariable EventType eventType,
#RequestParam(required = false, defaultValue = "") String entityId) {
log.info("Subscribing member with event type {} for entity id {}", eventType, entityId);
return emitterService.subscribe(entityId, eventType);
}
}
Actually, there can be multiple event type and multiple entity ids for which we should be able to send events from Server to Client.
The service class, responsible for sending the notification is as follows:
#Service
public class SseNotificationServiceImpl implements SseNotificationService {
private static final Logger log = LoggerFactory.getLogger(SseNotificationServiceImpl.class);
private final SseEmitterRepository emitterRepository;
private final DataMapper dataMapper;
public SseNotificationServiceImpl(
SseEmitterRepository emitterRepository,
DataMapper dataMapper) {
this.emitterRepository = emitterRepository;
this.dataMapper = dataMapper;
}
#Override
public void sendSseNotification(String memberId, MyDTO dto) {
sendNotification(memberId, dto);
}
private void sendNotification(String memberId, MyDTO dto) {
emitterRepository.get(memberId).ifPresentOrElse(sseEmitter -> {
try {
sseEmitter.send(dataMapper.map(dto));
} catch (IOException e) {
...
emitterRepository.remove(memberId);
}
}, () -> ...;
}
}
Finally, the place, where I send this Server Sent Event notification is as follows:
sseNotificationService.sendSseNotification(...);
Now, my questions are as follows:
if there are multiple, say 6 clients for the Spring Boot application deployed on single POD, will all of them receive notification through the single SseEmitter instance, as I have shown above ?
If not, could anyone please help me with the changes which I need to incorporate in the above code, in order to send Server Sent Event notification to all clients ? Thanks.
From this example:
#SpringBootApplication
#EnableBinding(MyProcessor.class)
public class MultipleOutputsServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MultipleOutputsServiceApplication.class, args);
}
#Autowired
private MyProcessor processor;
#StreamListener(MyProcessor.INPUT)
public void routeValues(Integer val) {
if (val < 10) {
processor.anOutput()
.send(message(val));
} else {
processor.anotherOutput()
.send(message(val));
}
}
private static final <T> Message<T> message(T val) {
return MessageBuilder.withPayload(val)
.build();
}
}
MyProcessor interface:
public interface MyProcessor {
String INPUT = "myInput";
#Input
SubscribableChannel myInput();
#Output("myOutput")
MessageChannel anOutput();
#Output
MessageChannel anotherOutput();
}
My question:
Why the method routeValues in MultipleOutputsServiceApplication class is annotated with MyProcessor.INPUT instead of MyProcessor.myOutput (after adding this member to MyProcessor interface) ?
From the docs, INPUT is for getting data and OUTPUT is for sending data. Why the example does the opposite and if I reverse it, nothing is working?
That method looks correct to me. It doesn't have to be annotated with #Output as your method doesn't have a return type and you are programmatically sending the output to arbitrary destinations (through two different output bindings) in the method. So you need to make sure that your outputs are bound properly as your program properly does through #EnableBinding(MyProcessor.class). You need the #StreamListener(MyProcessor.INPUT) on the method as MyProcessor.INPUT is the binding where StreamListener is listening from. Once you get data through that input, your code then programmatically takes over sending the data downstream. With that said, there are multiple ways to address these types of use cases. You can alternatively doing this too.
#StreamListener
public void routeValues(#Input("input")SubscribableChannel input,
#Output("mOutput") MessageChannel myOutput,
#Output("output")MessageChannel output {
input.subscribe(new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
int val = (int) message.getPayload();
if (val < 10) {
myOutput.send(message(val));
}
else {
output.send(message(val));
}
}
}
I have Spring service, which is actually actor, it is received info, but I cant pass it to another Spring service, because injection fails.
#Service("mailContainer")
#Scope("prototype")
#Component
public class MailContainer extends UntypedActor {
private final LoggingAdapter LOG = Logging.getLogger(getContext().system(), this);
private Mail value;
private List<Mail> mailList = new ArrayList<Mail>();
private Integer size;
#Autowired
#Qualifier("springService")
private SpringService springService;
//#Autowired
public void setSpringService(SpringService springService) {
this.springService = springService;
}
public MailContainer(Mail value) {
this.value = value;
}
#Override
public void onReceive(Object message) throws Exception {
// LOG.debug("+ MailContainer message: {} ", message);
if (message instanceof Mail) {
value = (Mail) message;
System.out.println("MailContainer get message with id " + value.getId());
System.out.println("With time " + value.getDateSend());
//getSender().tell(value, getSelf()); //heta uxarkum
//this.saveIt(value);
springService.add(value);
}
}
and second service
#Service("springService")
//#Component
#Scope("session")
public class SpringService {
private List<Mail> mailList = new ArrayList<Mail>();
public void add(Mail mail) {
System.out.println("Saving mail from Spring " +mail.getId());
mailList.add(mail);
}
public List<Mail> getMailList() {
return mailList;
}
}
Spring config, this is from akka spring example
#Configuration
//#EnableScheduling
//EnableAsync
#ComponentScan(basePackages = {"com"}, excludeFilters = {
#ComponentScan.Filter(Configuration.class)})
//#ImportResource("classpath:META-INF/spring/spring-data-context.xml")
//#EnableTransactionManagement
//#EnableMBeanExport
//#EnableWebMvc
public class CommonCoreConfig {
// the application context is needed to initialize the Akka Spring Extension
#Autowired
private ApplicationContext applicationContext;
/**
* Actor system singleton for this application.
*/
#Bean
public ActorSystem actorSystem() {
ActorSystem system = ActorSystem.create("AkkaJavaSpring");
// initialize the application context in the Akka Spring Extension
SpringExtProvider.get(system).initialize(applicationContext);
return system;
}
}
So, how I can inject just another Spring service?????????
Based on our discussions, I think it is due to the way you create the MailContainer actor. You aren't using the SpringExtProvider and instead are using Props.create directly. This means that Spring doesn't get the opportunity to perform dependency injection on your new actor.
Try changing this code:
#Override
public void preStart() throws Exception {
System.out.println("Mail collector preStart: {} ");
getContext().actorOf(Props.create(MailContainer.class, result), "one");
}
to use the the SpringExtProvider like this:
#Override
public void preStart() throws Exception {
System.out.println("Mail collector preStart: {} ");
getContext().actorOf(SpringExtProvider.get(getContext().system()).props("mailContainer"), "one");
}
This way you are asking the Spring extension to create the new actor and inject any required dependecnies.