I am trying to upload a multipart form-data with an attached xml file to integration server.
I am using a HttpRequestHandlingMessagingGateway with a RequestMapping bean.
#Bean("Inbound_GATEWAY_in")
public MessageChannel Inbound_GATEWAY_in() { return new DirectChannel(); }
#Bean
public HttpRequestHandlingMessagingGateway selerixInboundRequest() {
HttpRequestHandlingMessagingGateway gateway =
new HttpRequestHandlingMessagingGateway(true);
gateway.setRequestMapping(selerixMapping());
gateway.setMessageConverters( messageConverter() );
gateway.setMultipartResolver(multipartResolverBean());
gateway.setRequestTimeout(3000); // 3s
gateway.setReplyTimeout(5000); // 5s
gateway.setRequestChannelName("Inbound_GATEWAY_in");
gateway.setReplyChannelName("Outbound_GATEWAY_out");
return gateway;
}
#Bean
public RequestMapping selerixMapping() {
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/path");
requestMapping.setMethods(HttpMethod.POST);
requestMapping.setConsumes(MediaType.MULTIPART_FORM_DATA_VALUE);
return requestMapping;
}
#Bean
public MultipartResolver multipartResolverBean(){
return new CommonsMultipartResolver();
}
#ServiceActivator(inputChannel = "Inbound_GATEWAY_in")
public Message<?> headerEnrich_Inbound_GATEWAY_in(Message<?> message){
Message<?> outmessage = null;
LOGGER.info("message ", message); // returns blank message
But when I am trying to upload the xml file the message is coming as blank.
How can I find the xml file in the Message<?> or how can I check the Request object ?
Here is a simple test to demonstrate how we can upload the file using Spring Integration:
#SpringJUnitWebConfig
#DirtiesContext
public class FileUploadTests {
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
void commonsFileUploadValidation() throws Exception {
MockPart mockPart1 = new MockPart("file", "file.text", "ABC".getBytes(StandardCharsets.UTF_8));
mockPart1.getHeaders().setContentType(MediaType.TEXT_PLAIN);
this.mockMvc.perform(multipart("/path").part(mockPart1))
.andExpect(status().isOk())
.andExpect(content().string("File uploaded: file.text with content: ABC"));
}
#Configuration
#EnableIntegration
public static class ContextConfiguration {
#Bean("Inbound_GATEWAY_in")
public MessageChannel Inbound_GATEWAY_in() {
return new DirectChannel();
}
#Bean
public HttpRequestHandlingMessagingGateway selerixInboundRequest() {
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/path");
gateway.setRequestMapping(requestMapping);
gateway.setRequestChannelName("Inbound_GATEWAY_in");
return gateway;
}
#Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
#ServiceActivator(inputChannel = "Inbound_GATEWAY_in")
public String headerEnrich_Inbound_GATEWAY_in(MultiValueMap<String, MultipartFile> payload) throws IOException {
MultipartFile file = payload.getFirst("file");
return "File uploaded: " + file.getOriginalFilename() + " with content: " + new String(file.getBytes());
}
}
}
Note: the CommonsMultipartResolver is deprecated for a while and was removed from latest Spring. Please, be sure that you use the latest versions of the frameworks: https://spring.io/projects/spring-integration#learn
What I did is I created an interface
#Configuration
#MessagingGateway
#EnableIntegration
public interface IntegrationGateway2 {
#Gateway(requestChannel = "Inbound_CHANNEL_in", replyChannel = "Inbound_Channel_reply")
public Message<?> sendAndReceive(Message<?> input);
}
and then created a normal rest controller and fetched the multipart file and then converted to a message.
#RestController
#EnableIntegration
#Configuration
public class SelerixController {
#Bean("Inbound_CHANNEL_in")
public MessageChannel Inbound_Channel_in() {
return new DirectChannel();
}
#Bean("Inbound_Channel_reply")
public MessageChannel Inbound_Channel_reply() {
return new DirectChannel();
}
#Autowired
IntegrationGateway integrationGateway;
#PostMapping("/processFile")
public ResponseEntity<String> fileUpload(#RequestParam String partner, #RequestParam("file") MultipartFile file ) throws IOException {
Message reply = null;
String xmlInputData = "";
if (file != null) {
InputStream is = file.getInputStream();
xmlInputData = new BufferedReader(new InputStreamReader(is))
.lines().collect(Collectors.joining(""));
MapConversionUtility mcu = new MapConversionUtility();
String json = mcu.convertXmlToJSON(xmlInputData);
Map<String, Object> header = new HashMap<String, Object>();
header.put("uuid", UUID.randomUUID().toString());
header.put("REAL_TIME_FLAG", "TRUE"); //QUERY_PARAM
header.put("QUERY_PARAM", partner);
Message<?> request = MessageBuilder
.withPayload(json)
.copyHeaders(header)
.build();
reply = integrationGateway.sendAndReceive( request);
LOGGER.info("getting reply final *************** {}",reply.getPayload());
}
}
}
Related
My application is based on spring-boot 2.1.6 equipped with spring-batch (chunks approach) and spring-integration to handle the SFTP.
High level functionality is to fetch data from DB, generate a text file then send it through SFTP and this task run every 30 mins.
This application already running in production for some time, but if I see the logs there are error about ssh_msg_disconnect 11 idle connection. It will keep like that until I restart the app.
Below is my application code :
SftpConfig.java
#Configuration
public class SftpConfig {
#Autowired
ApplicationProperties applicationProperties;
#Bean
public SessionFactory<LsEntry> sftpSessionFactory() {
final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(applicationProperties.getSftp().getHost());
factory.setUser(applicationProperties.getSftp().getUser());
factory.setPassword(applicationProperties.getSftp().getPass());
factory.setAllowUnknownKeys(true);
return new CachingSessionFactory<>(factory);
}
#Bean
#ServiceActivator(inputChannel = "toSftpChannel", adviceChain = "retryAdvice")
public MessageHandler handler() {
final SftpMessageHandler handler = new SftpMessageHandler(this.sftpSessionFactory());
handler.setRemoteDirectoryExpression(new LiteralExpression(applicationProperties.getSftp().getPath()));
handler.setFileNameGenerator((final Message<?> message) -> {
if (message.getPayload() instanceof File) {
return ((File) message.getPayload()).getName();
} else {
throw new IllegalArgumentException("File expected as payload.");
}
});
return handler;
}
#Bean
public RequestHandlerRetryAdvice retryAdvice() {
final RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
final RetryTemplate retryTemplate = new RetryTemplate();
final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(NumberConstants.FIVE);
retryTemplate.setRetryPolicy(retryPolicy);
advice.setRetryTemplate(retryTemplate);
return advice;
}
#MessagingGateway
public interface UploadGateway {
#Gateway(requestChannel = "toSftpChannel")
void upload(File file);
}
}
step for sending file to sftp
#Autowired
UploadGateway uploadGateway;
private boolean uploadToSharedFolderSuccess(final PaymentStatus paymentStatus, final String strLocalTmpPath) {
try {
final File fileLocalTmpFullPath = new File(strLocalTmpPath);
uploadGateway.upload(fileLocalTmpFullPath);
} catch (final Exception e) {
paymentStatus.setStatus(ProcessStatus.ERROR.toString());
paymentStatus.setRemark(StringUtil.appendIfNotEmpty(paymentStatus.getRemark(),
"Error during upload to shared folder - " + e.getMessage()));
}
return !StringUtils.equalsIgnoreCase(ProcessStatus.ERROR.toString(), paymentStatus.getStatus());
}
From the error, I know that seems like I opened too many connection. But I'm not sure how to check if the connection are closed every end of the spring-batch.
If you don't wrap the session factory in a CachingSessionFactory, the session will be closed after each use.
#Bean
public DefaultSftpSessionFactory sftpSessionFactory() {
final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(applicationProperties.getSftp().getHost());
factory.setUser(applicationProperties.getSftp().getUser());
factory.setPassword(applicationProperties.getSftp().getPass());
factory.setAllowUnknownKeys(true);
return factory;
}
I can only listen to strings once I try to receive a custom object it throws the following error. It seems that I need to teach Spring to handle my custom object (B2BOrder)
org.springframework.messaging.converter.MessageConversionException: Cannot convert from [java.lang.String] to [br.com.b2breservas.api.model.B2BOrder] for GenericMessage [payload={"comments":"95d29059-8552-42fa-8fd9-a1d776416269"},
My SQSConfig
#Configuration
#EnableSqs
public class SqsConfig {
private static final String DEFAULT_THREAD_NAME_PREFIX = ClassUtils.getShortName(SimpleMessageListenerContainer.class) + "-";
#Bean
public QueueMessagingTemplate myMessagingTemplate(AmazonSQSAsync amazonSqs, ResourceIdResolver resolver) {
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JodaModule())
.registerModule(new JavaTimeModule());
// configure the Jackson mapper as needed
// maybe I need to do something here!
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setSerializedPayloadClass(String.class);
converter.setStrictContentTypeMatch(false);
converter.setObjectMapper(mapper);
return new QueueMessagingTemplate(amazonSqs, resolver, converter);
}
#Bean
public ClientConfiguration sqsClientConfiguration() {
return new ClientConfiguration()
.withConnectionTimeout(30000)
.withRequestTimeout(30000)
.withClientExecutionTimeout(30000);
}
#Bean
public ExecutorFactory sqsExecutorFactory() {
return new ExecutorFactory() {
#Override
public ExecutorService newExecutor() {
return new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
};
}
#Value("${b2b.b2b.accesstoken}")
public String accesstoken;
#Value("${b2b.b2b.secretkey}")
public String secretkey;
#Bean
public AmazonSQSAsync amazonSqs(ClientConfiguration sqsClientConfiguration, ExecutorFactory sqsExecutorFactory) {
BasicAWSCredentials credential = new BasicAWSCredentials(accesstoken, secretkey);
return AmazonSQSAsyncClientBuilder.standard()
.withClientConfiguration(sqsClientConfiguration)
.withExecutorFactory(sqsExecutorFactory)
// .withEndpointConfiguration(sqsEndpointConfiguration)
// .withCredentials(credentialsProvider)
.withCredentials(new AWSStaticCredentialsProvider(credential))
.build();
}
#Bean
public AsyncTaskExecutor queueContainerTaskEecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix(DEFAULT_THREAD_NAME_PREFIX);
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(2);
// No use of a thread pool executor queue to avoid retaining message to long in memory
threadPoolTaskExecutor.setQueueCapacity(0);
threadPoolTaskExecutor.afterPropertiesSet();
return threadPoolTaskExecutor;
}
#Bean
public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(AmazonSQSAsync amazonSqs, AsyncTaskExecutor queueContainerTaskEecutor) {
SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
factory.setAmazonSqs(amazonSqs);
factory.setAutoStartup(true);
// factory.setQueueMessageHandler();
factory.setMaxNumberOfMessages(1);
factory.setWaitTimeOut(20);
factory.setTaskExecutor(queueContainerTaskEecutor);
return factory;
}
}
Listener
#Component
public class SqsHub {
#SqsListener(
"https://sqs.us-west-2.amazonaws.com/3234/32443-checkout.fifo"
)
public void listen(B2BOrder message) {
// public void listen(String message) { THIS WORKS!!
System.out.println("!!!! received message {} {}" + message.toString());
}
}
Sending
....
#Autowired
AmazonSQSAsync amazonSqs;
#GetMapping("/yay")
public String yay() {
try {
B2BOrder pendingOrder = new B2BOrder();
pendingOrder.setComments(UUID.randomUUID().toString());
String pendingOrderJson = objectMapper.writeValueAsString(pendingOrder);
QueueMessagingTemplate queueMessagingTemplate = new QueueMessagingTemplate(amazonSqs);
Map<String, Object> headers = new HashMap<>();
headers.put(SqsMessageHeaders.SQS_GROUP_ID_HEADER, "my-application");
headers.put(SqsMessageHeaders.SQS_DEDUPLICATION_ID_HEADER, UUID.randomUUID().toString());
queueMessagingTemplate.convertAndSend("booking-checkout.fifo", pendingOrderJson, headers);
} catch (final AmazonClientException | JsonProcessingException ase) {
System.out.println("Error Message: " + ase.getMessage());
}
return "sdkjfn";
}
....
Simple Custom Object
public class B2BOrder implements Serializable {
#JsonProperty
private String comments;
}
UPDATE
#Michiel answer took me here, but still got the same error.
#Autowired
public ObjectMapper objectMapper;
#Bean
public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(AmazonSQSAsync amazonSqs, AsyncTaskExecutor queueContainerTaskEecutor) {
SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
factory.setAmazonSqs(amazonSqs);
factory.setAutoStartup(true);
QueueMessageHandlerFactory queueMessageHandlerFactory = new QueueMessageHandlerFactory();
queueMessageHandlerFactory.setAmazonSqs(amazonSqs);
MappingJackson2MessageConverter jsonMessageConverter = new MappingJackson2MessageConverter();
jsonMessageConverter.setObjectMapper(objectMapper);
queueMessageHandlerFactory.setMessageConverters(Collections.singletonList(jsonMessageConverter));
factory.setQueueMessageHandler(queueMessageHandlerFactory.createQueueMessageHandler());
// factory.setMaxNumberOfMessages(1);
factory.setWaitTimeOut(20);
factory.setTaskExecutor(queueContainerTaskEecutor);
return factory;
}
```
Although you have registered a MessageConverter, it is only configured to be used in the outgoing request (using the QueueMessagingTemplate). Your MessageListener does not have a MessageConverter configuration. Therefore, incoming messages can only be retrieved as a 'raw' type such as String.
In your snippet you have commented the following line of code:
// factory.setQueueMessageHandler();
This is the location where you could set a QueueMessageHandler that itself has one or more MessageConverters attached.
[edit]
Sure:
QueueMessageHandlerFactory handlerFactory = new QueueMessageHandlerFactory();
handlerFactory.setMessageConverters(yourJacksonConfig);
QueueMessageHandler messageHandler = handlerFactory.createQueueMessageHandler();
factory.setQueueMessageHandler(messageHandler);
This Spring documentation might be of help.
I'm using a SimpleMessageListenerContainer which is defined in a Bean as follows
#Bean
#Primary
fun simpleMessageListenerContainerFactory(amazonSQS: AmazonSQSAsync): SimpleMessageListenerContainerFactory =
SimpleMessageListenerContainerFactory().apply {
setAmazonSqs(amazonSQS)
setMaxNumberOfMessages(10)
setAutoStartup(false)
}
(by the way, I'm using Kotlin)
This is bean is created outside my project, so in order to inject into the SimpleMessageListenerContainerFactory a new converter I've done as #Michiel has suggested:
#Bean
fun Handler(): QueueMessageHandlerFactory {
val handlerFactory = QueueMessageHandlerFactory()
handlerFactory.messageConverters = listOf(jsonMessageConverter())
return handlerFactory
}
I have successfully been able send file from one FTP Server(source) to another FTP server (target). I first send files from source to the local directory using the inbound adapter and then send files from the local directory to the target using the outbound adapter. So far this is working fine.
What I want to achieve is: to enrich the header of the message at the source with a hash code (which is generated using the file on source that is transferred) and then get that header at the target and match it with the hash code (which is generated using the file on the target)
Here is what I have tried so far:
Application.java
#SpringBootApplication
public class Application {
#Autowired
private Hashing hashing;
public static ConfigurableApplicationContext context;
public static void main(String[] args) {
context = new SpringApplicationBuilder(Application.class)
.web(false)
.run(args);
}
#Bean
#ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler sourceHandler() {
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println("Reply channel isssss:"+message.getHeaders().getReplyChannel());
Object payload = message.getPayload();
System.out.println("Payload: " + payload);
File file = (File) payload;
// enrich header with hash code before sending to target FTP
Message<?> messageOut = MessageBuilder
.withPayload(message.getPayload())
.copyHeadersIfAbsent(message.getHeaders())
.setHeaderIfAbsent("hashCode", hashing.getHashCode(file)).build();
// send to target FTP
System.out.println("Trying to send " + file.getName() + " to target");
MyGateway gateway = context.getBean(MyGateway.class);
gateway.sendToFtp(messageOut);
}
};
}
}
FileTransferServiceConfig.java
#Configuration
#Component
public class FileTransferServiceConfig {
#Autowired
private ConfigurationService configurationService;
#Autowired
private Hashing hashing;
public static final String FILE_POLLING_DURATION = "5000";
#Bean
public SessionFactory<FTPFile> sourceFtpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost(configurationService.getSourceHostName());
sf.setPort(Integer.parseInt(configurationService.getSourcePort()));
sf.setUsername(configurationService.getSourceUsername());
sf.setPassword(configurationService.getSourcePassword());
return new CachingSessionFactory<>(sf);
}
#Bean
public SessionFactory<FTPFile> targetFtpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost(configurationService.getTargetHostName());
sf.setPort(Integer.parseInt(configurationService.getTargetPort()));
sf.setUsername(configurationService.getTargetUsername());
sf.setPassword(configurationService.getTargetPassword());
return new CachingSessionFactory<>(sf);
}
#MessagingGateway
public interface MyGateway {
#Gateway(requestChannel = "toFtpChannel")
void sendToFtp(Message message);
}
#Bean
public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() {
FtpInboundFileSynchronizer fileSynchronizer = new FtpInboundFileSynchronizer(sourceFtpSessionFactory());
fileSynchronizer.setDeleteRemoteFiles(false);
fileSynchronizer.setRemoteDirectory(configurationService.getSourceDirectory());
fileSynchronizer.setFilter(new FtpSimplePatternFileListFilter(
configurationService.getFileMask()));
return fileSynchronizer;
}
#Bean
public AcceptOnceFileListFilter<File> acceptOnceFileListFilter() {
return new AcceptOnceFileListFilter<>();
}
#Bean
#InboundChannelAdapter(channel = "ftpChannel",
poller = #Poller(fixedDelay = FILE_POLLING_DURATION))
public MessageSource<File> ftpMessageSource() {
FtpInboundFileSynchronizingMessageSource source
= new FtpInboundFileSynchronizingMessageSource(ftpInboundFileSynchronizer());
source.setLocalDirectory(new File(configurationService.getLocalDirectory()));
source.setAutoCreateLocalDirectory(true);
source.setLocalFilter(acceptOnceFileListFilter());
return source;
}
// makes sure transfer continues on connection reset
#Bean
public Advice expressionAdvice() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setTrapException(true);
advice.setOnFailureExpression("#acceptOnceFileListFilter.remove(payload)");
return advice;
}
#Bean
#ServiceActivator(inputChannel = "toFtpChannel")
public void listenOutboundMessage() {
// tried to subscribe to "toFtpChannel" but this was not triggered
System.out.println("Message received");
}
#Bean
#ServiceActivator(inputChannel = "ftpChannel", adviceChain = "expressionAdvice")
public MessageHandler targetHandler() {
FtpMessageHandler handler = new FtpMessageHandler(targetFtpSessionFactory());
handler.setRemoteDirectoryExpression(new LiteralExpression(
configurationService.getTargetDirectory()));
return handler;
}
}
Hashing.java
public interface Hashing {
public String getHashCode(File payload);
}
I have managed to enrich the message in sourceHandler(), built the message and sent it to the target but I cannot figure out how I can receive that message on the target so that I can get the header from the message?
Tell me if any more information is required. I would really appreciate your help.
You have two subscribers on ftpChannel - the target handler and your sourceHandler; they will get alternate messages unless ftpChannel is declared as a pubsub channel.
There should be no problems with your subscription to toFtpChannel.
Turn on DEBUG logging to see all the subscription activity when the application context starts.
EDIT
Remove the #Bean from the #ServiceActivator - such beans must be a MessageHandler.
#ServiceActivator(inputChannel = "toFtpChannel")
public void listenOutboundMessage(Message message) {
// tried to subscribe to "toFtpChannel" but this was not triggered
System.out.println("Message received:" + message);
}
works fine for me...
Payload: /tmp/foo/baz.txt
Trying to send baz.txt to target
Message received:GenericMessage [payload=/tmp/foo/baz.txt, headers={hashCode=foo, id=410eb9a2-fe8b-ea8a-015a-d5896387cf00, timestamp=1509115006278}]
Again; you must have only one subscriber on ftpChannel unless you make it a pubsub.
I would like to load a different set of messages_xx.properties based on the HttpServletRequest to differentiate them based on our customers.
In templates and through all the application, we have a #Bean which gives the actual customer based on the path of the request
#Component
public class CompanySelector {
#Autowired
private ICompanyService service;
public String getURLBase(HttpServletRequest request) throws MalformedURLException {
URL requestURL = new URL(request.getRequestURL().toString());
String port = requestURL.getPort() == -1 ? "" : ":" + requestURL.getPort();
return requestURL.getHost() + port;
}
public Company getActualCompany(HttpServletRequest request) throws MalformedURLException{
String url = getURLBase(request);
Company company = service.findByCompanyUrl(url);
if(company != null){
return company;
}
return null;
}
}
Now, we configure the MessageSource in WebConfig which extends WebMvcConfigurerAdapter and we would like to do something like that
#Configuration
#ComponentScan("it.besmart.eshare.web")
public class WebConfig extends WebMvcConfigurerAdapter{
public WebConfig(){
super();
}
#Autowired
CompanySelector companySelector;
#Autowired
HttpServletRequest request;
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
Company company = null;
try {
company = companySelector.getActualCompany(request);
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (company != null){
messageSource.setBasename("classpath:"+ company.getSlug() + "_messages");
} else {
messageSource.setBasename("classpath:messages");
}
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
but obviously it doesn't work because we don't have a request during configuration... Is there another way to load the messages file based on the request? Or any other best practice to adopt? Because our other choice would be using only one file per language and using the company.getSlug() at the beginning of each phrase, but we would decuplicate the size of file...
You need to declare every properties files like that:
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("company1_messages", "company2_messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
And to get the message:
#Autowired
private MessageSource messageSource;
public String myRequest(Locale locale) {
...
messageSource.getMessage(company.getSlug().".messageKey1", null, locale));
...
}
In company1_messages_fr.properties:
company1.messageKey1=messageCompany1
In company2_messages_fr.properties:
company2.messageKey1=messageCompany2
I am experimenting with the Spring Reactor 3 components and Spring Integration to create a reactive stream (Flux) from a JMS queue.
I am attempting to create a reactive stream (Spring Reactor 3 Flux) from a JMS queue (ActiveMQ using Spring Integration) for clients to get the JMS messages asynchronously. I believe that I have everything hooked up correctly but the client does not receive any of the JMS messages until the server is stopped. Then all of the messages get "pushed" to the client a once.
Any help would be appreciated.
Here is the configuration file that I am using to configure the JMS, Integration components and the reactive publisher:
#Configuration
#EnableJms
#EnableIntegration
public class JmsConfiguration {
#Value("${spring.activemq.broker-url:tcp://localhost:61616}")
private String defaultBrokerUrl;
#Value("${queues.patient:patient}")
private String patientQueue;
#Autowired
MessageListenerAdapter messageListenerAdapter;
#Bean
public DefaultJmsListenerContainerFactory myFactory(
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
configurer.configure(factory, jmsConnectionFactory());
return factory;
}
#Bean
public Queue patientQueue() {
return new ActiveMQQueue(patientQueue);
}
#Bean
public ActiveMQConnectionFactory jmsConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setBrokerURL(defaultBrokerUrl);
connectionFactory.setTrustedPackages(Arrays.asList("com.sapinero"));
return connectionFactory;
}
// Set the jackson message converter
#Bean
public JmsTemplate jmsTemplate() {
JmsTemplate template = new JmsTemplate();
template.setConnectionFactory(jmsConnectionFactory());
template.setDefaultDestinationName(patientQueue);
template.setMessageConverter(jacksonJmsMessageConverter());
return template;
}
#Bean
public MessageListenerAdapter messageListenerAdapter() {
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter();
messageListenerAdapter.setMessageConverter(jacksonJmsMessageConverter());
return messageListenerAdapter;
}
#Bean
public AbstractMessageListenerContainer messageListenerContainer() {
DefaultMessageListenerContainer defaultMessageListenerContainer = new DefaultMessageListenerContainer();
defaultMessageListenerContainer.setMessageConverter(jacksonJmsMessageConverter());
defaultMessageListenerContainer.setConnectionFactory(jmsConnectionFactory());
defaultMessageListenerContainer.setDestinationName(patientQueue);
defaultMessageListenerContainer.setMessageListener(messageListenerAdapter());
defaultMessageListenerContainer.setCacheLevel(100);
defaultMessageListenerContainer.setErrorHandler(new ErrorHandler() {
#Override
public void handleError(Throwable t) {
t.printStackTrace();
}
});
return defaultMessageListenerContainer;
}
#Bean // Serialize message content to json using TextMessage
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
#Bean
public MessageChannel jmsOutboundInboundReplyChannel() {
return MessageChannels.queue().get();
}
#Bean
public Publisher<Message<String>> pollableReactiveFlow() {
return IntegrationFlows
.from(Jms.messageDrivenChannelAdapter(messageListenerContainer()).get())
.channel(MessageChannels.queue())
.log(LoggingHandler.Level.DEBUG)
.log()
.toReactivePublisher();
}
#Bean
public MessageChannel jmsChannel() {
return new DirectChannel();
}
The controller that creates the Flux is:
#RestController
#RequestMapping("patients")
public class PatientChangePushController {
private LocalDateTime lastTimePatientDataRetrieved = LocalDateTime.now();
private int durationInSeconds = 30;
private Patient patient;
AtomicReference<SignalType> checkFinally = new AtomicReference<>();
#Autowired
PatientService patientService;
#Autowired
#Qualifier("pollableReactiveFlow")
private
Publisher<Message<String>> pollableReactiveFlow;
#Autowired
private JmsTemplate jmsTemplate;
#Autowired
private Queue patientQueue;
/**
* Subscribe to a Flux of a patient that has been updated.
*
* #param id
* #return
*/
#GetMapping(value = "/{id}/alerts", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Message<String>> getPatientAlerts(#PathVariable Long id) {
Flux<Message<String>> messageFlux = Flux.from(pollableReactiveFlow);
return messageFlux;
}
#GetMapping(value = "/generate")
public void generateJmsMessage() {
for (long i = 0L; i < 100; i++) {
Patient patient = new Patient();
patient.setId(i);
send(patient);
System.out.println("Message was sent to the Queue");
}
}
void send(Patient patient) {
this.jmsTemplate.convertAndSend(this.patientQueue, patient);
}
}
If anyone can tell me why the messages do not get sent to the client until after the server is killed, I would appreciate it.
Works well for me:
#SpringBootApplication
#RestController
public class SpringIntegrationSseDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringIntegrationSseDemoApplication.class, args);
}
#Autowired
private ConnectionFactory connectionFactory;
#Autowired
private JmsTemplate jmsTemplate;
#Bean
public Publisher<Message<String>> jmsReactiveSource() {
return IntegrationFlows
.from(Jms.messageDrivenChannelAdapter(this.connectionFactory)
.destination("testQueue"))
.channel(MessageChannels.queue())
.log(LoggingHandler.Level.DEBUG)
.log()
.toReactivePublisher();
}
#GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getPatientAlerts() {
return Flux.from(jmsReactiveSource())
.map(Message::getPayload);
}
#GetMapping(value = "/generate")
public void generateJmsMessage() {
for (int i = 0; i < 100; i++) {
this.jmsTemplate.convertAndSend("testQueue", "testMessage #" + (i + 1));
}
}
}
In one terminal I have curl http://localhost:8080/events which waits for SSEs from that Flux.
In other terminal I perform curl http://localhost:8080/generate and see in the first one:
data:testMessage #1
data:testMessage #2
data:testMessage #3
data:testMessage #4
I use Spring Boot 2.0.0.BUILD-SNAPSHOT.
Also see here: https://spring.io/blog/2017/03/08/spring-tips-server-sent-events-sse